]> git.lizzy.rs Git - micro.git/blobdiff - cmd/micro/buffer.go
Make tab views array public
[micro.git] / cmd / micro / buffer.go
index 0d893fdadaa629becd3387ad069e98c7e73e291c..b255fc00648c538ee513f37d45bb0ca44cfba25c 100644 (file)
@@ -2,16 +2,29 @@ package main
 
 import (
        "bytes"
+       "crypto/md5"
        "encoding/gob"
+       "errors"
+       "io"
        "io/ioutil"
        "os"
        "os/exec"
        "os/signal"
        "path/filepath"
+       "regexp"
        "strconv"
        "strings"
        "time"
        "unicode/utf8"
+
+       "github.com/zyedidia/micro/cmd/micro/highlight"
+)
+
+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
@@ -23,12 +36,16 @@ 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
+       // Absolute path to the file on disk
+       AbsPath string
        // Name of the buffer on the status line
-       Name string
+       name string
 
        // Whether or not the buffer has been modified since it was opened
        IsModified bool
@@ -36,10 +53,14 @@ 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
 
-       // Syntax highlighting rules
-       rules []SyntaxRule
+       syntaxDef   *highlight.Def
+       highlighter *highlight.Highlighter
+
+       // Hash of the original buffer -- empty if fastdirty is on
+       origHash [16]byte
 
        // Buffer local settings
        Settings map[string]interface{}
@@ -53,11 +74,64 @@ type SerializedBuffer struct {
        ModTime      time.Time
 }
 
-// NewBuffer creates a new buffer from `txt` with path and name `path`
-func NewBuffer(txt []byte, path string) *Buffer {
+// 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), int64(len(text)), path)
+}
+
+// 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
                                }
@@ -66,7 +140,7 @@ func NewBuffer(txt []byte, path string) *Buffer {
        }
 
        b := new(Buffer)
-       b.LineArray = NewLineArray(txt)
+       b.LineArray = NewLineArray(size, reader)
 
        b.Settings = DefaultLocalSettings()
        for k, v := range globalSettings {
@@ -75,21 +149,23 @@ func NewBuffer(txt []byte, path string) *Buffer {
                }
        }
 
-       b.Path = path
-       b.Name = path
-
-       // If the file doesn't have a path to disk then we give it no name
-       if path == "" {
-               b.Name = "No name"
+       if fileformat == 1 {
+               b.Settings["fileformat"] = "unix"
+       } else if fileformat == 2 {
+               b.Settings["fileformat"] = "dos"
        }
 
+       absPath, _ := filepath.Abs(path)
+
+       b.Path = path
+       b.AbsPath = absPath
+
        // The last time this file was modified
        b.ModTime, _ = GetModTime(b.Path)
 
        b.EventHandler = NewEventHandler(b)
 
        b.Update()
-       b.FindFileType()
        b.UpdateRules()
 
        if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
@@ -100,11 +176,18 @@ func NewBuffer(txt []byte, 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
@@ -133,11 +216,10 @@ func NewBuffer(txt []byte, 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
-               absPath, _ := filepath.Abs(b.Path)
-               file, err := os.Open(configDir + "/buffers/" + EscapePath(absPath))
+               file, err := os.Open(configDir + "/buffers/" + EscapePath(b.AbsPath))
                if err == nil {
                        var buffer SerializedBuffer
                        decoder := gob.NewDecoder(file)
@@ -153,7 +235,7 @@ func NewBuffer(txt []byte, 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
@@ -163,18 +245,96 @@ func NewBuffer(txt []byte, path string) *Buffer {
                file.Close()
        }
 
+       if !b.Settings["fastdirty"].(bool) {
+               if size > 50000 {
+                       // 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()))
+               }
+       }
+
+       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 == "" {
+                       return "No name"
+               }
+               return b.Path
+       }
+       return b.name
+}
+
 // UpdateRules updates the syntax rules and filetype for this buffer
 // This is called when the colorscheme changes
 func (b *Buffer) UpdateRules() {
-       b.rules = GetRules(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)
+               }
+       }
 
-// FindFileType identifies this buffer's filetype based on the extension or header
-func (b *Buffer) FindFileType() {
-       b.Settings["filetype"] = FindFileType(b)
+       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)
+                       }
+               }
+       }
 }
 
 // FileType returns the buffer's filetype
@@ -182,6 +342,14 @@ func (b *Buffer) FileType() string {
        return b.Settings["filetype"].(string)
 }
 
+// IndentString returns a string representing one level of indentation
+func (b *Buffer) IndentString() string {
+       if b.Settings["tabstospaces"].(bool) {
+               return Spaces(int(b.Settings["tabsize"].(float64)))
+       }
+       return "\t"
+}
+
 // CheckModTime makes sure that the file this buffer points to hasn't been updated
 // by an external program since it was last read
 // If it has, we ask the user if they would like to reload the file
@@ -225,6 +393,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)
@@ -238,8 +441,7 @@ 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) {
-               absPath, _ := filepath.Abs(b.Path)
-               file, err := os.Create(configDir + "/buffers/" + EscapePath(absPath))
+               file, err := os.Create(configDir + "/buffers/" + EscapePath(b.AbsPath))
                if err == nil {
                        enc := gob.NewEncoder(file)
                        gob.Register(TextEvent{})
@@ -249,7 +451,7 @@ func (b *Buffer) Serialize() error {
                                b.ModTime,
                        })
                }
