]> git.lizzy.rs Git - micro.git/blobdiff - cmd/micro/buffer.go
Code optimisation (#1117)
[micro.git] / cmd / micro / buffer.go
index f3f92d7629d5c36cb336eca8d99e83d8d2efe3cd..a9de2a7a3152dbc54ee0d0d91c6cced109f55855 100644 (file)
@@ -1,6 +1,7 @@
 package main
 
 import (
+       "bufio"
        "bytes"
        "crypto/md5"
        "encoding/gob"
@@ -11,15 +12,17 @@ import (
        "os/exec"
        "os/signal"
        "path/filepath"
-       "regexp"
        "strconv"
        "strings"
        "time"
+       "unicode"
        "unicode/utf8"
 
        "github.com/zyedidia/micro/cmd/micro/highlight"
 )
 
+const LargeFileThreshold = 50000
+
 var (
        // 0 - no line type detected
        // 1 - lf detected
@@ -60,7 +63,7 @@ type Buffer struct {
        highlighter *highlight.Highlighter
 
        // Hash of the original buffer -- empty if fastdirty is on
-       origHash [16]byte
+       origHash [md5.Size]byte
 
        // Buffer local settings
        Settings map[string]interface{}
@@ -74,6 +77,33 @@ type SerializedBuffer struct {
        ModTime      time.Time
 }
 
+// NewBufferFromFile opens a new buffer using the given filepath
+// It will also automatically handle `~`, and line/column with filename:l:c
+// It will return an empty buffer if the filepath does not exist
+// and an error if the file is a directory
+func NewBufferFromFile(path string) (*Buffer, error) {
+       filename := GetPath(path)
+       filename = ReplaceHome(filename)
+       file, err := os.Open(filename)
+       fileInfo, _ := os.Stat(filename)
+
+       if err == nil && fileInfo.IsDir() {
+               return nil, errors.New(filename + " is a directory")
+       }
+
+       defer file.Close()
+
+       var buf *Buffer
+       if err != nil {
+               // File does not exist -- create an empty buffer with that name
+               buf = NewBufferFromString("", path)
+       } else {
+               buf = NewBuffer(file, FSize(file), path)
+       }
+
+       return buf, nil
+}
+
 // NewBufferFromString creates a new buffer containing the given
 // string
 func NewBufferFromString(text, path string) *Buffer {
@@ -82,9 +112,29 @@ func NewBufferFromString(text, path string) *Buffer {
 
 // NewBuffer creates a new buffer from a given reader with a given path
 func NewBuffer(reader io.Reader, size int64, path string) *Buffer {
+       startpos := Loc{0, 0}
+       startposErr := true
+       if strings.Contains(path, ":") {
+               var err error
+               split := strings.Split(path, ":")
+               path = split[0]
+               startpos.Y, err = strconv.Atoi(split[1])
+               if err != nil {
+                       messenger.Error("Error opening file: ", err)
+               } else {
+                       startposErr = false
+                       if len(split) > 2 {
+                               startpos.X, err = strconv.Atoi(split[2])
+                               if err != nil {
+                                       messenger.Error("Error opening file: ", err)
+                               }
+                       }
+               }
+       }
+
        if path != "" {
                for _, tab := range tabs {
-                       for _, view := range tab.views {
+                       for _, view := range tab.Views {
                                if view.Buf.Path == path {
                                        return view.Buf
                                }
@@ -129,11 +179,18 @@ func NewBuffer(reader io.Reader, size int64, path string) *Buffer {
        cursorStartX := 0
        cursorStartY := 0
        // If -startpos LINE,COL was passed, use start position LINE,COL
-       if len(*flagStartPos) > 0 {
+       if len(*flagStartPos) > 0 || !startposErr {
                positions := strings.Split(*flagStartPos, ",")
-               if len(positions) == 2 {
-                       lineNum, errPos1 := strconv.Atoi(positions[0])
-                       colNum, errPos2 := strconv.Atoi(positions[1])
+               if len(positions) == 2 || !startposErr {
+                       var lineNum, colNum int
+                       var errPos1, errPos2 error
+                       if !startposErr {
+                               lineNum = startpos.Y
+                               colNum = startpos.X
+                       } else {
+                               lineNum, errPos1 = strconv.Atoi(positions[0])
+                               colNum, errPos2 = strconv.Atoi(positions[1])
+                       }
                        if errPos1 == nil && errPos2 == nil {
                                cursorStartX = colNum
                                cursorStartY = lineNum - 1
@@ -162,10 +219,11 @@ func NewBuffer(reader io.Reader, size int64, path string) *Buffer {
 
        InitLocalSettings(b)
 
-       if len(*flagStartPos) == 0 && (b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool)) {
+       if startposErr && len(*flagStartPos) == 0 && (b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool)) {
                // If either savecursor or saveundo is turned on, we need to load the serialized information
                // from ~/.config/micro/buffers
                file, err := os.Open(configDir + "/buffers/" + EscapePath(b.AbsPath))
+               defer file.Close()
                if err == nil {
                        var buffer SerializedBuffer
                        decoder := gob.NewDecoder(file)
@@ -188,15 +246,14 @@ func NewBuffer(reader io.Reader, size int64, path string) *Buffer {
                                }
                        }
                }
-               file.Close()
        }
 
        if !b.Settings["fastdirty"].(bool) {
-               if size > 50000 {
+               if size > LargeFileThreshold {
                        // If the file is larger than a megabyte fastdirty needs to be on
                        b.Settings["fastdirty"] = true
                } else {
-                       b.origHash = md5.Sum([]byte(b.String()))
+                       calcHash(b, &b.origHash)
                }
        }
 
@@ -386,38 +443,41 @@ func (b *Buffer) SaveWithSudo() error {
 
 // Serialize serializes the buffer to configDir/buffers
 func (b *Buffer) Serialize() error {
-       if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
-               file, err := os.Create(configDir + "/buffers/" + EscapePath(b.AbsPath))
-               if err == nil {
-                       enc := gob.NewEncoder(file)
-                       gob.Register(TextEvent{})
-                       err = enc.Encode(SerializedBuffer{
-                               b.EventHandler,
-                               b.Cursor,
-                               b.ModTime,
-                       })
-               }
-               err = file.Close()
-               return err
+       if !b.Settings["savecursor"].(bool) && !b.Settings["saveundo"].(bool) {
+               return nil
        }
-       return nil
+
+       name := configDir + "/buffers/" + EscapePath(b.AbsPath)
+
+       return overwriteFile(name, func(file io.Writer) error {
+               return gob.NewEncoder(file).Encode(SerializedBuffer{
+                       b.EventHandler,
+                       b.Cursor,
+                       b.ModTime,
+               })
+       })
+}
+
+func init() {
+       gob.Register(TextEvent{})
+       gob.Register(SerializedBuffer{})
 }
 
 // SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
 func (b *Buffer) SaveAs(filename string) error {
        b.UpdateRules()
        if b.Settings["rmtrailingws"].(bool) {
-               r, _ := regexp.Compile(`[ \t]+$`)
-               for lineNum, line := range b.Lines(0, b.NumLines) {
-                       indices := r.FindStringIndex(line)
-                       if indices == nil {
-                               continue
+               for i, l := range b.lines {
+                       pos := len(bytes.TrimRightFunc(l.data, unicode.IsSpace))
+
+                       if pos < len(l.data) {
+                               b.deleteToEnd(Loc{pos, i})
                        }
-                       startLoc := Loc{indices[0], lineNum}
-                       b.deleteToEnd(startLoc)
                }
+
                b.Cursor.Relocate()
        }
+
        if b.Settings["eofnewline"].(bool) {
                end := b.End()
                if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
@@ -430,7 +490,7 @@ func (b *Buffer) SaveAs(filename string) error {
        }()
 
        // Removes any tilde and replaces with the absolute path to home
-       var absFilename string = ReplaceHome(filename)
+       absFilename := ReplaceHome(filename)
 
        // Get the leading path to the file | "." is returned if there's no leading path provided
        if dirname := filepath.Dir(absFilename); dirname != "." {
@@ -450,28 +510,52 @@ func (b *Buffer) SaveAs(filename string) error {
                }
        }
 
-       f, err := os.OpenFile(absFilename, os.O_WRONLY|os.O_CREATE, 0644)
+       var fileSize int
+
+       err := overwriteFile(absFilename, func(file io.Writer) (e error) {
+               if len(b.lines) == 0 {
+                       return
+               }
+
+               // end of line
+               var eol []byte
+
+               if b.Settings["fileformat"] == "dos" {
+                       eol = []byte{'\r', '\n'}
+               } else {
+                       eol = []byte{'\n'}
+               }
+
+               // write lines
+               if fileSize, e = file.Write(b.lines[0].data); e != nil {
+                       return
+               }
+
+               for _, l := range b.lines[1:] {
+                       if _, e = file.Write(eol); e != nil {
+                               return
+                       }
+
+                       if _, e = file.Write(l.data); e != nil {
+                               return
+                       }
+
+                       fileSize += len(eol) + len(l.data)
+               }
+
+               return
+       })
+
        if err != nil {
                return err
        }
-       if err := f.Truncate(0); err != nil {
-               return err
-       }
-       useCrlf := b.Settings["fileformat"] == "dos"
-       for i, l := range b.lines {
-               if _, err := f.Write(l.data); err != nil {
-                       return err
-               }
-               if i != len(b.lines)-1 {
-                       if useCrlf {
-                               if _, err := f.Write([]byte{'\r', '\n'}); err != nil {
-                                       return err
-                               }
-                       } else {
-                               if _, err := f.Write([]byte{'\n'}); err != nil {
-                                       return err
-                               }
-                       }
+
+       if !b.Settings["fastdirty"].(bool) {
+               if fileSize > LargeFileThreshold {
+                       // For large files 'fastdirty' needs to be on
+                       b.Settings["fastdirty"] = true
+               } else {
+                       calcHash(b, &b.origHash)
                }
        }
 
@@ -480,6 +564,48 @@ func (b *Buffer) SaveAs(filename string) error {
        return b.Serialize()
 }
 
+// overwriteFile opens the given file for writing, truncating if one exists, and then calls
+// the supplied function with the file as io.Writer object, also making sure the file is
+// closed afterwards.
+func overwriteFile(name string, fn func(io.Writer) error) (err error) {
+       var file *os.File
+
+       if file, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
+               return
+       }
+
+       defer func() {
+               if e := file.Close(); e != nil && err == nil {
+                       err = e
+               }
+       }()
+
+       w := bufio.NewWriter(file)
+
+       if err = fn(w); err != nil {
+               return
+       }
+
+       err = w.Flush()
+       return
+}
+
+// calcHash calculates md5 hash of all lines in the buffer
+func calcHash(b *Buffer, out *[md5.Size]byte) {
+       h := md5.New()
+
+       if len(b.lines) > 0 {
+               h.Write(b.lines[0].data)
+
+               for _, l := range b.lines[1:] {
+                       h.Write([]byte{'\n'})
+                       h.Write(l.data)
+               }
+       }
+
+       h.Sum((*out)[:0])
+}
+
 // SaveAsWithSudo is the same as SaveAs except it uses a neat trick
 // with tee to use sudo so the user doesn't have to reopen micro with sudo
 func (b *Buffer) SaveAsWithSudo(filename string) error {
@@ -524,7 +650,11 @@ func (b *Buffer) Modified() bool {
        if b.Settings["fastdirty"].(bool) {
                return b.IsModified
        }
-       return b.origHash != md5.Sum([]byte(b.String()))
+
+       var buff [md5.Size]byte
+
+       calcHash(b, &buff)
+       return buff != b.origHash
 }
 
 func (b *Buffer) insert(pos Loc, value []byte) {
@@ -563,7 +693,7 @@ func (b *Buffer) RuneAt(loc Loc) rune {
        return '\n'
 }
 
-// Line returns a single line as an array of runes
+// LineBytes returns a single line as an array of runes
 func (b *Buffer) LineBytes(n int) []byte {
        if n >= len(b.lines) {
                return []byte{}
@@ -571,7 +701,7 @@ func (b *Buffer) LineBytes(n int) []byte {
        return b.lines[n].data
 }
 
-// Line returns a single line as an array of runes
+// LineRunes returns a single line as an array of runes
 func (b *Buffer) LineRunes(n int) []rune {
        if n >= len(b.lines) {
                return []rune{}
@@ -603,8 +733,16 @@ func (b *Buffer) Lines(start, end int) []string {
 }
 
 // Len gives the length of the buffer
-func (b *Buffer) Len() int {
-       return Count(b.String())
+func (b *Buffer) Len() (n int) {
+       for _, l := range b.lines {
+               n += utf8.RuneCount(l.data)
+       }
+
+       if len(b.lines) > 1 {
+               n += len(b.lines) - 1 // account for newlines
+       }
+
+       return
 }
 
 // MoveLinesUp moves the range of lines up one row