X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=cmd%2Fmicro%2Fbuffer.go;h=a9de2a7a3152dbc54ee0d0d91c6cced109f55855;hb=71af765b4e4f368c4bbbcb3947f3497e17271b62;hp=d9a0e05352aaebd94c1e4c701ce581f9fa3b0b5f;hpb=a19a6d28a721d39e5639fffb656a3e1b3277344f;p=micro.git diff --git a/cmd/micro/buffer.go b/cmd/micro/buffer.go index d9a0e053..a9de2a7a 100644 --- a/cmd/micro/buffer.go +++ b/cmd/micro/buffer.go @@ -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) { @@ -556,13 +686,29 @@ func (b *Buffer) End() Loc { // RuneAt returns the rune at a given location in the buffer func (b *Buffer) RuneAt(loc Loc) rune { - line := []rune(b.Line(loc.Y)) + line := b.LineRunes(loc.Y) if len(line) > 0 { return line[loc.X] } return '\n' } +// LineBytes returns a single line as an array of runes +func (b *Buffer) LineBytes(n int) []byte { + if n >= len(b.lines) { + return []byte{} + } + return b.lines[n].data +} + +// LineRunes returns a single line as an array of runes +func (b *Buffer) LineRunes(n int) []rune { + if n >= len(b.lines) { + return []rune{} + } + return toRunes(b.lines[n].data) +} + // Line returns a single line func (b *Buffer) Line(n int) string { if n >= len(b.lines) { @@ -587,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 @@ -663,12 +817,12 @@ var bracePairs = [][2]rune{ // It is given a brace type containing the open and closing character, (for example // '{' and '}') as well as the location to match from func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) Loc { - curLine := []rune(string(b.lines[start.Y].data)) + curLine := b.LineRunes(start.Y) startChar := curLine[start.X] var i int if startChar == braceType[0] { for y := start.Y; y < b.NumLines; y++ { - l := []rune(string(b.lines[y].data)) + l := b.LineRunes(y) xInit := 0 if y == start.Y { xInit = start.X