]> git.lizzy.rs Git - micro.git/blob - cmd/micro/buffer.go
d73b4c308a680e0bdd3623115e79f38ad8b5d6f6
[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         "strings"
12         "time"
13         "unicode/utf8"
14 )
15
16 // Buffer stores the text for files that are loaded into the text editor
17 // It uses a rope to efficiently store the string and contains some
18 // simple functions for saving and wrapper functions for modifying the rope
19 type Buffer struct {
20         // The eventhandler for undo/redo
21         *EventHandler
22         // This stores all the text in the buffer as an array of lines
23         *LineArray
24
25         Cursor Cursor
26
27         // Path to the file on disk
28         Path string
29         // Name of the buffer on the status line
30         Name string
31
32         // Whether or not the buffer has been modified since it was opened
33         IsModified bool
34
35         // Stores the last modification time of the file the buffer is pointing to
36         ModTime time.Time
37
38         NumLines int
39
40         // Syntax highlighting rules
41         rules []SyntaxRule
42
43         // Buffer local settings
44         Settings map[string]interface{}
45 }
46
47 // The SerializedBuffer holds the types that get serialized when a buffer is saved
48 // These are used for the savecursor and saveundo options
49 type SerializedBuffer struct {
50         EventHandler *EventHandler
51         Cursor       Cursor
52         ModTime      time.Time
53 }
54
55 // NewBuffer creates a new buffer from `txt` with path and name `path`
56 func NewBuffer(txt []byte, path string) *Buffer {
57         b := new(Buffer)
58         b.LineArray = NewLineArray(txt)
59
60         b.Settings = DefaultLocalSettings()
61         for k, v := range globalSettings {
62                 if _, ok := b.Settings[k]; ok {
63                         b.Settings[k] = v
64                 }
65         }
66
67         b.Path = path
68         b.Name = path
69
70         // If the file doesn't have a path to disk then we give it no name
71         if path == "" {
72                 b.Name = "No name"
73         }
74
75         // The last time this file was modified
76         b.ModTime, _ = GetModTime(b.Path)
77
78         b.EventHandler = NewEventHandler(b)
79
80         b.Update()
81         b.FindFileType()
82         b.UpdateRules()
83
84         if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
85                 os.Mkdir(configDir+"/buffers/", os.ModePerm)
86         }
87
88         // Put the cursor at the first spot
89         b.Cursor = Cursor{
90                 Loc: Loc{
91                         X: 0,
92                         Y: 0,
93                 },
94                 buf: b,
95         }
96
97         if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
98                 // If either savecursor or saveundo is turned on, we need to load the serialized information
99                 // from ~/.config/micro/buffers
100                 absPath, _ := filepath.Abs(b.Path)
101                 file, err := os.Open(configDir + "/buffers/" + EscapePath(absPath))
102                 if err == nil {
103                         var buffer SerializedBuffer
104                         decoder := gob.NewDecoder(file)
105                         gob.Register(TextEvent{})
106                         err = decoder.Decode(&buffer)
107                         if err != nil {
108                                 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.")
109                         }
110                         if b.Settings["savecursor"].(bool) {
111                                 b.Cursor = buffer.Cursor
112                                 b.Cursor.buf = b
113                                 b.Cursor.Relocate()
114                         }
115
116                         if b.Settings["saveundo"].(bool) {
117                                 // We should only use last time's eventhandler if the file wasn't by someone else in the meantime
118                                 if b.ModTime == buffer.ModTime {
119                                         b.EventHandler = buffer.EventHandler
120                                         b.EventHandler.buf = b
121                                 }
122                         }
123                 }
124                 file.Close()
125         }
126
127         _, err := Call("onBufferOpen", b)
128         if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
129                 TermMessage(err)
130         }
131
132         return b
133 }
134
135 // UpdateRules updates the syntax rules and filetype for this buffer
136 // This is called when the colorscheme changes
137 func (b *Buffer) UpdateRules() {
138         b.rules = GetRules(b)
139 }
140
141 // FindFileType identifies this buffer's filetype based on the extension or header
142 func (b *Buffer) FindFileType() {
143         b.Settings["filetype"] = FindFileType(b)
144 }
145
146 // FileType returns the buffer's filetype
147 func (b *Buffer) FileType() string {
148         return b.Settings["filetype"].(string)
149 }
150
151 // CheckModTime makes sure that the file this buffer points to hasn't been updated
152 // by an external program since it was last read
153 // If it has, we ask the user if they would like to reload the file
154 func (b *Buffer) CheckModTime() {
155         modTime, ok := GetModTime(b.Path)
156         if ok {
157                 if modTime != b.ModTime {
158                         choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)")
159                         messenger.Reset()
160                         messenger.Clear()
161                         if !choice || canceled {
162                                 // Don't load new changes -- do nothing
163                                 b.ModTime, _ = GetModTime(b.Path)
164                         } else {
165                                 // Load new changes
166                                 b.ReOpen()
167                         }
168                 }
169         }
170 }
171
172 // ReOpen reloads the current buffer from disk
173 func (b *Buffer) ReOpen() {
174         data, err := ioutil.ReadFile(b.Path)
175         txt := string(data)
176
177         if err != nil {
178                 messenger.Error(err.Error())
179                 return
180         }
181         b.EventHandler.ApplyDiff(txt)
182
183         b.ModTime, _ = GetModTime(b.Path)
184         b.IsModified = false
185         b.Update()
186         b.Cursor.Relocate()
187 }
188
189 // Update fetches the string from the rope and updates the `text` and `lines` in the buffer
190 func (b *Buffer) Update() {
191         b.NumLines = len(b.lines)
192 }
193
194 // Save saves the buffer to its default path
195 func (b *Buffer) Save() error {
196         return b.SaveAs(b.Path)
197 }
198
199 // SaveWithSudo saves the buffer to the default path with sudo
200 func (b *Buffer) SaveWithSudo() error {
201         return b.SaveAsWithSudo(b.Path)
202 }
203
204 // Serialize serializes the buffer to configDir/buffers
205 func (b *Buffer) Serialize() error {
206         if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
207                 absPath, _ := filepath.Abs(b.Path)
208                 file, err := os.Create(configDir + "/buffers/" + EscapePath(absPath))
209                 if err == nil {
210                         enc := gob.NewEncoder(file)
211                         gob.Register(TextEvent{})
212                         err = enc.Encode(SerializedBuffer{
213                                 b.EventHandler,
214                                 b.Cursor,
215                                 b.ModTime,
216                         })
217                 }
218                 file.Close()
219                 return err
220         }
221         return nil
222 }
223
224 // SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
225 func (b *Buffer) SaveAs(filename string) error {
226         b.UpdateRules()
227         b.Name = filename
228         b.Path = filename
229         data := []byte(b.String())
230         err := ioutil.WriteFile(filename, data, 0644)
231         if err == nil {
232                 b.IsModified = false
233                 b.ModTime, _ = GetModTime(filename)
234                 return b.Serialize()
235         }
236         return err
237 }
238
239 // SaveAsWithSudo is the same as SaveAs except it uses a neat trick
240 // with tee to use sudo so the user doesn't have to reopen micro with sudo
241 func (b *Buffer) SaveAsWithSudo(filename string) error {
242         b.UpdateRules()
243         b.Name = filename
244         b.Path = filename
245
246         // The user may have already used sudo in which case we won't need the password
247         // It's a bit nicer for them if they don't have to enter the password every time
248         _, err := RunShellCommand("sudo -v")
249         needPassword := err != nil
250
251         // If we need the password, we have to close the screen and ask using the shell
252         if needPassword {
253                 // Shut down the screen because we're going to interact directly with the shell
254                 screen.Fini()
255                 screen = nil
256         }
257
258         // Set up everything for the command
259         cmd := exec.Command("sudo", "tee", filename)
260         cmd.Stdin = bytes.NewBufferString(b.String())
261
262         // This is a trap for Ctrl-C so that it doesn't kill micro
263         // Instead we trap Ctrl-C to kill the program we're running
264         c := make(chan os.Signal, 1)
265         signal.Notify(c, os.Interrupt)
266         go func() {
267                 for range c {
268                         cmd.Process.Kill()
269                 }
270         }()
271
272         // Start the command
273         cmd.Start()
274         err = cmd.Wait()
275
276         // If we needed the password, we closed the screen, so we have to initialize it again
277         if needPassword {
278                 // Start the screen back up
279                 InitScreen()
280         }
281         if err == nil {
282                 b.IsModified = false
283                 b.ModTime, _ = GetModTime(filename)
284                 b.Serialize()
285         }
286         return err
287 }
288
289 func (b *Buffer) insert(pos Loc, value []byte) {
290         b.IsModified = true
291         b.LineArray.insert(pos, value)
292         b.Update()
293 }
294 func (b *Buffer) remove(start, end Loc) string {
295         b.IsModified = true
296         sub := b.LineArray.remove(start, end)
297         b.Update()
298         return sub
299 }
300
301 // Start returns the location of the first character in the buffer
302 func (b *Buffer) Start() Loc {
303         return Loc{0, 0}
304 }
305
306 // End returns the location of the last character in the buffer
307 func (b *Buffer) End() Loc {
308         return Loc{utf8.RuneCount(b.lines[b.NumLines-1]), b.NumLines - 1}
309 }
310
311 // Line returns a single line
312 func (b *Buffer) Line(n int) string {
313         return string(b.lines[n])
314 }
315
316 // Lines returns an array of strings containing the lines from start to end
317 func (b *Buffer) Lines(start, end int) []string {
318         lines := b.lines[start:end]
319         var slice []string
320         for _, line := range lines {
321                 slice = append(slice, string(line))
322         }
323         return slice
324 }
325
326 // Len gives the length of the buffer
327 func (b *Buffer) Len() int {
328         return Count(b.String())
329 }