]> git.lizzy.rs Git - micro.git/blob - cmd/micro/buffer.go
c74ee3b743e4705ff3423b6318f287537bf0387d
[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         if txt == "" {
159                 b.r = new(rope.Rope)
160         } else {
161                 b.r = rope.New(txt)
162         }
163
164         b.ModTime, _ = GetModTime(b.Path)
165         b.Cursor.Relocate()
166         b.IsModified = false
167         b.Update()
168 }
169
170 // Update fetches the string from the rope and updates the `text` and `lines` in the buffer
171 func (b *Buffer) Update() {
172         b.Lines = strings.Split(b.String(), "\n")
173         b.NumLines = len(b.Lines)
174 }
175
176 // Save saves the buffer to its default path
177 func (b *Buffer) Save() error {
178         return b.SaveAs(b.Path)
179 }
180
181 // Serialize serializes the buffer to configDir/buffers
182 func (b *Buffer) Serialize() error {
183         if settings["savecursor"].(bool) || settings["saveundo"].(bool) {
184                 absPath, _ := filepath.Abs(b.Path)
185                 file, err := os.Create(configDir + "/buffers/" + EscapePath(absPath))
186                 if err == nil {
187                         enc := gob.NewEncoder(file)
188                         gob.Register(TextEvent{})
189                         err = enc.Encode(SerializedBuffer{
190                                 b.EventHandler,
191                                 b.Cursor,
192                                 b.ModTime,
193                         })
194                         // err = enc.Encode(b.Cursor)
195                 }
196                 file.Close()
197                 return err
198         }
199         return nil
200 }
201
202 // SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
203 func (b *Buffer) SaveAs(filename string) error {
204         b.UpdateRules()
205         b.Name = filename
206         b.Path = filename
207         data := []byte(b.String())
208         err := ioutil.WriteFile(filename, data, 0644)
209         if err == nil {
210                 b.IsModified = false
211                 b.ModTime, _ = GetModTime(filename)
212                 return b.Serialize()
213         }
214         return err
215 }
216
217 // This directly inserts value at idx, bypassing all undo/redo
218 func (b *Buffer) insert(idx int, value string) {
219         b.IsModified = true
220         b.r = b.r.Insert(idx, value)
221         b.Update()
222 }
223
224 // Remove a slice of the rope from start to end (exclusive)
225 // Returns the string that was removed
226 // This directly removes from start to end from the buffer, bypassing all undo/redo
227 func (b *Buffer) remove(start, end int) string {
228         b.IsModified = true
229         if start < 0 {
230                 start = 0
231         }
232         if end > b.Len() {
233                 end = b.Len()
234         }
235         if start == end {
236                 return ""
237         }
238         removed := b.Substr(start, end)
239         // The rope implenentation I am using wants indicies starting at 1 instead of 0
240         start++
241         end++
242         b.r = b.r.Delete(start, end-start)
243         b.Update()
244         return removed
245 }
246
247 // Substr returns the substring of the rope from start to end
248 func (b *Buffer) Substr(start, end int) string {
249         return b.r.Substr(start+1, end-start).String()
250 }
251
252 // Len gives the length of the buffer
253 func (b *Buffer) Len() int {
254         return b.r.Len()
255 }