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