17 luar "layeh.com/gopher-luar"
19 "github.com/zyedidia/micro/internal/config"
20 ulua "github.com/zyedidia/micro/internal/lua"
21 "github.com/zyedidia/micro/internal/screen"
22 "github.com/zyedidia/micro/internal/util"
23 "github.com/zyedidia/micro/pkg/highlight"
24 "golang.org/x/text/encoding/htmlindex"
25 "golang.org/x/text/encoding/unicode"
26 "golang.org/x/text/transform"
27 dmp "github.com/sergi/go-diff/diffmatchpatch"
30 const backupTime = 8000
33 // OpenBuffers is a list of the currently open buffers
35 // LogBuf is a reference to the log buffer which can be opened with the
40 // The BufType defines what kind of buffer this is
43 Readonly bool // The buffer cannot be edited
44 Scratch bool // The buffer cannot be saved
45 Syntax bool // Syntax highlighting is enabled
49 // BTDefault is a default buffer
50 BTDefault = BufType{0, false, false, true}
51 // BTHelp is a help buffer
52 BTHelp = BufType{1, true, true, true}
53 // BTLog is a log buffer
54 BTLog = BufType{2, true, true, false}
55 // BTScratch is a buffer that cannot be saved (for scratch work)
56 BTScratch = BufType{3, false, true, false}
57 // BTRaw is is a buffer that shows raw terminal events
58 BTRaw = BufType{4, false, true, false}
59 // BTInfo is a buffer for inputting information
60 BTInfo = BufType{5, false, true, false}
62 // ErrFileTooLarge is returned when the file is too large to hash
63 // (fastdirty is automatically enabled)
64 ErrFileTooLarge = errors.New("File is too large to hash")
67 // SharedBuffer is a struct containing info that is shared among buffers
68 // that have the same file open
69 type SharedBuffer struct {
71 // Stores the last modification time of the file the buffer is pointing to
73 // Type of the buffer (e.g. help, raw, scratch etc..)
77 // Whether or not suggestions can be autocompleted must be shared because
78 // it changes based on how the buffer has changed
81 // Modifications is the list of modified regions for syntax highlighting
85 func (b *SharedBuffer) insert(pos Loc, value []byte) {
87 b.HasSuggestions = false
88 b.LineArray.insert(pos, value)
90 // b.Modifications is cleared every screen redraw so it's
91 // ok to append duplicates
92 b.Modifications = append(b.Modifications, Loc{pos.Y, pos.Y + bytes.Count(value, []byte{'\n'})})
94 func (b *SharedBuffer) remove(start, end Loc) []byte {
96 b.HasSuggestions = false
97 b.Modifications = append(b.Modifications, Loc{start.Y, start.Y})
98 return b.LineArray.remove(start, end)
110 // Buffer stores the main information about a currently open file including
111 // the actual text (in a LineArray), the undo/redo stack (in an EventHandler)
112 // all the cursors, the syntax highlighting info, the settings for the buffer
113 // and some misc info about modification time and path location.
114 // The syntax highlighting info must be stored with the buffer because the syntax
115 // highlighter attaches information to each line of the buffer for optimization
116 // purposes so it doesn't have to rehighlight everything on every update.
125 // Path to the file on disk
127 // Absolute path to the file on disk
129 // Name of the buffer on the status line
132 // SyntaxDef represents the syntax highlighting definition being used
133 // This stores the highlighting rules and filetype detection info
134 SyntaxDef *highlight.Def
135 // The Highlighter struct actually performs the highlighting
136 Highlighter *highlight.Highlighter
137 HighlightLock sync.Mutex
139 // Hash of the original buffer -- empty if fastdirty is on
140 origHash [md5.Size]byte
142 // Settings customized by the user
143 Settings map[string]interface{}
151 updateDiffTimer *time.Timer
153 diffBaseLineCount int
154 diffLock sync.RWMutex
155 diff map[int]DiffStatus
157 // counts the number of edits
158 // resets every backupTime edits
162 // NewBufferFromFile opens a new buffer using the given path
163 // It will also automatically handle `~`, and line/column with filename:l:c
164 // It will return an empty buffer if the path does not exist
165 // and an error if the file is a directory
166 func NewBufferFromFile(path string, btype BufType) (*Buffer, error) {
168 filename, cursorPos := util.GetPathAndCursorPosition(path)
169 filename, err = util.ReplaceHome(filename)
174 file, err := os.Open(filename)
175 fileInfo, _ := os.Stat(filename)
177 if err == nil && fileInfo.IsDir() {
178 return nil, errors.New("Error: " + filename + " is a directory and cannot be opened")
183 cursorLoc, cursorerr := ParseCursorLocation(cursorPos)
184 if cursorerr != nil {
185 cursorLoc = Loc{-1, -1}
190 // File does not exist -- create an empty buffer with that name
191 buf = NewBufferFromString("", filename, btype)
193 buf = NewBuffer(file, util.FSize(file), filename, cursorLoc, btype)
199 // NewBufferFromString creates a new buffer containing the given string
200 func NewBufferFromString(text, path string, btype BufType) *Buffer {
201 return NewBuffer(strings.NewReader(text), int64(len(text)), path, Loc{-1, -1}, btype)
204 // NewBuffer creates a new buffer from a given reader with a given path
205 // Ensure that ReadSettings and InitGlobalSettings have been called before creating
207 // Places the cursor at startcursor. If startcursor is -1, -1 places the
208 // cursor at an autodetected location (based on savecursor or :LINE:COL)
209 func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufType) *Buffer {
210 absPath, _ := filepath.Abs(path)
214 b.Settings = config.DefaultCommonSettings()
215 for k, v := range config.GlobalSettings {
216 if _, ok := b.Settings[k]; ok {
220 config.InitLocalSettings(b.Settings, path)
222 enc, err := htmlindex.Get(b.Settings["encoding"].(string))
225 b.Settings["encoding"] = "utf-8"
228 reader := transform.NewReader(r, enc.NewDecoder())
232 for _, buf := range OpenBuffers {
233 if buf.AbsPath == absPath && buf.Type != BTInfo {
235 b.SharedBuffer = buf.SharedBuffer
236 b.EventHandler = buf.EventHandler
245 b.SharedBuffer = new(SharedBuffer)
248 hasBackup := b.ApplyBackup(size)
251 b.LineArray = NewLineArray(uint64(size), FFAuto, reader)
253 b.EventHandler = NewEventHandler(b.SharedBuffer, b.cursors)
256 if b.Settings["readonly"].(bool) && b.Type == BTDefault {
257 b.Type.Readonly = true
260 // The last time this file was modified
265 b.Settings["fileformat"] = "unix"
267 b.Settings["fileformat"] = "dos"
271 config.InitLocalSettings(b.Settings, b.Path)
273 if _, err := os.Stat(config.ConfigDir + "/buffers/"); os.IsNotExist(err) {
274 os.Mkdir(config.ConfigDir+"/buffers/", os.ModePerm)
277 if startcursor.X != -1 && startcursor.Y != -1 {
278 b.StartCursor = startcursor
280 if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
281 err := b.Unserialize()
283 screen.TermMessage(err)
288 b.AddCursor(NewCursor(b, b.StartCursor))
289 b.GetActiveCursor().Relocate()
291 if !b.Settings["fastdirty"].(bool) {
292 if size > LargeFileThreshold {
293 // If the file is larger than LargeFileThreshold fastdirty needs to be on
294 b.Settings["fastdirty"] = true
296 calcHash(b, &b.origHash)
300 err = config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b))
302 screen.TermMessage(err)
305 b.Modifications = make([]Loc, 0, 10)
307 OpenBuffers = append(OpenBuffers, b)
312 // Close removes this buffer from the list of open buffers
313 func (b *Buffer) Close() {
314 for i, buf := range OpenBuffers {
317 copy(OpenBuffers[i:], OpenBuffers[i+1:])
318 OpenBuffers[len(OpenBuffers)-1] = nil
319 OpenBuffers = OpenBuffers[:len(OpenBuffers)-1]
325 // Fini should be called when a buffer is closed and performs
327 func (b *Buffer) Fini() {
334 // GetName returns the name that should be displayed in the statusline
336 func (b *Buffer) GetName() string {
346 //SetName changes the name for this buffer
347 func (b *Buffer) SetName(s string) {
351 // Insert inserts the given string of text at the start location
352 func (b *Buffer) Insert(start Loc, text string) {
353 if !b.Type.Readonly {
354 b.EventHandler.cursors = b.cursors
355 b.EventHandler.active = b.curCursor
356 b.EventHandler.Insert(start, text)
362 // Remove removes the characters between the start and end locations
363 func (b *Buffer) Remove(start, end Loc) {
364 if !b.Type.Readonly {
365 b.EventHandler.cursors = b.cursors
366 b.EventHandler.active = b.curCursor
367 b.EventHandler.Remove(start, end)
373 // ClearModifications clears the list of modified lines in this buffer
374 // The list of modified lines is used for syntax highlighting so that
375 // we can selectively highlight only the necessary lines
376 // This function should be called every time this buffer is drawn to
378 func (b *Buffer) ClearModifications() {
379 // clear slice without resetting the cap
380 b.Modifications = b.Modifications[:0]
383 // FileType returns the buffer's filetype
384 func (b *Buffer) FileType() string {
385 return b.Settings["filetype"].(string)
388 // ExternallyModified returns whether the file being edited has
389 // been modified by some external process
390 func (b *Buffer) ExternallyModified() bool {
391 modTime, err := util.GetModTime(b.Path)
393 return modTime != b.ModTime
398 // UpdateModTime updates the modtime of this file
399 func (b *Buffer) UpdateModTime() (err error) {
400 b.ModTime, err = util.GetModTime(b.Path)
404 // ReOpen reloads the current buffer from disk
405 func (b *Buffer) ReOpen() error {
406 file, err := os.Open(b.Path)
411 enc, err := htmlindex.Get(b.Settings["encoding"].(string))
416 reader := transform.NewReader(file, enc.NewDecoder())
417 data, err := ioutil.ReadAll(reader)
423 b.EventHandler.ApplyDiff(txt)
425 err = b.UpdateModTime()
431 // RelocateCursors relocates all cursors (makes sure they are in the buffer)
432 func (b *Buffer) RelocateCursors() {
433 for _, c := range b.cursors {
438 // RuneAt returns the rune at a given location in the buffer
439 func (b *Buffer) RuneAt(loc Loc) rune {
440 line := b.LineBytes(loc.Y)
444 r, size := utf8.DecodeRune(line)
456 // Modified returns if this buffer has been modified since
458 func (b *Buffer) Modified() bool {
463 if b.Settings["fastdirty"].(bool) {
467 var buff [md5.Size]byte
470 return buff != b.origHash
473 // calcHash calculates md5 hash of all lines in the buffer
474 func calcHash(b *Buffer, out *[md5.Size]byte) error {
478 if len(b.lines) > 0 {
479 n, e := h.Write(b.lines[0].data)
485 for _, l := range b.lines[1:] {
486 n, e = h.Write([]byte{'\n'})
491 n, e = h.Write(l.data)
499 if size > LargeFileThreshold {
500 return ErrFileTooLarge
507 // UpdateRules updates the syntax rules and filetype for this buffer
508 // This is called when the colorscheme changes
509 func (b *Buffer) UpdateRules() {
513 ft := b.Settings["filetype"].(string)
518 var header *highlight.Header
519 for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
520 data, err := f.Data()
522 screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error())
526 header, err = highlight.MakeHeader(data)
528 screen.TermMessage("Error reading syntax header file", f.Name(), err)
532 if ft == "unknown" || ft == "" {
533 if highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data) {
534 syntaxFile = f.Name()
537 } else if header.FileType == ft {
538 syntaxFile = f.Name()
543 if syntaxFile == "" {
544 // search for the syntax file in the user's custom syntax files
545 for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
546 data, err := f.Data()
548 screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
552 header, err = highlight.MakeHeaderYaml(data)
553 file, err := highlight.ParseFile(data)
555 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
559 if (ft == "unknown" || ft == "" && highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data)) || header.FileType == ft {
560 syndef, err := highlight.ParseDef(file, header)
562 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
570 for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
571 if f.Name() == syntaxFile {
572 data, err := f.Data()
574 screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
578 file, err := highlight.ParseFile(data)
580 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
584 syndef, err := highlight.ParseDef(file, header)
586 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
595 if b.SyntaxDef != nil && highlight.HasIncludes(b.SyntaxDef) {
596 includes := highlight.GetIncludes(b.SyntaxDef)
598 var files []*highlight.File
599 for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
600 data, err := f.Data()
602 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
605 header, err := highlight.MakeHeaderYaml(data)
607 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
611 for _, i := range includes {
612 if header.FileType == i {
613 file, err := highlight.ParseFile(data)
615 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
618 files = append(files, file)
622 if len(files) >= len(includes) {
627 highlight.ResolveIncludes(b.SyntaxDef, files)
630 if b.Highlighter == nil || syntaxFile != "" {
631 if b.SyntaxDef != nil {
632 b.Settings["filetype"] = b.SyntaxDef.FileType
635 b.SyntaxDef = &highlight.EmptyDef
638 if b.SyntaxDef != nil {
639 b.Highlighter = highlight.NewHighlighter(b.SyntaxDef)
640 if b.Settings["syntax"].(bool) {
642 b.Highlighter.HighlightStates(b)
643 b.Highlighter.HighlightMatches(b, 0, b.End().Y)
644 screen.DrawChan <- true
650 // ClearMatches clears all of the syntax highlighting for the buffer
651 func (b *Buffer) ClearMatches() {
652 for i := range b.lines {
658 // IndentString returns this buffer's indent method (a tabstop or n spaces
659 // depending on the settings)
660 func (b *Buffer) IndentString(tabsize int) string {
661 if b.Settings["tabstospaces"].(bool) {
662 return util.Spaces(tabsize)
667 // SetCursors resets this buffer's cursors to a new list
668 func (b *Buffer) SetCursors(c []*Cursor) {
670 b.EventHandler.cursors = b.cursors
671 b.EventHandler.active = b.curCursor
674 // AddCursor adds a new cursor to the list
675 func (b *Buffer) AddCursor(c *Cursor) {
676 b.cursors = append(b.cursors, c)
677 b.EventHandler.cursors = b.cursors
678 b.EventHandler.active = b.curCursor
682 // SetCurCursor sets the current cursor
683 func (b *Buffer) SetCurCursor(n int) {
687 // GetActiveCursor returns the main cursor in this buffer
688 func (b *Buffer) GetActiveCursor() *Cursor {
689 return b.cursors[b.curCursor]
692 // GetCursor returns the nth cursor
693 func (b *Buffer) GetCursor(n int) *Cursor {
697 // GetCursors returns the list of cursors in this buffer
698 func (b *Buffer) GetCursors() []*Cursor {
702 // NumCursors returns the number of cursors
703 func (b *Buffer) NumCursors() int {
704 return len(b.cursors)
707 // MergeCursors merges any cursors that are at the same position
709 func (b *Buffer) MergeCursors() {
710 var cursors []*Cursor
711 for i := 0; i < len(b.cursors); i++ {
714 for j := 0; j < len(b.cursors); j++ {
716 if c2 != nil && i != j && c1.Loc == c2.Loc {
720 cursors = append(cursors, c1)
726 for i := range b.cursors {
730 if b.curCursor >= len(b.cursors) {
731 b.curCursor = len(b.cursors) - 1
733 b.EventHandler.cursors = b.cursors
734 b.EventHandler.active = b.curCursor
737 // UpdateCursors updates all the cursors indicies
738 func (b *Buffer) UpdateCursors() {
739 b.EventHandler.cursors = b.cursors
740 b.EventHandler.active = b.curCursor
741 for i, c := range b.cursors {
746 func (b *Buffer) RemoveCursor(i int) {
747 copy(b.cursors[i:], b.cursors[i+1:])
748 b.cursors[len(b.cursors)-1] = nil
749 b.cursors = b.cursors[:len(b.cursors)-1]
750 b.curCursor = util.Clamp(b.curCursor, 0, len(b.cursors)-1)
754 // ClearCursors removes all extra cursors
755 func (b *Buffer) ClearCursors() {
756 for i := 1; i < len(b.cursors); i++ {
759 b.cursors = b.cursors[:1]
762 b.GetActiveCursor().ResetSelection()
765 // MoveLinesUp moves the range of lines up one row
766 func (b *Buffer) MoveLinesUp(start int, end int) {
767 if start < 1 || start >= end || end > len(b.lines) {
770 l := string(b.LineBytes(start - 1))
771 if end == len(b.lines) {
774 utf8.RuneCount(b.lines[end-1].data),
791 // MoveLinesDown moves the range of lines down one row
792 func (b *Buffer) MoveLinesDown(start int, end int) {
793 if start < 0 || start >= end || end >= len(b.lines)-1 {
796 l := string(b.LineBytes(end))
808 var BracePairs = [][2]rune{
814 // FindMatchingBrace returns the location in the buffer of the matching bracket
815 // It is given a brace type containing the open and closing character, (for example
816 // '{' and '}') as well as the location to match from
817 // TODO: maybe can be more efficient with utf8 package
818 // returns the location of the matching brace
819 // if the boolean returned is true then the original matching brace is one character left
820 // of the starting location
821 func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool) {
822 curLine := []rune(string(b.LineBytes(start.Y)))
824 if start.X >= 0 && start.X < len(curLine) {
825 startChar = curLine[start.X]
828 if start.X-1 >= 0 && start.X-1 < len(curLine) {
829 leftChar = curLine[start.X-1]
832 if startChar == braceType[0] || leftChar == braceType[0] {
833 for y := start.Y; y < b.LinesNum(); y++ {
834 l := []rune(string(b.LineBytes(y)))
837 if startChar == braceType[0] {
843 for x := xInit; x < len(l); x++ {
845 if r == braceType[0] {
847 } else if r == braceType[1] {
850 if startChar == braceType[0] {
851 return Loc{x, y}, false
853 return Loc{x, y}, true
858 } else if startChar == braceType[1] || leftChar == braceType[1] {
859 for y := start.Y; y >= 0; y-- {
860 l := []rune(string(b.lines[y].data))
863 if leftChar == braceType[1] {
869 for x := xInit; x >= 0; x-- {
871 if r == braceType[0] {
874 if leftChar == braceType[1] {
875 return Loc{x, y}, true
877 return Loc{x, y}, false
879 } else if r == braceType[1] {
888 // Retab changes all tabs to spaces or vice versa
889 func (b *Buffer) Retab() {
890 toSpaces := b.Settings["tabstospaces"].(bool)
891 tabsize := util.IntOpt(b.Settings["tabsize"])
894 for i := 0; i < b.LinesNum(); i++ {
897 ws := util.GetLeadingWhitespace(l)
900 ws = bytes.Replace(ws, []byte{'\t'}, bytes.Repeat([]byte{' '}, tabsize), -1)
902 ws = bytes.Replace(ws, bytes.Repeat([]byte{' '}, tabsize), []byte{'\t'}, -1)
906 l = bytes.TrimLeft(l, " \t")
907 b.lines[i].data = append(ws, l...)
914 // ParseCursorLocation turns a cursor location like 10:5 (LINE:COL)
916 func ParseCursorLocation(cursorPositions []string) (Loc, error) {
917 startpos := Loc{0, 0}
920 // if no positions are available exit early
921 if cursorPositions == nil {
922 return startpos, errors.New("No cursor positions were provided.")
925 startpos.Y, err = strconv.Atoi(cursorPositions[0])
928 if len(cursorPositions) > 1 {
929 startpos.X, err = strconv.Atoi(cursorPositions[1])
939 // Line returns the string representation of the given line number
940 func (b *Buffer) Line(i int) string {
941 return string(b.LineBytes(i))
944 func (b *Buffer) Write(bytes []byte) (n int, err error) {
945 b.EventHandler.InsertBytes(b.End(), bytes)
946 return len(bytes), nil
949 func (b *Buffer) updateDiffSync() {
951 defer b.diffLock.Unlock()
953 b.diff = make(map[int]DiffStatus)
955 if b.diffBase == nil {
960 baseRunes, bufferRunes, _ := differ.DiffLinesToRunes(string(b.diffBase), string(b.Bytes()))
961 diffs := differ.DiffMainRunes(baseRunes, bufferRunes, false)
964 for _, diff := range diffs {
965 lineCount := len([]rune(diff.Text))
971 var status DiffStatus
972 if b.diff[lineN] == DSDeletedAbove {
977 for i := 0; i < lineCount; i++ {
978 b.diff[lineN] = status
982 b.diff[lineN] = DSDeletedAbove
987 // UpdateDiff computes the diff between the diff base and the buffer content.
988 // The update may be performed synchronously or asynchronously.
989 // UpdateDiff calls the supplied callback when the update is complete.
990 // The argument passed to the callback is set to true if and only if
991 // the update was performed synchronously.
992 // If an asynchronous update is already pending when UpdateDiff is called,
993 // UpdateDiff does not schedule another update, in which case the callback
995 func (b *Buffer) UpdateDiff(callback func(bool)) {
996 if b.updateDiffTimer != nil {
1000 lineCount := b.LinesNum()
1001 if b.diffBaseLineCount > lineCount {
1002 lineCount = b.diffBaseLineCount
1005 if lineCount < 1000 {
1008 } else if lineCount < 30000 {
1009 b.updateDiffTimer = time.AfterFunc(500*time.Millisecond, func() {
1010 b.updateDiffTimer = nil
1015 // Don't compute diffs for very large files
1017 b.diff = make(map[int]DiffStatus)
1023 // SetDiffBase sets the text that is used as the base for diffing the buffer content
1024 func (b *Buffer) SetDiffBase(diffBase []byte) {
1025 b.diffBase = diffBase
1026 if diffBase == nil {
1027 b.diffBaseLineCount = 0
1029 b.diffBaseLineCount = strings.Count(string(diffBase), "\n")
1031 b.UpdateDiff(func(synchronous bool) {
1032 screen.DrawChan <- true
1036 // DiffStatus returns the diff status for a line in the buffer
1037 func (b *Buffer) DiffStatus(lineN int) DiffStatus {
1039 defer b.diffLock.RUnlock()
1040 // Note that the zero value for DiffStatus is equal to DSUnchanged
1041 return b.diff[lineN]
1044 // WriteLog writes a string to the log buffer
1045 func WriteLog(s string) {
1046 LogBuf.EventHandler.Insert(LogBuf.End(), s)
1049 // GetLogBuf returns the log buffer
1050 func GetLogBuf() *Buffer {