]> git.lizzy.rs Git - micro.git/blob - cmd/micro/buffer.go
0357db52ab8cd304899e02f519879df9e328ea86
[micro.git] / cmd / micro / buffer.go
1 package main
2
3 import (
4         "encoding/gob"
5         "io/ioutil"
6         "os"
7         "path/filepath"
8         "strings"
9         "time"
10
11         "github.com/vinzmay/go-rope"
12 )
13
14 // Buffer stores the text for files that are loaded into the text editor
15 // It uses a rope to efficiently store the string and contains some
16 // simple functions for saving and wrapper functions for modifying the rope
17 type Buffer struct {
18         // The eventhandler for undo/redo
19         *EventHandler
20
21         // Stores the text of the buffer
22         r *rope.Rope
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         IsModified bool
32
33         // Stores the last modification time of the file the buffer is pointing to
34         ModTime time.Time
35
36         // Provide efficient and easy access to text and lines so the rope String does not
37         // need to be constantly recalculated
38         // These variables are updated in the update() function
39         Lines    []string
40         NumLines int
41
42         // Syntax highlighting rules
43         rules []SyntaxRule
44         // The buffer's filetype
45         FileType string
46 }
47
48 // The SerializedBuffer holds the types that get serialized when a buffer is saved
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, path string) *Buffer {
57         b := new(Buffer)
58         if txt == "" {
59                 b.r = new(rope.Rope)
60         } else {
61                 b.r = rope.New(txt)
62         }
63         b.Path = path
64         b.Name = path
65
66         b.ModTime, _ = GetModTime(b.Path)
67
68         b.EventHandler = NewEventHandler(b)
69
70         b.Update()
71         b.UpdateRules()
72
73         if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
74                 os.Mkdir(configDir+"/buffers/", os.ModePerm)
75         }
76
77         // Put the cursor at the first spot
78         b.Cursor = Cursor{
79                 X:   0,
80                 Y:   0,
81                 buf: b,
82         }
83
84         if settings["savecursor"].(bool) || settings["saveundo"].(bool) {
85                 absPath, _ := filepath.Abs(b.Path)
86                 file, err := os.Open(configDir + "/buffers/" + EscapePath(absPath))
87                 if err == nil {
88                         var buffer SerializedBuffer
89                         decoder := gob.NewDecoder(file)
90                         gob.Register(TextEvent{})
91                         err = decoder.Decode(&buffer)
92                         if err != nil {
93                                 TermMessage(err.Error())
94                         }
95                         if settings["savecursor"].(bool) {
96                                 b.Cursor = buffer.Cursor
97                                 b.Cursor.buf = b
98                                 b.Cursor.Relocate()
99                         }
100
101                         if settings["saveundo"].(bool) {
102                                 // We should only use last time's eventhandler if the file wasn't by someone else in the meantime
103                                 if b.ModTime == buffer.ModTime {
104                                         b.EventHandler = buffer.EventHandler
105                                         b.EventHandler.buf = b
106                                 }
107                         }
108                 }
109                 file.Close()
110         }
111
112         return b
113 }
114
115 // UpdateRules updates the syntax rules and filetype for this buffer
116 // This is called when the colorscheme changes
117 func (b *Buffer) UpdateRules() {
118         b.rules, b.FileType = GetRules(b)
119 }
120
121 func (b *Buffer) String() string {
122         if b.r.Len() != 0 {
123                 return b.r.String()
124         }
125         return ""
126 }
127
128 // CheckModTime makes sure that the file this buffer points to hasn't been updated
129 // by an external program since it was last read
130 // If it has, we ask the user if they would like to reload the file
131 func (b *Buffer) CheckModTime() {
132         modTime, ok := GetModTime(b.Path)
133         if ok {
134                 if modTime != b.ModTime {
135                         choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)")
136                         messenger.Reset()
137                         messenger.Clear()
138                         if !choice || canceled {
139                                 // Don't load new changes -- do nothing
140                                 b.ModTime, _ = GetModTime(b.Path)
141                         } else {
142                                 // Load new changes
143                                 b.ReOpen()
144                         }
145                 }
146         }
147 }
148
149 // ReOpen reloads the current buffer from disk
150 func (b *Buffer) ReOpen() {
151         data, err := ioutil.ReadFile(b.Path)
152         txt := string(data)
153
154         if err != nil {
155                 messenger.Error(err.Error())
156                 return
157         }
158         b.EventHandler.ApplyDiff(txt)
159
160         b.ModTime, _ = GetModTime(b.Path)
161         b.IsModified = false
162         b.Update()
163         b.Cursor.Relocate()
164 }
165
166 // Update fetches the string from the rope and updates the `text` and `lines` in the buffer
167 func (b *Buffer) Update() {
168         b.Lines = strings.Split(b.String(), "\n")
169         b.NumLines = len(b.Lines)
170 }
171
172 // Save saves the buffer to its default path
173 func (b *Buffer) Save() error {
174         return b.SaveAs(b.Path)
175 }
176
177 // Serialize serializes the buffer to configDir/buffers
178 func (b *Buffer) Serialize() error {
179         if settings["savecursor"].(bool) || settings["saveundo"].(bool) {
180                 absPath, _ := filepath.Abs(b.Path)
181                 file, err := os.Create(configDir + "/buffers/" + EscapePath(absPath))
182                 if err == nil {
183                         enc := gob.NewEncoder(file)
184                         gob.Register(TextEvent{})
185                         err = enc.Encode(SerializedBuffer{
186                                 b.EventHandler,
187                                 b.Cursor,
188                                 b.ModTime,
189                         })
190                         // err = enc.Encode(b.Cursor)
191                 }
192                 file.Close()
193                 return err
194         }
195         return nil
196 }
197
198 // SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
199 func (b *Buffer) SaveAs(filename string) error {
200         b.UpdateRules()
201         b.Name = filename
202         b.Path = filename
203         data := []byte(b.String())
204         err := ioutil.WriteFile(filename, data, 0644)
205         if err == nil {
206                 b.IsModified = false
207                 b.ModTime, _ = GetModTime(filename)
208                 return b.Serialize()
209         }
210         return err
211 }
212
213 // This directly inserts value at idx, bypassing all undo/redo
214 func (b *Buffer) insert(idx int, value string) {
215         b.IsModified = true
216         b.r = b.r.Insert(idx, value)
217         b.Update()
218 }
219
220 // Remove a slice of the rope from start to end (exclusive)
221 // Returns the string that was removed
222 // This directly removes from start to end from the buffer, bypassing all undo/redo
223 func (b *Buffer) remove(start, end int) string {
224         b.IsModified = true
225         if start < 0 {
226                 start = 0
227         }
228         if end > b.Len() {
229                 end = b.Len()
230         }
231         if start == end {
232                 return ""
233         }
234         removed := b.Substr(start, end)
235         // The rope implenentation I am using wants indicies starting at 1 instead of 0
236         start++
237         end++
238         b.r = b.r.Delete(start, end-start)
239         b.Update()
240         return removed
241 }
242
243 // Substr returns the substring of the rope from start to end
244 func (b *Buffer) Substr(start, end int) string {
245         return b.r.Substr(start+1, end-start).String()
246 }
247
248 // Len gives the length of the buffer
249 func (b *Buffer) Len() int {
250         return b.r.Len()
251 }