]> git.lizzy.rs Git - micro.git/blob - internal/buffer/buffer.go
56302e9dc5f3404575400e5d78b1ee4f414ae1f4
[micro.git] / internal / buffer / buffer.go
1 package buffer
2
3 import (
4         "bufio"
5         "bytes"
6         "crypto/md5"
7         "errors"
8         "fmt"
9         "io"
10         "io/ioutil"
11         "os"
12         "path"
13         "path/filepath"
14         "strconv"
15         "strings"
16         "sync"
17         "sync/atomic"
18         "time"
19
20         luar "layeh.com/gopher-luar"
21
22         dmp "github.com/sergi/go-diff/diffmatchpatch"
23         "github.com/zyedidia/micro/v2/internal/config"
24         ulua "github.com/zyedidia/micro/v2/internal/lua"
25         "github.com/zyedidia/micro/v2/internal/screen"
26         "github.com/zyedidia/micro/v2/internal/util"
27         "github.com/zyedidia/micro/v2/pkg/highlight"
28         "golang.org/x/text/encoding/htmlindex"
29         "golang.org/x/text/encoding/unicode"
30         "golang.org/x/text/transform"
31 )
32
33 const backupTime = 8000
34
35 var (
36         // OpenBuffers is a list of the currently open buffers
37         OpenBuffers []*Buffer
38         // LogBuf is a reference to the log buffer which can be opened with the
39         // `> log` command
40         LogBuf *Buffer
41 )
42
43 // The BufType defines what kind of buffer this is
44 type BufType struct {
45         Kind     int
46         Readonly bool // The buffer cannot be edited
47         Scratch  bool // The buffer cannot be saved
48         Syntax   bool // Syntax highlighting is enabled
49 }
50
51 var (
52         // BTDefault is a default buffer
53         BTDefault = BufType{0, false, false, true}
54         // BTHelp is a help buffer
55         BTHelp = BufType{1, true, true, true}
56         // BTLog is a log buffer
57         BTLog = BufType{2, true, true, false}
58         // BTScratch is a buffer that cannot be saved (for scratch work)
59         BTScratch = BufType{3, false, true, false}
60         // BTRaw is is a buffer that shows raw terminal events
61         BTRaw = BufType{4, false, true, false}
62         // BTInfo is a buffer for inputting information
63         BTInfo = BufType{5, false, true, false}
64         // BTStdout is a buffer that only writes to stdout
65         // when closed
66         BTStdout = BufType{6, false, true, true}
67
68         // ErrFileTooLarge is returned when the file is too large to hash
69         // (fastdirty is automatically enabled)
70         ErrFileTooLarge = errors.New("File is too large to hash")
71 )
72
73 // SharedBuffer is a struct containing info that is shared among buffers
74 // that have the same file open
75 type SharedBuffer struct {
76         *LineArray
77         // Stores the last modification time of the file the buffer is pointing to
78         ModTime time.Time
79         // Type of the buffer (e.g. help, raw, scratch etc..)
80         Type BufType
81
82         // Path to the file on disk
83         Path string
84         // Absolute path to the file on disk
85         AbsPath string
86         // Name of the buffer on the status line
87         name string
88
89         toStdout bool
90
91         // Settings customized by the user
92         Settings map[string]interface{}
93
94         Suggestions   []string
95         Completions   []string
96         CurSuggestion int
97
98         Messages []*Message
99
100         updateDiffTimer   *time.Timer
101         diffBase          []byte
102         diffBaseLineCount int
103         diffLock          sync.RWMutex
104         diff              map[int]DiffStatus
105
106         requestedBackup bool
107
108         // ReloadDisabled allows the user to disable reloads if they
109         // are viewing a file that is constantly changing
110         ReloadDisabled bool
111
112         isModified bool
113         // Whether or not suggestions can be autocompleted must be shared because
114         // it changes based on how the buffer has changed
115         HasSuggestions bool
116
117         // The Highlighter struct actually performs the highlighting
118         Highlighter *highlight.Highlighter
119         // SyntaxDef represents the syntax highlighting definition being used
120         // This stores the highlighting rules and filetype detection info
121         SyntaxDef *highlight.Def
122
123         ModifiedThisFrame bool
124
125         // Hash of the original buffer -- empty if fastdirty is on
126         origHash [md5.Size]byte
127 }
128
129 func (b *SharedBuffer) insert(pos Loc, value []byte) {
130         b.isModified = true
131         b.HasSuggestions = false
132         b.LineArray.insert(pos, value)
133
134         inslines := bytes.Count(value, []byte{'\n'})
135         b.MarkModified(pos.Y, pos.Y+inslines)
136 }
137 func (b *SharedBuffer) remove(start, end Loc) []byte {
138         b.isModified = true
139         b.HasSuggestions = false
140         defer b.MarkModified(start.Y, end.Y)
141         return b.LineArray.remove(start, end)
142 }
143
144 // MarkModified marks the buffer as modified for this frame
145 // and performs rehighlighting if syntax highlighting is enabled
146 func (b *SharedBuffer) MarkModified(start, end int) {
147         b.ModifiedThisFrame = true
148
149         start = util.Clamp(start, 0, len(b.lines)-1)
150         end = util.Clamp(end, 0, len(b.lines)-1)
151
152         if b.Settings["syntax"].(bool) && b.SyntaxDef != nil {
153                 l := -1
154                 for i := start; i <= end; i++ {
155                         l = util.Max(b.Highlighter.ReHighlightStates(b, i), l)
156                 }
157                 b.Highlighter.HighlightMatches(b, start, l)
158         }
159
160         for i := start; i <= end; i++ {
161                 b.LineArray.invalidateSearchMatches(i)
162         }
163 }
164
165 // DisableReload disables future reloads of this sharedbuffer
166 func (b *SharedBuffer) DisableReload() {
167         b.ReloadDisabled = true
168 }
169
170 const (
171         DSUnchanged    = 0
172         DSAdded        = 1
173         DSModified     = 2
174         DSDeletedAbove = 3
175 )
176
177 type DiffStatus byte
178
179 // Buffer stores the main information about a currently open file including
180 // the actual text (in a LineArray), the undo/redo stack (in an EventHandler)
181 // all the cursors, the syntax highlighting info, the settings for the buffer
182 // and some misc info about modification time and path location.
183 // The syntax highlighting info must be stored with the buffer because the syntax
184 // highlighter attaches information to each line of the buffer for optimization
185 // purposes so it doesn't have to rehighlight everything on every update.
186 // Likewise for the search highlighting.
187 type Buffer struct {
188         *EventHandler
189         *SharedBuffer
190
191         fini        int32
192         cursors     []*Cursor
193         curCursor   int
194         StartCursor Loc
195
196         // OptionCallback is called after a buffer option value is changed.
197         // The display module registers its OptionCallback to ensure the buffer window
198         // is properly updated when needed. This is a workaround for the fact that
199         // the buffer module cannot directly call the display's API (it would mean
200         // a circular dependency between packages).
201         OptionCallback func(option string, nativeValue interface{})
202
203         // The display module registers its own GetVisualX function for getting
204         // the correct visual x location of a cursor when softwrap is used.
205         // This is hacky. Maybe it would be better to move all the visual x logic
206         // from buffer to display, but it would require rewriting a lot of code.
207         GetVisualX func(loc Loc) int
208
209         // Last search stores the last successful search
210         LastSearch      string
211         LastSearchRegex bool
212         // HighlightSearch enables highlighting all instances of the last successful search
213         HighlightSearch bool
214 }
215
216 // NewBufferFromFileAtLoc opens a new buffer with a given cursor location
217 // If cursorLoc is {-1, -1} the location does not overwrite what the cursor location
218 // would otherwise be (start of file, or saved cursor position if `savecursor` is
219 // enabled)
220 func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer, error) {
221         var err error
222         filename := path
223         if config.GetGlobalOption("parsecursor").(bool) && cursorLoc.X == -1 && cursorLoc.Y == -1 {
224                 var cursorPos []string
225                 filename, cursorPos = util.GetPathAndCursorPosition(filename)
226                 cursorLoc, err = ParseCursorLocation(cursorPos)
227                 if err != nil {
228                         cursorLoc = Loc{-1, -1}
229                 }
230         }
231
232         filename, err = util.ReplaceHome(filename)
233         if err != nil {
234                 return nil, err
235         }
236
237         f, err := os.OpenFile(filename, os.O_WRONLY, 0)
238         readonly := os.IsPermission(err)
239         f.Close()
240
241         fileInfo, serr := os.Stat(filename)
242         if serr != nil && !os.IsNotExist(serr) {
243                 return nil, serr
244         }
245         if serr == nil && fileInfo.IsDir() {
246                 return nil, errors.New("Error: " + filename + " is a directory and cannot be opened")
247         }
248
249         file, err := os.Open(filename)
250         if err == nil {
251                 defer file.Close()
252         }
253
254         var buf *Buffer
255         if os.IsNotExist(err) {
256                 // File does not exist -- create an empty buffer with that name
257                 buf = NewBufferFromString("", filename, btype)
258         } else if err != nil {
259                 return nil, err
260         } else {
261                 buf = NewBuffer(file, util.FSize(file), filename, cursorLoc, btype)
262                 if buf == nil {
263                         return nil, errors.New("could not open file")
264                 }
265         }
266
267         if readonly && prompt != nil {
268                 prompt.Message(fmt.Sprintf("Warning: file is readonly - %s will be attempted when saving", config.GlobalSettings["sucmd"].(string)))
269                 // buf.SetOptionNative("readonly", true)
270         }
271
272         return buf, nil
273 }
274
275 // NewBufferFromFile opens a new buffer using the given path
276 // It will also automatically handle `~`, and line/column with filename:l:c
277 // It will return an empty buffer if the path does not exist
278 // and an error if the file is a directory
279 func NewBufferFromFile(path string, btype BufType) (*Buffer, error) {
280         return NewBufferFromFileAtLoc(path, btype, Loc{-1, -1})
281 }
282
283 // NewBufferFromStringAtLoc creates a new buffer containing the given string with a cursor loc
284 func NewBufferFromStringAtLoc(text, path string, btype BufType, cursorLoc Loc) *Buffer {
285         return NewBuffer(strings.NewReader(text), int64(len(text)), path, cursorLoc, btype)
286 }
287
288 // NewBufferFromString creates a new buffer containing the given string
289 func NewBufferFromString(text, path string, btype BufType) *Buffer {
290         return NewBuffer(strings.NewReader(text), int64(len(text)), path, Loc{-1, -1}, btype)
291 }
292
293 // NewBuffer creates a new buffer from a given reader with a given path
294 // Ensure that ReadSettings and InitGlobalSettings have been called before creating
295 // a new buffer
296 // Places the cursor at startcursor. If startcursor is -1, -1 places the
297 // cursor at an autodetected location (based on savecursor or :LINE:COL)
298 func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufType) *Buffer {
299         absPath, err := filepath.Abs(path)
300         if err != nil {
301                 absPath = path
302         }
303
304         b := new(Buffer)
305
306         found := false
307         if len(path) > 0 {
308                 for _, buf := range OpenBuffers {
309                         if buf.AbsPath == absPath && buf.Type != BTInfo {
310                                 found = true
311                                 b.SharedBuffer = buf.SharedBuffer
312                                 b.EventHandler = buf.EventHandler
313                         }
314                 }
315         }
316
317         hasBackup := false
318         if !found {
319                 b.SharedBuffer = new(SharedBuffer)
320                 b.Type = btype
321
322                 b.AbsPath = absPath
323                 b.Path = path
324
325                 // this is a little messy since we need to know some settings to read
326                 // the file properly, but some settings depend on the filetype, which
327                 // we don't know until reading the file. We first read the settings
328                 // into a local variable and then use that to determine the encoding,
329                 // readonly, and fileformat necessary for reading the file and
330                 // assigning the filetype.
331                 settings := config.DefaultCommonSettings()
332                 b.Settings = config.DefaultCommonSettings()
333                 for k, v := range config.GlobalSettings {
334                         if _, ok := config.DefaultGlobalOnlySettings[k]; !ok {
335                                 // make sure setting is not global-only
336                                 settings[k] = v
337                                 b.Settings[k] = v
338                         }
339                 }
340                 config.InitLocalSettings(settings, absPath)
341                 b.Settings["readonly"] = settings["readonly"]
342                 b.Settings["filetype"] = settings["filetype"]
343                 b.Settings["syntax"] = settings["syntax"]
344
345                 enc, err := htmlindex.Get(settings["encoding"].(string))
346                 if err != nil {
347                         enc = unicode.UTF8
348                         b.Settings["encoding"] = "utf-8"
349                 }
350
351                 var ok bool
352                 hasBackup, ok = b.ApplyBackup(size)
353
354                 if !ok {
355                         return NewBufferFromString("", "", btype)
356                 }
357                 if !hasBackup {
358                         reader := bufio.NewReader(transform.NewReader(r, enc.NewDecoder()))
359
360                         var ff FileFormat = FFAuto
361
362                         if size == 0 {
363                                 // for empty files, use the fileformat setting instead of
364                                 // autodetection
365                                 switch settings["fileformat"] {
366                                 case "unix":
367                                         ff = FFUnix
368                                 case "dos":
369                                         ff = FFDos
370                                 }
371                         }
372
373                         b.LineArray = NewLineArray(uint64(size), ff, reader)
374                 }
375                 b.EventHandler = NewEventHandler(b.SharedBuffer, b.cursors)
376
377                 // The last time this file was modified
378                 b.UpdateModTime()
379         }
380
381         if b.Settings["readonly"].(bool) && b.Type == BTDefault {
382                 b.Type.Readonly = true
383         }
384
385         switch b.Endings {
386         case FFUnix:
387                 b.Settings["fileformat"] = "unix"
388         case FFDos:
389                 b.Settings["fileformat"] = "dos"
390         }
391
392         b.UpdateRules()
393         // init local settings again now that we know the filetype
394         config.InitLocalSettings(b.Settings, b.Path)
395
396         if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); os.IsNotExist(err) {
397                 os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm)
398         }
399
400         if startcursor.X != -1 && startcursor.Y != -1 {
401                 b.StartCursor = startcursor
402         } else if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
403                 err := b.Unserialize()
404                 if err != nil {
405                         screen.TermMessage(err)
406                 }
407         }
408
409         b.AddCursor(NewCursor(b, b.StartCursor))
410         b.GetActiveCursor().Relocate()
411
412         if !b.Settings["fastdirty"].(bool) && !found {
413                 if size > LargeFileThreshold {
414                         // If the file is larger than LargeFileThreshold fastdirty needs to be on
415                         b.Settings["fastdirty"] = true
416                 } else if !hasBackup {
417                         // since applying a backup does not save the applied backup to disk, we should
418                         // not calculate the original hash based on the backup data
419                         calcHash(b, &b.origHash)
420                 }
421         }
422
423         err = config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b))
424         if err != nil {
425                 screen.TermMessage(err)
426         }
427
428         OpenBuffers = append(OpenBuffers, b)
429
430         return b
431 }
432
433 // Close removes this buffer from the list of open buffers
434 func (b *Buffer) Close() {
435         for i, buf := range OpenBuffers {
436                 if b == buf {
437                         b.Fini()
438                         copy(OpenBuffers[i:], OpenBuffers[i+1:])
439                         OpenBuffers[len(OpenBuffers)-1] = nil
440                         OpenBuffers = OpenBuffers[:len(OpenBuffers)-1]
441                         return
442                 }
443         }
444 }
445
446 // Fini should be called when a buffer is closed and performs
447 // some cleanup
448 func (b *Buffer) Fini() {
449         if !b.Modified() {
450                 b.Serialize()
451         }
452         b.RemoveBackup()
453
454         if b.Type == BTStdout {
455                 fmt.Fprint(util.Stdout, string(b.Bytes()))
456         }
457
458         atomic.StoreInt32(&(b.fini), int32(1))
459 }
460
461 // GetName returns the name that should be displayed in the statusline
462 // for this buffer
463 func (b *Buffer) GetName() string {
464         name := b.name
465         if name == "" {
466                 if b.Path == "" {
467                         return "No name"
468                 }
469                 name = b.Path
470         }
471         if b.Settings["basename"].(bool) {
472                 return path.Base(name)
473         }
474         return name
475 }
476
477 //SetName changes the name for this buffer
478 func (b *Buffer) SetName(s string) {
479         b.name = s
480 }
481
482 // Insert inserts the given string of text at the start location
483 func (b *Buffer) Insert(start Loc, text string) {
484         if !b.Type.Readonly {
485                 b.EventHandler.cursors = b.cursors
486                 b.EventHandler.active = b.curCursor
487                 b.EventHandler.Insert(start, text)
488
489                 b.RequestBackup()
490         }
491 }
492
493 // Remove removes the characters between the start and end locations
494 func (b *Buffer) Remove(start, end Loc) {
495         if !b.Type.Readonly {
496                 b.EventHandler.cursors = b.cursors
497                 b.EventHandler.active = b.curCursor
498                 b.EventHandler.Remove(start, end)
499
500                 b.RequestBackup()
501         }
502 }
503
504 // FileType returns the buffer's filetype
505 func (b *Buffer) FileType() string {
506         return b.Settings["filetype"].(string)
507 }
508
509 // ExternallyModified returns whether the file being edited has
510 // been modified by some external process
511 func (b *Buffer) ExternallyModified() bool {
512         modTime, err := util.GetModTime(b.Path)
513         if err == nil {
514                 return modTime != b.ModTime
515         }
516         return false
517 }
518
519 // UpdateModTime updates the modtime of this file
520 func (b *Buffer) UpdateModTime() (err error) {
521         b.ModTime, err = util.GetModTime(b.Path)
522         return
523 }
524
525 // ReOpen reloads the current buffer from disk
526 func (b *Buffer) ReOpen() error {
527         file, err := os.Open(b.Path)
528         if err != nil {
529                 return err
530         }
531
532         enc, err := htmlindex.Get(b.Settings["encoding"].(string))
533         if err != nil {
534                 return err
535         }
536
537         reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder()))
538         data, err := ioutil.ReadAll(reader)
539         txt := string(data)
540
541         if err != nil {
542                 return err
543         }
544         b.EventHandler.ApplyDiff(txt)
545
546         err = b.UpdateModTime()
547         if !b.Settings["fastdirty"].(bool) {
548                 calcHash(b, &b.origHash)
549         }
550         b.isModified = false
551         b.RelocateCursors()
552         return err
553 }
554
555 // RelocateCursors relocates all cursors (makes sure they are in the buffer)
556 func (b *Buffer) RelocateCursors() {
557         for _, c := range b.cursors {
558                 c.Relocate()
559         }
560 }
561
562 // RuneAt returns the rune at a given location in the buffer
563 func (b *Buffer) RuneAt(loc Loc) rune {
564         line := b.LineBytes(loc.Y)
565         if len(line) > 0 {
566                 i := 0
567                 for len(line) > 0 {
568                         r, _, size := util.DecodeCharacter(line)
569                         line = line[size:]
570
571                         if i == loc.X {
572                                 return r
573                         }
574
575                         i++
576                 }
577         }
578         return '\n'
579 }
580
581 // WordAt returns the word around a given location in the buffer
582 func (b *Buffer) WordAt(loc Loc) []byte {
583         if len(b.LineBytes(loc.Y)) == 0 || !util.IsWordChar(b.RuneAt(loc)) {
584                 return []byte{}
585         }
586
587         start := loc
588         end := loc.Move(1, b)
589
590         for start.X > 0 && util.IsWordChar(b.RuneAt(start.Move(-1, b))) {
591                 start.X--
592         }
593
594         lineLen := util.CharacterCount(b.LineBytes(loc.Y))
595         for end.X < lineLen && util.IsWordChar(b.RuneAt(end)) {
596                 end.X++
597         }
598
599         return b.Substr(start, end)
600 }
601
602 // Modified returns if this buffer has been modified since
603 // being opened
604 func (b *Buffer) Modified() bool {
605         if b.Type.Scratch {
606                 return false
607         }
608
609         if b.Settings["fastdirty"].(bool) {
610                 return b.isModified
611         }
612
613         var buff [md5.Size]byte
614
615         calcHash(b, &buff)
616         return buff != b.origHash
617 }
618
619 // Size returns the number of bytes in the current buffer
620 func (b *Buffer) Size() int {
621         nb := 0
622         for i := 0; i < b.LinesNum(); i++ {
623                 nb += len(b.LineBytes(i))
624
625                 if i != b.LinesNum()-1 {
626                         if b.Endings == FFDos {
627                                 nb++ // carriage return
628                         }
629                         nb++ // newline
630                 }
631         }
632         return nb
633 }
634
635 // calcHash calculates md5 hash of all lines in the buffer
636 func calcHash(b *Buffer, out *[md5.Size]byte) error {
637         h := md5.New()
638
639         size := 0
640         if len(b.lines) > 0 {
641                 n, e := h.Write(b.lines[0].data)
642                 if e != nil {
643                         return e
644                 }
645                 size += n
646
647                 for _, l := range b.lines[1:] {
648                         n, e = h.Write([]byte{'\n'})
649                         if e != nil {
650                                 return e
651                         }
652                         size += n
653                         n, e = h.Write(l.data)
654                         if e != nil {
655                                 return e
656                         }
657                         size += n
658                 }
659         }
660
661         if size > LargeFileThreshold {
662                 return ErrFileTooLarge
663         }
664
665         h.Sum((*out)[:0])
666         return nil
667 }
668
669 // UpdateRules updates the syntax rules and filetype for this buffer
670 // This is called when the colorscheme changes
671 func (b *Buffer) UpdateRules() {
672         if !b.Type.Syntax {
673                 return
674         }
675         ft := b.Settings["filetype"].(string)
676         if ft == "off" {
677                 return
678         }
679         syntaxFile := ""
680         foundDef := false
681         var header *highlight.Header
682         // search for the syntax file in the user's custom syntax files
683         for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
684                 data, err := f.Data()
685                 if err != nil {
686                         screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
687                         continue
688                 }
689
690                 header, err = highlight.MakeHeaderYaml(data)
691                 if err != nil {
692                         screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error())
693                 }
694                 file, err := highlight.ParseFile(data)
695                 if err != nil {
696                         screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
697                         continue
698                 }
699
700                 if ((ft == "unknown" || ft == "") && highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data)) || header.FileType == ft {
701                         syndef, err := highlight.ParseDef(file, header)
702                         if err != nil {
703                                 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
704                                 continue
705                         }
706                         b.SyntaxDef = syndef
707                         syntaxFile = f.Name()
708                         foundDef = true
709                         break
710                 }
711         }
712
713         // search in the default syntax files
714         for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
715                 data, err := f.Data()
716                 if err != nil {
717                         screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error())
718                         continue
719                 }
720
721                 header, err = highlight.MakeHeader(data)
722                 if err != nil {
723                         screen.TermMessage("Error reading syntax header file", f.Name(), err)
724                         continue
725                 }
726
727                 if ft == "unknown" || ft == "" {
728                         if highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data) {
729                                 syntaxFile = f.Name()
730                                 break
731                         }
732                 } else if header.FileType == ft {
733                         syntaxFile = f.Name()
734                         break
735                 }
736         }
737
738         if syntaxFile != "" && !foundDef {
739                 // we found a syntax file using a syntax header file
740                 for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
741                         if f.Name() == syntaxFile {
742                                 data, err := f.Data()
743                                 if err != nil {
744                                         screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
745                                         continue
746                                 }
747
748                                 file, err := highlight.ParseFile(data)
749                                 if err != nil {
750                                         screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
751                                         continue
752                                 }
753
754                                 syndef, err := highlight.ParseDef(file, header)
755                                 if err != nil {
756                                         screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
757                                         continue
758                                 }
759                                 b.SyntaxDef = syndef
760                                 break
761                         }
762                 }
763         }
764
765         if b.SyntaxDef != nil && highlight.HasIncludes(b.SyntaxDef) {
766                 includes := highlight.GetIncludes(b.SyntaxDef)
767
768                 var files []*highlight.File
769                 for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
770                         data, err := f.Data()
771                         if err != nil {
772                                 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
773                                 continue
774                         }
775                         header, err := highlight.MakeHeaderYaml(data)
776                         if err != nil {
777                                 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
778                                 continue
779                         }
780
781                         for _, i := range includes {
782                                 if header.FileType == i {
783                                         file, err := highlight.ParseFile(data)
784                                         if err != nil {
785                                                 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
786                                                 continue
787                                         }
788                                         files = append(files, file)
789                                         break
790                                 }
791                         }
792                         if len(files) >= len(includes) {
793                                 break
794                         }
795                 }
796
797                 highlight.ResolveIncludes(b.SyntaxDef, files)
798         }
799
800         if b.Highlighter == nil || syntaxFile != "" {
801                 if b.SyntaxDef != nil {
802                         b.Settings["filetype"] = b.SyntaxDef.FileType
803                 }
804         } else {
805                 b.SyntaxDef = &highlight.EmptyDef
806         }
807
808         if b.SyntaxDef != nil {
809                 b.Highlighter = highlight.NewHighlighter(b.SyntaxDef)
810                 if b.Settings["syntax"].(bool) {
811                         go func() {
812                                 b.Highlighter.HighlightStates(b)
813                                 b.Highlighter.HighlightMatches(b, 0, b.End().Y)
814                                 screen.Redraw()
815                         }()
816                 }
817         }
818 }
819
820 // ClearMatches clears all of the syntax highlighting for the buffer
821 func (b *Buffer) ClearMatches() {
822         for i := range b.lines {
823                 b.SetMatch(i, nil)
824                 b.SetState(i, nil)
825         }
826 }
827
828 // IndentString returns this buffer's indent method (a tabstop or n spaces
829 // depending on the settings)
830 func (b *Buffer) IndentString(tabsize int) string {
831         if b.Settings["tabstospaces"].(bool) {
832                 return util.Spaces(tabsize)
833         }
834         return "\t"
835 }
836
837 // SetCursors resets this buffer's cursors to a new list
838 func (b *Buffer) SetCursors(c []*Cursor) {
839         b.cursors = c
840         b.EventHandler.cursors = b.cursors
841         b.EventHandler.active = b.curCursor
842 }
843
844 // AddCursor adds a new cursor to the list
845 func (b *Buffer) AddCursor(c *Cursor) {
846         b.cursors = append(b.cursors, c)
847         b.EventHandler.cursors = b.cursors
848         b.EventHandler.active = b.curCursor
849         b.UpdateCursors()
850 }
851
852 // SetCurCursor sets the current cursor
853 func (b *Buffer) SetCurCursor(n int) {
854         b.curCursor = n
855 }
856
857 // GetActiveCursor returns the main cursor in this buffer
858 func (b *Buffer) GetActiveCursor() *Cursor {
859         return b.cursors[b.curCursor]
860 }
861
862 // GetCursor returns the nth cursor
863 func (b *Buffer) GetCursor(n int) *Cursor {
864         return b.cursors[n]
865 }
866
867 // GetCursors returns the list of cursors in this buffer
868 func (b *Buffer) GetCursors() []*Cursor {
869         return b.cursors
870 }
871
872 // NumCursors returns the number of cursors
873 func (b *Buffer) NumCursors() int {
874         return len(b.cursors)
875 }
876
877 // MergeCursors merges any cursors that are at the same position
878 // into one cursor
879 func (b *Buffer) MergeCursors() {
880         var cursors []*Cursor
881         for i := 0; i < len(b.cursors); i++ {
882                 c1 := b.cursors[i]
883                 if c1 != nil {
884                         for j := 0; j < len(b.cursors); j++ {
885                                 c2 := b.cursors[j]
886                                 if c2 != nil && i != j && c1.Loc == c2.Loc {
887                                         b.cursors[j] = nil
888                                 }
889                         }
890                         cursors = append(cursors, c1)
891                 }
892         }
893
894         b.cursors = cursors
895
896         for i := range b.cursors {
897                 b.cursors[i].Num = i
898         }
899
900         if b.curCursor >= len(b.cursors) {
901                 b.curCursor = len(b.cursors) - 1
902         }
903         b.EventHandler.cursors = b.cursors
904         b.EventHandler.active = b.curCursor
905 }
906
907 // UpdateCursors updates all the cursors indicies
908 func (b *Buffer) UpdateCursors() {
909         b.EventHandler.cursors = b.cursors
910         b.EventHandler.active = b.curCursor
911         for i, c := range b.cursors {
912                 c.Num = i
913         }
914 }
915
916 func (b *Buffer) RemoveCursor(i int) {
917         copy(b.cursors[i:], b.cursors[i+1:])
918         b.cursors[len(b.cursors)-1] = nil
919         b.cursors = b.cursors[:len(b.cursors)-1]
920         b.curCursor = util.Clamp(b.curCursor, 0, len(b.cursors)-1)
921         b.UpdateCursors()
922 }
923
924 // ClearCursors removes all extra cursors
925 func (b *Buffer) ClearCursors() {
926         for i := 1; i < len(b.cursors); i++ {
927                 b.cursors[i] = nil
928         }
929         b.cursors = b.cursors[:1]
930         b.UpdateCursors()
931         b.curCursor = 0
932         b.GetActiveCursor().ResetSelection()
933 }
934
935 // MoveLinesUp moves the range of lines up one row
936 func (b *Buffer) MoveLinesUp(start int, end int) {
937         if start < 1 || start >= end || end > len(b.lines) {
938                 return
939         }
940         l := string(b.LineBytes(start - 1))
941         if end == len(b.lines) {
942                 b.insert(
943                         Loc{
944                                 util.CharacterCount(b.lines[end-1].data),
945                                 end - 1,
946                         },
947                         []byte{'\n'},
948                 )
949         }
950         b.Insert(
951                 Loc{0, end},
952                 l+"\n",
953         )
954         b.Remove(
955                 Loc{0, start - 1},
956                 Loc{0, start},
957         )
958 }
959
960 // MoveLinesDown moves the range of lines down one row
961 func (b *Buffer) MoveLinesDown(start int, end int) {
962         if start < 0 || start >= end || end >= len(b.lines) {
963                 return
964         }
965         l := string(b.LineBytes(end))
966         b.Insert(
967                 Loc{0, start},
968                 l+"\n",
969         )
970         end++
971         b.Remove(
972                 Loc{0, end},
973                 Loc{0, end + 1},
974         )
975 }
976
977 var BracePairs = [][2]rune{
978         {'(', ')'},
979         {'{', '}'},
980         {'[', ']'},
981 }
982
983 // FindMatchingBrace returns the location in the buffer of the matching bracket
984 // It is given a brace type containing the open and closing character, (for example
985 // '{' and '}') as well as the location to match from
986 // TODO: maybe can be more efficient with utf8 package
987 // returns the location of the matching brace
988 // if the boolean returned is true then the original matching brace is one character left
989 // of the starting location
990 func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool, bool) {
991         curLine := []rune(string(b.LineBytes(start.Y)))
992         startChar := ' '
993         if start.X >= 0 && start.X < len(curLine) {
994                 startChar = curLine[start.X]
995         }
996         leftChar := ' '
997         if start.X-1 >= 0 && start.X-1 < len(curLine) {
998                 leftChar = curLine[start.X-1]
999         }
1000         var i int
1001         if startChar == braceType[0] || leftChar == braceType[0] {
1002                 for y := start.Y; y < b.LinesNum(); y++ {
1003                         l := []rune(string(b.LineBytes(y)))
1004                         xInit := 0
1005                         if y == start.Y {
1006                                 if startChar == braceType[0] {
1007                                         xInit = start.X
1008                                 } else {
1009                                         xInit = start.X - 1
1010                                 }
1011                         }
1012                         for x := xInit; x < len(l); x++ {
1013                                 r := l[x]
1014                                 if r == braceType[0] {
1015                                         i++
1016                                 } else if r == braceType[1] {
1017                                         i--
1018                                         if i == 0 {
1019                                                 if startChar == braceType[0] {
1020                                                         return Loc{x, y}, false, true
1021                                                 }
1022                                                 return Loc{x, y}, true, true
1023                                         }
1024                                 }
1025                         }
1026                 }
1027         } else if startChar == braceType[1] || leftChar == braceType[1] {
1028                 for y := start.Y; y >= 0; y-- {
1029                         l := []rune(string(b.lines[y].data))
1030                         xInit := len(l) - 1
1031                         if y == start.Y {
1032                                 if leftChar == braceType[1] {
1033                                         xInit = start.X - 1
1034                                 } else {
1035                                         xInit = start.X
1036                                 }
1037                         }
1038                         for x := xInit; x >= 0; x-- {
1039                                 r := l[x]
1040                                 if r == braceType[0] {
1041                                         i--
1042                                         if i == 0 {
1043                                                 if leftChar == braceType[1] {
1044                                                         return Loc{x, y}, true, true
1045                                                 }
1046                                                 return Loc{x, y}, false, true
1047                                         }
1048                                 } else if r == braceType[1] {
1049                                         i++
1050                                 }
1051                         }
1052                 }
1053         }
1054         return start, true, false
1055 }
1056
1057 // Retab changes all tabs to spaces or vice versa
1058 func (b *Buffer) Retab() {
1059         toSpaces := b.Settings["tabstospaces"].(bool)
1060         tabsize := util.IntOpt(b.Settings["tabsize"])
1061         dirty := false
1062
1063         for i := 0; i < b.LinesNum(); i++ {
1064                 l := b.LineBytes(i)
1065
1066                 ws := util.GetLeadingWhitespace(l)
1067                 if len(ws) != 0 {
1068                         if toSpaces {
1069                                 ws = bytes.ReplaceAll(ws, []byte{'\t'}, bytes.Repeat([]byte{' '}, tabsize))
1070                         } else {
1071                                 ws = bytes.ReplaceAll(ws, bytes.Repeat([]byte{' '}, tabsize), []byte{'\t'})
1072                         }
1073                 }
1074
1075                 l = bytes.TrimLeft(l, " \t")
1076                 b.lines[i].data = append(ws, l...)
1077                 b.MarkModified(i, i)
1078                 dirty = true
1079         }
1080
1081         b.isModified = dirty
1082 }
1083
1084 // ParseCursorLocation turns a cursor location like 10:5 (LINE:COL)
1085 // into a loc
1086 func ParseCursorLocation(cursorPositions []string) (Loc, error) {
1087         startpos := Loc{0, 0}
1088         var err error
1089
1090         // if no positions are available exit early
1091         if cursorPositions == nil {
1092                 return startpos, errors.New("No cursor positions were provided.")
1093         }
1094
1095         startpos.Y, err = strconv.Atoi(cursorPositions[0])
1096         startpos.Y--
1097         if err == nil {
1098                 if len(cursorPositions) > 1 {
1099                         startpos.X, err = strconv.Atoi(cursorPositions[1])
1100                         if startpos.X > 0 {
1101                                 startpos.X--
1102                         }
1103                 }
1104         }
1105
1106         return startpos, err
1107 }
1108
1109 // Line returns the string representation of the given line number
1110 func (b *Buffer) Line(i int) string {
1111         return string(b.LineBytes(i))
1112 }
1113
1114 func (b *Buffer) Write(bytes []byte) (n int, err error) {
1115         b.EventHandler.InsertBytes(b.End(), bytes)
1116         return len(bytes), nil
1117 }
1118
1119 func (b *Buffer) updateDiffSync() {
1120         b.diffLock.Lock()
1121         defer b.diffLock.Unlock()
1122
1123         b.diff = make(map[int]DiffStatus)
1124
1125         if b.diffBase == nil {
1126                 return
1127         }
1128
1129         differ := dmp.New()
1130         baseRunes, bufferRunes, _ := differ.DiffLinesToRunes(string(b.diffBase), string(b.Bytes()))
1131         diffs := differ.DiffMainRunes(baseRunes, bufferRunes, false)
1132         lineN := 0
1133
1134         for _, diff := range diffs {
1135                 lineCount := len([]rune(diff.Text))
1136
1137                 switch diff.Type {
1138                 case dmp.DiffEqual:
1139                         lineN += lineCount
1140                 case dmp.DiffInsert:
1141                         var status DiffStatus
1142                         if b.diff[lineN] == DSDeletedAbove {
1143                                 status = DSModified
1144                         } else {
1145                                 status = DSAdded
1146                         }
1147                         for i := 0; i < lineCount; i++ {
1148                                 b.diff[lineN] = status
1149                                 lineN++
1150                         }
1151                 case dmp.DiffDelete:
1152                         b.diff[lineN] = DSDeletedAbove
1153                 }
1154         }
1155 }
1156
1157 // UpdateDiff computes the diff between the diff base and the buffer content.
1158 // The update may be performed synchronously or asynchronously.
1159 // UpdateDiff calls the supplied callback when the update is complete.
1160 // The argument passed to the callback is set to true if and only if
1161 // the update was performed synchronously.
1162 // If an asynchronous update is already pending when UpdateDiff is called,
1163 // UpdateDiff does not schedule another update, in which case the callback
1164 // is not called.
1165 func (b *Buffer) UpdateDiff(callback func(bool)) {
1166         if b.updateDiffTimer != nil {
1167                 return
1168         }
1169
1170         lineCount := b.LinesNum()
1171         if b.diffBaseLineCount > lineCount {
1172                 lineCount = b.diffBaseLineCount
1173         }
1174
1175         if lineCount < 1000 {
1176                 b.updateDiffSync()
1177                 callback(true)
1178         } else if lineCount < 30000 {
1179                 b.updateDiffTimer = time.AfterFunc(500*time.Millisecond, func() {
1180                         b.updateDiffTimer = nil
1181                         b.updateDiffSync()
1182                         callback(false)
1183                 })
1184         } else {
1185                 // Don't compute diffs for very large files
1186                 b.diffLock.Lock()
1187                 b.diff = make(map[int]DiffStatus)
1188                 b.diffLock.Unlock()
1189                 callback(true)
1190         }
1191 }
1192
1193 // SetDiffBase sets the text that is used as the base for diffing the buffer content
1194 func (b *Buffer) SetDiffBase(diffBase []byte) {
1195         b.diffBase = diffBase
1196         if diffBase == nil {
1197                 b.diffBaseLineCount = 0
1198         } else {
1199                 b.diffBaseLineCount = strings.Count(string(diffBase), "\n")
1200         }
1201         b.UpdateDiff(func(synchronous bool) {
1202                 screen.Redraw()
1203         })
1204 }
1205
1206 // DiffStatus returns the diff status for a line in the buffer
1207 func (b *Buffer) DiffStatus(lineN int) DiffStatus {
1208         b.diffLock.RLock()
1209         defer b.diffLock.RUnlock()
1210         // Note that the zero value for DiffStatus is equal to DSUnchanged
1211         return b.diff[lineN]
1212 }
1213
1214 // SearchMatch returns true if the given location is within a match of the last search.
1215 // It is used for search highlighting
1216 func (b *Buffer) SearchMatch(pos Loc) bool {
1217         return b.LineArray.SearchMatch(b, pos)
1218 }
1219
1220 // WriteLog writes a string to the log buffer
1221 func WriteLog(s string) {
1222         LogBuf.EventHandler.Insert(LogBuf.End(), s)
1223 }
1224
1225 // GetLogBuf returns the log buffer
1226 func GetLogBuf() *Buffer {
1227         return LogBuf
1228 }