]> git.lizzy.rs Git - micro.git/blobdiff - internal/buffer/buffer.go
Don't block when redraw channel becomes full
[micro.git] / internal / buffer / buffer.go
index 16bc28f794e1129d0c9ce33e36dfd7d94f90838f..c3b8fa85ef7812b5231eee363dcc17fedd6f6643 100644 (file)
@@ -1,20 +1,24 @@
 package buffer
 
 import (
+       "bufio"
        "bytes"
        "crypto/md5"
        "errors"
        "io"
        "io/ioutil"
+       "log"
        "os"
        "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"
@@ -28,8 +32,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 +48,26 @@ 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}
 
+       // 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
@@ -62,19 +79,36 @@ type SharedBuffer struct {
        // Whether or not suggestions can be autocompleted must be shared because
        // it changes based on how the buffer has changed
        HasSuggestions bool
+
+       // Modifications is the list of modified regions for syntax highlighting
+       Modifications []Loc
 }
 
 func (b *SharedBuffer) insert(pos Loc, value []byte) {
        b.isModified = true
        b.HasSuggestions = false
        b.LineArray.insert(pos, value)
+
+       // b.Modifications is cleared every screen redraw so it's
+       // ok to append duplicates
+       b.Modifications = append(b.Modifications, Loc{pos.Y, pos.Y + bytes.Count(value, []byte{'\n'})})
 }
 func (b *SharedBuffer) remove(start, end Loc) []byte {
        b.isModified = true
        b.HasSuggestions = false
+       b.Modifications = append(b.Modifications, Loc{start.Y, start.Y})
        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
@@ -97,8 +131,12 @@ type Buffer struct {
        // Name of the buffer on the status line
        name string
 
-       SyntaxDef   *highlight.Def
-       Highlighter *highlight.Highlighter
+       // SyntaxDef represents the syntax highlighting definition being used
+       // This stores the highlighting rules and filetype detection info
+       SyntaxDef *highlight.Def
+       // The Highlighter struct actually performs the highlighting
+       Highlighter   *highlight.Highlighter
+       HighlightLock sync.Mutex
 
        // Hash of the original buffer -- empty if fastdirty is on
        origHash [md5.Size]byte
@@ -112,6 +150,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
@@ -133,7 +177,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()
@@ -183,7 +227,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
                b.Settings["encoding"] = "utf-8"
        }
 
-       reader := transform.NewReader(r, enc.NewDecoder())
+       reader := bufio.NewReader(transform.NewReader(r, enc.NewDecoder()))
 
        found := false
        if len(path) > 0 {
@@ -211,7 +255,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
                b.EventHandler = NewEventHandler(b.SharedBuffer, b.cursors)
        }
 
-       if b.Settings["readonly"].(bool) {
+       if b.Settings["readonly"].(bool) && b.Type == BTDefault {
                b.Type.Readonly = true
        }
 
@@ -228,8 +272,8 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
        b.UpdateRules()
        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 {
@@ -260,6 +304,8 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
                screen.TermMessage(err)
        }
 
+       b.Modifications = make([]Loc, 0, 10)
+
        OpenBuffers = append(OpenBuffers, b)
 
        return b
@@ -304,6 +350,7 @@ 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
@@ -314,6 +361,7 @@ func (b *Buffer) Insert(start Loc, text string) {
        }
 }
 
+// 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
@@ -324,6 +372,16 @@ func (b *Buffer) Remove(start, end Loc) {
        }
 }
 
+// ClearModifications clears the list of modified lines in this buffer
+// The list of modified lines is used for syntax highlighting so that
+// we can selectively highlight only the necessary lines
+// This function should be called every time this buffer is drawn to
+// the screen
+func (b *Buffer) ClearModifications() {
+       // clear slice without resetting the cap
+       b.Modifications = b.Modifications[:0]
+}
+
 // FileType returns the buffer's filetype
 func (b *Buffer) FileType() string {
        return b.Settings["filetype"].(string)
@@ -357,7 +415,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)
 
@@ -372,6 +430,7 @@ func (b *Buffer) ReOpen() error {
        return err
 }
 
+// RelocateCursors relocates all cursors (makes sure they are in the buffer)
 func (b *Buffer) RelocateCursors() {
        for _, c := range b.cursors {
                c.Relocate()
@@ -453,8 +512,11 @@ func (b *Buffer) UpdateRules() {
        if !b.Type.Syntax {
                return
        }
-       syntaxFile := ""
        ft := b.Settings["filetype"].(string)
+       if ft == "off" {
+               return
+       }
+       syntaxFile := ""
        var header *highlight.Header
        for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
                data, err := f.Data()
@@ -483,6 +545,7 @@ 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) {
+                       log.Println("real runtime file", f.Name())
                        data, err := f.Data()
                        if err != nil {
                                screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
@@ -496,7 +559,7 @@ func (b *Buffer) UpdateRules() {
                                continue
                        }
 
-                       if (ft == "unknown" || ft == "" && highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data)) || header.FileType == ft {
+                       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())
@@ -570,10 +633,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()
+                       }()
                }
        }
 }
@@ -836,6 +908,7 @@ func (b *Buffer) Retab() {
 
                l = bytes.TrimLeft(l, " \t")
                b.lines[i].data = append(ws, l...)
+               b.Modifications = append(b.Modifications, Loc{i, i})
                dirty = true
        }
 
@@ -854,12 +927,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 +945,112 @@ 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
+}