]> git.lizzy.rs Git - micro.git/blobdiff - internal/buffer/buffer.go
Support csharp-script syntax. (#1425)
[micro.git] / internal / buffer / buffer.go
index 16bc28f794e1129d0c9ce33e36dfd7d94f90838f..fc74efe5b9b1cede591db6507b4fb94c6961c0a3 100644 (file)
@@ -1,20 +1,25 @@
 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"
@@ -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
+
+       // 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
@@ -89,32 +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
-
-       // counts the number of edits
-       // resets every backupTime edits
-       lastbackup time.Time
 }
 
 // NewBufferFromFile opens a new buffer using the given path
@@ -133,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()
@@ -169,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 {
@@ -196,28 +255,44 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
                }
        }
 
-       b.Path = path
-       b.AbsPath = absPath
-
        if !found {
                b.SharedBuffer = new(SharedBuffer)
                b.Type = btype
 
+               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
        }
 
-       // The last time this file was modified
-       b.UpdateModTime()
-
        switch b.Endings {
        case FFUnix:
                b.Settings["fileformat"] = "unix"
@@ -226,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 {
@@ -246,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
@@ -255,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)
        }
@@ -285,18 +361,26 @@ func (b *Buffer) Fini() {
                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
+       }
+       if b.Settings["basename"].(bool) {
+               return path.Base(name)
        }
-       return b.name
+       return name
 }
 
 //SetName changes the name for this buffer
@@ -304,6 +388,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 +399,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
@@ -357,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)
 
@@ -367,11 +453,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()
@@ -453,9 +543,42 @@ 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)
+               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 +603,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 +668,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()
+                       }()
                }
        }
 }
@@ -749,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) {
@@ -779,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
                                        }
                                }
                        }
@@ -803,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++
@@ -813,7 +920,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
@@ -836,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
        }
 
@@ -854,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--
                        }
                }
        }
@@ -872,7 +980,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
+}