Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Diff gutter #1487

Merged
merged 1 commit into from
Feb 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions internal/action/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,21 @@ func (h *BufPane) HalfPageDown() bool {
return true
}

// ToggleDiffGutter turns the diff gutter off and on
func (h *BufPane) ToggleDiffGutter() bool {
if !h.Buf.Settings["diffgutter"].(bool) {
h.Buf.Settings["diffgutter"] = true
h.Buf.UpdateDiff(func(synchronous bool) {
screen.DrawChan <- true
})
InfoBar.Message("Enabled diff gutter")
} else {
h.Buf.Settings["diffgutter"] = false
InfoBar.Message("Disabled diff gutter")
}
return true
}

// ToggleRuler turns line numbers off and on
func (h *BufPane) ToggleRuler() bool {
if !h.Buf.Settings["ruler"].(bool) {
Expand Down
1 change: 1 addition & 0 deletions internal/action/bufpane.go
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,7 @@ var BufKeyActions = map[string]BufKeyAction{
"EndOfLine": (*BufPane).EndOfLine,
"ToggleHelp": (*BufPane).ToggleHelp,
"ToggleKeyMenu": (*BufPane).ToggleKeyMenu,
"ToggleDiffGutter": (*BufPane).ToggleDiffGutter,
"ToggleRuler": (*BufPane).ToggleRuler,
"ClearStatus": (*BufPane).ClearStatus,
"ShellMode": (*BufPane).ShellMode,
Expand Down
1 change: 1 addition & 0 deletions internal/action/infopane.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ var InfoNones = []string{
"HalfPageDown",
"ToggleHelp",
"ToggleKeyMenu",
"ToggleDiffGutter",
"ToggleRuler",
"JumpLine",
"ClearStatus",
Expand Down
111 changes: 111 additions & 0 deletions internal/buffer/buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
dmp "github.com/sergi/go-diff/diffmatchpatch"
)

const backupTime = 8000
Expand Down Expand Up @@ -97,6 +98,15 @@ func (b *SharedBuffer) remove(start, end Loc) []byte {
return b.LineArray.remove(start, end)
}

const (
DSUnchanged = 0
DSAdded = 1
DSModified = 2
DSDeletedAbove = 3
)

type DiffStatus byte

// Buffer stores the main information about a currently open file including
// the actual text (in a LineArray), the undo/redo stack (in an EventHandler)
// all the cursors, the syntax highlighting info, the settings for the buffer
Expand Down Expand Up @@ -138,6 +148,12 @@ type Buffer struct {

Messages []*Message

updateDiffTimer *time.Timer
diffBase []byte
diffBaseLineCount int
diffLock sync.RWMutex
diff map[int]DiffStatus

// counts the number of edits
// resets every backupTime edits
lastbackup time.Time
Expand Down Expand Up @@ -930,6 +946,101 @@ func (b *Buffer) Write(bytes []byte) (n int, err error) {
return len(bytes), nil
}

func (b *Buffer) updateDiffSync() {
b.diffLock.Lock()
defer b.diffLock.Unlock()

b.diff = make(map[int]DiffStatus)

if b.diffBase == nil {
return
}

differ := dmp.New()
baseRunes, bufferRunes, _ := differ.DiffLinesToRunes(string(b.diffBase), string(b.Bytes()))
diffs := differ.DiffMainRunes(baseRunes, bufferRunes, false)
lineN := 0

for _, diff := range diffs {
lineCount := len([]rune(diff.Text))

switch diff.Type {
case dmp.DiffEqual:
lineN += lineCount
case dmp.DiffInsert:
var status DiffStatus
if b.diff[lineN] == DSDeletedAbove {
status = DSModified
} else {
status = DSAdded
}
for i := 0; i < lineCount; i++ {
b.diff[lineN] = status
lineN++
}
case dmp.DiffDelete:
b.diff[lineN] = DSDeletedAbove
}
}
}

// UpdateDiff computes the diff between the diff base and the buffer content.
// The update may be performed synchronously or asynchronously.
// UpdateDiff calls the supplied callback when the update is complete.
// The argument passed to the callback is set to true if and only if
// the update was performed synchronously.
// If an asynchronous update is already pending when UpdateDiff is called,
// UpdateDiff does not schedule another update, in which case the callback
// is not called.
func (b *Buffer) UpdateDiff(callback func(bool)) {
if b.updateDiffTimer != nil {
return
}

lineCount := b.LinesNum()
if b.diffBaseLineCount > lineCount {
lineCount = b.diffBaseLineCount
}

if lineCount < 1000 {
b.updateDiffSync()
callback(true)
} else if lineCount < 30000 {
b.updateDiffTimer = time.AfterFunc(500*time.Millisecond, func() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the point of the updateDiffTimer? This will call the given function in a separate goroutine after 500ms. Wouldn't it be better to simply call the function in a separate goroutine right away? Something like

go func() {
    b.updateDiffSync()
    callback(false)
}()

and then get rid of updateDiffTimer entirely?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a debounce, a rate-limiting mechanism. It consolidates multiple calls to UpdateDiff made within a short period of time into a single diff operation. Note that UpdateDiff returns immediately if a timer is active (i.e., if a diff operation is already scheduled).

As an example, imagine you are editing a file with 20,000 lines (for which a diff operation takes about 30 milliseconds) and typing 10 characters per second. Without the debounce, Micro would perform 10 diff operations per second, taking 300 milliseconds of core time, or 30% of a single CPU core. Additionally, each of these operations would schedule a redraw once it is complete. With the debounce, Micro performs only 2 diff operations and redraws per second, using just 6% of one CPU core.

In other words, this code substantially reduces the CPU usage, at the small price of having to wait a maximum of 0.5 seconds to see diff updates.

b.updateDiffTimer = nil
b.updateDiffSync()
callback(false)
})
} else {
// Don't compute diffs for very large files
b.diffLock.Lock()
b.diff = make(map[int]DiffStatus)
b.diffLock.Unlock()
callback(true)
}
}

// SetDiffBase sets the text that is used as the base for diffing the buffer content
func (b *Buffer) SetDiffBase(diffBase []byte) {
b.diffBase = diffBase
if diffBase == nil {
b.diffBaseLineCount = 0
} else {
b.diffBaseLineCount = strings.Count(string(diffBase), "\n")
}
b.UpdateDiff(func(synchronous bool) {
screen.DrawChan <- true
})
}

// DiffStatus returns the diff status for a line in the buffer
func (b *Buffer) DiffStatus(lineN int) DiffStatus {
b.diffLock.RLock()
defer b.diffLock.RUnlock()
// Note that the zero value for DiffStatus is equal to DSUnchanged
return b.diff[lineN]
}

// WriteLog writes a string to the log buffer
func WriteLog(s string) {
LogBuf.EventHandler.Insert(LogBuf.End(), s)
Expand Down
127 changes: 99 additions & 28 deletions internal/config/runtime.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions internal/config/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ var defaultCommonSettings = map[string]interface{}{
"basename": false,
"colorcolumn": float64(0),
"cursorline": true,
"diffgutter": true,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be false by default. I'm open to hearing other opinions though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer having it true by default as in most other editors out there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be disabled by default. I feel like micro should be as simple as possible without configuration. Thought that's my philosophy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe Micro should be as useful as possible without configuration. 99% of programmers use Git, and this feature provides an immediate benefit to them (not having to run git diff in order to see changes) without negatively impacting anyone else. I am not aware of any editor that has a built-in diff gutter and disables it by default.

I have carefully designed this feature to be as unobtrusive as possible even for people who don't need/want it. The gutter occupies only a single cell of horizontal space. The impact on CPU and memory usage should be minimal (see above). I definitely think this should be a default feature, especially since (to my knowledge) Micro will be the first terminal-based text editor with a built-in diff gutter and displaying this capability by default could raise its popularity. It definitely was one of the first things I missed when I tried out Micro.

"encoding": "utf-8",
"eofnewline": false,
"fastdirty": true,
Expand Down
91 changes: 78 additions & 13 deletions internal/display/bufwindow.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ func (w *BufWindow) LocFromVisual(svloc buffer.Loc) buffer.Loc {
if hasMessage {
vloc.X += 2
}
if b.Settings["diffgutter"].(bool) {
vloc.X++
}
if b.Settings["ruler"].(bool) {
vloc.X += maxLineNumLength + 1
}
Expand Down Expand Up @@ -273,6 +276,9 @@ func (w *BufWindow) LocFromVisual(svloc buffer.Loc) buffer.Loc {
break
}
vloc.X = 0
if b.Settings["diffgutter"].(bool) {
vloc.X++
}
// This will draw an empty line number because the current line is wrapped
if b.Settings["ruler"].(bool) {
vloc.X += maxLineNumLength + 1
Expand Down Expand Up @@ -311,6 +317,34 @@ func (w *BufWindow) drawGutter(vloc *buffer.Loc, bloc *buffer.Loc) {
vloc.X++
}

func (w *BufWindow) drawDiffGutter(backgroundStyle tcell.Style, softwrapped bool, vloc *buffer.Loc, bloc *buffer.Loc) {
symbol := ' '
styleName := ""

switch w.Buf.DiffStatus(bloc.Y) {
case buffer.DSAdded:
symbol = '\u258C' // Left half block
styleName = "diff-added"
case buffer.DSModified:
symbol = '\u258C' // Left half block
styleName = "diff-modified"
case buffer.DSDeletedAbove:
if !softwrapped {
symbol = '\u2594' // Upper one eighth block
styleName = "diff-deleted"
}
}

style := backgroundStyle
if s, ok := config.Colorscheme[styleName]; ok {
foreground, _, _ := s.Decompose()
style = style.Foreground(foreground)
}

screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, symbol, nil, style)
vloc.X++
}

func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, maxLineNumLength int, vloc *buffer.Loc, bloc *buffer.Loc) {
lineNum := strconv.Itoa(bloc.Y + 1)

Expand Down Expand Up @@ -373,15 +407,33 @@ func (w *BufWindow) displayBuffer() {
bufWidth--
}

if b.Settings["syntax"].(bool) && b.SyntaxDef != nil {
for _, r := range b.Modifications {
final := -1
for i := r.X; i <= r.Y; i++ {
final = util.Max(b.Highlighter.ReHighlightStates(b, i), final)
if len(b.Modifications) > 0 {
if b.Settings["syntax"].(bool) && b.SyntaxDef != nil {
for _, r := range b.Modifications {
final := -1
for i := r.X; i <= r.Y; i++ {
final = util.Max(b.Highlighter.ReHighlightStates(b, i), final)
}
b.Highlighter.HighlightMatches(b, r.X, final+1)
}
b.Highlighter.HighlightMatches(b, r.X, final+1)
}

b.ClearModifications()

if b.Settings["diffgutter"].(bool) {
b.UpdateDiff(func(synchronous bool) {
// If the diff was updated asynchronously, the outer call to
// displayBuffer might already be completed and we need to
// schedule a redraw in order to display the new diff.
// Note that this cannot lead to an infinite recursion
// because the modifications were cleared above so there won't
// be another call to UpdateDiff when displayBuffer is called
// during the redraw.
if !synchronous {
screen.DrawChan <- true
}
})
}
}

var matchingBraces []buffer.Loc
Expand Down Expand Up @@ -444,18 +496,28 @@ func (w *BufWindow) displayBuffer() {
for vloc.Y = 0; vloc.Y < bufHeight; vloc.Y++ {
vloc.X = 0

currentLine := false
for _, c := range cursors {
if bloc.Y == c.Y && w.active {
currentLine = true
break
}
}

s := lineNumStyle
if currentLine {
s = curNumStyle
}

if hasMessage {
w.drawGutter(&vloc, &bloc)
}

if b.Settings["diffgutter"].(bool) {
w.drawDiffGutter(s, false, &vloc, &bloc)
}

if b.Settings["ruler"].(bool) {
s := lineNumStyle
for _, c := range cursors {
if bloc.Y == c.Y && w.active {
s = curNumStyle
break
}
}
w.drawLineNum(s, false, maxLineNumLength, &vloc, &bloc)
}

Expand Down Expand Up @@ -579,6 +641,9 @@ func (w *BufWindow) displayBuffer() {
break
}
vloc.X = 0
if b.Settings["diffgutter"].(bool) {
w.drawDiffGutter(lineNumStyle, true, &vloc, &bloc)
}
// This will draw an empty line number because the current line is wrapped
if b.Settings["ruler"].(bool) {
w.drawLineNum(lineNumStyle, true, maxLineNumLength, &vloc, &bloc)
Expand Down
3 changes: 3 additions & 0 deletions runtime/colorschemes/atom-dark.micro
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ color-link tabbar "#1D1F21,#C5C8C6"
color-link indent-char "#505050,#1D1F21"
color-link line-number "#656866,#232526"
color-link current-line-number "#656866,#1D1F21"
color-link diff-added "#00AF00"
color-link diff-modified "#FFAF00"
color-link diff-deleted "#D70000"
color-link gutter-error "#FF4444,#1D1F21"
color-link gutter-warning "#EEEE77,#1D1F21"
color-link cursor-line "#2D2F31"
Expand Down
3 changes: 3 additions & 0 deletions runtime/colorschemes/bubblegum.micro
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ color-link underlined "underline 241,231"
color-link todo "246,231"
color-link statusline "241,254"
color-link tabbar "241,254"
color-link diff-added "34"
color-link diff-modified "214"
color-link diff-deleted "160"
color-link gutter-error "197,231"
color-link gutter-warning "134,231"
color-link line-number "246,254"
Expand Down
3 changes: 3 additions & 0 deletions runtime/colorschemes/cmc-16.micro
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ color-link statusline "white,blue"
color-link tabbar "white,blue"
color-link current-line-number "red"
color-link current-line-number.scroller "red"
color-link diff-added "green"
color-link diff-modified "yellow"
color-link diff-deleted "red"
color-link gutter-error ",red"
color-link gutter-warning "red"
color-link color-column "cyan"
Expand Down
5 changes: 4 additions & 1 deletion runtime/colorschemes/cmc-tc.micro
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ color-link statusline "#aaaaaa,#8a496b"
color-link tabbar "#aaaaaa,#8a496b"
color-link current-line-number "bold #e34234,#424549"
color-link current-line-number.scroller "red"
color-link diff-added "#00AF00"
color-link diff-modified "#FFAF00"
color-link diff-deleted "#D70000"
color-link gutter-error ",#e34234"
color-link gutter-warning "#e34234"
color-link color-column "#f26522"
color-link constant.bool "bold #55ffff"
color-link constant.bool.true "bold #85ff85"
color-link constant.bool.false "bold #ff8585"
color-link constant.bool.false "bold #ff8585"
3 changes: 3 additions & 0 deletions runtime/colorschemes/darcula.micro
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ color-link tabbar "#242424,#CCCCCC"
color-link indent-char "#4F4F4F,#242424"
color-link line-number "#666666,#2C2C2C"
color-link current-line-number "#666666,#242424"
color-link diff-added "#00AF00"
color-link diff-modified "#FFAF00"
color-link diff-deleted "#D70000"
color-link gutter-error "#CB4B16,#242424"
color-link gutter-warning "#E6DB74,#242424"
color-link cursor-line "#2C2C2C"
Expand Down
3 changes: 3 additions & 0 deletions runtime/colorschemes/default.micro
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ color-link tabbar "#282828,#F8F8F2"
color-link indent-char "#505050,#282828"
color-link line-number "#AAAAAA,#323232"
color-link current-line-number "#AAAAAA,#282828"
color-link diff-added "#00AF00"
color-link diff-modified "#FFAF00"
color-link diff-deleted "#D70000"
color-link gutter-error "#CB4B16,#282828"
color-link gutter-warning "#E6DB74,#282828"
color-link cursor-line "#323232"
Expand Down
3 changes: 3 additions & 0 deletions runtime/colorschemes/geany.micro
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@ color-link current-line-number ""
color-link statusline "black,white"
color-link tabbar "black,white"
color-link color-column "bold geren"
color-link diff-added "green"
color-link diff-modified "yellow"
color-link diff-deleted "red"
color-link gutter-error ",red"
color-link gutter-warning "red"
Loading