]> git.lizzy.rs Git - micro.git/blobdiff - internal/buffer/buffer.go
Use abspath for local glob settings
[micro.git] / internal / buffer / buffer.go
index 16bc28f794e1129d0c9ce33e36dfd7d94f90838f..f33b24389b225425bdcf2b61503cae3e4b1e79db 100644 (file)
@@ -1,25 +1,30 @@
 package buffer
 
 import (
+       "bufio"
        "bytes"
        "crypto/md5"
        "errors"
+       "fmt"
        "io"
        "io/ioutil"
        "os"
+       "path"
        "path/filepath"
        "strconv"
        "strings"
+       "sync"
+       "sync/atomic"
        "time"
-       "unicode/utf8"
 
        luar "layeh.com/gopher-luar"
 
-       "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/pkg/highlight"
+       dmp "github.com/sergi/go-diff/diffmatchpatch"
+       "github.com/zyedidia/micro/v2/internal/config"
+       ulua "github.com/zyedidia/micro/v2/internal/lua"
+       "github.com/zyedidia/micro/v2/internal/screen"
+       "github.com/zyedidia/micro/v2/internal/util"
+       "github.com/zyedidia/micro/v2/pkg/highlight"
        "golang.org/x/text/encoding/htmlindex"
        "golang.org/x/text/encoding/unicode"
        "golang.org/x/text/transform"
@@ -28,8 +33,11 @@ import (
 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
@@ -41,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
@@ -58,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
+
+       requestedBackup bool
+
+       // 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
+
+       start = util.Clamp(start, 0, len(b.lines)-1)
+       end = util.Clamp(end, 0, len(b.lines)-1)
+
+       if b.Settings["syntax"].(bool) && b.SyntaxDef != nil {
+               l := -1
+               for i := start; i <= end; i++ {
+                       l = util.Max(b.Highlighter.ReHighlightStates(b, i), l)
+               }
+               b.Highlighter.HighlightMatches(b, start, l)
+       }
+
+       for i := start; i <= end; i++ {
+               b.LineArray.invalidateSearchMatches(i)
+       }
+}
+
+// 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
@@ -82,78 +183,108 @@ func (b *SharedBuffer) remove(start, end Loc) []byte {
 // The syntax highlighting info must be stored with the buffer because the syntax
 // highlighter attaches information to each line of the buffer for optimization
 // purposes so it doesn't have to rehighlight everything on every update.
+// Likewise for the search highlighting.
 type Buffer struct {
        *EventHandler
        *SharedBuffer
 
+       fini        int32
        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
-
-       // counts the number of edits
-       // resets every backupTime edits
-       lastbackup time.Time
+       // OptionCallback is called after a buffer option value is changed.
+       // The display module registers its OptionCallback to ensure the buffer window
+       // is properly updated when needed. This is a workaround for the fact that
+       // the buffer module cannot directly call the display's API (it would mean
+       // a circular dependency between packages).
+       OptionCallback func(option string, nativeValue interface{})
+
+       // The display module registers its own GetVisualX function for getting
+       // the correct visual x location of a cursor when softwrap is used.
+       // This is hacky. Maybe it would be better to move all the visual x logic
+       // from buffer to display, but it would require rewriting a lot of code.
+       GetVisualX func(loc Loc) int
+
+       // Last search stores the last successful search
+       LastSearch      string
+       LastSearchRegex bool
+       // HighlightSearch enables highlighting all instances of the last successful search
+       HighlightSearch bool
 }
 
-// NewBufferFromFile opens a new buffer using the given path
-// It will also automatically handle `~`, and line/column with filename:l:c
-// It will return an empty buffer if the path does not exist
-// and an error if the file is a directory
-func NewBufferFromFile(path string, btype BufType) (*Buffer, error) {
+// NewBufferFromFileAtLoc opens a new buffer with a given cursor location
+// If cursorLoc is {-1, -1} the location does not overwrite what the cursor location
+// would otherwise be (start of file, or saved cursor position if `savecursor` is
+// enabled)
+func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer, error) {
        var err error
-       filename, cursorPos := util.GetPathAndCursorPosition(path)
+       filename := path
+       if config.GetGlobalOption("parsecursor").(bool) && cursorLoc.X == -1 && cursorLoc.Y == -1 {
+               var cursorPos []string
+               filename, cursorPos = util.GetPathAndCursorPosition(filename)
+               cursorLoc, err = ParseCursorLocation(cursorPos)
+               if err != nil {
+                       cursorLoc = Loc{-1, -1}
+               }
+       }
+
        filename, err = util.ReplaceHome(filename)
        if err != nil {
                return nil, err
        }
 
-       file, err := os.Open(filename)
-       fileInfo, _ := os.Stat(filename)
+       f, err := os.OpenFile(filename, os.O_WRONLY, 0)
+       readonly := os.IsPermission(err)
+       f.Close()
 
-       if err == nil && fileInfo.IsDir() {
-               return nil, errors.New(filename + " is a directory")
+       fileInfo, serr := os.Stat(filename)
+       if serr != nil && !os.IsNotExist(serr) {
+               return nil, serr
+       }
+       if serr == nil && fileInfo.IsDir() {
+               return nil, errors.New("Error: " + filename + " is a directory and cannot be opened")
        }
 
-       defer file.Close()
-
-       cursorLoc, cursorerr := ParseCursorLocation(cursorPos)
-       if cursorerr != nil {
-               cursorLoc = Loc{-1, -1}
+       file, err := os.Open(filename)
+       if err == nil {
+               defer file.Close()
        }
 
        var buf *Buffer
-       if err != nil {
+       if os.IsNotExist(err) {
                // File does not exist -- create an empty buffer with that name
                buf = NewBufferFromString("", filename, btype)
+       } else if err != nil {
+               return nil, err
        } else {
                buf = NewBuffer(file, util.FSize(file), filename, cursorLoc, btype)
+               if buf == nil {
+                       return nil, errors.New("could not open file")
+               }
+       }
+
+       if readonly && prompt != nil {
+               prompt.Message(fmt.Sprintf("Warning: file is readonly - %s will be attempted when saving", config.GlobalSettings["sucmd"].(string)))
+               // buf.SetOptionNative("readonly", true)
        }
 
        return buf, nil
 }
 
+// NewBufferFromFile opens a new buffer using the given path
+// It will also automatically handle `~`, and line/column with filename:l:c
+// It will return an empty buffer if the path does not exist
+// and an error if the file is a directory
+func NewBufferFromFile(path string, btype BufType) (*Buffer, error) {
+       return NewBufferFromFileAtLoc(path, btype, Loc{-1, -1})
+}
+
+// NewBufferFromStringAtLoc creates a new buffer containing the given string with a cursor loc
+func NewBufferFromStringAtLoc(text, path string, btype BufType, cursorLoc Loc) *Buffer {
+       return NewBuffer(strings.NewReader(text), int64(len(text)), path, cursorLoc, btype)
+}
+
 // NewBufferFromString creates a new buffer containing the given string
 func NewBufferFromString(text, path string, btype BufType) *Buffer {
        return NewBuffer(strings.NewReader(text), int64(len(text)), path, Loc{-1, -1}, btype)
@@ -165,25 +296,12 @@ func NewBufferFromString(text, path string, btype BufType) *Buffer {
 // Places the cursor at startcursor. If startcursor is -1, -1 places the
 // cursor at an autodetected location (based on savecursor or :LINE:COL)
 func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufType) *Buffer {
-       absPath, _ := filepath.Abs(path)
-
-       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))
+       absPath, err := filepath.Abs(path)
        if err != nil {
-               enc = unicode.UTF8
-               b.Settings["encoding"] = "utf-8"
+               absPath = path
        }
 
-       reader := transform.NewReader(r, enc.NewDecoder())
+       b := new(Buffer)
 
        found := false
        if len(path) > 0 {
@@ -196,28 +314,74 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
                }
        }
 
-       b.Path = path
-       b.AbsPath = absPath
-
+       hasBackup := false
        if !found {
                b.SharedBuffer = new(SharedBuffer)
                b.Type = btype
 
-               hasBackup := b.ApplyBackup(size)
+               b.AbsPath = absPath
+               b.Path = path
+
+               // this is a little messy since we need to know some settings to read
+               // the file properly, but some settings depend on the filetype, which
+               // we don't know until reading the file. We first read the settings
+               // into a local variable and then use that to determine the encoding,
+               // readonly, and fileformat necessary for reading the file and
+               // assigning the filetype.
+               settings := config.DefaultCommonSettings()
+               b.Settings = config.DefaultCommonSettings()
+               for k, v := range config.GlobalSettings {
+                       if _, ok := config.DefaultGlobalOnlySettings[k]; !ok {
+                               // make sure setting is not global-only
+                               settings[k] = v
+                               b.Settings[k] = v
+                       }
+               }
+               config.InitLocalSettings(settings, absPath)
+               b.Settings["readonly"] = settings["readonly"]
+               b.Settings["filetype"] = settings["filetype"]
+               b.Settings["syntax"] = settings["syntax"]
+
+               enc, err := htmlindex.Get(settings["encoding"].(string))
+               if err != nil {
+                       enc = unicode.UTF8
+                       b.Settings["encoding"] = "utf-8"
+               }
+
+               var ok bool
+               hasBackup, ok = b.ApplyBackup(size)
 
+               if !ok {
+                       return NewBufferFromString("", "", btype)
+               }
                if !hasBackup {
-                       b.LineArray = NewLineArray(uint64(size), FFAuto, reader)
+                       reader := bufio.NewReader(transform.NewReader(r, enc.NewDecoder()))
+
+                       var ff FileFormat = FFAuto
+
+                       if size == 0 {
+                               // for empty files, use the fileformat setting instead of
+                               // autodetection
+                               switch settings["fileformat"] {
+                               case "unix":
+                                       ff = FFUnix
+                               case "dos":
+                                       ff = FFDos
+                               }
+                       }
+
+                       b.LineArray = NewLineArray(uint64(size), ff, 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
        }
 
-       // The last time this file was modified
-       b.UpdateModTime()
-
        switch b.Endings {
        case FFUnix:
                b.Settings["fileformat"] = "unix"
@@ -226,36 +390,37 @@ 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 {
                b.StartCursor = startcursor
-       } else {
-               if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
-                       err := b.Unserialize()
-                       if err != nil {
-                               screen.TermMessage(err)
-                       }
+       } else if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
+               err := b.Unserialize()
+               if err != nil {
+                       screen.TermMessage(err)
                }
        }
 
        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
-               } else {
+               } else if !hasBackup {
+                       // since applying a backup does not save the applied backup to disk, we should
+                       // not calculate the original hash based on the backup data
                        calcHash(b, &b.origHash)
                }
        }
 
-       err = config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b))
+       err := config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b))
        if err != nil {
                screen.TermMessage(err)
        }
