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