]> git.lizzy.rs Git - micro.git/blob - internal/buffer/buffer.go
Support for highlighting all search matches (hlsearch) (#1762)
[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, _ := filepath.Abs(path)
300
301         b := new(Buffer)
302
303         found := false
304         if len(path) > 0 {
305                 for _, buf := range OpenBuffers {
306                         if buf.AbsPath == absPath && buf.Type != BTInfo {
307                                 found = true
308                                 b.SharedBuffer = buf.SharedBuffer
309                                 b.EventHandler = buf.EventHandler
310                         }
311                 }
312         }
313
314         hasBackup := false
315         if !found {
316                 b.SharedBuffer = new(SharedBuffer)
317                 b.Type = btype
318
319                 b.AbsPath = absPath
320                 b.Path = path
321
322                 // this is a little messy since we need to know some settings to read
323                 // the file properly, but some settings depend on the filetype, which
324                 // we don't know until reading the file. We first read the settings
325                 // into a local variable and then use that to determine the encoding,
326                 // readonly, and fileformat necessary for reading the file and
327                 // assigning the filetype.
328                 settings := config.DefaultCommonSettings()
329                 b.Settings = config.DefaultCommonSettings()
330                 for k, v := range config.GlobalSettings {
331                         if _, ok := config.DefaultGlobalOnlySettings[k]; !ok {
332                                 // make sure setting is not global-only
333                                 settings[k] = v
334                                 b.Settings[k] = v
335                         }
336                 }
337                 config.InitLocalSettings(settings, path)
338                 b.Settings["readonly"] = settings["readonly"]
339                 b.Settings["filetype"] = settings["filetype"]
340                 b.Settings["syntax"] = settings["syntax"]
341
342                 enc, err := htmlindex.Get(settings["encoding"].(string))
343                 if err != nil {
344                         enc = unicode.UTF8
345                         b.Settings["encoding"] = "utf-8"
346                 }
347
348                 var ok bool
349                 hasBackup, ok = b.ApplyBackup(size)
350
351                 if !ok {
352                         return NewBufferFromString("", "", btype)
353                 }
354                 if !hasBackup {
355                         reader := bufio.NewReader(transform.NewReader(r, enc.NewDecoder()))
356
357                         var ff FileFormat = FFAuto
358
359                         if size == 0 {
360                                 // for empty files, use the fileformat setting instead of
361                                 // autodetection
362                                 switch settings["fileformat"] {
363                                 case "unix":
364                                         ff = FFUnix
365                                 case "dos":
366                                         ff = FFDos
367                                 }
368                         }
369
370                         b.LineArray = NewLineArray(uint64(size), ff, reader)
371                 }
372                 b.EventHandler = NewEventHandler(b.SharedBuffer, b.cursors)
373
374                 // The last time this file was modified
375                 b.UpdateModTime()
376         }
377
378         if b.Settings["readonly"].(bool) && b.Type == BTDefault {
379                 b.Type.Readonly = true
380         }
381
382         switch b.Endings {
383         case FFUnix:
384                 b.Settings["fileformat"] = "unix"
385         case FFDos:
386                 b.Settings["fileformat"] = "dos"
387         }
388
389         b.UpdateRules()
390         // init local settings again now that we know the filetype
391         config.InitLocalSettings(b.Settings, b.Path)
392
393         if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); os.IsNotExist(err) {
394                 os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm)
395         }
396
397         if startcursor.X != -1 && startcursor.Y != -1 {
398                 b.StartCursor = startcursor
399         } else if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
400                 err := b.Unserialize()
401                 if err != nil {
402                         screen.TermMessage(err)
403                 }
404         }
405
406         b.AddCursor(NewCursor(b, b.StartCursor))
407         b.GetActiveCursor().Relocate()
408
409         if !b.Settings["fastdirty"].(bool) && !found {
410                 if size > LargeFileThreshold {
411                         // If the file is larger than LargeFileThreshold fastdirty needs to be on
412                         b.Settings["fastdirty"] = true
413                 } else if !hasBackup {
414                         // since applying a backup does not save the applied backup to disk, we should
415                         // not calculate the original hash based on the backup data
416                         calcHash(b, &b.origHash)
417                 }
418         }
419
420         err := config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b))
421         if err != nil {
422                 screen.TermMessage(err)
423         }
424
425         OpenBuffers = append(OpenBuffers, b)
426
427         return b
428 }
429
430 // Close removes this buffer from the list of open buffers
431 func (b *Buffer) Close() {
432         for i, buf := range OpenBuffers {
433                 if b == buf {
434                         b.Fini()
435                         copy(OpenBuffers[i:], OpenBuffers[i+1:])
436                         OpenBuffers[len(OpenBuffers)-1] = nil
437                         OpenBuffers = OpenBuffers[:len(OpenBuffers)-1]
438                         return
439                 }
440         }
441 }
442
443 // Fini should be called when a buffer is closed and performs
444 // some cleanup
445 func (b *Buffer) Fini() {
446         if !b.Modified() {
447                 b.Serialize()
448         }
449         b.RemoveBackup()
450
451         if b.Type == BTStdout {
452                 fmt.Fprint(util.Stdout, string(b.Bytes()))
453         }
454
455         atomic.StoreInt32(&(b.fini), int32(1))
456 }
457
458 // GetName returns the name that should be displayed in the statusline
459 // for this buffer
460 func (b *Buffer) GetName() string {
461         name := b.name
462         if name == "" {
463                 if b.Path == "" {
464                         return "No name"
465                 }
466                 name = b.Path
467         }
468         if b.Settings["basename"].(bool) {
469                 return path.Base(name)
470         }
471         return name
472 }
473
474 //SetName changes the name for this buffer
475 func (b *Buffer) SetName(s string) {
476         b.name = s
477 }
478
479 // Insert inserts the given string of text at the start location
480 func (b *Buffer) Insert(start Loc, text string) {
481         if !b.Type.Readonly {
482                 b.EventHandler.cursors = b.cursors
483                 b.EventHandler.active = b.curCursor
484                 b.EventHandler.Insert(start, text)
485
486                 b.RequestBackup()
487         }
488 }
489
490 // Remove removes the characters between the start and end locations
491 func (b *Buffer) Remove(start, end Loc) {
492         if !b.Type.Readonly {
493                 b.EventHandler.cursors = b.cursors
494                 b.EventHandler.active = b.curCursor
495                 b.EventHandler.Remove(start, end)
496
497                 b.RequestBackup()
498         }
499 }
500
501 // FileType returns the buffer's filetype
502 func (b *Buffer) FileType() string {
503         return b.Settings["filetype"].(string)
504 }
505
506 // ExternallyModified returns whether the file being edited has
507 // been modified by some external process
508 func (b *Buffer) ExternallyModified() bool {
509         modTime, err := util.GetModTime(b.Path)
510         if err == nil {
511                 return modTime != b.ModTime
512         }
513         return false
514 }
515
516 // UpdateModTime updates the modtime of this file
517 func (b *Buffer) UpdateModTime() (err error) {
518         b.ModTime, err = util.GetModTime(b.Path)
519         return
520 }
521
522 // ReOpen reloads the current buffer from disk
523 func (b *Buffer) ReOpen() error {
524         file, err := os.Open(b.Path)
525         if err != nil {
526                 return err
527         }
528
529         enc, err := htmlindex.Get(b.Settings["encoding"].(string))
530         if err != nil {
531                 return err
532         }
533
534         reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder()))
535         data, err := ioutil.ReadAll(reader)
536         txt := string(data)
537
538         if err != nil {
539                 return err
540         }
541         b.EventHandler.ApplyDiff(txt)
542
543         err = b.UpdateModTime()
544         if !b.Settings["fastdirty"].(bool) {
545                 calcHash(b, &b.origHash)
546         }
547         b.isModified = false
548         b.RelocateCursors()
549         return err
550 }
551
552 // RelocateCursors relocates all cursors (makes sure they are in the buffer)
553 func (b *Buffer) RelocateCursors() {
554         for _, c := range b.cursors {
555                 c.Relocate()
556         }
557 }
558
559 // RuneAt returns the rune at a given location in the buffer
560 func (b *Buffer) RuneAt(loc Loc) rune {
561         line := b.LineBytes(loc.Y)
562         if len(line) > 0 {
563                 i := 0
564                 for len(line) > 0 {
565                         r, _, size := util.DecodeCharacter(line)
566                         line = line[size:]
567
568                         if i == loc.X {
569                                 return r
570                         }
571
572                         i++
573                 }
574         }
575         return '\n'
576 }
577
578 // WordAt returns the word around a given location in the buffer
579 func (b *Buffer) WordAt(loc Loc) []byte {
580         if len(b.LineBytes(loc.Y)) == 0 || !util.IsWordChar(b.RuneAt(loc)) {
581                 return []byte{}
582         }
583
584         start := loc
585         end := loc.Move(1, b)
586
587         for start.X > 0 && util.IsWordChar(b.RuneAt(start.Move(-1, b))) {
588                 start.X--
589         }
590
591         lineLen := util.CharacterCount(b.LineBytes(loc.Y))
592         for end.X < lineLen && util.IsWordChar(b.RuneAt(end)) {
593                 end.X++
594         }
595
596         return b.Substr(start, end)
597 }
598
599 // Modified returns if this buffer has been modified since
600 // being opened
601 func (b *Buffer) Modified() bool {
602         if b.Type.Scratch {
603                 return false
604         }
605
606         if b.Settings["fastdirty"].(bool) {
607                 return b.isModified
608         }
609
610         var buff [md5.Size]byte
611
612         calcHash(b, &buff)
613         return buff != b.origHash
614 }
615
616 // Size returns the number of bytes in the current buffer
617 func (b *Buffer) Size() int {
618         nb := 0
619         for i := 0; i < b.LinesNum(); i++ {
620                 nb += len(b.LineBytes(i))
621
622                 if i != b.LinesNum()-1 {
623                         if b.Endings == FFDos {
624                                 nb++ // carriage return
625                         }
626                         nb++ // newline
627                 }
628         }
629         return nb
630 }
631
632 // calcHash calculates md5 hash of all lines in the buffer
633 func calcHash(b *Buffer, out *[md5.Size]byte) error {
634         h := md5.New()
635
636         size := 0
637         if len(b.lines) > 0 {
638                 n, e := h.Write(b.lines[0].data)
639                 if e != nil {
640                         return e
641                 }
642                 size += n
643
644                 for _, l := range b.lines[1:] {
645                         n, e = h.Write([]byte{'\n'})
646                         if e != nil {
647                                 return e
648                         }
649                         size += n
650                         n, e = h.Write(l.data)
651                         if e != nil {
652                                 return e
653                         }
654                         size += n
655                 }
656         }
657
658         if size > LargeFileThreshold {
659                 return ErrFileTooLarge
660         }
661
662         h.Sum((*out)[:0])
663         return nil
664 }
665
666 // UpdateRules updates the syntax rules and filetype for this buffer
667 // This is called when the colorscheme changes
668 func (b *Buffer) UpdateRules() {
669         if !b.Type.Syntax {
670                 return
671         }
672         ft := b.Settings["filetype"].(string)
673         if ft == "off" {
674                 return
675         }
676         syntaxFile := ""
677         foundDef := false
678         var header *highlight.Header
679         // search for the syntax file in the user's custom syntax files
680         for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
681                 data, err := f.Data()
682                 if err != nil {
683                         screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
684                         continue
685                 }
686
687                 header, err = highlight.MakeHeaderYaml(data)
688                 if err != nil {
689                         screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error())
690                 }
691                 file, err := highlight.ParseFile(data)
692                 if err != nil {
693                         screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
694                         continue
695                 }
696
697                 if ((ft == "unknown" || ft == "") && highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data)) || header.FileType == ft {
698                         syndef, err := highlight.ParseDef(file, header)
699                         if err != nil {
700                                 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
701                                 continue
702                         }
703                         b.SyntaxDef = syndef
704                         syntaxFile = f.Name()
705                         foundDef = true
706                         break
707                 }
708         }
709
710         // search in the default syntax files
711         for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
712                 data, err := f.Data()
713                 if err != nil {
714                         screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error())
715                         continue
716                 }
717
718                 header, err = highlight.MakeHeader(data)
719                 if err != nil {
720                         screen.TermMessage("Error reading syntax header file", f.Name(), err)
721                         continue
722                 }
723
724                 if ft == "unknown" || ft == "" {
725                         if highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data) {
726                                 syntaxFile = f.Name()
727                                 break
728                         }
729                 } else if header.FileType == ft {
730                         syntaxFile = f.Name()
731                         break
732                 }
733         }
734
735         if syntaxFile != "" && !foundDef {
736                 // we found a syntax file using a syntax header file
737                 for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
738                         if f.Name() == syntaxFile {
739                                 data, err := f.Data()
740                                 if err != nil {
741                                         screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
742                                         continue
743                                 }
744
745                                 file, err := highlight.ParseFile(data)
746                                 if err != nil {
747                                         screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
748                                         continue
749                                 }
750
751                                 syndef, err := highlight.ParseDef(file, header)
752                                 if err != nil {
753                                         screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
754                                         continue
755                                 }
756                                 b.SyntaxDef = syndef
757                                 break
758                         }
759                 }
760         }
761
762         if b.SyntaxDef != nil && highlight.HasIncludes(b.SyntaxDef) {
763                 includes := highlight.GetIncludes(b.SyntaxDef)
764
765                 var files []*highlight.File
766                 for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
767                         data, err := f.Data()
768                         if err != nil {
769                                 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
770                                 continue
771                         }
772                         header, err := highlight.MakeHeaderYaml(data)
773                         if err != nil {
774                                 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
775                                 continue
776                         }
777
778                         for _, i := range includes {
779                                 if header.FileType == i {
780                                         file, err := highlight.ParseFile(data)
781                                         if err != nil {
782                                                 screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
783                                                 continue
784                                         }
785                                         files = append(files, file)
786                                         break
787                                 }
788                         }
789                         if len(files) >= len(includes) {
790                                 break
791                         }
792                 }
793
794                 highlight.ResolveIncludes(b.SyntaxDef, files)
795         }
796
797         if b.Highlighter == nil || syntaxFile != "" {
798                 if b.SyntaxDef != nil {
799                         b.Settings["filetype"] = b.SyntaxDef.FileType
800                 }
801         } else {
802                 b.SyntaxDef = &highlight.EmptyDef
803         }
804
805         if b.SyntaxDef != nil {
806                 b.Highlighter = highlight.NewHighlighter(b.SyntaxDef)
807                 if b.Settings["syntax"].(bool) {
808                         go func() {
809                                 b.Highlighter.HighlightStates(b)
810                                 b.Highlighter.HighlightMatches(b, 0, b.End().Y)
811                                 screen.Redraw()
812                         }()
813                 }
814         }
815 }
816
817 // ClearMatches clears all of the syntax highlighting for the buffer
818 func (b *Buffer) ClearMatches() {
819         for i := range b.lines {
820                 b.SetMatch(i, nil)
821                 b.SetState(i, nil)
822         }
823 }
824
825 // IndentString returns this buffer's indent method (a tabstop or n spaces
826 // depending on the settings)
827 func (b *Buffer) IndentString(tabsize int) string {
828         if b.Settings["tabstospaces"].(bool) {
829                 return util.Spaces(tabsize)
830         }
831         return "\t"
832 }
833
834 // SetCursors resets this buffer's cursors to a new list
835 func (b *Buffer) SetCursors(c []*Cursor) {
836         b.cursors = c
837         b.EventHandler.cursors = b.cursors
838         b.EventHandler.active = b.curCursor
839 }
840
841 // AddCursor adds a new cursor to the list
842 func (b *Buffer) AddCursor(c *Cursor) {
843         b.cursors = append(b.cursors, c)
844         b.EventHandler.cursors = b.cursors
845         b.EventHandler.active = b.curCursor
846         b.UpdateCursors()
847 }
848
849 // SetCurCursor sets the current cursor
850 func (b *Buffer) SetCurCursor(n int) {
851         b.curCursor = n
852 }
853
854 // GetActiveCursor returns the main cursor in this buffer
855 func (b *Buffer) GetActiveCursor() *Cursor {
856         return b.cursors[b.curCursor]
857 }
858
859 // GetCursor returns the nth cursor
860 func (b *Buffer) GetCursor(n int) *Cursor {
861         return b.cursors[n]
862 }
863
864 // GetCursors returns the list of cursors in this buffer
865 func (b *Buffer) GetCursors() []*Cursor {
866         return b.cursors
867 }
868
869 // NumCursors returns the number of cursors
870 func (b *Buffer) NumCursors() int {
871         return len(b.cursors)
872 }
873
874 // MergeCursors merges any cursors that are at the same position
875 // into one cursor
876 func (b *Buffer) MergeCursors() {
877         var cursors []*Cursor
878         for i := 0; i < len(b.cursors); i++ {
879                 c1 := b.cursors[i]
880                 if c1 != nil {
881                         for j := 0; j < len(b.cursors); j++ {
882                                 c2 := b.cursors[j]
883                                 if c2 != nil && i != j && c1.Loc == c2.Loc {
884                                         b.cursors[j] = nil
885                                 }
886                         }
887                         cursors = append(cursors, c1)
888                 }
889         }
890
891         b.cursors = cursors
892
893         for i := range b.cursors {
894                 b.cursors[i].Num = i
895         }
896
897         if b.curCursor >= len(b.cursors) {
898                 b.curCursor = len(b.cursors) - 1
899         }
900         b.EventHandler.cursors = b.cursors
901         b.EventHandler.active = b.curCursor
902 }
903
904 // UpdateCursors updates all the cursors indicies
905 func (b *Buffer) UpdateCursors() {
906         b.EventHandler.cursors = b.cursors
907         b.EventHandler.active = b.curCursor
908         for i, c := range b.cursors {
909                 c.Num = i
910         }
911 }
912
913 func (b *Buffer) RemoveCursor(i int) {
914         copy(b.cursors[i:], b.cursors[i+1:])
915         b.cursors[len(b.cursors)-1] = nil
916         b.cursors = b.cursors[:len(b.cursors)-1]
917         b.curCursor = util.Clamp(b.curCursor, 0, len(b.cursors)-1)
918         b.UpdateCursors()
919 }
920
921 // ClearCursors removes all extra cursors
922 func (b *Buffer) ClearCursors() {
923         for i := 1; i < len(b.cursors); i++ {
924                 b.cursors[i] = nil
925         }
926         b.cursors = b.cursors[:1]
927         b.UpdateCursors()
928         b.curCursor = 0
929         b.GetActiveCursor().ResetSelection()
930 }
931
932 // MoveLinesUp moves the range of lines up one row
933 func (b *Buffer) MoveLinesUp(start int, end int) {
934         if start < 1 || start >= end || end > len(b.lines) {
935                 return
936         }
937         l := string(b.LineBytes(start - 1))
938         if end == len(b.lines) {
939                 b.insert(
940                         Loc{
941                                 util.CharacterCount(b.lines[end-1].data),
942                                 end - 1,
943                         },
944                         []byte{'\n'},
945                 )
946         }
947         b.Insert(
948                 Loc{0, end},
949                 l+"\n",
950         )
951         b.Remove(
952                 Loc{0, start - 1},
953                 Loc{0, start},
954         )
955 }
956
957 // MoveLinesDown moves the range of lines down one row
958 func (b *Buffer) MoveLinesDown(start int, end int) {
959         if start < 0 || start >= end || end >= len(b.lines) {
960                 return
961         }
962         l := string(b.LineBytes(end))
963         b.Insert(
964                 Loc{0, start},
965                 l+"\n",
966         )
967         end++
968         b.Remove(
969                 Loc{0, end},
970                 Loc{0, end + 1},
971         )
972 }
973
974 var BracePairs = [][2]rune{
975         {'(', ')'},
976         {'{', '}'},
977         {'[', ']'},
978 }
979
980 // FindMatchingBrace returns the location in the buffer of the matching bracket
981 // It is given a brace type containing the open and closing character, (for example
982 // '{' and '}') as well as the location to match from
983 // TODO: maybe can be more efficient with utf8 package
984 // returns the location of the matching brace
985 // if the boolean returned is true then the original matching brace is one character left
986 // of the starting location
987 func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool, bool) {
988         curLine := []rune(string(b.LineBytes(start.Y)))
989         startChar := ' '
990         if start.X >= 0 && start.X < len(curLine) {
991                 startChar = curLine[start.X]
992         }
993         leftChar := ' '
994         if start.X-1 >= 0 && start.X-1 < len(curLine) {
995                 leftChar = curLine[start.X-1]
996         }
997         var i int
998         if startChar == braceType[0] || leftChar == braceType[0] {
999                 for y := start.Y; y < b.LinesNum(); y++ {
1000                         l := []rune(string(b.LineBytes(y)))
1001                         xInit := 0
1002                         if y == start.Y {
1003                                 if startChar == braceType[0] {
1004                                         xInit = start.X
1005                                 } else {
1006                                         xInit = start.X - 1
1007                                 }
1008                         }
1009                         for x := xInit; x < len(l); x++ {
1010                                 r := l[x]
1011                                 if r == braceType[0] {
1012                                         i++
1013                                 } else if r == braceType[1] {
1014                                         i--
1015                                         if i == 0 {
1016                                                 if startChar == braceType[0] {
1017                                                         return Loc{x, y}, false, true
1018                                                 }
1019                                                 return Loc{x, y}, true, true
1020                                         }
1021                                 }
1022                         }
1023                 }
1024         } else if startChar == braceType[1] || leftChar == braceType[1] {
1025                 for y := start.Y; y >= 0; y-- {
1026                         l := []rune(string(b.lines[y].data))
1027                         xInit := len(l) - 1
1028                         if y == start.Y {
1029                                 if leftChar == braceType[1] {
1030                                         xInit = start.X - 1
1031                                 } else {
1032                                         xInit = start.X
1033                                 }
1034                         }
1035                         for x := xInit; x >= 0; x-- {
1036                                 r := l[x]
1037                                 if r == braceType[0] {
1038                                         i--
1039                                         if i == 0 {
1040                                                 if leftChar == braceType[1] {
1041                                                         return Loc{x, y}, true, true
1042                                                 }
1043                                                 return Loc{x, y}, false, true
1044                                         }
1045                                 } else if r == braceType[1] {
1046                                         i++
1047                                 }
1048                         }
1049                 }
1050         }
1051         return start, true, false
1052 }
1053
1054 // Retab changes all tabs to spaces or vice versa
1055 func (b *Buffer) Retab() {
1056         toSpaces := b.Settings["tabstospaces"].(bool)
1057         tabsize := util.IntOpt(b.Settings["tabsize"])
1058         dirty := false
1059
1060         for i := 0; i < b.LinesNum(); i++ {
1061                 l := b.LineBytes(i)
1062
1063                 ws := util.GetLeadingWhitespace(l)
1064                 if len(ws) != 0 {
1065                         if toSpaces {
1066                                 ws = bytes.ReplaceAll(ws, []byte{'\t'}, bytes.Repeat([]byte{' '}, tabsize))
1067                         } else {
1068                                 ws = bytes.ReplaceAll(ws, bytes.Repeat([]byte{' '}, tabsize), []byte{'\t'})
1069                         }
1070                 }
1071
1072                 l = bytes.TrimLeft(l, " \t")
1073                 b.lines[i].data = append(ws, l...)
1074                 b.MarkModified(i, i)
1075                 dirty = true
1076         }
1077
1078         b.isModified = dirty
1079 }
1080
1081 // ParseCursorLocation turns a cursor location like 10:5 (LINE:COL)
1082 // into a loc
1083 func ParseCursorLocation(cursorPositions []string) (Loc, error) {
1084         startpos := Loc{0, 0}
1085         var err error
1086
1087         // if no positions are available exit early
1088         if cursorPositions == nil {
1089                 return startpos, errors.New("No cursor positions were provided.")
1090         }
1091
1092         startpos.Y, err = strconv.Atoi(cursorPositions[0])
1093         startpos.Y--
1094         if err == nil {
1095                 if len(cursorPositions) > 1 {
1096                         startpos.X, err = strconv.Atoi(cursorPositions[1])
1097                         if startpos.X > 0 {
1098                                 startpos.X--
1099                         }
1100                 }
1101         }
1102
1103         return startpos, err
1104 }
1105
1106 // Line returns the string representation of the given line number
1107 func (b *Buffer) Line(i int) string {
1108         return string(b.LineBytes(i))
1109 }
1110
1111 func (b *Buffer) Write(bytes []byte) (n int, err error) {
1112         b.EventHandler.InsertBytes(b.End(), bytes)
1113         return len(bytes), nil
1114 }
1115
1116 func (b *Buffer) updateDiffSync() {
1117         b.diffLock.Lock()
1118         defer b.diffLock.Unlock()
1119
1120         b.diff = make(map[int]DiffStatus)
1121
1122         if b.diffBase == nil {
1123                 return
1124         }
1125
1126         differ := dmp.New()
1127         baseRunes, bufferRunes, _ := differ.DiffLinesToRunes(string(b.diffBase), string(b.Bytes()))
1128         diffs := differ.DiffMainRunes(baseRunes, bufferRunes, false)
1129         lineN := 0
1130
1131         for _, diff := range diffs {
1132                 lineCount := len([]rune(diff.Text))
1133
1134                 switch diff.Type {
1135                 case dmp.DiffEqual:
1136                         lineN += lineCount
1137                 case dmp.DiffInsert:
1138                         var status DiffStatus
1139                         if b.diff[lineN] == DSDeletedAbove {
1140                                 status = DSModified
1141                         } else {
1142                                 status = DSAdded
1143                         }
1144                         for i := 0; i < lineCount; i++ {
1145                                 b.diff[lineN] = status
1146                                 lineN++
1147                         }
1148                 case dmp.DiffDelete:
1149                         b.diff[lineN] = DSDeletedAbove
1150                 }
1151         }
1152 }
1153
1154 // UpdateDiff computes the diff between the diff base and the buffer content.
1155 // The update may be performed synchronously or asynchronously.
1156 // UpdateDiff calls the supplied callback when the update is complete.
1157 // The argument passed to the callback is set to true if and only if
1158 // the update was performed synchronously.
1159 // If an asynchronous update is already pending when UpdateDiff is called,
1160 // UpdateDiff does not schedule another update, in which case the callback
1161 // is not called.
1162 func (b *Buffer) UpdateDiff(callback func(bool)) {
1163         if b.updateDiffTimer != nil {
1164                 return
1165         }
1166
1167         lineCount := b.LinesNum()
1168         if b.diffBaseLineCount > lineCount {
1169                 lineCount = b.diffBaseLineCount
1170         }
1171
1172         if lineCount < 1000 {
1173                 b.updateDiffSync()
1174                 callback(true)
1175         } else if lineCount < 30000 {
1176                 b.updateDiffTimer = time.AfterFunc(500*time.Millisecond, func() {
1177                         b.updateDiffTimer = nil
1178                         b.updateDiffSync()
1179                         callback(false)
1180                 })
1181         } else {
1182                 // Don't compute diffs for very large files
1183                 b.diffLock.Lock()
1184                 b.diff = make(map[int]DiffStatus)
1185                 b.diffLock.Unlock()
1186                 callback(true)
1187         }
1188 }
1189
1190 // SetDiffBase sets the text that is used as the base for diffing the buffer content
1191 func (b *Buffer) SetDiffBase(diffBase []byte) {
1192         b.diffBase = diffBase
1193         if diffBase == nil {
1194                 b.diffBaseLineCount = 0
1195         } else {
1196                 b.diffBaseLineCount = strings.Count(string(diffBase), "\n")
1197         }
1198         b.UpdateDiff(func(synchronous bool) {
1199                 screen.Redraw()
1200         })
1201 }
1202
1203 // DiffStatus returns the diff status for a line in the buffer
1204 func (b *Buffer) DiffStatus(lineN int) DiffStatus {
1205         b.diffLock.RLock()
1206         defer b.diffLock.RUnlock()
1207         // Note that the zero value for DiffStatus is equal to DSUnchanged
1208         return b.diff[lineN]
1209 }
1210
1211 // SearchMatch returns true if the given location is within a match of the last search.
1212 // It is used for search highlighting
1213 func (b *Buffer) SearchMatch(pos Loc) bool {
1214         return b.LineArray.SearchMatch(b, pos)
1215 }
1216
1217 // WriteLog writes a string to the log buffer
1218 func WriteLog(s string) {
1219         LogBuf.EventHandler.Insert(LogBuf.End(), s)
1220 }
1221
1222 // GetLogBuf returns the log buffer
1223 func GetLogBuf() *Buffer {
1224         return LogBuf
1225 }