]> git.lizzy.rs Git - micro.git/blobdiff - internal/buffer/buffer.go
Support csharp-script syntax. (#1425)
[micro.git] / internal / buffer / buffer.go
index 44f0e1e972379a157cc0471c7e1c2ffb334f466d..fc74efe5b9b1cede591db6507b4fb94c6961c0a3 100644 (file)
@@ -1,33 +1,43 @@
 package buffer
 
 import (
+       "bufio"
        "bytes"
        "crypto/md5"
        "errors"
+       "fmt"
        "io"
        "io/ioutil"
        "os"
+       "path"
        "path/filepath"
        "strconv"
        "strings"
+       "sync"
        "time"
        "unicode/utf8"
 
        luar "layeh.com/gopher-luar"
 
+       dmp "github.com/sergi/go-diff/diffmatchpatch"
        "github.com/zyedidia/micro/internal/config"
        ulua "github.com/zyedidia/micro/internal/lua"
        "github.com/zyedidia/micro/internal/screen"
-       "github.com/zyedidia/micro/internal/util"
+       "github.com/zyedidia/micro/internal/util"
        "github.com/zyedidia/micro/pkg/highlight"
        "golang.org/x/text/encoding/htmlindex"
        "golang.org/x/text/encoding/unicode"
        "golang.org/x/text/transform"
 )
 
+const backupTime = 8000
+
 var (
+       // OpenBuffers is a list of the currently open buffers
        OpenBuffers []*Buffer
-       LogBuf      *Buffer
+       // LogBuf is a reference to the log buffer which can be opened with the
+       // `> log` command
+       LogBuf *Buffer
 )
 
 // The BufType defines what kind of buffer this is
@@ -39,16 +49,29 @@ type BufType struct {
 }
 
 var (
+       // BTDefault is a default buffer
        BTDefault = BufType{0, false, false, true}
-       BTHelp    = BufType{1, true, true, true}
-       BTLog     = BufType{2, true, true, false}
+       // BTHelp is a help buffer
+       BTHelp = BufType{1, true, true, true}
+       // BTLog is a log buffer
+       BTLog = BufType{2, true, true, false}
+       // BTScratch is a buffer that cannot be saved (for scratch work)
        BTScratch = BufType{3, false, true, false}
-       BTRaw     = BufType{4, false, true, false}
-       BTInfo    = BufType{5, false, true, false}
-
+       // BTRaw is is a buffer that shows raw terminal events
+       BTRaw = BufType{4, false, true, false}
+       // BTInfo is a buffer for inputting information
+       BTInfo = BufType{5, false, true, false}
+       // BTStdout is a buffer that only writes to stdout
+       // when closed
+       BTStdout = BufType{6, false, true, true}
+
+       // ErrFileTooLarge is returned when the file is too large to hash
+       // (fastdirty is automatically enabled)
        ErrFileTooLarge = errors.New("File is too large to hash")
 )
 
+// SharedBuffer is a struct containing info that is shared among buffers
+// that have the same file open
 type SharedBuffer struct {
        *LineArray
        // Stores the last modification time of the file the buffer is pointing to
@@ -56,23 +79,103 @@ type SharedBuffer struct {
        // Type of the buffer (e.g. help, raw, scratch etc..)
        Type BufType
 
+       // Path to the file on disk
+       Path string
+       // Absolute path to the file on disk
+       AbsPath string
+       // Name of the buffer on the status line
+       name string
+
+       toStdout bool
+
+       // Settings customized by the user
+       Settings map[string]interface{}
+
+       Suggestions   []string
+       Completions   []string
+       CurSuggestion int
+
+       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
+
+       // ReloadDisabled allows the user to disable reloads if they
+       // are viewing a file that is constantly changing
+       ReloadDisabled bool
+
        isModified bool
        // Whether or not suggestions can be autocompleted must be shared because
        // it changes based on how the buffer has changed
        HasSuggestions bool
+
+       // The Highlighter struct actually performs the highlighting
+       Highlighter *highlight.Highlighter
+       // SyntaxDef represents the syntax highlighting definition being used
+       // This stores the highlighting rules and filetype detection info
+       SyntaxDef *highlight.Def
+
+       ModifiedThisFrame bool
+
+       // Hash of the original buffer -- empty if fastdirty is on
+       origHash [md5.Size]byte
 }
 
 func (b *SharedBuffer) insert(pos Loc, value []byte) {
        b.isModified = true
        b.HasSuggestions = false
        b.LineArray.insert(pos, value)
+
+       inslines := bytes.Count(value, []byte{'\n'})
+       b.MarkModified(pos.Y, pos.Y+inslines)
 }
 func (b *SharedBuffer) remove(start, end Loc) []byte {
        b.isModified = true
        b.HasSuggestions = false
+       defer b.MarkModified(start.Y, end.Y)
        return b.LineArray.remove(start, end)
 }
 
+// MarkModified marks the buffer as modified for this frame
+// and performs rehighlighting if syntax highlighting is enabled
+func (b *SharedBuffer) MarkModified(start, end int) {
+       b.ModifiedThisFrame = true
+
+       if !b.Settings["syntax"].(bool) || b.SyntaxDef == nil {
+               return
+       }
+
+       start = util.Clamp(start, 0, len(b.lines))
+       end = util.Clamp(end, 0, len(b.lines))
+
+       l := -1
+       for i := start; i <= end; i++ {
+               l = util.Max(b.Highlighter.ReHighlightStates(b, i), l)
+       }
+       b.Highlighter.HighlightMatches(b, start, l+1)
+}
+
+// DisableReload disables future reloads of this sharedbuffer
+func (b *SharedBuffer) DisableReload() {
+       b.ReloadDisabled = true
+}
+
+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
@@ -87,28 +190,6 @@ type Buffer struct {
        cursors     []*Cursor
        curCursor   int
        StartCursor Loc
-
-       // Path to the file on disk
-       Path string
-       // Absolute path to the file on disk
-       AbsPath string
-       // Name of the buffer on the status line
-       name string
-
-       SyntaxDef   *highlight.Def
-       Highlighter *highlight.Highlighter
-
-       // Hash of the original buffer -- empty if fastdirty is on
-       origHash [md5.Size]byte
-
-       // Settings customized by the user
-       Settings map[string]interface{}
-
-       Suggestions   []string
-       Completions   []string
-       CurSuggestion int
-
-       Messages []*Message
 }
 
 // NewBufferFromFile opens a new buffer using the given path
@@ -117,8 +198,8 @@ type Buffer struct {
 // and an error if the file is a directory
 func NewBufferFromFile(path string, btype BufType) (*Buffer, error) {
        var err error
-       filename, cursorPos := GetPathAndCursorPosition(path)
-       filename, err = ReplaceHome(filename)
+       filename, cursorPos := util.GetPathAndCursorPosition(path)
+       filename, err = util.ReplaceHome(filename)
        if err != nil {
                return nil, err
        }
@@ -127,7 +208,7 @@ func NewBufferFromFile(path string, btype BufType) (*Buffer, error) {
        fileInfo, _ := os.Stat(filename)
 
        if err == nil && fileInfo.IsDir() {
-               return nil, errors.New(filename + " is a directory")
+               return nil, errors.New("Error: " + filename + " is a directory and cannot be opened")
        }
 
        defer file.Close()
@@ -142,7 +223,7 @@ func NewBufferFromFile(path string, btype BufType) (*Buffer, error) {
                // File does not exist -- create an empty buffer with that name
                buf = NewBufferFromString("", filename, btype)
        } else {
-               buf = NewBuffer(file, FSize(file), filename, cursorLoc, btype)
+               buf = NewBuffer(file, util.FSize(file), filename, cursorLoc, btype)
        }
 
        return buf, nil
@@ -163,22 +244,6 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
 
        b := new(Buffer)
 
-       b.Settings = config.DefaultCommonSettings()
-       for k, v := range config.GlobalSettings {
-               if _, ok := b.Settings[k]; ok {
-                       b.Settings[k] = v
-               }
-       }
-       config.InitLocalSettings(b.Settings, path)
-
-       enc, err := htmlindex.Get(b.Settings["encoding"].(string))
-       if err != nil {
-               enc = unicode.UTF8
-               b.Settings["encoding"] = "utf-8"
-       }
-
-       reader := transform.NewReader(r, enc.NewDecoder())
-
        found := false
        if len(path) > 0 {
                for _, buf := range OpenBuffers {
@@ -193,20 +258,41 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
        if !found {
                b.SharedBuffer = new(SharedBuffer)
                b.Type = btype
-               b.LineArray = NewLineArray(uint64(size), FFAuto, reader)
+
+               b.AbsPath = absPath
+               b.Path = path
+
+               b.Settings = config.DefaultCommonSettings()
+               for k, v := range config.GlobalSettings {
+                       if _, ok := config.DefaultGlobalOnlySettings[k]; !ok {
+                               // make sure setting is not global-only
+                               b.Settings[k] = v
+                       }
+               }
+               config.InitLocalSettings(b.Settings, path)
+
+               enc, err := htmlindex.Get(b.Settings["encoding"].(string))
+               if err != nil {
+                       enc = unicode.UTF8
+                       b.Settings["encoding"] = "utf-8"
+               }
+
+               hasBackup := b.ApplyBackup(size)
+
+               if !hasBackup {
+                       reader := bufio.NewReader(transform.NewReader(r, enc.NewDecoder()))
+                       b.LineArray = NewLineArray(uint64(size), FFAuto, reader)
+               }
                b.EventHandler = NewEventHandler(b.SharedBuffer, b.cursors)
+
+               // The last time this file was modified
+               b.UpdateModTime()
        }
 
-       if b.Settings["readonly"].(bool) {
+       if b.Settings["readonly"].(bool) && b.Type == BTDefault {
                b.Type.Readonly = true
        }
 
-       b.Path = path
-       b.AbsPath = absPath
-
-       // The last time this file was modified
-       b.ModTime, _ = GetModTime(b.Path)
-
        switch b.Endings {
        case FFUnix:
                b.Settings["fileformat"] = "unix"
@@ -215,10 +301,11 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
        }
 
        b.UpdateRules()
+       // init local settings again now that we know the filetype
        config.InitLocalSettings(b.Settings, b.Path)
 
-       if _, err := os.Stat(config.ConfigDir + "/buffers/"); os.IsNotExist(err) {
-               os.Mkdir(config.ConfigDir+"/buffers/", os.ModePerm)
+       if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); os.IsNotExist(err) {
+               os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm)
        }
 
        if startcursor.X != -1 && startcursor.Y != -1 {
@@ -235,7 +322,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
        b.AddCursor(NewCursor(b, b.StartCursor))
        b.GetActiveCursor().Relocate()
 
-       if !b.Settings["fastdirty"].(bool) {
+       if !b.Settings["fastdirty"].(bool) && !found {
                if size > LargeFileThreshold {
                        // If the file is larger than LargeFileThreshold fastdirty needs to be on
                        b.Settings["fastdirty"] = true
@@ -244,7 +331,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
                }
        }
 
-       err = config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b))
+       err := config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b))
        if err != nil {
                screen.TermMessage(err)
        }
@@ -273,18 +360,27 @@ func (b *Buffer) Fini() {
        if !b.Modified() {
                b.Serialize()
        }
+       b.RemoveBackup()
+
+       if b.Type == BTStdout {
+               fmt.Fprint(util.Stdout, string(b.Bytes()))
+       }
 }
 
 // GetName returns the name that should be displayed in the statusline
 // for this buffer
 func (b *Buffer) GetName() string {
-       if b.name == "" {
+       name := b.name
+       if name == "" {
                if b.Path == "" {
                        return "No name"
                }
-               return b.Path
+               name = b.Path
        }
-       return b.name
+       if b.Settings["basename"].(bool) {
+               return path.Base(name)
+       }
+       return name
 }
 
 //SetName changes the name for this buffer
@@ -292,19 +388,25 @@ func (b *Buffer) SetName(s string) {
        b.name = s
 }
 
+// Insert inserts the given string of text at the start location
 func (b *Buffer) Insert(start Loc, text string) {
        if !b.Type.Readonly {
                b.EventHandler.cursors = b.cursors
                b.EventHandler.active = b.curCursor
                b.EventHandler.Insert(start, text)
+
+               go b.Backup(true)
        }
 }
 
+// Remove removes the characters between the start and end locations
 func (b *Buffer) Remove(start, end Loc) {
        if !b.Type.Readonly {
                b.EventHandler.cursors = b.cursors
                b.EventHandler.active = b.curCursor
                b.EventHandler.Remove(start, end)
+
+               go b.Backup(true)
        }
 }
 
@@ -313,6 +415,22 @@ func (b *Buffer) FileType() string {
        return b.Settings["filetype"].(string)
 }
 
+// ExternallyModified returns whether the file being edited has
+// been modified by some external process
+func (b *Buffer) ExternallyModified() bool {
+       modTime, err := util.GetModTime(b.Path)
+       if err == nil {
+               return modTime != b.ModTime
+       }
+       return false
+}
+
+// UpdateModTime updates the modtime of this file
+func (b *Buffer) UpdateModTime() (err error) {
+       b.ModTime, err = util.GetModTime(b.Path)
+       return
+}
+
 // ReOpen reloads the current buffer from disk
 func (b *Buffer) ReOpen() error {
        file, err := os.Open(b.Path)
@@ -325,7 +443,7 @@ func (b *Buffer) ReOpen() error {
                return err
        }
 
-       reader := transform.NewReader(file, enc.NewDecoder())
+       reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder()))
        data, err := ioutil.ReadAll(reader)
        txt := string(data)
 
@@ -334,12 +452,16 @@ func (b *Buffer) ReOpen() error {
        }
        b.EventHandler.ApplyDiff(txt)
 
-       b.ModTime, err = GetModTime(b.Path)
+       err = b.UpdateModTime()
+       if !b.Settings["fastdirty"].(bool) {
+               calcHash(b, &b.origHash)
+       }
        b.isModified = false
        b.RelocateCursors()
        return err
 }
 
+// RelocateCursors relocates all cursors (makes sure they are in the buffer)
 func (b *Buffer) RelocateCursors() {
        for _, c := range b.cursors {
                c.Relocate()
@@ -421,65 +543,144 @@ func (b *Buffer) UpdateRules() {
        if !b.Type.Syntax {
                return
        }
-       rehighlight := false
-       var files []*highlight.File
-       for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
+       ft := b.Settings["filetype"].(string)
+       if ft == "off" {
+               return
+       }
+       syntaxFile := ""
+       foundDef := false
+       var header *highlight.Header
+       // search for the syntax file in the user's custom syntax files
+       for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
                data, err := f.Data()
                if err != nil {
                        screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
-               } else {
-                       file, err := highlight.ParseFile(data)
+                       continue
+               }
+
+               header, err = highlight.MakeHeaderYaml(data)
+               file, err := highlight.ParseFile(data)
+               if err != nil {
+                       screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
+                       continue
+               }
+
+               if ((ft == "unknown" || ft == "") && highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data)) || header.FileType == ft {
+                       syndef, err := highlight.ParseDef(file, header)
                        if err != nil {
-                               screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
+                               screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
                                continue
                        }
-                       ftdetect, err := highlight.ParseFtDetect(file)
+                       b.SyntaxDef = syndef
+                       syntaxFile = f.Name()
+                       foundDef = true
+                       break
+               }
+       }
+
+       // search in the default syntax files
+       for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
+               data, err := f.Data()
+               if err != nil {
+                       screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error())
+                       continue
+               }
+
+               header, err = highlight.MakeHeader(data)
+               if err != nil {
+                       screen.TermMessage("Error reading syntax header file", f.Name(), err)
+                       continue
+               }
+
+               if ft == "unknown" || ft == "" {
+                       if highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data) {
+                               syntaxFile = f.Name()
+                               break
+                       }
+               } else if header.FileType == ft {
+                       syntaxFile = f.Name()
+                       break
+               }
+       }
+
+       if syntaxFile != "" && !foundDef {
+               // we found a syntax file using a syntax header file
+               for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
+                       if f.Name() == syntaxFile {
+                               data, err := f.Data()
+                               if err != nil {
+                                       screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
+                                       continue
+                               }
+
+                               file, err := highlight.ParseFile(data)
+                               if err != nil {
+                                       screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
+                                       continue
+                               }
+
+                               syndef, err := highlight.ParseDef(file, header)
+                               if err != nil {
+                                       screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
+                                       continue
+                               }
+                               b.SyntaxDef = syndef
+                               break
+                       }
+               }
+       }
+
+       if b.SyntaxDef != nil && highlight.HasIncludes(b.SyntaxDef) {
+               includes := highlight.GetIncludes(b.SyntaxDef)
+
+               var files []*highlight.File
+               for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
+                       data, err := f.Data()
                        if err != nil {
-                               screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
+                               screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
+                               continue
+                       }
+                       header, err := highlight.MakeHeaderYaml(data)
+                       if err != nil {
+                               screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
                                continue
                        }
 
-                       ft := b.Settings["filetype"].(string)
-                       if (ft == "unknown" || ft == "") && !rehighlight {
-                               if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
-                                       header := new(highlight.Header)
-                                       header.FileType = file.FileType
-                                       header.FtDetect = ftdetect
-                                       b.SyntaxDef, err = highlight.ParseDef(file, header)
+                       for _, i := range includes {
+                               if header.FileType == i {
+                                       file, err := highlight.ParseFile(data)
                                        if err != nil {
-                                               screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
+                                               screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
                                                continue
                                        }
-                                       rehighlight = true
-                               }
-                       } else {
-                               if file.FileType == ft && !rehighlight {
-                                       header := new(highlight.Header)
-                                       header.FileType = file.FileType
-                                       header.FtDetect = ftdetect
-                                       b.SyntaxDef, err = highlight.ParseDef(file, header)
-                                       if err != nil {
-                                               screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
-                                               continue
-                                       }
-                                       rehighlight = true
+                                       files = append(files, file)
+                                       break
                                }
                        }
-                       files = append(files, file)
+                       if len(files) >= len(includes) {
+                               break
+                       }
                }
-       }
 
-       if b.SyntaxDef != nil {
                highlight.ResolveIncludes(b.SyntaxDef, files)
        }
 
-       if b.Highlighter == nil || rehighlight {
+       if b.Highlighter == nil || syntaxFile != "" {
                if b.SyntaxDef != nil {
                        b.Settings["filetype"] = b.SyntaxDef.FileType
-                       b.Highlighter = highlight.NewHighlighter(b.SyntaxDef)
-                       if b.Settings["syntax"].(bool) {
+               }
+       } else {
+               b.SyntaxDef = &highlight.EmptyDef
+       }
+
+       if b.SyntaxDef != nil {
+               b.Highlighter = highlight.NewHighlighter(b.SyntaxDef)
+               if b.Settings["syntax"].(bool) {
+                       go func() {
                                b.Highlighter.HighlightStates(b)
-                       }
+                               b.Highlighter.HighlightMatches(b, 0, b.End().Y)
+                               screen.Redraw()
+                       }()
                }
        }
 }
@@ -496,7 +697,7 @@ func (b *Buffer) ClearMatches() {
 // depending on the settings)
 func (b *Buffer) IndentString(tabsize int) string {
        if b.Settings["tabstospaces"].(bool) {
-               return Spaces(tabsize)
+               return util.Spaces(tabsize)
        }
        return "\t"
 }
@@ -584,7 +785,7 @@ func (b *Buffer) RemoveCursor(i int) {
        copy(b.cursors[i:], b.cursors[i+1:])
        b.cursors[len(b.cursors)-1] = nil
        b.cursors = b.cursors[:len(b.cursors)-1]
-       b.curCursor = Clamp(b.curCursor, 0, len(b.cursors)-1)
+       b.curCursor = util.Clamp(b.curCursor, 0, len(b.cursors)-1)
        b.UpdateCursors()
 }
 
@@ -655,7 +856,7 @@ var BracePairs = [][2]rune{
 // returns the location of the matching brace
 // if the boolean returned is true then the original matching brace is one character left
 // of the starting location
-func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool) {
+func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool, bool) {
        curLine := []rune(string(b.LineBytes(start.Y)))
        startChar := ' '
        if start.X >= 0 && start.X < len(curLine) {
@@ -685,9 +886,9 @@ func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool) {
                                        i--
                                        if i == 0 {
                                                if startChar == braceType[0] {
-                                                       return Loc{x, y}, false
+                                                       return Loc{x, y}, false, true
                                                }
-                                               return Loc{x, y}, true
+                                               return Loc{x, y}, true, true
                                        }
                                }
                        }
@@ -709,9 +910,9 @@ func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool) {
                                        i--
                                        if i == 0 {
                                                if leftChar == braceType[1] {
-                                                       return Loc{x, y}, true
+                                                       return Loc{x, y}, true, true
                                                }
-                                               return Loc{x, y}, false
+                                               return Loc{x, y}, false, true
                                        }
                                } else if r == braceType[1] {
                                        i++
@@ -719,19 +920,19 @@ func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool) {
                        }
                }
        }