@@ -285,18 +450,28 @@ func (b *Buffer) Fini() {
                b.Serialize()
        }
        b.RemoveBackup()
+
+       if b.Type == BTStdout {
+               fmt.Fprint(util.Stdout, string(b.Bytes()))
+       }
+
+       atomic.StoreInt32(&(b.fini), int32(1))
 }
 
 // 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
@@ -304,23 +479,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)
+               b.RequestBackup()
        }
 }
 
+// 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)
+               b.RequestBackup()
        }
 }
 
@@ -357,7 +534,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)
 
@@ -367,11 +544,15 @@ func (b *Buffer) ReOpen() error {
        b.EventHandler.ApplyDiff(txt)
 
        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()
@@ -384,18 +565,40 @@ func (b *Buffer) RuneAt(loc Loc) rune {
        if len(line) > 0 {
                i := 0
                for len(line) > 0 {
-                       r, size := utf8.DecodeRune(line)
+                       r, _, size := util.DecodeCharacter(line)
                        line = line[size:]
-                       i++
 
                        if i == loc.X {
                                return r
                        }
+
+                       i++
                }
        }
        return '\n'
 }
 
+// WordAt returns the word around a given location in the buffer
+func (b *Buffer) WordAt(loc Loc) []byte {
+       if len(b.LineBytes(loc.Y)) == 0 || !util.IsWordChar(b.RuneAt(loc)) {
+               return []byte{}
+       }
+
+       start := loc
+       end := loc.Move(1, b)
+
+       for start.X > 0 && util.IsWordChar(b.RuneAt(start.Move(-1, b))) {
+               start.X--
+       }
+
+       lineLen := util.CharacterCount(b.LineBytes(loc.Y))
+       for end.X < lineLen && util.IsWordChar(b.RuneAt(end)) {
+               end.X++
+       }
+
+       return b.Substr(start, end)
+}
+
 // Modified returns if this buffer has been modified since
 // being opened
 func (b *Buffer) Modified() bool {
@@ -413,6 +616,22 @@ func (b *Buffer) Modified() bool {
        return buff != b.origHash
 }
 
+// Size returns the number of bytes in the current buffer
+func (b *Buffer) Size() int {
+       nb := 0
+       for i := 0; i < b.LinesNum(); i++ {
+               nb += len(b.LineBytes(i))
+
+               if i != b.LinesNum()-1 {
+                       if b.Endings == FFDos {
+                               nb++ // carriage return
+                       }
+                       nb++ // newline
+               }
+       }
+       return nb
+}
+
 // calcHash calculates md5 hash of all lines in the buffer
 func calcHash(b *Buffer, out *[md5.Size]byte) error {
        h := md5.New()
@@ -453,9 +672,45 @@ func (b *Buffer) UpdateRules() {
        if !b.Type.Syntax {
                return
        }
-       syntaxFile := ""
        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())
+                       continue
+               }
+
+               header, err = highlight.MakeHeaderYaml(data)
+               if err != nil {
+                       screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error())
+               }
+               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 parsing syntax file " + f.Name() + ": " + err.Error())
+                               continue
+                       }
+                       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 {
@@ -480,33 +735,8 @@ func (b *Buffer) UpdateRules() {
                }
        }
 
