]> git.lizzy.rs Git - micro.git/blob - cmd/micro/buffer.go
e533f9bbb12cbd26fcd4b8da087c1f9dcd149402
[micro.git] / cmd / micro / buffer.go
1 package main
2
3 import (
4         "bytes"
5         "crypto/md5"
6         "encoding/gob"
7         "io"
8         "io/ioutil"
9         "os"
10         "os/exec"
11         "os/signal"
12         "path/filepath"
13         "regexp"
14         "strconv"
15         "strings"
16         "time"
17         "unicode/utf8"
18
19         "github.com/zyedidia/micro/cmd/micro/highlight"
20 )
21
22 var (
23         // 0 - no line type detected
24         // 1 - lf detected
25         // 2 - crlf detected
26         fileformat = 0
27 )
28
29 // Buffer stores the text for files that are loaded into the text editor
30 // It uses a rope to efficiently store the string and contains some
31 // simple functions for saving and wrapper functions for modifying the rope
32 type Buffer struct {
33         // The eventhandler for undo/redo
34         *EventHandler
35         // This stores all the text in the buffer as an array of lines
36         *LineArray
37
38         Cursor    Cursor
39         cursors   []*Cursor // for multiple cursors
40         curCursor int       // the current cursor
41
42         // Path to the file on disk
43         Path string
44         // Absolute path to the file on disk
45         AbsPath string
46         // Name of the buffer on the status line
47         name string
48
49         // Whether or not the buffer has been modified since it was opened
50         IsModified bool
51
52         // Stores the last modification time of the file the buffer is pointing to
53         ModTime time.Time
54
55         NumLines int
56
57         syntaxDef   *highlight.Def
58         highlighter *highlight.Highlighter
59
60         // Hash of the original buffer -- empty if fastdirty is on
61         origHash [16]byte
62
63         // Buffer local settings
64         Settings map[string]interface{}
65 }
66
67 // The SerializedBuffer holds the types that get serialized when a buffer is saved
68 // These are used for the savecursor and saveundo options
69 type SerializedBuffer struct {
70         EventHandler *EventHandler
71         Cursor       Cursor
72         ModTime      time.Time
73 }
74
75 func NewBufferFromString(text, path string) *Buffer {
76         return NewBuffer(strings.NewReader(text), int64(len(text)), path)
77 }
78
79 // NewBuffer creates a new buffer from a given reader with a given path
80 func NewBuffer(reader io.Reader, size int64, path string) *Buffer {
81         if path != "" {
82                 for _, tab := range tabs {
83                         for _, view := range tab.views {
84                                 if view.Buf.Path == path {
85                                         return view.Buf
86                                 }
87                         }
88                 }
89         }
90
91         b := new(Buffer)
92         b.LineArray = NewLineArray(size, reader)
93
94         b.Settings = DefaultLocalSettings()
95         for k, v := range globalSettings {
96                 if _, ok := b.Settings[k]; ok {
97                         b.Settings[k] = v
98                 }
99         }
100
101         if fileformat == 1 {
102                 b.Settings["fileformat"] = "unix"
103         } else if fileformat == 2 {
104                 b.Settings["fileformat"] = "dos"
105         }
106
107         absPath, _ := filepath.Abs(path)
108
109         b.Path = path
110         b.AbsPath = absPath
111
112         // The last time this file was modified
113         b.ModTime, _ = GetModTime(b.Path)
114
115         b.EventHandler = NewEventHandler(b)
116
117         b.Update()
118         b.UpdateRules()
119
120         if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
121                 os.Mkdir(configDir+"/buffers/", os.ModePerm)
122         }
123
124         // Put the cursor at the first spot
125         cursorStartX := 0
126         cursorStartY := 0
127         // If -startpos LINE,COL was passed, use start position LINE,COL
128         if len(*flagStartPos) > 0 {
129                 positions := strings.Split(*flagStartPos, ",")
130                 if len(positions) == 2 {
131                         lineNum, errPos1 := strconv.Atoi(positions[0])
132                         colNum, errPos2 := strconv.Atoi(positions[1])
133                         if errPos1 == nil && errPos2 == nil {
134                                 cursorStartX = colNum
135                                 cursorStartY = lineNum - 1
136                                 // Check to avoid line overflow
137                                 if cursorStartY > b.NumLines {
138                                         cursorStartY = b.NumLines - 1
139                                 } else if cursorStartY < 0 {
140                                         cursorStartY = 0
141                                 }
142                                 // Check to avoid column overflow
143                                 if cursorStartX > len(b.Line(cursorStartY)) {
144                                         cursorStartX = len(b.Line(cursorStartY))
145                                 } else if cursorStartX < 0 {
146                                         cursorStartX = 0
147                                 }
148                         }
149                 }
150         }
151         b.Cursor = Cursor{
152                 Loc: Loc{
153                         X: cursorStartX,
154                         Y: cursorStartY,
155                 },
156                 buf: b,
157         }
158
159         InitLocalSettings(b)
160
161         if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
162                 // If either savecursor or saveundo is turned on, we need to load the serialized information
163                 // from ~/.config/micro/buffers
164                 file, err := os.Open(configDir + "/buffers/" + EscapePath(b.AbsPath))
165                 if err == nil {
166                         var buffer SerializedBuffer
167                         decoder := gob.NewDecoder(file)
168                         gob.Register(TextEvent{})
169                         err = decoder.Decode(&buffer)
170                         if err != nil {
171                                 TermMessage(err.Error(), "\n", "You may want to remove the files in ~/.config/micro/buffers (these files store the information for the 'saveundo' and 'savecursor' options) if this problem persists.")
172                         }
173                         if b.Settings["savecursor"].(bool) {
174                                 b.Cursor = buffer.Cursor
175                                 b.Cursor.buf = b
176                                 b.Cursor.Relocate()
177                         }
178
179                         if b.Settings["saveundo"].(bool) {
180                                 // We should only use last time's eventhandler if the file wasn't modified by someone else in the meantime
181                                 if b.ModTime == buffer.ModTime {
182                                         b.EventHandler = buffer.EventHandler
183                                         b.EventHandler.buf = b
184                                 }
185                         }
186                 }
187                 file.Close()
188         }
189
190         if !b.Settings["fastdirty"].(bool) {
191                 if size > 50000 {
192                         // If the file is larger than a megabyte fastdirty needs to be on
193                         b.Settings["fastdirty"] = true
194                 } else {
195                         b.origHash = md5.Sum([]byte(b.String()))
196                 }
197         }
198
199         b.cursors = []*Cursor{&b.Cursor}
200
201         return b
202 }
203
204 func (b *Buffer) GetName() string {
205         if b.name == "" {
206                 if b.Path == "" {
207                         return "No name"
208                 }
209                 return b.Path
210         }
211         return b.name
212 }
213
214 // UpdateRules updates the syntax rules and filetype for this buffer
215 // This is called when the colorscheme changes
216 func (b *Buffer) UpdateRules() {
217         rehighlight := false
218         var files []*highlight.File
219         for _, f := range ListRuntimeFiles(RTSyntax) {
220                 data, err := f.Data()
221                 if err != nil {
222                         TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
223                 } else {
224                         file, err := highlight.ParseFile(data)
225                         if err != nil {
226                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
227                                 continue
228                         }
229                         ftdetect, err := highlight.ParseFtDetect(file)
230                         if err != nil {
231                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
232                                 continue
233                         }
234
235                         ft := b.Settings["filetype"].(string)
236                         if (ft == "Unknown" || ft == "") && !rehighlight {
237                                 if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
238                                         header := new(highlight.Header)
239                                         header.FileType = file.FileType
240                                         header.FtDetect = ftdetect
241                                         b.syntaxDef, err = highlight.ParseDef(file, header)
242                                         if err != nil {
243                                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
244                                                 continue
245                                         }
246                                         rehighlight = true
247                                 }
248                         } else {
249                                 if file.FileType == ft && !rehighlight {
250                                         header := new(highlight.Header)
251                                         header.FileType = file.FileType
252                                         header.FtDetect = ftdetect
253                                         b.syntaxDef, err = highlight.ParseDef(file, header)
254                                         if err != nil {
255                                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
256                                                 continue
257                                         }
258                                         rehighlight = true
259                                 }
260                         }
261                         files = append(files, file)
262                 }
263         }
264
265         if b.syntaxDef != nil {
266                 highlight.ResolveIncludes(b.syntaxDef, files)
267         }
268
269         if b.highlighter == nil || rehighlight {
270                 if b.syntaxDef != nil {
271                         b.Settings["filetype"] = b.syntaxDef.FileType
272                         b.highlighter = highlight.NewHighlighter(b.syntaxDef)
273                         if b.Settings["syntax"].(bool) {
274                                 b.highlighter.HighlightStates(b)
275                         }
276                 }
277         }
278 }
279
280 // FileType returns the buffer's filetype
281 func (b *Buffer) FileType() string {
282         return b.Settings["filetype"].(string)
283 }
284
285 // IndentString returns a string representing one level of indentation
286 func (b *Buffer) IndentString() string {
287         if b.Settings["tabstospaces"].(bool) {
288                 return Spaces(int(b.Settings["tabsize"].(float64)))
289         }
290         return "\t"
291 }
292
293 // CheckModTime makes sure that the file this buffer points to hasn't been updated
294 // by an external program since it was last read
295 // If it has, we ask the user if they would like to reload the file
296 func (b *Buffer) CheckModTime() {
297         modTime, ok := GetModTime(b.Path)
298         if ok {
299                 if modTime != b.ModTime {
300                         choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)")
301                         messenger.Reset()
302                         messenger.Clear()
303                         if !choice || canceled {
304                                 // Don't load new changes -- do nothing
305                                 b.ModTime, _ = GetModTime(b.Path)
306                         } else {
307                                 // Load new changes
308                                 b.ReOpen()
309                         }
310                 }
311         }
312 }
313
314 // ReOpen reloads the current buffer from disk
315 func (b *Buffer) ReOpen() {
316         data, err := ioutil.ReadFile(b.Path)
317         txt := string(data)
318
319         if err != nil {
320                 messenger.Error(err.Error())
321                 return
322         }
323         b.EventHandler.ApplyDiff(txt)
324
325         b.ModTime, _ = GetModTime(b.Path)
326         b.IsModified = false
327         b.Update()
328         b.Cursor.Relocate()
329 }
330
331 // Update fetches the string from the rope and updates the `text` and `lines` in the buffer
332 func (b *Buffer) Update() {
333         b.NumLines = len(b.lines)
334 }
335
336 func (b *Buffer) MergeCursors() {
337         var cursors []*Cursor
338         for i := 0; i < len(b.cursors); i++ {
339                 c1 := b.cursors[i]
340                 if c1 != nil {
341                         for j := 0; j < len(b.cursors); j++ {
342                                 c2 := b.cursors[j]
343                                 if c2 != nil && i != j && c1.Loc == c2.Loc {
344                                         b.cursors[j] = nil
345                                 }
346                         }
347                         cursors = append(cursors, c1)
348                 }
349         }
350
351         b.cursors = cursors
352 }
353
354 func (b *Buffer) UpdateCursors() {
355         for i, c := range b.cursors {
356                 c.Num = i
357         }
358 }
359
360 // Save saves the buffer to its default path
361 func (b *Buffer) Save() error {
362         return b.SaveAs(b.Path)
363 }
364
365 // SaveWithSudo saves the buffer to the default path with sudo
366 func (b *Buffer) SaveWithSudo() error {
367         return b.SaveAsWithSudo(b.Path)
368 }
369
370 // Serialize serializes the buffer to configDir/buffers
371 func (b *Buffer) Serialize() error {
372         if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
373                 file, err := os.Create(configDir + "/buffers/" + EscapePath(b.AbsPath))
374                 if err == nil {
375                         enc := gob.NewEncoder(file)
376                         gob.Register(TextEvent{})
377                         err = enc.Encode(SerializedBuffer{
378                                 b.EventHandler,
379                                 b.Cursor,
380                                 b.ModTime,
381                         })
382                 }
383                 err = file.Close()
384                 return err
385         }
386         return nil
387 }
388
389 // SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
390 func (b *Buffer) SaveAs(filename string) error {
391         b.UpdateRules()
392         if b.Settings["rmtrailingws"].(bool) {
393                 r, _ := regexp.Compile(`[ \t]+$`)
394                 for lineNum, line := range b.Lines(0, b.NumLines) {
395                         indices := r.FindStringIndex(line)
396                         if indices == nil {
397                                 continue
398                         }
399                         startLoc := Loc{indices[0], lineNum}
400                         b.deleteToEnd(startLoc)
401                 }
402                 b.Cursor.Relocate()
403         }
404         if b.Settings["eofnewline"].(bool) {
405                 end := b.End()
406                 if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
407                         b.Insert(end, "\n")
408                 }
409         }
410         str := b.SaveString(b.Settings["fileformat"] == "dos")
411         data := []byte(str)
412         err := ioutil.WriteFile(ReplaceHome(filename), data, 0644)
413         if err == nil {
414                 b.Path = filename
415                 b.IsModified = false
416                 b.ModTime, _ = GetModTime(filename)
417                 return b.Serialize()
418         }
419         b.ModTime, _ = GetModTime(filename)
420         return err
421 }
422
423 // SaveAsWithSudo is the same as SaveAs except it uses a neat trick
424 // with tee to use sudo so the user doesn't have to reopen micro with sudo
425 func (b *Buffer) SaveAsWithSudo(filename string) error {
426         b.UpdateRules()
427         b.Path = filename
428
429         // Shut down the screen because we're going to interact directly with the shell
430         screen.Fini()
431         screen = nil
432
433         // Set up everything for the command
434         cmd := exec.Command(b.Settings["sucmd"].(string), "tee", filename)
435         cmd.Stdin = bytes.NewBufferString(b.SaveString(b.Settings["fileformat"] == "dos"))
436
437         // This is a trap for Ctrl-C so that it doesn't kill micro
438         // Instead we trap Ctrl-C to kill the program we're running
439         c := make(chan os.Signal, 1)
440         signal.Notify(c, os.Interrupt)
441         go func() {
442                 for range c {
443                         cmd.Process.Kill()
444                 }
445         }()
446
447         // Start the command
448         cmd.Start()
449         err := cmd.Wait()
450
451         // Start the screen back up
452         InitScreen()
453         if err == nil {
454                 b.IsModified = false
455                 b.ModTime, _ = GetModTime(filename)
456                 b.Serialize()
457         }
458         return err
459 }
460
461 func (b *Buffer) Modified() bool {
462         if b.Settings["fastdirty"].(bool) {
463                 return b.IsModified
464         }
465         return b.origHash != md5.Sum([]byte(b.String()))
466 }
467
468 func (b *Buffer) insert(pos Loc, value []byte) {
469         b.IsModified = true
470         b.LineArray.insert(pos, value)
471         b.Update()
472 }
473 func (b *Buffer) remove(start, end Loc) string {
474         b.IsModified = true
475         sub := b.LineArray.remove(start, end)
476         b.Update()
477         return sub
478 }
479 func (b *Buffer) deleteToEnd(start Loc) {
480         b.IsModified = true
481         b.LineArray.DeleteToEnd(start)
482         b.Update()
483 }
484
485 // Start returns the location of the first character in the buffer
486 func (b *Buffer) Start() Loc {
487         return Loc{0, 0}
488 }
489
490 // End returns the location of the last character in the buffer
491 func (b *Buffer) End() Loc {
492         return Loc{utf8.RuneCount(b.lines[b.NumLines-1].data), b.NumLines - 1}
493 }
494
495 // RuneAt returns the rune at a given location in the buffer
496 func (b *Buffer) RuneAt(loc Loc) rune {
497         line := []rune(b.Line(loc.Y))
498         if len(line) > 0 {
499                 return line[loc.X]
500         }
501         return '\n'
502 }
503
504 // Line returns a single line
505 func (b *Buffer) Line(n int) string {
506         if n >= len(b.lines) {
507                 return ""
508         }
509         return string(b.lines[n].data)
510 }
511
512 func (b *Buffer) LinesNum() int {
513         return len(b.lines)
514 }
515
516 // Lines returns an array of strings containing the lines from start to end
517 func (b *Buffer) Lines(start, end int) []string {
518         lines := b.lines[start:end]
519         var slice []string
520         for _, line := range lines {
521                 slice = append(slice, string(line.data))
522         }
523         return slice
524 }
525
526 // Len gives the length of the buffer
527 func (b *Buffer) Len() int {
528         return Count(b.String())
529 }
530
531 // MoveLinesUp moves the range of lines up one row
532 func (b *Buffer) MoveLinesUp(start int, end int) {
533         // 0 < start < end <= len(b.lines)
534         if start < 1 || start >= end || end > len(b.lines) {
535                 return // what to do? FIXME
536         }
537         if end == len(b.lines) {
538                 b.Insert(
539                         Loc{
540                                 utf8.RuneCount(b.lines[end-1].data),
541                                 end - 1,
542                         },
543                         "\n"+b.Line(start-1),
544                 )
545         } else {
546                 b.Insert(
547                         Loc{0, end},
548                         b.Line(start-1)+"\n",
549                 )
550         }
551         b.Remove(
552                 Loc{0, start - 1},
553                 Loc{0, start},
554         )
555 }
556
557 // MoveLinesDown moves the range of lines down one row
558 func (b *Buffer) MoveLinesDown(start int, end int) {
559         // 0 <= start < end < len(b.lines)
560         // if end == len(b.lines), we can't do anything here because the
561         // last line is unaccessible, FIXME
562         if start < 0 || start >= end || end >= len(b.lines)-1 {
563                 return // what to do? FIXME
564         }
565         b.Insert(
566                 Loc{0, start},
567                 b.Line(end)+"\n",
568         )
569         end++
570         b.Remove(
571                 Loc{0, end},
572                 Loc{0, end + 1},
573         )
574 }
575
576 // ClearMatches clears all of the syntax highlighting for this buffer
577 func (b *Buffer) ClearMatches() {
578         for i := range b.lines {
579                 b.SetMatch(i, nil)
580                 b.SetState(i, nil)
581         }
582 }
583
584 func (b *Buffer) clearCursors() {
585         for i := 1; i < len(b.cursors); i++ {
586                 b.cursors[i] = nil
587         }
588         b.cursors = b.cursors[:1]
589         b.UpdateCursors()
590         b.Cursor.ResetSelection()
591 }