-       return start, true
+       return start, true, false
 }
 
 // Retab changes all tabs to spaces or vice versa
 func (b *Buffer) Retab() {
        toSpaces := b.Settings["tabstospaces"].(bool)
-       tabsize := IntOpt(b.Settings["tabsize"])
+       tabsize := util.IntOpt(b.Settings["tabsize"])
        dirty := false
 
        for i := 0; i < b.LinesNum(); i++ {
                l := b.LineBytes(i)
 
-               ws := GetLeadingWhitespace(l)
+               ws := util.GetLeadingWhitespace(l)
                if len(ws) != 0 {
                        if toSpaces {
                                ws = bytes.Replace(ws, []byte{'\t'}, bytes.Repeat([]byte{' '}, tabsize), -1)
@@ -742,6 +943,7 @@ func (b *Buffer) Retab() {
 
                l = bytes.TrimLeft(l, " \t")
                b.lines[i].data = append(ws, l...)
+               b.MarkModified(i, i)
                dirty = true
        }
 
@@ -760,12 +962,12 @@ func ParseCursorLocation(cursorPositions []string) (Loc, error) {
        }
 
        startpos.Y, err = strconv.Atoi(cursorPositions[0])
-       startpos.Y -= 1
+       startpos.Y--
        if err == nil {
                if len(cursorPositions) > 1 {
                        startpos.X, err = strconv.Atoi(cursorPositions[1])
                        if startpos.X > 0 {
-                               startpos.X -= 1
+                               startpos.X--
                        }
                }
        }
@@ -773,10 +975,117 @@ func ParseCursorLocation(cursorPositions []string) (Loc, error) {
        return startpos, err
 }
 
+// Line returns the string representation of the given line number
 func (b *Buffer) Line(i int) string {
        return string(b.LineBytes(i))
 }
 
+func (b *Buffer) Write(bytes []byte) (n int, err error) {
+       b.EventHandler.InsertBytes(b.End(), bytes)
+       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() {
+                       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.Redraw()
+       })
+}
+
+// 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)
 }
+
+// GetLogBuf returns the log buffer
+func GetLogBuf() *Buffer {
+       return LogBuf
+}