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"
)
// 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), 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
}
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 == "" {
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++ {
}
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
// 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()
if b.Settings["rmtrailingws"].(bool) {
r, _ := regexp.Compile(`[ \t]+$`)
for lineNum, line := range b.Lines(0, b.NumLines) {
b.Insert(end, "\n")
}
}
- str := b.SaveString(b.Settings["fileformat"] == "dos")
- data := []byte(str)
- err := ioutil.WriteFile(filename, data, 0644)
- if err == nil {
- b.Path = strings.Replace(filename, "~", dir, 1)
- 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
screen = nil
// Set up everything for the command
- cmd := exec.Command("sudo", "tee", filename)
+ 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
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
+}