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