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
// 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
NumLines int
- // Syntax highlighting rules
- rules []SyntaxRule
+ syntaxDef *highlight.Def
+ highlighter *highlight.Highlighter
// Buffer local settings
Settings map[string]interface{}
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 = DefaultLocalSettings()
for k, v := range globalSettings {
}
}
- 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)
b.EventHandler = NewEventHandler(b)
b.Update()
- b.FindFileType()
b.UpdateRules()
if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
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)
file.Close()
}
+ 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 = 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)
+ }
+ 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
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
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)
// 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{})
// 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())
+ 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
}
// 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)
// 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)
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 {
// 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
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
}
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)
+ }
+}