package buffer
import (
+ "bufio"
"bytes"
"crypto/md5"
"errors"
+ "fmt"
"io"
"io/ioutil"
"os"
+ "path"
"path/filepath"
"strconv"
"strings"
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"
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)
// 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
- // Modifications is the list of modified regions for syntax highlighting
- Modifications []Loc
+ // 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.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'})})
+ 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
- b.Modifications = append(b.Modifications, Loc{start.Y, start.Y})
+ 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
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 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
-
- // 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
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 {
}
}
- 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) && 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"
}
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.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
}
}
- err = config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b))
+ err := config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b))
if err != nil {
screen.TermMessage(err)
}
- b.Modifications = make([]Loc, 0, 10)
-
OpenBuffers = append(OpenBuffers, b)
return b
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
}
}
-// 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)
return err
}
- reader := transform.NewReader(file, enc.NewDecoder())
+ reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder()))
data, err := ioutil.ReadAll(reader)
txt := string(data)
b.EventHandler.ApplyDiff(txt)
err = b.UpdateModTime()
+ if !b.Settings["fastdirty"].(bool) {
+ calcHash(b, &b.origHash)
+ }
b.isModified = false
b.RelocateCursors()
return err
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 {
}
}
- 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()
go func() {
b.Highlighter.HighlightStates(b)
b.Highlighter.HighlightMatches(b, 0, b.End().Y)
- screen.DrawChan <- true
+ screen.Redraw()
}()
}
}
// 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) {
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
}
}
}
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++
}
}
}
- return start, true
+ return start, true, false
}
// Retab changes all tabs to spaces or vice versa
l = bytes.TrimLeft(l, " \t")
b.lines[i].data = append(ws, l...)
+ b.MarkModified(i, i)
dirty = true
}
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)