-       if syntaxFile == "" {
-               // 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())
-                               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 parsing syntax file " + f.Name() + ": " + err.Error())
-                                       continue
-                               }
-                               b.SyntaxDef = syndef
-                               break
-                       }
-               }
-       } else {
+       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()
@@ -570,10 +800,19 @@ func (b *Buffer) UpdateRules() {
        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()
+                       }()
                }
        }
 }
@@ -700,19 +939,18 @@ func (b *Buffer) MoveLinesUp(start int, end int) {
        }
        l := string(b.LineBytes(start - 1))
        if end == len(b.lines) {
-               b.Insert(
+               b.insert(
                        Loc{
-                               utf8.RuneCount(b.lines[end-1].data),
+                               util.CharacterCount(b.lines[end-1].data),
                                end - 1,
                        },
-                       "\n"+l,
-               )
-       } else {
-               b.Insert(
-                       Loc{0, end},
-                       l+"\n",
+                       []byte{'\n'},
                )
        }
+       b.Insert(
+               Loc{0, end},
+               l+"\n",
+       )
        b.Remove(
                Loc{0, start - 1},
                Loc{0, start},
@@ -721,7 +959,7 @@ func (b *Buffer) MoveLinesUp(start int, end int) {
 
 // MoveLinesDown moves the range of lines down one row
 func (b *Buffer) MoveLinesDown(start int, end int) {
-       if start < 0 || start >= end || end >= len(b.lines)-1 {
+       if start < 0 || start >= end || end >= len(b.lines) {
                return
        }
        l := string(b.LineBytes(end))
@@ -749,7 +987,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) {
@@ -779,9 +1017,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
                                        }
                                }
                        }
@@ -803,9 +1041,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++
@@ -813,7 +1051,7 @@ 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
@@ -828,14 +1066,15 @@ func (b *Buffer) Retab() {
                ws := util.GetLeadingWhitespace(l)
                if len(ws) != 0 {
                        if toSpaces {
-                               ws = bytes.Replace(ws, []byte{'\t'}, bytes.Repeat([]byte{' '}, tabsize), -1)
+                               ws = bytes.ReplaceAll(ws, []byte{'\t'}, bytes.Repeat([]byte{' '}, tabsize))
                        } else {
-                               ws = bytes.Replace(ws, bytes.Repeat([]byte{' '}, tabsize), []byte{'\t'}, -1)
+                               ws = bytes.ReplaceAll(ws, bytes.Repeat([]byte{' '}, tabsize), []byte{'\t'})
                        }
                }
 
                l = bytes.TrimLeft(l, " \t")
                b.lines[i].data = append(ws, l...)
+               b.MarkModified(i, i)
                dirty = true
        }
 
@@ -854,12 +1093,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--
                        }
                }
        }
@@ -872,7 +1111,118 @@ 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]
+}
+
+// SearchMatch returns true if the given location is within a match of the last search.
+// It is used for search highlighting
+func (b *Buffer) SearchMatch(pos Loc) bool {
+       return b.LineArray.SearchMatch(b, pos)
+}
+
 // 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
+}