]> git.lizzy.rs Git - micro.git/blob - cmd/micro/buffer/buffer.go
Cursor improvements
[micro.git] / cmd / micro / buffer / buffer.go
1 package buffer
2
3 import (
4         "bufio"
5         "bytes"
6         "crypto/md5"
7         "encoding/gob"
8         "errors"
9         "io"
10         "io/ioutil"
11         "os"
12         "os/exec"
13         "os/signal"
14         "path/filepath"
15         "strings"
16         "time"
17         "unicode/utf8"
18
19         "github.com/zyedidia/micro/cmd/micro/config"
20         "github.com/zyedidia/micro/cmd/micro/highlight"
21
22         . "github.com/zyedidia/micro/cmd/micro/util"
23 )
24
25 // LargeFileThreshold is the number of bytes when fastdirty is forced
26 // because hashing is too slow
27 const LargeFileThreshold = 50000
28
29 // overwriteFile opens the given file for writing, truncating if one exists, and then calls
30 // the supplied function with the file as io.Writer object, also making sure the file is
31 // closed afterwards.
32 func overwriteFile(name string, fn func(io.Writer) error) (err error) {
33         var file *os.File
34
35         if file, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
36                 return
37         }
38
39         defer func() {
40                 if e := file.Close(); e != nil && err == nil {
41                         err = e
42                 }
43         }()
44
45         w := bufio.NewWriter(file)
46
47         if err = fn(w); err != nil {
48                 return
49         }
50
51         err = w.Flush()
52         return
53 }
54
55 // The BufType defines what kind of buffer this is
56 type BufType struct {
57         Kind     int
58         Readonly bool // The file cannot be edited
59         Scratch  bool // The file cannot be saved
60 }
61
62 var (
63         BTDefault = BufType{0, false, false}
64         BTHelp    = BufType{1, true, true}
65         BTLog     = BufType{2, true, true}
66         BTScratch = BufType{3, false, true}
67         BTRaw     = BufType{4, true, true}
68 )
69
70 type Buffer struct {
71         *LineArray
72         *EventHandler
73
74         cursors     []*Cursor
75         StartCursor Loc
76
77         // Path to the file on disk
78         Path string
79         // Absolute path to the file on disk
80         AbsPath string
81         // Name of the buffer on the status line
82         name string
83
84         // Whether or not the buffer has been modified since it was opened
85         isModified bool
86
87         // Stores the last modification time of the file the buffer is pointing to
88         ModTime time.Time
89
90         SyntaxDef   *highlight.Def
91         Highlighter *highlight.Highlighter
92
93         // Hash of the original buffer -- empty if fastdirty is on
94         origHash [md5.Size]byte
95
96         // Settings customized by the user
97         Settings map[string]interface{}
98
99         // Type of the buffer (e.g. help, raw, scratch etc..)
100         Type BufType
101 }
102
103 // The SerializedBuffer holds the types that get serialized when a buffer is saved
104 // These are used for the savecursor and saveundo options
105 type SerializedBuffer struct {
106         EventHandler *EventHandler
107         Cursor       Loc
108         ModTime      time.Time
109 }
110
111 // NewBufferFromFile opens a new buffer using the given path
112 // It will also automatically handle `~`, and line/column with filename:l:c
113 // It will return an empty buffer if the path does not exist
114 // and an error if the file is a directory
115 func NewBufferFromFile(path string) (*Buffer, error) {
116         var err error
117         filename, cursorPosition := GetPathAndCursorPosition(path)
118         filename, err = ReplaceHome(filename)
119         if err != nil {
120                 return nil, err
121         }
122
123         file, err := os.Open(filename)
124         fileInfo, _ := os.Stat(filename)
125
126         if err == nil && fileInfo.IsDir() {
127                 return nil, errors.New(filename + " is a directory")
128         }
129
130         defer file.Close()
131
132         var buf *Buffer
133         if err != nil {
134                 // File does not exist -- create an empty buffer with that name
135                 buf = NewBufferFromString("", filename)
136         } else {
137                 buf = NewBuffer(file, FSize(file), filename, cursorPosition)
138         }
139
140         return buf, nil
141 }
142
143 // NewBufferFromString creates a new buffer containing the given string
144 func NewBufferFromString(text, path string) *Buffer {
145         return NewBuffer(strings.NewReader(text), int64(len(text)), path, nil)
146 }
147
148 // NewBuffer creates a new buffer from a given reader with a given path
149 // Ensure that ReadSettings and InitGlobalSettings have been called before creating
150 // a new buffer
151 func NewBuffer(reader io.Reader, size int64, path string, cursorPosition []string) *Buffer {
152         b := new(Buffer)
153
154         b.Settings = config.DefaultLocalSettings()
155         for k, v := range config.GlobalSettings {
156                 if _, ok := b.Settings[k]; ok {
157                         b.Settings[k] = v
158                 }
159         }
160         config.InitLocalSettings(b.Settings, b.Path)
161
162         b.LineArray = NewLineArray(uint64(size), FFAuto, reader)
163
164         absPath, _ := filepath.Abs(path)
165
166         b.Path = path
167         b.AbsPath = absPath
168
169         // The last time this file was modified
170         b.ModTime, _ = GetModTime(b.Path)
171
172         b.EventHandler = NewEventHandler(b)
173
174         b.UpdateRules()
175
176         if _, err := os.Stat(config.ConfigDir + "/buffers/"); os.IsNotExist(err) {
177                 os.Mkdir(config.ConfigDir+"/buffers/", os.ModePerm)
178         }
179
180         // cursorLocation, err := GetBufferCursorLocation(cursorPosition, b)
181         // b.startcursor = Cursor{
182         //      Loc: cursorLocation,
183         //      buf: b,
184         // }
185         // TODO flagstartpos
186         if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
187                 err := b.Unserialize()
188                 if err != nil {
189                         TermMessage(err)
190                 }
191         }
192
193         if !b.Settings["fastdirty"].(bool) {
194                 if size > LargeFileThreshold {
195                         // If the file is larger than LargeFileThreshold fastdirty needs to be on
196                         b.Settings["fastdirty"] = true
197                 } else {
198                         calcHash(b, &b.origHash)
199                 }
200         }
201
202         return b
203 }
204
205 // GetName returns the name that should be displayed in the statusline
206 // for this buffer
207 func (b *Buffer) GetName() string {
208         if b.name == "" {
209                 if b.Path == "" {
210                         return "No name"
211                 }
212                 return b.Path
213         }
214         return b.name
215 }
216
217 // FileType returns the buffer's filetype
218 func (b *Buffer) FileType() string {
219         return b.Settings["filetype"].(string)
220 }
221
222 // ReOpen reloads the current buffer from disk
223 func (b *Buffer) ReOpen() error {
224         data, err := ioutil.ReadFile(b.Path)
225         txt := string(data)
226
227         if err != nil {
228                 return err
229         }
230         b.EventHandler.ApplyDiff(txt)
231
232         b.ModTime, err = GetModTime(b.Path)
233         b.isModified = false
234         return err
235         // TODO: buffer cursor
236         // b.Cursor.Relocate()
237 }
238
239 // Saving
240
241 // Save saves the buffer to its default path
242 func (b *Buffer) Save() error {
243         return b.SaveAs(b.Path)
244 }
245
246 // SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
247 func (b *Buffer) SaveAs(filename string) error {
248         // TODO: rmtrailingws and updaterules
249         b.UpdateRules()
250         // if b.Settings["rmtrailingws"].(bool) {
251         //      for i, l := range b.lines {
252         //              pos := len(bytes.TrimRightFunc(l.data, unicode.IsSpace))
253         //
254         //              if pos < len(l.data) {
255         //                      b.deleteToEnd(Loc{pos, i})
256         //              }
257         //      }
258         //
259         //      b.Cursor.Relocate()
260         // }
261
262         if b.Settings["eofnewline"].(bool) {
263                 end := b.End()
264                 if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
265                         b.Insert(end, "\n")
266                 }
267         }
268
269         // Update the last time this file was updated after saving
270         defer func() {
271                 b.ModTime, _ = GetModTime(filename)
272         }()
273
274         // Removes any tilde and replaces with the absolute path to home
275         absFilename, _ := ReplaceHome(filename)
276
277         // TODO: save creates parent dirs
278         // // Get the leading path to the file | "." is returned if there's no leading path provided
279         // if dirname := filepath.Dir(absFilename); dirname != "." {
280         //      // Check if the parent dirs don't exist
281         //      if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) {
282         //              // Prompt to make sure they want to create the dirs that are missing
283         //              if yes, canceled := messenger.YesNoPrompt("Parent folders \"" + dirname + "\" do not exist. Create them? (y,n)"); yes && !canceled {
284         //                      // Create all leading dir(s) since they don't exist
285         //                      if mkdirallErr := os.MkdirAll(dirname, os.ModePerm); mkdirallErr != nil {
286         //                              // If there was an error creating the dirs
287         //                              return mkdirallErr
288         //                      }
289         //              } else {
290         //                      // If they canceled the creation of leading dirs
291         //                      return errors.New("Save aborted")
292         //              }
293         //      }
294         // }
295
296         var fileSize int
297
298         err := overwriteFile(absFilename, func(file io.Writer) (e error) {
299                 if len(b.lines) == 0 {
300                         return
301                 }
302
303                 // end of line
304                 var eol []byte
305                 if b.Settings["fileformat"] == "dos" {
306                         eol = []byte{'\r', '\n'}
307                 } else {
308                         eol = []byte{'\n'}
309                 }
310
311                 // write lines
312                 if fileSize, e = file.Write(b.lines[0].data); e != nil {
313                         return
314                 }
315
316                 for _, l := range b.lines[1:] {
317                         if _, e = file.Write(eol); e != nil {
318                                 return
319                         }
320                         if _, e = file.Write(l.data); e != nil {
321                                 return
322                         }
323                         fileSize += len(eol) + len(l.data)
324                 }
325                 return
326         })
327
328         if err != nil {
329                 return err
330         }
331
332         if !b.Settings["fastdirty"].(bool) {
333                 if fileSize > LargeFileThreshold {
334                         // For large files 'fastdirty' needs to be on
335                         b.Settings["fastdirty"] = true
336                 } else {
337                         calcHash(b, &b.origHash)
338                 }
339         }
340
341         b.Path = filename
342         absPath, _ := filepath.Abs(filename)
343         b.AbsPath = absPath
344         b.isModified = false
345         return b.Serialize()
346 }
347
348 // SaveWithSudo saves the buffer to the default path with sudo
349 func (b *Buffer) SaveWithSudo() error {
350         return b.SaveAsWithSudo(b.Path)
351 }
352
353 // SaveAsWithSudo is the same as SaveAs except it uses a neat trick
354 // with tee to use sudo so the user doesn't have to reopen micro with sudo
355 func (b *Buffer) SaveAsWithSudo(filename string) error {
356         b.UpdateRules()
357         b.Path = filename
358         absPath, _ := filepath.Abs(filename)
359         b.AbsPath = absPath
360
361         // Set up everything for the command
362         cmd := exec.Command(config.GlobalSettings["sucmd"].(string), "tee", filename)
363         cmd.Stdin = bytes.NewBuffer(b.Bytes())
364
365         // This is a trap for Ctrl-C so that it doesn't kill micro
366         // Instead we trap Ctrl-C to kill the program we're running
367         c := make(chan os.Signal, 1)
368         signal.Notify(c, os.Interrupt)
369         go func() {
370                 for range c {
371                         cmd.Process.Kill()
372                 }
373         }()
374
375         // Start the command
376         cmd.Start()
377         err := cmd.Wait()
378
379         if err == nil {
380                 b.isModified = false
381                 b.ModTime, _ = GetModTime(filename)
382                 return b.Serialize()
383         }
384         return err
385 }
386
387 func (b *Buffer) SetCursors(c []*Cursor) {
388         b.cursors = c
389 }
390
391 func (b *Buffer) GetActiveCursor() *Cursor {
392         return b.cursors[0]
393 }
394
395 func (b *Buffer) GetCursor(n int) *Cursor {
396         return b.cursors[n]
397 }
398
399 func (b *Buffer) GetCursors() []*Cursor {
400         return b.cursors
401 }
402
403 func (b *Buffer) NumCursors() int {
404         return len(b.cursors)
405 }
406
407 func (b *Buffer) LineBytes(n int) []byte {
408         if n >= len(b.lines) || n < 0 {
409                 return []byte{}
410         }
411         return b.lines[n].data
412 }
413
414 func (b *Buffer) LinesNum() int {
415         return len(b.lines)
416 }
417
418 func (b *Buffer) Start() Loc {
419         return Loc{0, 0}
420 }
421
422 // End returns the location of the last character in the buffer
423 func (b *Buffer) End() Loc {
424         numlines := len(b.lines)
425         return Loc{utf8.RuneCount(b.lines[numlines-1].data), numlines - 1}
426 }
427
428 // RuneAt returns the rune at a given location in the buffer
429 func (b *Buffer) RuneAt(loc Loc) rune {
430         line := b.LineBytes(loc.Y)
431         if len(line) > 0 {
432                 i := 0
433                 for len(line) > 0 {
434                         r, size := utf8.DecodeRune(line)
435                         line = line[size:]
436                         i++
437
438                         if i == loc.X {
439                                 return r
440                         }
441                 }
442         }
443         return '\n'
444 }
445
446 // Modified returns if this buffer has been modified since
447 // being opened
448 func (b *Buffer) Modified() bool {
449         if b.Settings["fastdirty"].(bool) {
450                 return b.isModified
451         }
452
453         var buff [md5.Size]byte
454
455         calcHash(b, &buff)
456         return buff != b.origHash
457 }
458
459 // calcHash calculates md5 hash of all lines in the buffer
460 func calcHash(b *Buffer, out *[md5.Size]byte) {
461         h := md5.New()
462
463         if len(b.lines) > 0 {
464                 h.Write(b.lines[0].data)
465
466                 for _, l := range b.lines[1:] {
467                         h.Write([]byte{'\n'})
468                         h.Write(l.data)
469                 }
470         }
471
472         h.Sum((*out)[:0])
473 }
474
475 func init() {
476         gob.Register(TextEvent{})
477         gob.Register(SerializedBuffer{})
478 }
479
480 // Serialize serializes the buffer to config.ConfigDir/buffers
481 func (b *Buffer) Serialize() error {
482         if !b.Settings["savecursor"].(bool) && !b.Settings["saveundo"].(bool) {
483                 return nil
484         }
485
486         name := config.ConfigDir + "/buffers/" + EscapePath(b.AbsPath)
487
488         return overwriteFile(name, func(file io.Writer) error {
489                 return gob.NewEncoder(file).Encode(SerializedBuffer{
490                         b.EventHandler,
491                         b.GetActiveCursor().Loc,
492                         b.ModTime,
493                 })
494         })
495 }
496
497 func (b *Buffer) Unserialize() error {
498         // If either savecursor or saveundo is turned on, we need to load the serialized information
499         // from ~/.config/micro/buffers
500         file, err := os.Open(config.ConfigDir + "/buffers/" + EscapePath(b.AbsPath))
501         defer file.Close()
502         if err == nil {
503                 var buffer SerializedBuffer
504                 decoder := gob.NewDecoder(file)
505                 gob.Register(TextEvent{})
506                 err = decoder.Decode(&buffer)
507                 if err != nil {
508                         return errors.New(err.Error() + "\nYou may want to remove the files in ~/.config/micro/buffers (these files store the information for the 'saveundo' and 'savecursor' options) if this problem persists.")
509                 }
510                 if b.Settings["savecursor"].(bool) {
511                         b.StartCursor = buffer.Cursor
512                 }
513
514                 if b.Settings["saveundo"].(bool) {
515                         // We should only use last time's eventhandler if the file wasn't modified by someone else in the meantime
516                         if b.ModTime == buffer.ModTime {
517                                 b.EventHandler = buffer.EventHandler
518                                 b.EventHandler.buf = b
519                         }
520                 }
521         }
522         return err
523 }
524
525 // UpdateRules updates the syntax rules and filetype for this buffer
526 // This is called when the colorscheme changes
527 func (b *Buffer) UpdateRules() {
528         rehighlight := false
529         var files []*highlight.File
530         for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
531                 data, err := f.Data()
532                 if err != nil {
533                         TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
534                 } else {
535                         file, err := highlight.ParseFile(data)
536                         if err != nil {
537                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
538                                 continue
539                         }
540                         ftdetect, err := highlight.ParseFtDetect(file)
541                         if err != nil {
542                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
543                                 continue
544                         }
545
546                         ft := b.Settings["filetype"].(string)
547                         if (ft == "Unknown" || ft == "") && !rehighlight {
548                                 if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
549                                         header := new(highlight.Header)
550                                         header.FileType = file.FileType
551                                         header.FtDetect = ftdetect
552                                         b.SyntaxDef, err = highlight.ParseDef(file, header)
553                                         if err != nil {
554                                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
555                                                 continue
556                                         }
557                                         rehighlight = true
558                                 }
559                         } else {
560                                 if file.FileType == ft && !rehighlight {
561                                         header := new(highlight.Header)
562                                         header.FileType = file.FileType
563                                         header.FtDetect = ftdetect
564                                         b.SyntaxDef, err = highlight.ParseDef(file, header)
565                                         if err != nil {
566                                                 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
567                                                 continue
568                                         }
569                                         rehighlight = true
570                                 }
571                         }
572                         files = append(files, file)
573                 }
574         }
575
576         if b.SyntaxDef != nil {
577                 highlight.ResolveIncludes(b.SyntaxDef, files)
578         }
579
580         if b.Highlighter == nil || rehighlight {
581                 if b.SyntaxDef != nil {
582                         b.Settings["filetype"] = b.SyntaxDef.FileType
583                         b.Highlighter = highlight.NewHighlighter(b.SyntaxDef)
584                         if b.Settings["syntax"].(bool) {
585                                 b.Highlighter.HighlightStates(b)
586                         }
587                 }
588         }
589 }