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