]> git.lizzy.rs Git - micro.git/blob - cmd/micro/buffer.go
0d893fdadaa629becd3387ad069e98c7e73e291c
[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 // CheckModTime makes sure that the file this buffer points to hasn't been updated
186 // by an external program since it was last read
187 // If it has, we ask the user if they would like to reload the file
188 func (b *Buffer) CheckModTime() {
189         modTime, ok := GetModTime(b.Path)
190         if ok {
191                 if modTime != b.ModTime {
192                         choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)")
193                         messenger.Reset()
194                         messenger.Clear()
195                         if !choice || canceled {
196                                 // Don't load new changes -- do nothing
197                                 b.ModTime, _ = GetModTime(b.Path)
198                         } else {
199                                 // Load new changes
200                                 b.ReOpen()
201                         }
202                 }
203         }
204 }
205
206 // ReOpen reloads the current buffer from disk
207 func (b *Buffer) ReOpen() {
208         data, err := ioutil.ReadFile(b.Path)
209         txt := string(data)
210
211         if err != nil {
212                 messenger.Error(err.Error())
213                 return
214         }
215         b.EventHandler.ApplyDiff(txt)
216
217         b.ModTime, _ = GetModTime(b.Path)
218         b.IsModified = false
219         b.Update()
220         b.Cursor.Relocate()
221 }
222
223 // Update fetches the string from the rope and updates the `text` and `lines` in the buffer
224 func (b *Buffer) Update() {
225         b.NumLines = len(b.lines)
226 }
227
228 // Save saves the buffer to its default path
229 func (b *Buffer) Save() error {
230         return b.SaveAs(b.Path)
231 }
232
233 // SaveWithSudo saves the buffer to the default path with sudo
234 func (b *Buffer) SaveWithSudo() error {
235         return b.SaveAsWithSudo(b.Path)
236 }
237
238 // Serialize serializes the buffer to configDir/buffers
239 func (b *Buffer) Serialize() error {
240         if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
241                 absPath, _ := filepath.Abs(b.Path)
242                 file, err := os.Create(configDir + "/buffers/" + EscapePath(absPath))
243                 if err == nil {
244                         enc := gob.NewEncoder(file)
245                         gob.Register(TextEvent{})
246                         err = enc.Encode(SerializedBuffer{
247                                 b.EventHandler,
248                                 b.Cursor,
249                                 b.ModTime,
250                         })
251                 }
252                 file.Close()
253                 return err
254         }
255         return nil
256 }
257
258 // SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
259 func (b *Buffer) SaveAs(filename string) error {
260         b.FindFileType()
261         b.UpdateRules()
262         b.Name = filename
263         b.Path = filename
264         data := []byte(b.String())
265         err := ioutil.WriteFile(filename, data, 0644)
266         if err == nil {
267                 b.IsModified = false
268                 b.ModTime, _ = GetModTime(filename)
269                 return b.Serialize()
270         }
271         return err
272 }
273
274 // SaveAsWithSudo is the same as SaveAs except it uses a neat trick
275 // with tee to use sudo so the user doesn't have to reopen micro with sudo
276 func (b *Buffer) SaveAsWithSudo(filename string) error {
277         b.FindFileType()
278         b.UpdateRules()
279         b.Name = filename
280         b.Path = filename
281
282         // The user may have already used sudo in which case we won't need the password
283         // It's a bit nicer for them if they don't have to enter the password every time
284         _, err := RunShellCommand("sudo -v")
285         needPassword := err != nil
286
287         // If we need the password, we have to close the screen and ask using the shell
288         if needPassword {
289                 // Shut down the screen because we're going to interact directly with the shell
290                 screen.Fini()
291                 screen = nil
292         }
293
294         // Set up everything for the command
295         cmd := exec.Command("sudo", "tee", filename)
296         cmd.Stdin = bytes.NewBufferString(b.String())
297
298         // This is a trap for Ctrl-C so that it doesn't kill micro
299         // Instead we trap Ctrl-C to kill the program we're running
300         c := make(chan os.Signal, 1)
301         signal.Notify(c, os.Interrupt)
302         go func() {
303                 for range c {
304                         cmd.Process.Kill()
305                 }
306         }()
307
308         // Start the command
309         cmd.Start()
310         err = cmd.Wait()
311
312         // If we needed the password, we closed the screen, so we have to initialize it again
313         if needPassword {
314                 // Start the screen back up
315                 InitScreen()
316         }
317         if err == nil {
318                 b.IsModified = false
319                 b.ModTime, _ = GetModTime(filename)
320                 b.Serialize()
321         }
322         return err
323 }
324
325 func (b *Buffer) insert(pos Loc, value []byte) {
326         b.IsModified = true
327         b.LineArray.insert(pos, value)
328         b.Update()
329 }
330 func (b *Buffer) remove(start, end Loc) string {
331         b.IsModified = true
332         sub := b.LineArray.remove(start, end)
333         b.Update()
334         return sub
335 }
336
337 // Start returns the location of the first character in the buffer
338 func (b *Buffer) Start() Loc {
339         return Loc{0, 0}
340 }
341
342 // End returns the location of the last character in the buffer
343 func (b *Buffer) End() Loc {
344         return Loc{utf8.RuneCount(b.lines[b.NumLines-1]), b.NumLines - 1}
345 }
346
347 // Line returns a single line
348 func (b *Buffer) Line(n int) string {
349         return string(b.lines[n])
350 }
351
352 // Lines returns an array of strings containing the lines from start to end
353 func (b *Buffer) Lines(start, end int) []string {
354         lines := b.lines[start:end]
355         var slice []string
356         for _, line := range lines {
357                 slice = append(slice, string(line))
358         }
359         return slice
360 }
361
362 // Len gives the length of the buffer
363 func (b *Buffer) Len() int {
364         return Count(b.String())
365 }