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