import (
"bytes"
+ "crypto/md5"
"encoding/gob"
+ "errors"
"io"
"io/ioutil"
"os"
"time"
"unicode/utf8"
- "github.com/mitchellh/go-homedir"
"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
// It uses a rope to efficiently store the string and contains some
// simple functions for saving and wrapper functions for modifying the rope
// 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
// 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 [16]byte
+
// Buffer local settings
Settings map[string]interface{}
}
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
}
}
b := new(Buffer)
- b.LineArray = NewLineArray(reader)
+ b.LineArray = NewLineArray(size, reader)
b.Settings = DefaultLocalSettings()
for k, v := range globalSettings {
}
}
+ if fileformat == 1 {
+ b.Settings["fileformat"] = "unix"
+ } else if fileformat == 2 {
+ b.Settings["fileformat"] = "dos"
+ }
+
absPath, _ := filepath.Abs(path)
b.Path = path
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
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))
}
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 > 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 == "" {
// 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)
+ }
}
}
}
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)
b.ModTime,
})
}
- file.Close()
+ err = file.Close()
return err
}
return nil
// 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) {
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
+ 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")
+ }
+ }
}
- b.ModTime, _ = GetModTime(filename)
- 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
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
// 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)
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)
// 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'
}
+// 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 {
if n >= len(b.lines) {
return string(b.lines[n].data)
}
+// LinesNum returns the number of lines in the buffer
func (b *Buffer) LinesNum() int {
return len(b.lines)
}
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
+}