X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=cmd%2Fmicro%2Fbuffer.go;h=a9de2a7a3152dbc54ee0d0d91c6cced109f55855;hb=71af765b4e4f368c4bbbcb3947f3497e17271b62;hp=9e1cbee0a33b59916ea81aa4ec0a2a218db6f967;hpb=12d74b99e8f044aa3c336700a84470c1e0987478;p=micro.git diff --git a/cmd/micro/buffer.go b/cmd/micro/buffer.go index 9e1cbee0..a9de2a7a 100644 --- a/cmd/micro/buffer.go +++ b/cmd/micro/buffer.go @@ -1,24 +1,35 @@ package main import ( + "bufio" "bytes" + "crypto/md5" "encoding/gob" + "errors" "io" "io/ioutil" "os" "os/exec" "os/signal" "path/filepath" - "regexp" "strconv" "strings" "time" + "unicode" "unicode/utf8" - "github.com/mitchellh/go-homedir" "github.com/zyedidia/micro/cmd/micro/highlight" ) +const LargeFileThreshold = 50000 + +var ( + // 0 - no line type detected + // 1 - lf detected + // 2 - crlf detected + fileformat = 0 +) + // Buffer stores the text for files that are loaded into the text editor // It uses a rope to efficiently store the string and contains some // simple functions for saving and wrapper functions for modifying the rope @@ -28,7 +39,9 @@ type Buffer struct { // This stores all the text in the buffer as an array of lines *LineArray - Cursor Cursor + Cursor Cursor + cursors []*Cursor // for multiple cursors + curCursor int // the current cursor // Path to the file on disk Path string @@ -43,11 +56,15 @@ type Buffer struct { // Stores the last modification time of the file the buffer is pointing to ModTime time.Time + // NumLines is the number of lines in the buffer NumLines int syntaxDef *highlight.Def highlighter *highlight.Highlighter + // Hash of the original buffer -- empty if fastdirty is on + origHash [md5.Size]byte + // Buffer local settings Settings map[string]interface{} } @@ -60,15 +77,64 @@ 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 { - return NewBuffer(strings.NewReader(text), path) + return NewBuffer(strings.NewReader(text), int64(len(text)), path) } // NewBuffer creates a new buffer from a given reader with a given path -func NewBuffer(reader io.Reader, path string) *Buffer { +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 } @@ -77,7 +143,7 @@ func NewBuffer(reader io.Reader, path string) *Buffer { } b := new(Buffer) - b.LineArray = NewLineArray(reader) + b.LineArray = NewLineArray(size, reader) b.Settings = DefaultLocalSettings() for k, v := range globalSettings { @@ -86,6 +152,12 @@ func NewBuffer(reader io.Reader, path string) *Buffer { } } + if fileformat == 1 { + b.Settings["fileformat"] = "unix" + } else if fileformat == 2 { + b.Settings["fileformat"] = "dos" + } + absPath, _ := filepath.Abs(path) b.Path = path @@ -107,11 +179,18 @@ func NewBuffer(reader io.Reader, 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 @@ -140,10 +219,11 @@ func NewBuffer(reader io.Reader, path string) *Buffer { InitLocalSettings(b) - if 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) @@ -159,19 +239,31 @@ func NewBuffer(reader io.Reader, path string) *Buffer { } if b.Settings["saveundo"].(bool) { - // We should only use last time's eventhandler if the file wasn't by someone else in the meantime + // We should only use last time's eventhandler if the file wasn't modified by someone else in the meantime if b.ModTime == buffer.ModTime { b.EventHandler = buffer.EventHandler b.EventHandler.buf = b } } } - file.Close() } + if !b.Settings["fastdirty"].(bool) { + if size > LargeFileThreshold { + // If the file is larger than a megabyte fastdirty needs to be on + b.Settings["fastdirty"] = true + } else { + calcHash(b, &b.origHash) + } + } + + b.cursors = []*Cursor{&b.Cursor} + return b } +// GetName returns the name that should be displayed in the statusline +// for this buffer func (b *Buffer) GetName() string { if b.name == "" { if b.Path == "" { @@ -185,12 +277,65 @@ func (b *Buffer) GetName() string { // UpdateRules updates the syntax rules and filetype for this buffer // This is called when the colorscheme changes func (b *Buffer) UpdateRules() { - b.syntaxDef = highlight.DetectFiletype(syntaxDefs, b.Path, []byte(b.Line(0))) - if b.highlighter == nil || b.Settings["filetype"].(string) != b.syntaxDef.FileType { - b.Settings["filetype"] = b.syntaxDef.FileType - b.highlighter = highlight.NewHighlighter(b.syntaxDef) - if b.Settings["syntax"].(bool) { - b.highlighter.HighlightStates(b) + rehighlight := false + var files []*highlight.File + for _, f := range ListRuntimeFiles(RTSyntax) { + data, err := f.Data() + if err != nil { + TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error()) + } else { + file, err := highlight.ParseFile(data) + if err != nil { + TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error()) + continue + } + ftdetect, err := highlight.ParseFtDetect(file) + if err != nil { + TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error()) + continue + } + + ft := b.Settings["filetype"].(string) + if (ft == "Unknown" || ft == "") && !rehighlight { + if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) { + header := new(highlight.Header) + header.FileType = file.FileType + header.FtDetect = ftdetect + b.syntaxDef, err = highlight.ParseDef(file, header) + if err != nil { + TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error()) + continue + } + rehighlight = true + } + } else { + if file.FileType == ft && !rehighlight { + header := new(highlight.Header) + header.FileType = file.FileType + header.FtDetect = ftdetect + b.syntaxDef, err = highlight.ParseDef(file, header) + if err != nil { + TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error()) + continue + } + rehighlight = true + } + } + files = append(files, file) + } + } + + if b.syntaxDef != nil { + highlight.ResolveIncludes(b.syntaxDef, files) + } + + if b.highlighter == nil || rehighlight { + if b.syntaxDef != nil { + b.Settings["filetype"] = b.syntaxDef.FileType + b.highlighter = highlight.NewHighlighter(b.syntaxDef) + if b.Settings["syntax"].(bool) { + b.highlighter.HighlightStates(b) + } } } } @@ -251,6 +396,41 @@ func (b *Buffer) Update() { b.NumLines = len(b.lines) } +// MergeCursors merges any cursors that are at the same position +// into one cursor +func (b *Buffer) MergeCursors() { + var cursors []*Cursor + for i := 0; i < len(b.cursors); i++ { + c1 := b.cursors[i] + if c1 != nil { + for j := 0; j < len(b.cursors); j++ { + c2 := b.cursors[j] + if c2 != nil && i != j && c1.Loc == c2.Loc { + b.cursors[j] = nil + } + } + cursors = append(cursors, c1) + } + } + + b.cursors = cursors + + for i := range b.cursors { + b.cursors[i].Num = i + } + + if b.curCursor >= len(b.cursors) { + b.curCursor = len(b.cursors) - 1 + } +} + +// UpdateCursors updates all the cursors indicies +func (b *Buffer) UpdateCursors() { + for i, c := range b.cursors { + c.Num = i + } +} + // Save saves the buffer to its default path func (b *Buffer) Save() error { return b.SaveAs(b.Path) @@ -263,56 +443,167 @@ 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, - }) - } - 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() - dir, _ := homedir.Dir() - b.Path = strings.Replace(filename, "~", dir, 1) 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' { b.Insert(end, "\n") } } - str := b.String() - data := []byte(str) - err := ioutil.WriteFile(filename, data, 0644) - if err == nil { - b.IsModified = false + + defer func() { b.ModTime, _ = GetModTime(filename) - return b.Serialize() + }() + + // Removes any tilde and replaces with the absolute path to home + 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 != "." { + // Check if the parent dirs don't exist + if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) { + // Prompt to make sure they want to create the dirs that are missing + if yes, canceled := messenger.YesNoPrompt("Parent folders \"" + dirname + "\" do not exist. Create them? (y,n)"); yes && !canceled { + // Create all leading dir(s) since they don't exist + if mkdirallErr := os.MkdirAll(dirname, os.ModePerm); mkdirallErr != nil { + // If there was an error creating the dirs + return mkdirallErr + } + } else { + // If they canceled the creation of leading dirs + return errors.New("Save aborted") + } + } } - b.ModTime, _ = GetModTime(filename) - return err + + 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 !b.Settings["fastdirty"].(bool) { + if fileSize > LargeFileThreshold { + // For large files 'fastdirty' needs to be on + b.Settings["fastdirty"] = true + } else { + calcHash(b, &b.origHash) + } + } + + b.Path = filename + b.IsModified = false + 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 @@ -321,21 +612,13 @@ func (b *Buffer) SaveAsWithSudo(filename string) error { b.UpdateRules() b.Path = filename - // The user may have already used sudo in which case we won't need the password - // It's a bit nicer for them if they don't have to enter the password every time - _, err := RunShellCommand("sudo -v") - needPassword := err != nil - - // If we need the password, we have to close the screen and ask using the shell - if needPassword { - // Shut down the screen because we're going to interact directly with the shell - screen.Fini() - screen = nil - } + // Shut down the screen because we're going to interact directly with the shell + screen.Fini() + screen = nil // Set up everything for the command - cmd := exec.Command("sudo", "tee", filename) - cmd.Stdin = bytes.NewBufferString(b.String()) + cmd := exec.Command(globalSettings["sucmd"].(string), "tee", filename) + cmd.Stdin = bytes.NewBufferString(b.SaveString(b.Settings["fileformat"] == "dos")) // This is a trap for Ctrl-C so that it doesn't kill micro // Instead we trap Ctrl-C to kill the program we're running @@ -349,13 +632,10 @@ func (b *Buffer) SaveAsWithSudo(filename string) error { // Start the command cmd.Start() - err = cmd.Wait() + err := cmd.Wait() - // If we needed the password, we closed the screen, so we have to initialize it again - if needPassword { - // Start the screen back up - InitScreen() - } + // Start the screen back up + InitScreen() if err == nil { b.IsModified = false b.ModTime, _ = GetModTime(filename) @@ -364,6 +644,19 @@ func (b *Buffer) SaveAsWithSudo(filename string) error { return err } +// Modified returns if this buffer has been modified since +// being opened +func (b *Buffer) Modified() bool { + if b.Settings["fastdirty"].(bool) { + return b.IsModified + } + + var buff [md5.Size]byte + + calcHash(b, &buff) + return buff != b.origHash +} + func (b *Buffer) insert(pos Loc, value []byte) { b.IsModified = true b.LineArray.insert(pos, value) @@ -393,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) { @@ -408,6 +717,7 @@ func (b *Buffer) Line(n int) string { return string(b.lines[n].data) } +// LinesNum returns the number of lines in the buffer func (b *Buffer) LinesNum() int { return len(b.lines) } @@ -423,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 @@ -479,3 +797,67 @@ func (b *Buffer) ClearMatches() { b.SetState(i, nil) } } + +func (b *Buffer) clearCursors() { + for i := 1; i < len(b.cursors); i++ { + b.cursors[i] = nil + } + b.cursors = b.cursors[:1] + b.UpdateCursors() + b.Cursor.ResetSelection() +} + +var bracePairs = [][2]rune{ + {'(', ')'}, + {'{', '}'}, + {'[', ']'}, +} + +// FindMatchingBrace returns the location in the buffer of the matching bracket +// 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 := b.LineRunes(start.Y) + startChar := curLine[start.X] + var i int + if startChar == braceType[0] { + for y := start.Y; y < b.NumLines; y++ { + l := b.LineRunes(y) + xInit := 0 + if y == start.Y { + xInit = start.X + } + for x := xInit; x < len(l); x++ { + r := l[x] + if r == braceType[0] { + i++ + } else if r == braceType[1] { + i-- + if i == 0 { + return Loc{x, y} + } + } + } + } + } else if startChar == braceType[1] { + for y := start.Y; y >= 0; y-- { + l := []rune(string(b.lines[y].data)) + xInit := len(l) - 1 + if y == start.Y { + xInit = start.X + } + for x := xInit; x >= 0; x-- { + r := l[x] + if r == braceType[0] { + i-- + if i == 0 { + return Loc{x, y} + } + } else if r == braceType[1] { + i++ + } + } + } + } + return start +}