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