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