]> git.lizzy.rs Git - micro.git/blob - cmd/micro/buffer/buffer.go
Add some comments
[micro.git] / cmd / micro / buffer / buffer.go
1 package buffer
2
3 import (
4         "bufio"
5         "crypto/md5"
6         "errors"
7         "io"
8         "io/ioutil"
9         "os"
10         "path/filepath"
11         "strings"
12         "time"
13         "unicode/utf8"
14
15         "github.com/zyedidia/micro/cmd/micro/config"
16         "github.com/zyedidia/micro/cmd/micro/highlight"
17
18         . "github.com/zyedidia/micro/cmd/micro/util"
19 )
20
21 // LargeFileThreshold is the number of bytes when fastdirty is forced
22 // because hashing is too slow
23 const LargeFileThreshold = 50000
24
25 // overwriteFile opens the given file for writing, truncating if one exists, and then calls
26 // the supplied function with the file as io.Writer object, also making sure the file is
27 // closed afterwards.
28 func overwriteFile(name string, fn func(io.Writer) error) (err error) {
29         var file *os.File
30
31         if file, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
32                 return
33         }
34
35         defer func() {
36                 if e := file.Close(); e != nil && err == nil {
37                         err = e
38                 }
39         }()
40
41         w := bufio.NewWriter(file)
42
43         if err = fn(w); err != nil {
44                 return
45         }
46
47         err = w.Flush()
48         return
49 }
50
51 // The BufType defines what kind of buffer this is
52 type BufType struct {
53         Kind     int
54         Readonly bool // The file cannot be edited
55         Scratch  bool // The file cannot be saved
56 }
57
58 var (
59         BTDefault = BufType{0, false, false}
60         BTHelp    = BufType{1, true, true}
61         BTLog     = BufType{2, true, true}
62         BTScratch = BufType{3, false, true}
63         BTRaw     = BufType{4, true, true}
64 )
65
66 // Buffer stores the main information about a currently open file including
67 // the actual text (in a LineArray), the undo/redo stack (in an EventHandler)
68 // all the cursors, the syntax highlighting info, the settings for the buffer
69 // and some misc info about modification time and path location.
70 // The syntax highlighting info must be stored with the buffer because the syntax
71 // highlighter attaches information to each line of the buffer for optimization
72 // purposes so it doesn't have to rehighlight everything on every update.
73 type Buffer struct {
74         *LineArray
75         *EventHandler
76
77         cursors     []*Cursor
78         StartCursor Loc
79
80         // Path to the file on disk
81         Path string
82         // Absolute path to the file on disk
83         AbsPath string
84         // Name of the buffer on the status line
85         name string
86
87         // Whether or not the buffer has been modified since it was opened
88         isModified bool
89
90         // Stores the last modification time of the file the buffer is pointing to
91         ModTime time.Time
92
93         SyntaxDef   *highlight.Def
94         Highlighter *highlight.Highlighter
95
96         // Hash of the original buffer -- empty if fastdirty is on
97         origHash [md5.Size]byte
98
99         // Settings customized by the user
100         Settings map[string]interface{}
101
102         // Type of the buffer (e.g. help, raw, scratch etc..)
103         Type BufType
104 }
105
106 // NewBufferFromFile opens a new buffer using the given path
107 // It will also automatically handle `~`, and line/column with filename:l:c
108 // It will return an empty buffer if the path does not exist
109 // and an error if the file is a directory
110 func NewBufferFromFile(path string) (*Buffer, error) {
111         var err error
112         filename, cursorPosition := GetPathAndCursorPosition(path)
113         filename, err = ReplaceHome(filename)
114         if err != nil {
115                 return nil, err
116         }
117
118         file, err := os.Open(filename)
119         fileInfo, _ := os.Stat(filename)
120
121         if err == nil && fileInfo.IsDir() {
122                 return nil, errors.New(filename + " is a directory")
123         }
124
125         defer file.Close()
126
127         var buf *Buffer
128         if err != nil {
129                 // File does not exist -- create an empty buffer with that name
130                 buf = NewBufferFromString("", filename)
131         } else {
132                 buf = NewBuffer(file, FSize(file), filename, cursorPosition)
133         }
134
135         return buf, nil
136 }
137
138 // NewBufferFromString creates a new buffer containing the given string
139 func NewBufferFromString(text, path string) *Buffer {
140         return NewBuffer(strings.NewReader(text), int64(len(text)), path, nil)
141 }
142
143 // NewBuffer creates a new buffer from a given reader with a given path
144 // Ensure that ReadSettings and InitGlobalSettings have been called before creating
145 // a new buffer
146 func NewBuffer(reader io.Reader, size int64, path string, cursorPosition []string) *Buffer {
147         b := new(Buffer)
148
149         b.Settings = config.DefaultLocalSettings()
150         for k, v := range config.GlobalSettings {
151                 if _, ok := b.Settings[k]; ok {
152                         b.Settings[k] = v
153                 }
154         }
155         config.InitLocalSettings(b.Settings, b.Path)
156
157         b.LineArray = NewLineArray(uint64(size), FFAuto, reader)
158
159         absPath, _ := filepath.Abs(path)
160
161         b.Path = path
162         b.AbsPath = absPath
163
164         // The last time this file was modified
165         b.ModTime, _ = GetModTime(b.Path)
166
167         b.EventHandler = NewEventHandler(b)
168
169         b.UpdateRules()
170
171         if _, err := os.Stat(config.ConfigDir + "/buffers/"); os.IsNotExist(err) {
172                 os.Mkdir(config.ConfigDir+"/buffers/", os.ModePerm)
173         }
174
175         // cursorLocation, err := GetBufferCursorLocation(cursorPosition, b)
176         // b.startcursor = Cursor{
177         //      Loc: cursorLocation,
178         //      buf: b,
179         // }
180         // TODO flagstartpos
181         if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
182                 err := b.Unserialize()
183                 if err != nil {
184                         TermMessage(err)
185                 }
186         }
187
188         if !b.Settings["fastdirty"].(bool) {
189                 if size > LargeFileThreshold {
190                         // If the file is larger than LargeFileThreshold fastdirty needs to be on
191                         b.Settings["fastdirty"] = true
192                 } else {
193                         calcHash(b, &b.origHash)
194                 }
195         }
196
197         return b
198 }
199
200 // GetName returns the name that should be displayed in the statusline
201 // for this buffer
202 func (b *Buffer) GetName() string {
203         if b.name == "" {
204                 if b.Path == "" {
205                         return "No name"
206                 }
207                 return b.Path
208         }
209         return b.name
210 }
211
212 // FileType returns the buffer's filetype
213 func (b *Buffer) FileType() string {
214         return b.Settings["filetype"].(string)
215 }
216
217 // ReOpen reloads the current buffer from disk
218 func (b *Buffer) ReOpen() error {
219         data, err := ioutil.ReadFile(b.Path)
220         txt := string(data)
221
222         if err != nil {
223                 return err
224         }
225         b.EventHandler.ApplyDiff(txt)
226
227         b.ModTime, err = GetModTime(b.Path)
228         b.isModified = false
229         return err
230         // TODO: buffer cursor
231         // b.Cursor.Relocate()
232 }
233
234 // SetCursors resets this buffer's cursors to a new list
235 func (b *Buffer) SetCursors(c []*Cursor) {
236         b.cursors = c
237 }
238
239 // GetActiveCursor returns the main cursor in this buffer
240 func (b *Buffer) GetActiveCursor() *Cursor {
241         return b.cursors[0]
242 }
243
244 // GetCursor returns the nth cursor
245 func (b *Buffer) GetCursor(n int) *Cursor {
246         return b.cursors[n]
247 }
248
249 // GetCursors returns the list of cursors in this buffer
250 func (b *Buffer) GetCursors() []*Cursor {
251         return b.cursors
252 }
253
254 // NumCursors returns the number of cursors
255 func (b *Buffer) NumCursors() int {
256         return len(b.cursors)
257 }
258
259 // LineBytes returns line n as an array of bytes
260 func (b *Buffer) LineBytes(n int) []byte {
261         if n >= len(b.lines) || n < 0 {
262                 return []byte{}
263         }
264         return b.lines[n].data
265 }
266
267 // LinesNum returns the number of lines in the buffer
268 func (b *Buffer) LinesNum() int {
269         return len(b.lines)
270 }
271
272 // Start returns the start of the buffer
273 func (b *Buffer) Start() Loc {
274         return Loc{0, 0}
275 }
276
277 // End returns the location of the last character in the buffer
278 func (b *Buffer) End() Loc {
279         numlines := len(b.lines)
280         return Loc{utf8.RuneCount(b.lines[numlines-1].data), numlines - 1}
281 }
282
283 // RuneAt returns the rune at a given location in the buffer
284 func (b *Buffer) RuneAt(loc Loc) rune {
285         line := b.LineBytes(loc.Y)
286         if len(line) > 0 {
287                 i := 0
288                 for len(line) > 0 {
289                         r, size := utf8.DecodeRune(line)
290                         line = line[size:]
291                         i++
292
293                         if i == loc.X {
294                                 return r
295                         }
296                 }
297         }
298         return '\n'
299 }
300
301 // Modified returns if this buffer has been modified since
302 // being opened
303 func (b *Buffer) Modified() bool {
304         if b.Settings["fastdirty"].(bool) {
305                 return b.isModified
306         }
307
308         var buff [md5.Size]byte
309
310         calcHash(b, &buff)
311         return buff != b.origHash
312 }
313
314 // calcHash calculates md5 hash of all lines in the buffer
315 func calcHash(b *Buffer, out *[md5.Size]byte) {
316         h := md5.New()
317
318         if len(b.lines) > 0 {
319                 h.Write(b.lines[0].data)
320
321                 for _, l := range b.lines[1:] {
322                         h.Write([]byte{'\n'})
323                         h.Write(l.data)
324                 }
325         }
326
327         h.Sum((*out)[:0])
328 }
329
330 func (b *Buffer) insert(pos Loc, value []byte) {
331         b.isModified = true
332         b.LineArray.insert(pos, value)
333 }
334 func (b *Buffer) remove(start, end Loc) []byte {
335         b.isModified = true
336         sub := b.LineArray.remove(start, end)
337         return sub
338 }
339 func (b *Buffer) deleteToEnd(start Loc) {
340         b.isModified = true
341         b.LineArray.deleteToEnd(start)
342 }
343
344 // UpdateRules updates the syntax rules and filetype for this buffer
345 // This is called when the colorscheme changes
346 func (b *Buffer) UpdateRules() {
347         rehighlight := false
348         var files []*highlight.File
349         for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
350                 data, err := f.Data()
351                 if err != nil {
352                         TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
353                 } else {
354                         file, err := highlight.ParseFile(data)
355                         if err != nil {
356                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
357                                 continue
358                         }
359                         ftdetect, err := highlight.ParseFtDetect(file)
360                         if err != nil {
361                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
362                                 continue
363                         }
364
365                         ft := b.Settings["filetype"].(string)
366                         if (ft == "Unknown" || ft == "") && !rehighlight {
367                                 if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
368                                         header := new(highlight.Header)
369                                         header.FileType = file.FileType
370                                         header.FtDetect = ftdetect
371                                         b.SyntaxDef, err = highlight.ParseDef(file, header)
372                                         if err != nil {
373                                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
374                                                 continue
375                                         }
376                                         rehighlight = true
377                                 }
378                         } else {
379                                 if file.FileType == ft && !rehighlight {
380                                         header := new(highlight.Header)
381                                         header.FileType = file.FileType
382                                         header.FtDetect = ftdetect
383                                         b.SyntaxDef, err = highlight.ParseDef(file, header)
384                                         if err != nil {
385                                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
386                                                 continue
387                                         }
388                                         rehighlight = true
389                                 }
390                         }
391                         files = append(files, file)
392                 }
393         }
394
395         if b.SyntaxDef != nil {
396                 highlight.ResolveIncludes(b.SyntaxDef, files)
397         }
398
399         if b.Highlighter == nil || rehighlight {
400                 if b.SyntaxDef != nil {
401                         b.Settings["filetype"] = b.SyntaxDef.FileType
402                         b.Highlighter = highlight.NewHighlighter(b.SyntaxDef)
403                         if b.Settings["syntax"].(bool) {
404                                 b.Highlighter.HighlightStates(b)
405                         }
406                 }
407         }
408 }
409
410 // IndentString returns this buffer's indent method (a tabstop or n spaces
411 // depending on the settings)
412 func (b *Buffer) IndentString(tabsize int) string {
413         if b.Settings["tabstospaces"].(bool) {
414                 return Spaces(tabsize)
415         }
416         return "\t"
417 }