]> git.lizzy.rs Git - micro.git/blobdiff - cmd/micro/buffer.go
Always use custom syntax files over default
[micro.git] / cmd / micro / buffer.go
index a4ba792280587cbf7a3975719a94586448545186..b8071839c909f913be227814b7155ecd6e89dbc9 100644 (file)
@@ -3,14 +3,20 @@ package main
 import (
        "bytes"
        "encoding/gob"
+       "io"
        "io/ioutil"
        "os"
        "os/exec"
        "os/signal"
        "path/filepath"
+       "regexp"
+       "strconv"
        "strings"
        "time"
        "unicode/utf8"
+
+       "github.com/mitchellh/go-homedir"
+       "github.com/zyedidia/micro/cmd/micro/highlight"
 )
 
 // Buffer stores the text for files that are loaded into the text editor
@@ -22,12 +28,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
@@ -37,10 +47,8 @@ type Buffer struct {
 
        NumLines int
 
-       // Syntax highlighting rules
-       rules []SyntaxRule
-       // The buffer's filetype
-       FileType string
+       syntaxDef   *highlight.Def
+       highlighter *highlight.Highlighter
 
        // Buffer local settings
        Settings map[string]interface{}
@@ -54,23 +62,36 @@ 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 {
+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 {
+       if path != "" {
+               for _, tab := range tabs {
+                       for _, view := range tab.views {
+                               if view.Buf.Path == path {
+                                       return view.Buf
+                               }
+                       }
+               }
+       }
+
        b := new(Buffer)
-       b.LineArray = NewLineArray(txt)
+       b.LineArray = NewLineArray(size, reader)
 
-       b.Settings = make(map[string]interface{})
+       b.Settings = DefaultLocalSettings()
        for k, v := range globalSettings {
-               b.Settings[k] = v
+               if _, ok := b.Settings[k]; ok {
+                       b.Settings[k] = v
+               }
        }
 
-       b.Path = path
-       b.Name = path
+       absPath, _ := filepath.Abs(path)
 
-       // If the file doesn't have a path to disk then we give it no name
-       if path == "" {
-               b.Name = "No name"
-       }
+       b.Path = path
+       b.AbsPath = absPath
 
        // The last time this file was modified
        b.ModTime, _ = GetModTime(b.Path)
@@ -85,19 +106,46 @@ func NewBuffer(txt []byte, path string) *Buffer {
        }
 
        // Put the cursor at the first spot
+       cursorStartX := 0
+       cursorStartY := 0
+       // If -startpos LINE,COL was passed, use start position LINE,COL
+       if len(*flagStartPos) > 0 {
+               positions := strings.Split(*flagStartPos, ",")
+               if len(positions) == 2 {
+                       lineNum, errPos1 := strconv.Atoi(positions[0])
+                       colNum, errPos2 := strconv.Atoi(positions[1])
+                       if errPos1 == nil && errPos2 == nil {
+                               cursorStartX = colNum
+                               cursorStartY = lineNum - 1
+                               // Check to avoid line overflow
+                               if cursorStartY > b.NumLines {
+                                       cursorStartY = b.NumLines - 1
+                               } else if cursorStartY < 0 {
+                                       cursorStartY = 0
+                               }
+                               // Check to avoid column overflow
+                               if cursorStartX > len(b.Line(cursorStartY)) {
+                                       cursorStartX = len(b.Line(cursorStartY))
+                               } else if cursorStartX < 0 {
+                                       cursorStartX = 0
+                               }
+                       }
+               }
+       }
        b.Cursor = Cursor{
                Loc: Loc{
-                       X: 0,
-                       Y: 0,
+                       X: cursorStartX,
+                       Y: cursorStartY,
                },
                buf: b,
        }
 
+       InitLocalSettings(b)
+
        if 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)
@@ -123,18 +171,99 @@ func NewBuffer(txt []byte, path string) *Buffer {
                file.Close()
        }
 
-       _, err := Call("onBufferOpen", b)
-       if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
-               TermMessage(err)
-       }
+       b.cursors = []*Cursor{&b.Cursor}
 
        return b
 }
 
+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, b.FileType = 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)
+               }
+       }
+
+       if b.syntaxDef != nil {
+               highlight.ResolveIncludes(b.syntaxDef, files)
+       }
+       files = nil
+
+       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
+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
@@ -180,6 +309,30 @@ func (b *Buffer) Update() {
        b.NumLines = len(b.lines)
 }
 
+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
+}
+
+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)
@@ -193,8 +346,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{})
@@ -213,15 +365,35 @@ 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.UpdateRules()
-       b.Name = filename
-       b.Path = filename
-       data := []byte(b.String())
+       dir, _ := homedir.Dir()
+       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")
+               }
+       }
+       str := b.String()
+       data := []byte(str)
        err := ioutil.WriteFile(filename, data, 0644)
        if err == nil {
+               b.Path = strings.Replace(filename, "~", dir, 1)
                b.IsModified = false
                b.ModTime, _ = GetModTime(filename)
                return b.Serialize()
        }
+       b.ModTime, _ = GetModTime(filename)
        return err
 }
 
@@ -229,20 +401,11 @@ func (b *Buffer) SaveAs(filename string) error {
 // with tee to use sudo so the user doesn't have to reopen micro with sudo
 func (b *Buffer) SaveAsWithSudo(filename string) error {
        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)
@@ -260,13 +423,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)
@@ -286,6 +446,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 {
@@ -294,12 +459,28 @@ 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 := []rune(b.Line(loc.Y))
+       if len(line) > 0 {
+               return line[loc.X]
+       }
+       return '\n'
 }
 
 // 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)
+}
+
+func (b *Buffer) LinesNum() int {
+       return len(b.lines)
 }
 
 // Lines returns an array of strings containing the lines from start to end
@@ -307,7 +488,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
 }
@@ -316,3 +497,56 @@ 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)
+       }
+}