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