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"
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
}
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
// 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
// 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
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
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 {
b.EventHandler = NewEventHandler(b.SharedBuffer, b.cursors)
}
- if b.Settings["readonly"].(bool) {
+ if b.Settings["readonly"].(bool) && b.Type == BTDefault {
b.Type.Readonly = true
}
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 {
screen.TermMessage(err)
}
+ b.Modifications = make([]Loc, 0, 10)
+
OpenBuffers = append(OpenBuffers, b)
return b
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
}
}
+// 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
}
}
+// 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)
return err
}
+// RelocateCursors relocates all cursors (makes sure they are in the buffer)
func (b *Buffer) RelocateCursors() {
for _, c := range b.cursors {
c.Relocate()
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())
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())
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()
+ }()
}
}
}
l = bytes.TrimLeft(l, " \t")
b.lines[i].data = append(ws, l...)
+ b.Modifications = append(b.Modifications, Loc{i, i})
dirty = true
}
}
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--
}
}
}
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
+}