]> git.lizzy.rs Git - micro.git/blob - cmd/micro/buffer/buffer.go
Fix serialization
[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 type Buffer struct {
67         *LineArray
68         *EventHandler
69
70         cursors     []*Cursor
71         StartCursor Loc
72
73         // Path to the file on disk
74         Path string
75         // Absolute path to the file on disk
76         AbsPath string
77         // Name of the buffer on the status line
78         name string
79
80         // Whether or not the buffer has been modified since it was opened
81         isModified bool
82
83         // Stores the last modification time of the file the buffer is pointing to
84         ModTime time.Time
85
86         SyntaxDef   *highlight.Def
87         Highlighter *highlight.Highlighter
88
89         // Hash of the original buffer -- empty if fastdirty is on
90         origHash [md5.Size]byte
91
92         // Settings customized by the user
93         Settings map[string]interface{}
94
95         // Type of the buffer (e.g. help, raw, scratch etc..)
96         Type BufType
97 }
98
99 // NewBufferFromFile opens a new buffer using the given path
100 // It will also automatically handle `~`, and line/column with filename:l:c
101 // It will return an empty buffer if the path does not exist
102 // and an error if the file is a directory
103 func NewBufferFromFile(path string) (*Buffer, error) {
104         var err error
105         filename, cursorPosition := GetPathAndCursorPosition(path)
106         filename, err = ReplaceHome(filename)
107         if err != nil {
108                 return nil, err
109         }
110
111         file, err := os.Open(filename)
112         fileInfo, _ := os.Stat(filename)
113
114         if err == nil && fileInfo.IsDir() {
115                 return nil, errors.New(filename + " is a directory")
116         }
117
118         defer file.Close()
119
120         var buf *Buffer
121         if err != nil {
122                 // File does not exist -- create an empty buffer with that name
123                 buf = NewBufferFromString("", filename)
124         } else {
125                 buf = NewBuffer(file, FSize(file), filename, cursorPosition)
126         }
127
128         return buf, nil
129 }
130
131 // NewBufferFromString creates a new buffer containing the given string
132 func NewBufferFromString(text, path string) *Buffer {
133         return NewBuffer(strings.NewReader(text), int64(len(text)), path, nil)
134 }
135
136 // NewBuffer creates a new buffer from a given reader with a given path
137 // Ensure that ReadSettings and InitGlobalSettings have been called before creating
138 // a new buffer
139 func NewBuffer(reader io.Reader, size int64, path string, cursorPosition []string) *Buffer {
140         b := new(Buffer)
141
142         b.Settings = config.DefaultLocalSettings()
143         for k, v := range config.GlobalSettings {
144                 if _, ok := b.Settings[k]; ok {
145                         b.Settings[k] = v
146                 }
147         }
148         config.InitLocalSettings(b.Settings, b.Path)
149
150         b.LineArray = NewLineArray(uint64(size), FFAuto, reader)
151
152         absPath, _ := filepath.Abs(path)
153
154         b.Path = path
155         b.AbsPath = absPath
156
157         // The last time this file was modified
158         b.ModTime, _ = GetModTime(b.Path)
159
160         b.EventHandler = NewEventHandler(b)
161
162         b.UpdateRules()
163
164         if _, err := os.Stat(config.ConfigDir + "/buffers/"); os.IsNotExist(err) {
165                 os.Mkdir(config.ConfigDir+"/buffers/", os.ModePerm)
166         }
167
168         // cursorLocation, err := GetBufferCursorLocation(cursorPosition, b)
169         // b.startcursor = Cursor{
170         //      Loc: cursorLocation,
171         //      buf: b,
172         // }
173         // TODO flagstartpos
174         if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
175                 err := b.Unserialize()
176                 if err != nil {
177                         TermMessage(err)
178                 }
179         }
180
181         if !b.Settings["fastdirty"].(bool) {
182                 if size > LargeFileThreshold {
183                         // If the file is larger than LargeFileThreshold fastdirty needs to be on
184                         b.Settings["fastdirty"] = true
185                 } else {
186                         calcHash(b, &b.origHash)
187                 }
188         }
189
190         return b
191 }
192
193 // GetName returns the name that should be displayed in the statusline
194 // for this buffer
195 func (b *Buffer) GetName() string {
196         if b.name == "" {
197                 if b.Path == "" {
198                         return "No name"
199                 }
200                 return b.Path
201         }
202         return b.name
203 }
204
205 // FileType returns the buffer's filetype
206 func (b *Buffer) FileType() string {
207         return b.Settings["filetype"].(string)
208 }
209
210 // ReOpen reloads the current buffer from disk
211 func (b *Buffer) ReOpen() error {
212         data, err := ioutil.ReadFile(b.Path)
213         txt := string(data)
214
215         if err != nil {
216                 return err
217         }
218         b.EventHandler.ApplyDiff(txt)
219
220         b.ModTime, err = GetModTime(b.Path)
221         b.isModified = false
222         return err
223         // TODO: buffer cursor
224         // b.Cursor.Relocate()
225 }
226
227 func (b *Buffer) SetCursors(c []*Cursor) {
228         b.cursors = c
229 }
230
231 func (b *Buffer) GetActiveCursor() *Cursor {
232         return b.cursors[0]
233 }
234
235 func (b *Buffer) GetCursor(n int) *Cursor {
236         return b.cursors[n]
237 }
238
239 func (b *Buffer) GetCursors() []*Cursor {
240         return b.cursors
241 }
242
243 func (b *Buffer) NumCursors() int {
244         return len(b.cursors)
245 }
246
247 func (b *Buffer) LineBytes(n int) []byte {
248         if n >= len(b.lines) || n < 0 {
249                 return []byte{}
250         }
251         return b.lines[n].data
252 }
253
254 func (b *Buffer) LinesNum() int {
255         return len(b.lines)
256 }
257
258 func (b *Buffer) Start() Loc {
259         return Loc{0, 0}
260 }
261
262 // End returns the location of the last character in the buffer
263 func (b *Buffer) End() Loc {
264         numlines := len(b.lines)
265         return Loc{utf8.RuneCount(b.lines[numlines-1].data), numlines - 1}
266 }
267
268 // RuneAt returns the rune at a given location in the buffer
269 func (b *Buffer) RuneAt(loc Loc) rune {
270         line := b.LineBytes(loc.Y)
271         if len(line) > 0 {
272                 i := 0
273                 for len(line) > 0 {
274                         r, size := utf8.DecodeRune(line)
275                         line = line[size:]
276                         i++
277
278                         if i == loc.X {
279                                 return r
280                         }
281                 }
282         }
283         return '\n'
284 }
285
286 // Modified returns if this buffer has been modified since
287 // being opened
288 func (b *Buffer) Modified() bool {
289         if b.Settings["fastdirty"].(bool) {
290                 return b.isModified
291         }
292
293         var buff [md5.Size]byte
294
295         calcHash(b, &buff)
296         return buff != b.origHash
297 }
298
299 // calcHash calculates md5 hash of all lines in the buffer
300 func calcHash(b *Buffer, out *[md5.Size]byte) {
301         h := md5.New()
302
303         if len(b.lines) > 0 {
304                 h.Write(b.lines[0].data)
305
306                 for _, l := range b.lines[1:] {
307                         h.Write([]byte{'\n'})
308                         h.Write(l.data)
309                 }
310         }
311
312         h.Sum((*out)[:0])
313 }
314
315 func (b *Buffer) insert(pos Loc, value []byte) {
316         b.isModified = true
317         b.LineArray.insert(pos, value)
318 }
319 func (b *Buffer) remove(start, end Loc) []byte {
320         b.isModified = true
321         sub := b.LineArray.remove(start, end)
322         return sub
323 }
324 func (b *Buffer) deleteToEnd(start Loc) {
325         b.isModified = true
326         b.LineArray.deleteToEnd(start)
327 }
328
329 // UpdateRules updates the syntax rules and filetype for this buffer
330 // This is called when the colorscheme changes
331 func (b *Buffer) UpdateRules() {
332         rehighlight := false
333         var files []*highlight.File
334         for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
335                 data, err := f.Data()
336                 if err != nil {
337                         TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
338                 } else {
339                         file, err := highlight.ParseFile(data)
340                         if err != nil {
341                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
342                                 continue
343                         }
344                         ftdetect, err := highlight.ParseFtDetect(file)
345                         if err != nil {
346                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
347                                 continue
348                         }
349
350                         ft := b.Settings["filetype"].(string)
351                         if (ft == "Unknown" || ft == "") && !rehighlight {
352                                 if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
353                                         header := new(highlight.Header)
354                                         header.FileType = file.FileType
355                                         header.FtDetect = ftdetect
356                                         b.SyntaxDef, err = highlight.ParseDef(file, header)
357                                         if err != nil {
358                                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
359                                                 continue
360                                         }
361                                         rehighlight = true
362                                 }
363                         } else {
364                                 if file.FileType == ft && !rehighlight {
365                                         header := new(highlight.Header)
366                                         header.FileType = file.FileType
367                                         header.FtDetect = ftdetect
368                                         b.SyntaxDef, err = highlight.ParseDef(file, header)
369                                         if err != nil {
370                                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
371                                                 continue
372                                         }
373                                         rehighlight = true
374                                 }
375                         }
376                         files = append(files, file)
377                 }
378         }
379
380         if b.SyntaxDef != nil {
381                 highlight.ResolveIncludes(b.SyntaxDef, files)
382         }
383
384         if b.Highlighter == nil || rehighlight {
385                 if b.SyntaxDef != nil {
386                         b.Settings["filetype"] = b.SyntaxDef.FileType
387                         b.Highlighter = highlight.NewHighlighter(b.SyntaxDef)
388                         if b.Settings["syntax"].(bool) {
389                                 b.Highlighter.HighlightStates(b)
390                         }
391                 }
392         }
393 }