]> git.lizzy.rs Git - micro.git/blob - internal/buffer/buffer.go
Better backup behavior
[micro.git] / internal / buffer / buffer.go
1 package buffer
2
3 import (
4         "bytes"
5         "crypto/md5"
6         "errors"
7         "fmt"
8         "io"
9         "io/ioutil"
10         "log"
11         "os"
12         "path/filepath"
13         "strconv"
14         "strings"
15         "time"
16         "unicode/utf8"
17
18         luar "layeh.com/gopher-luar"
19
20         "github.com/zyedidia/micro/internal/config"
21         ulua "github.com/zyedidia/micro/internal/lua"
22         "github.com/zyedidia/micro/internal/screen"
23         . "github.com/zyedidia/micro/internal/util"
24         "github.com/zyedidia/micro/pkg/highlight"
25         "golang.org/x/text/encoding/htmlindex"
26         "golang.org/x/text/encoding/unicode"
27         "golang.org/x/text/transform"
28 )
29
30 const backup_time = 8000
31
32 var (
33         OpenBuffers []*Buffer
34         LogBuf      *Buffer
35 )
36
37 // The BufType defines what kind of buffer this is
38 type BufType struct {
39         Kind     int
40         Readonly bool // The buffer cannot be edited
41         Scratch  bool // The buffer cannot be saved
42         Syntax   bool // Syntax highlighting is enabled
43 }
44
45 var (
46         BTDefault = BufType{0, false, false, true}
47         BTHelp    = BufType{1, true, true, true}
48         BTLog     = BufType{2, true, true, false}
49         BTScratch = BufType{3, false, true, false}
50         BTRaw     = BufType{4, false, true, false}
51         BTInfo    = BufType{5, false, true, false}
52
53         ErrFileTooLarge = errors.New("File is too large to hash")
54 )
55
56 type SharedBuffer struct {
57         *LineArray
58         // Stores the last modification time of the file the buffer is pointing to
59         ModTime time.Time
60         // Type of the buffer (e.g. help, raw, scratch etc..)
61         Type BufType
62
63         isModified bool
64         // Whether or not suggestions can be autocompleted must be shared because
65         // it changes based on how the buffer has changed
66         HasSuggestions bool
67 }
68
69 func (b *SharedBuffer) insert(pos Loc, value []byte) {
70         b.isModified = true
71         b.HasSuggestions = false
72         b.LineArray.insert(pos, value)
73 }
74 func (b *SharedBuffer) remove(start, end Loc) []byte {
75         b.isModified = true
76         b.HasSuggestions = false
77         return b.LineArray.remove(start, end)
78 }
79
80 // Buffer stores the main information about a currently open file including
81 // the actual text (in a LineArray), the undo/redo stack (in an EventHandler)
82 // all the cursors, the syntax highlighting info, the settings for the buffer
83 // and some misc info about modification time and path location.
84 // The syntax highlighting info must be stored with the buffer because the syntax
85 // highlighter attaches information to each line of the buffer for optimization
86 // purposes so it doesn't have to rehighlight everything on every update.
87 type Buffer struct {
88         *EventHandler
89         *SharedBuffer
90
91         cursors     []*Cursor
92         curCursor   int
93         StartCursor Loc
94
95         // Path to the file on disk
96         Path string
97         // Absolute path to the file on disk
98         AbsPath string
99         // Name of the buffer on the status line
100         name string
101
102         SyntaxDef   *highlight.Def
103         Highlighter *highlight.Highlighter
104
105         // Hash of the original buffer -- empty if fastdirty is on
106         origHash [md5.Size]byte
107
108         // Settings customized by the user
109         Settings map[string]interface{}
110
111         Suggestions   []string
112         Completions   []string
113         CurSuggestion int
114
115         Messages []*Message
116
117         // counts the number of edits
118         // resets every backup_time edits
119         lastbackup time.Time
120 }
121
122 // NewBufferFromFile opens a new buffer using the given path
123 // It will also automatically handle `~`, and line/column with filename:l:c
124 // It will return an empty buffer if the path does not exist
125 // and an error if the file is a directory
126 func NewBufferFromFile(path string, btype BufType) (*Buffer, error) {
127         var err error
128         filename, cursorPos := GetPathAndCursorPosition(path)
129         filename, err = ReplaceHome(filename)
130         if err != nil {
131                 return nil, err
132         }
133
134         file, err := os.Open(filename)
135         fileInfo, _ := os.Stat(filename)
136
137         if err == nil && fileInfo.IsDir() {
138                 return nil, errors.New(filename + " is a directory")
139         }
140
141         defer file.Close()
142
143         cursorLoc, cursorerr := ParseCursorLocation(cursorPos)
144         if cursorerr != nil {
145                 cursorLoc = Loc{-1, -1}
146         }
147
148         var buf *Buffer
149         if err != nil {
150                 // File does not exist -- create an empty buffer with that name
151                 buf = NewBufferFromString("", filename, btype)
152         } else {
153                 buf = NewBuffer(file, FSize(file), filename, cursorLoc, btype)
154         }
155
156         return buf, nil
157 }
158
159 // NewBufferFromString creates a new buffer containing the given string
160 func NewBufferFromString(text, path string, btype BufType) *Buffer {
161         return NewBuffer(strings.NewReader(text), int64(len(text)), path, Loc{-1, -1}, btype)
162 }
163
164 // NewBuffer creates a new buffer from a given reader with a given path
165 // Ensure that ReadSettings and InitGlobalSettings have been called before creating
166 // a new buffer
167 // Places the cursor at startcursor. If startcursor is -1, -1 places the
168 // cursor at an autodetected location (based on savecursor or :LINE:COL)
169 func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufType) *Buffer {
170         absPath, _ := filepath.Abs(path)
171
172         b := new(Buffer)
173
174         b.Settings = config.DefaultCommonSettings()
175         for k, v := range config.GlobalSettings {
176                 if _, ok := b.Settings[k]; ok {
177                         b.Settings[k] = v
178                 }
179         }
180         config.InitLocalSettings(b.Settings, path)
181
182         enc, err := htmlindex.Get(b.Settings["encoding"].(string))
183         if err != nil {
184                 enc = unicode.UTF8
185                 b.Settings["encoding"] = "utf-8"
186         }
187
188         reader := transform.NewReader(r, enc.NewDecoder())
189
190         found := false
191         if len(path) > 0 {
192                 for _, buf := range OpenBuffers {
193                         if buf.AbsPath == absPath && buf.Type != BTInfo {
194                                 found = true
195                                 b.SharedBuffer = buf.SharedBuffer
196                                 b.EventHandler = buf.EventHandler
197                         }
198                 }
199         }
200
201         if !found {
202                 choice := 1 // ignore by default
203
204                 b.SharedBuffer = new(SharedBuffer)
205                 b.Type = btype
206
207                 if b.Settings["backup"].(bool) {
208                         backupfile := config.ConfigDir + "/backups/" + EscapePath(absPath)
209                         if info, err := os.Stat(backupfile); err == nil {
210                                 backup, err := os.Open(backupfile)
211                                 if err == nil {
212                                         t := info.ModTime()
213                                         msg := fmt.Sprintf(backupMsg, t.Format("Mon Jan _2 15:04 2006"))
214                                         choice = screen.TermPrompt(msg, []string{"r", "i", "recover", "ignore"}, true)
215                                         log.Println("Choice:", choice)
216
217                                         if choice%2 == 0 {
218                                                 // recover
219                                                 b.LineArray = NewLineArray(uint64(size), FFAuto, backup)
220                                                 b.isModified = true
221                                         } else if choice%2 == 1 {
222                                                 // delete
223                                                 os.Remove(backupfile)
224                                         }
225                                         backup.Close()
226                                 }
227                         }
228                 }
229
230                 if choice > 0 {
231                         b.LineArray = NewLineArray(uint64(size), FFAuto, reader)
232                 }
233                 b.EventHandler = NewEventHandler(b.SharedBuffer, b.cursors)
234         }
235
236         if b.Settings["readonly"].(bool) {
237                 b.Type.Readonly = true
238         }
239
240         b.Path = path
241         b.AbsPath = absPath
242
243         // The last time this file was modified
244         b.ModTime, _ = GetModTime(b.Path)
245
246         switch b.Endings {
247         case FFUnix:
248                 b.Settings["fileformat"] = "unix"
249         case FFDos:
250                 b.Settings["fileformat"] = "dos"
251         }
252
253         b.UpdateRules()
254         config.InitLocalSettings(b.Settings, b.Path)
255
256         if _, err := os.Stat(config.ConfigDir + "/buffers/"); os.IsNotExist(err) {
257                 os.Mkdir(config.ConfigDir+"/buffers/", os.ModePerm)
258         }
259
260         if startcursor.X != -1 && startcursor.Y != -1 {
261                 b.StartCursor = startcursor
262         } else {
263                 if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
264                         err := b.Unserialize()
265                         if err != nil {
266                                 screen.TermMessage(err)
267                         }
268                 }
269         }
270
271         b.AddCursor(NewCursor(b, b.StartCursor))
272         b.GetActiveCursor().Relocate()
273
274         if !b.Settings["fastdirty"].(bool) {
275                 if size > LargeFileThreshold {
276                         // If the file is larger than LargeFileThreshold fastdirty needs to be on
277                         b.Settings["fastdirty"] = true
278                 } else {
279                         calcHash(b, &b.origHash)
280                 }
281         }
282
283         err = config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b))
284         if err != nil {
285                 screen.TermMessage(err)
286         }
287
288         OpenBuffers = append(OpenBuffers, b)
289
290         return b
291 }
292
293 // Close removes this buffer from the list of open buffers
294 func (b *Buffer) Close() {
295         for i, buf := range OpenBuffers {
296                 if b == buf {
297                         b.Fini()
298                         copy(OpenBuffers[i:], OpenBuffers[i+1:])
299                         OpenBuffers[len(OpenBuffers)-1] = nil
300                         OpenBuffers = OpenBuffers[:len(OpenBuffers)-1]
301                         return
302                 }
303         }
304 }
305
306 // Fini should be called when a buffer is closed and performs
307 // some cleanup
308 func (b *Buffer) Fini() {
309         if !b.Modified() {
310                 b.Serialize()
311         }
312         b.RemoveBackup()
313 }
314
315 // GetName returns the name that should be displayed in the statusline
316 // for this buffer
317 func (b *Buffer) GetName() string {
318         if b.name == "" {
319                 if b.Path == "" {
320                         return "No name"
321                 }
322                 return b.Path
323         }
324         return b.name
325 }
326
327 //SetName changes the name for this buffer
328 func (b *Buffer) SetName(s string) {
329         b.name = s
330 }
331
332 func (b *Buffer) Insert(start Loc, text string) {
333         if !b.Type.Readonly {
334                 b.EventHandler.cursors = b.cursors
335                 b.EventHandler.active = b.curCursor
336                 b.EventHandler.Insert(start, text)
337
338                 go b.Backup(true)
339         }
340 }
341
342 func (b *Buffer) Remove(start, end Loc) {
343         if !b.Type.Readonly {
344                 b.EventHandler.cursors = b.cursors
345                 b.EventHandler.active = b.curCursor
346                 b.EventHandler.Remove(start, end)
347
348                 go b.Backup(true)
349         }
350 }
351
352 // FileType returns the buffer's filetype
353 func (b *Buffer) FileType() string {
354         return b.Settings["filetype"].(string)
355 }
356
357 // ReOpen reloads the current buffer from disk
358 func (b *Buffer) ReOpen() error {
359         file, err := os.Open(b.Path)
360         if err != nil {
361                 return err
362         }
363
364         enc, err := htmlindex.Get(b.Settings["encoding"].(string))
365         if err != nil {
366                 return err
367         }
368
369         reader := transform.NewReader(file, enc.NewDecoder())
370         data, err := ioutil.ReadAll(reader)
371         txt := string(data)
372
373         if err != nil {
374                 return err
375         }
376         b.EventHandler.ApplyDiff(txt)
377
378         b.ModTime, err = GetModTime(b.Path)
379         b.isModified = false
380         b.RelocateCursors()
381         return err
382 }
383
384 func (b *Buffer) RelocateCursors() {
385         for _, c := range b.cursors {
386                 c.Relocate()
387         }
388 }
389
390 // RuneAt returns the rune at a given location in the buffer
391 func (b *Buffer) RuneAt(loc Loc) rune {
392         line := b.LineBytes(loc.Y)
393         if len(line) > 0 {
394                 i := 0
395                 for len(line) > 0 {
396                         r, size := utf8.DecodeRune(line)
397                         line = line[size:]
398                         i++
399
400                         if i == loc.X {
401                                 return r
402                         }
403                 }
404         }
405         return '\n'
406 }
407
408 // Modified returns if this buffer has been modified since
409 // being opened
410 func (b *Buffer) Modified() bool {
411         if b.Type.Scratch {
412                 return false
413         }
414
415         if b.Settings["fastdirty"].(bool) {
416                 return b.isModified
417         }
418
419         var buff [md5.Size]byte
420
421         calcHash(b, &buff)
422         return buff != b.origHash
423 }
424
425 // calcHash calculates md5 hash of all lines in the buffer
426 func calcHash(b *Buffer, out *[md5.Size]byte) error {
427         h := md5.New()
428
429         size := 0
430         if len(b.lines) > 0 {
431                 n, e := h.Write(b.lines[0].data)
432                 if e != nil {
433                         return e
434                 }
435                 size += n
436
437                 for _, l := range b.lines[1:] {
438                         n, e = h.Write([]byte{'\n'})
439                         if e != nil {
440                                 return e
441                         }
442                         size += n
443                         n, e = h.Write(l.data)
444                         if e != nil {
445                                 return e
446                         }
447                         size += n
448                 }
449         }
450
451         if size > LargeFileThreshold {
452                 return ErrFileTooLarge
453         }
454
455         h.Sum((*out)[:0])
456         return nil
457 }
458
459 // UpdateRules updates the syntax rules and filetype for this buffer
460 // This is called when the colorscheme changes
461 func (b *Buffer) UpdateRules() {
462         if !b.Type.Syntax {
463                 return
464         }
465         rehighlight := false
466         var files []*highlight.File
467         for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
468                 data, err := f.Data()
469                 if err != nil {
470                         screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
471                 } else {
472                         file, err := highlight.ParseFile(data)
473                         if err != nil {
474                                 screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
475                                 continue
476                         }
477                         ftdetect, err := highlight.ParseFtDetect(file)
478                         if err != nil {
479                                 screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
480                                 continue
481                         }
482
483                         ft := b.Settings["filetype"].(string)
484                         if (ft == "unknown" || ft == "") && !rehighlight {
485                                 if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
486                                         header := new(highlight.Header)
487                                         header.FileType = file.FileType
488                                         header.FtDetect = ftdetect
489                                         b.SyntaxDef, err = highlight.ParseDef(file, header)
490                                         if err != nil {
491                                                 screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
492                                                 continue
493                                         }
494                                         rehighlight = true
495                                 }
496                         } else {
497                                 if file.FileType == ft && !rehighlight {
498                                         header := new(highlight.Header)
499                                         header.FileType = file.FileType
500                                         header.FtDetect = ftdetect
501                                         b.SyntaxDef, err = highlight.ParseDef(file, header)
502                                         if err != nil {
503                                                 screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
504                                                 continue
505                                         }
506                                         rehighlight = true
507                                 }
508                         }
509                         files = append(files, file)
510                 }
511         }
512
513         if b.SyntaxDef != nil {
514                 highlight.ResolveIncludes(b.SyntaxDef, files)
515         }
516
517         if b.Highlighter == nil || rehighlight {
518                 if b.SyntaxDef != nil {
519                         b.Settings["filetype"] = b.SyntaxDef.FileType
520                         b.Highlighter = highlight.NewHighlighter(b.SyntaxDef)
521                         if b.Settings["syntax"].(bool) {
522                                 b.Highlighter.HighlightStates(b)
523                         }
524                 }
525         }
526 }
527
528 // ClearMatches clears all of the syntax highlighting for the buffer
529 func (b *Buffer) ClearMatches() {
530         for i := range b.lines {
531                 b.SetMatch(i, nil)
532                 b.SetState(i, nil)
533         }
534 }
535
536 // IndentString returns this buffer's indent method (a tabstop or n spaces
537 // depending on the settings)
538 func (b *Buffer) IndentString(tabsize int) string {
539         if b.Settings["tabstospaces"].(bool) {
540                 return Spaces(tabsize)
541         }
542         return "\t"
543 }
544
545 // SetCursors resets this buffer's cursors to a new list
546 func (b *Buffer) SetCursors(c []*Cursor) {
547         b.cursors = c
548         b.EventHandler.cursors = b.cursors
549         b.EventHandler.active = b.curCursor
550 }
551
552 // AddCursor adds a new cursor to the list
553 func (b *Buffer) AddCursor(c *Cursor) {
554         b.cursors = append(b.cursors, c)
555         b.EventHandler.cursors = b.cursors
556         b.EventHandler.active = b.curCursor
557         b.UpdateCursors()
558 }
559
560 // SetCurCursor sets the current cursor
561 func (b *Buffer) SetCurCursor(n int) {
562         b.curCursor = n
563 }
564
565 // GetActiveCursor returns the main cursor in this buffer
566 func (b *Buffer) GetActiveCursor() *Cursor {
567         return b.cursors[b.curCursor]
568 }
569
570 // GetCursor returns the nth cursor
571 func (b *Buffer) GetCursor(n int) *Cursor {
572         return b.cursors[n]
573 }
574
575 // GetCursors returns the list of cursors in this buffer
576 func (b *Buffer) GetCursors() []*Cursor {
577         return b.cursors
578 }
579
580 // NumCursors returns the number of cursors
581 func (b *Buffer) NumCursors() int {
582         return len(b.cursors)
583 }
584
585 // MergeCursors merges any cursors that are at the same position
586 // into one cursor
587 func (b *Buffer) MergeCursors() {
588         var cursors []*Cursor
589         for i := 0; i < len(b.cursors); i++ {
590                 c1 := b.cursors[i]
591                 if c1 != nil {
592                         for j := 0; j < len(b.cursors); j++ {
593                                 c2 := b.cursors[j]
594                                 if c2 != nil && i != j && c1.Loc == c2.Loc {
595                                         b.cursors[j] = nil
596                                 }
597                         }
598                         cursors = append(cursors, c1)
599                 }
600         }
601
602         b.cursors = cursors
603
604         for i := range b.cursors {
605                 b.cursors[i].Num = i
606         }
607
608         if b.curCursor >= len(b.cursors) {
609                 b.curCursor = len(b.cursors) - 1
610         }
611         b.EventHandler.cursors = b.cursors
612         b.EventHandler.active = b.curCursor
613 }
614
615 // UpdateCursors updates all the cursors indicies
616 func (b *Buffer) UpdateCursors() {
617         b.EventHandler.cursors = b.cursors
618         b.EventHandler.active = b.curCursor
619         for i, c := range b.cursors {
620                 c.Num = i
621         }
622 }
623
624 func (b *Buffer) RemoveCursor(i int) {
625         copy(b.cursors[i:], b.cursors[i+1:])
626         b.cursors[len(b.cursors)-1] = nil
627         b.cursors = b.cursors[:len(b.cursors)-1]
628         b.curCursor = Clamp(b.curCursor, 0, len(b.cursors)-1)
629         b.UpdateCursors()
630 }
631
632 // ClearCursors removes all extra cursors
633 func (b *Buffer) ClearCursors() {
634         for i := 1; i < len(b.cursors); i++ {
635                 b.cursors[i] = nil
636         }
637         b.cursors = b.cursors[:1]
638         b.UpdateCursors()
639         b.curCursor = 0
640         b.GetActiveCursor().ResetSelection()
641 }
642
643 // MoveLinesUp moves the range of lines up one row
644 func (b *Buffer) MoveLinesUp(start int, end int) {
645         if start < 1 || start >= end || end > len(b.lines) {
646                 return
647         }
648         l := string(b.LineBytes(start - 1))
649         if end == len(b.lines) {
650                 b.Insert(
651                         Loc{
652                                 utf8.RuneCount(b.lines[end-1].data),
653                                 end - 1,
654                         },
655                         "\n"+l,
656                 )
657         } else {
658                 b.Insert(
659                         Loc{0, end},
660                         l+"\n",
661                 )
662         }
663         b.Remove(
664                 Loc{0, start - 1},
665                 Loc{0, start},
666         )
667 }
668
669 // MoveLinesDown moves the range of lines down one row
670 func (b *Buffer) MoveLinesDown(start int, end int) {
671         if start < 0 || start >= end || end >= len(b.lines)-1 {
672                 return
673         }
674         l := string(b.LineBytes(end))
675         b.Insert(
676                 Loc{0, start},
677                 l+"\n",
678         )
679         end++
680         b.Remove(
681                 Loc{0, end},
682                 Loc{0, end + 1},
683         )
684 }
685
686 var BracePairs = [][2]rune{
687         {'(', ')'},
688         {'{', '}'},
689         {'[', ']'},
690 }
691
692 // FindMatchingBrace returns the location in the buffer of the matching bracket
693 // It is given a brace type containing the open and closing character, (for example
694 // '{' and '}') as well as the location to match from
695 // TODO: maybe can be more efficient with utf8 package
696 // returns the location of the matching brace
697 // if the boolean returned is true then the original matching brace is one character left
698 // of the starting location
699 func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool) {
700         curLine := []rune(string(b.LineBytes(start.Y)))
701         startChar := ' '
702         if start.X >= 0 && start.X < len(curLine) {
703                 startChar = curLine[start.X]
704         }
705         leftChar := ' '
706         if start.X-1 >= 0 && start.X-1 < len(curLine) {
707                 leftChar = curLine[start.X-1]
708         }
709         var i int
710         if startChar == braceType[0] || leftChar == braceType[0] {
711                 for y := start.Y; y < b.LinesNum(); y++ {
712                         l := []rune(string(b.LineBytes(y)))
713                         xInit := 0
714                         if y == start.Y {
715                                 if startChar == braceType[0] {
716                                         xInit = start.X
717                                 } else {
718                                         xInit = start.X - 1
719                                 }
720                         }
721                         for x := xInit; x < len(l); x++ {
722                                 r := l[x]
723                                 if r == braceType[0] {
724                                         i++
725                                 } else if r == braceType[1] {
726                                         i--
727                                         if i == 0 {
728                                                 if startChar == braceType[0] {
729                                                         return Loc{x, y}, false
730                                                 }
731                                                 return Loc{x, y}, true
732                                         }
733                                 }
734                         }
735                 }
736         } else if startChar == braceType[1] || leftChar == braceType[1] {
737                 for y := start.Y; y >= 0; y-- {
738                         l := []rune(string(b.lines[y].data))
739                         xInit := len(l) - 1
740                         if y == start.Y {
741                                 if leftChar == braceType[1] {
742                                         xInit = start.X - 1
743                                 } else {
744                                         xInit = start.X
745                                 }
746                         }
747                         for x := xInit; x >= 0; x-- {
748                                 r := l[x]
749                                 if r == braceType[0] {
750                                         i--
751                                         if i == 0 {
752                                                 if leftChar == braceType[1] {
753                                                         return Loc{x, y}, true
754                                                 }
755                                                 return Loc{x, y}, false
756                                         }
757                                 } else if r == braceType[1] {
758                                         i++
759                                 }
760                         }
761                 }
762         }
763         return start, true
764 }
765
766 // Retab changes all tabs to spaces or vice versa
767 func (b *Buffer) Retab() {
768         toSpaces := b.Settings["tabstospaces"].(bool)
769         tabsize := IntOpt(b.Settings["tabsize"])
770         dirty := false
771
772         for i := 0; i < b.LinesNum(); i++ {
773                 l := b.LineBytes(i)
774
775                 ws := GetLeadingWhitespace(l)
776                 if len(ws) != 0 {
777                         if toSpaces {
778                                 ws = bytes.Replace(ws, []byte{'\t'}, bytes.Repeat([]byte{' '}, tabsize), -1)
779                         } else {
780                                 ws = bytes.Replace(ws, bytes.Repeat([]byte{' '}, tabsize), []byte{'\t'}, -1)
781                         }
782                 }
783
784                 l = bytes.TrimLeft(l, " \t")
785                 b.lines[i].data = append(ws, l...)
786                 dirty = true
787         }
788
789         b.isModified = dirty
790 }
791
792 // ParseCursorLocation turns a cursor location like 10:5 (LINE:COL)
793 // into a loc
794 func ParseCursorLocation(cursorPositions []string) (Loc, error) {
795         startpos := Loc{0, 0}
796         var err error
797
798         // if no positions are available exit early
799         if cursorPositions == nil {
800                 return startpos, errors.New("No cursor positions were provided.")
801         }
802
803         startpos.Y, err = strconv.Atoi(cursorPositions[0])
804         startpos.Y -= 1
805         if err == nil {
806                 if len(cursorPositions) > 1 {
807                         startpos.X, err = strconv.Atoi(cursorPositions[1])
808                         if startpos.X > 0 {
809                                 startpos.X -= 1
810                         }
811                 }
812         }
813
814         return startpos, err
815 }
816
817 func (b *Buffer) Line(i int) string {
818         return string(b.LineBytes(i))
819 }
820
821 func WriteLog(s string) {
822         LogBuf.EventHandler.Insert(LogBuf.End(), s)
823 }