-               file.Close()
+               err = file.Close()
                return err
        }
        return nil
@@ -257,43 +459,94 @@ func (b *Buffer) Serialize() error {
 
 // 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.FindFileType()
        b.UpdateRules()
-       b.Name = filename
-       b.Path = filename
-       data := []byte(b.String())
-       err := ioutil.WriteFile(filename, data, 0644)
-       if err == nil {
-               b.IsModified = false
+       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
+                       }
+                       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")
+               }
+       }
+
+       defer func() {
                b.ModTime, _ = GetModTime(filename)
-               return b.Serialize()
+       }()
+
+       // Removes any tilde and replaces with the absolute path to home
+       var absFilename string = 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")
+                       }
+               }
        }
-       return err
+
+       f, err := os.OpenFile(absFilename, os.O_WRONLY|os.O_CREATE, 0644)
+       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
+                               }
+                       }
+               }
+       }
+
+       b.Path = filename
+       b.IsModified = false
+       return b.Serialize()
 }
 
 // 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 {
-       b.FindFileType()
        b.UpdateRules()
-       b.Name = filename
        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
@@ -307,13 +560,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)
@@ -322,6 +572,15 @@ 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
+       }
+       return b.origHash != md5.Sum([]byte(b.String()))
+}
+
 func (b *Buffer) insert(pos Loc, value []byte) {
        b.IsModified = true
        b.LineArray.insert(pos, value)
@@ -333,6 +592,11 @@ func (b *Buffer) remove(start, end Loc) string {
        b.Update()
        return sub
 }
+func (b *Buffer) deleteToEnd(start Loc) {
+       b.IsModified = true
+       b.LineArray.DeleteToEnd(start)
+       b.Update()
+}
 
 // Start returns the location of the first character in the buffer
 func (b *Buffer) Start() Loc {
@@ -341,12 +605,45 @@ func (b *Buffer) Start() Loc {
 
 // End returns the location of the last character in the buffer
 func (b *Buffer) End() Loc {
-       return Loc{utf8.RuneCount(b.lines[b.NumLines-1]), b.NumLines - 1}
+       return Loc{utf8.RuneCount(b.lines[b.NumLines-1].data), b.NumLines - 1}
+}
+
+// RuneAt returns the rune at a given location in the buffer
+func (b *Buffer) RuneAt(loc Loc) rune {
+       line := b.LineRunes(loc.Y)
+       if len(line) > 0 {
+               return line[loc.X]
+       }
+       return '\n'
+}
+
+// Line 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
+}
+
+// Line 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 {
-       return string(b.lines[n])
+       if n >= len(b.lines) {
+               return ""
+       }
+       return string(b.lines[n].data)
+}
+
+// LinesNum returns the number of lines in the buffer
+func (b *Buffer) LinesNum() int {
+       return len(b.lines)
 }
 
 // Lines returns an array of strings containing the lines from start to end
@@ -354,7 +651,7 @@ func (b *Buffer) Lines(start, end int) []string {
        lines := b.lines[start:end]
        var slice []string
        for _, line := range lines {
-               slice = append(slice, string(line))
+               slice = append(slice, string(line.data))
        }
        return slice
 }
@@ -363,3 +660,120 @@ func (b *Buffer) Lines(start, end int) []string {
 func (b *Buffer) Len() int {
        return Count(b.String())
 }
+
+// MoveLinesUp moves the range of lines up one row
+func (b *Buffer) MoveLinesUp(start int, end int) {
+       // 0 < start < end <= len(b.lines)
+       if start < 1 || start >= end || end > len(b.lines) {
+               return // what to do? FIXME
+       }
+       if end == len(b.lines) {
+               b.Insert(
+                       Loc{
+                               utf8.RuneCount(b.lines[end-1].data),
+                               end - 1,
+                       },
+                       "\n"+b.Line(start-1),
+               )
+       } else {
+               b.Insert(
+                       Loc{0, end},
+                       b.Line(start-1)+"\n",
+               )
+       }
+       b.Remove(
+               Loc{0, start - 1},
+               Loc{0, start},
+       )
+}
+
+// MoveLinesDown moves the range of lines down one row
+func (b *Buffer) MoveLinesDown(start int, end int) {
+       // 0 <= start < end < len(b.lines)
+       // if end == len(b.lines), we can't do anything here because the
+       // last line is unaccessible, FIXME
+       if start < 0 || start >= end || end >= len(b.lines)-1 {
+               return // what to do? FIXME
+       }
+       b.Insert(
+               Loc{0, start},
+               b.Line(end)+"\n",
+       )
+       end++
+       b.Remove(
+               Loc{0, end},
+               Loc{0, end + 1},
+       )
+}
+
+// ClearMatches clears all of the syntax highlighting for this buffer
+func (b *Buffer) ClearMatches() {
+       for i := range b.lines {
+               b.SetMatch(i, nil)
+               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
+}