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