package main
import (
+ "bytes"
"encoding/gob"
+ "io"
"io/ioutil"
"os"
+ "os/exec"
+ "os/signal"
"path/filepath"
+ "regexp"
+ "strconv"
"strings"
+ "time"
+ "unicode/utf8"
- "github.com/vinzmay/go-rope"
+ "github.com/mitchellh/go-homedir"
)
// Buffer stores the text for files that are loaded into the text editor
type Buffer struct {
// The eventhandler for undo/redo
*EventHandler
-
- // Stores the text of the buffer
- r *rope.Rope
+ // This stores all the text in the buffer as an array of lines
+ *LineArray
Cursor 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
- // Provide efficient and easy access to text and lines so the rope String does not
- // need to be constantly recalculated
- // These variables are updated in the update() function
- Lines []string
+ // Stores the last modification time of the file the buffer is pointing to
+ ModTime time.Time
+
NumLines int
// Syntax highlighting rules
rules []SyntaxRule
- // The buffer's filetype
- FileType string
+
+ // Buffer local settings
+ Settings map[string]interface{}
+}
+
+// The SerializedBuffer holds the types that get serialized when a buffer is saved
+// These are used for the savecursor and saveundo options
+type SerializedBuffer struct {
+ EventHandler *EventHandler
+ Cursor Cursor
+ ModTime time.Time
+}
+
+func NewBufferFromString(text, path string) *Buffer {
+ return NewBuffer(strings.NewReader(text), path)
}
-// NewBuffer creates a new buffer from `txt` with path and name `path`
-func NewBuffer(txt, path string) *Buffer {
+// NewBuffer creates a new buffer from a given reader with a given path
+func NewBuffer(reader io.Reader, 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)
- if txt == "" {
- b.r = new(rope.Rope)
- } else {
- b.r = rope.New(txt)
+ b.LineArray = NewLineArray(reader)
+
+ b.Settings = DefaultLocalSettings()
+ for k, v := range globalSettings {
+ if _, ok := b.Settings[k]; ok {
+ b.Settings[k] = v
+ }
}
+
+ absPath, _ := filepath.Abs(path)
+
b.Path = path
- b.Name = 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) {
os.Mkdir(configDir+"/buffers/", os.ModePerm)
}
- if settings["savecursor"].(bool) {
- absPath, _ := filepath.Abs(b.Path)
- file, err := os.Open(configDir + "/buffers/" + EscapePath(absPath))
+ // 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: 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
+ file, err := os.Open(configDir + "/buffers/" + EscapePath(b.AbsPath))
if err == nil {
- var cursor Cursor
+ var buffer SerializedBuffer
decoder := gob.NewDecoder(file)
- err = decoder.Decode(&cursor)
+ gob.Register(TextEvent{})
+ err = decoder.Decode(&buffer)
if err != nil {
- TermMessage(err.Error())
+ TermMessage(err.Error(), "\n", "You may want to remove the files in ~/.config/micro/buffers (these files store the information for the 'saveundo' and 'savecursor' options) if this problem persists.")
+ }
+ if b.Settings["savecursor"].(bool) {
+ b.Cursor = buffer.Cursor
+ b.Cursor.buf = b
+ b.Cursor.Relocate()
}
- b.Cursor = cursor
- b.Cursor.buf = b
- b.Cursor.Clamp()
- } else {
- // Put the cursor at the first spot
- b.Cursor = Cursor{
- X: 0,
- Y: 0,
- buf: b,
+
+ if b.Settings["saveundo"].(bool) {
+ // We should only use last time's eventhandler if the file wasn't by someone else in the meantime
+ if b.ModTime == buffer.ModTime {
+ b.EventHandler = buffer.EventHandler
+ b.EventHandler.buf = b
+ }
}
}
file.Close()
- } else {
- // Put the cursor at the first spot
- b.Cursor = Cursor{
- X: 0,
- Y: 0,
- buf: b,
- }
}
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)
+ b.rules = GetRules(b)
+}
+
+// FindFileType identifies this buffer's filetype based on the extension or header
+func (b *Buffer) FindFileType() {
+ b.Settings["filetype"] = FindFileType(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
+// by an external program since it was last read
+// If it has, we ask the user if they would like to reload the file
+func (b *Buffer) CheckModTime() {
+ modTime, ok := GetModTime(b.Path)
+ if ok {
+ if modTime != b.ModTime {
+ choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)")
+ messenger.Reset()
+ messenger.Clear()
+ if !choice || canceled {
+ // Don't load new changes -- do nothing
+ b.ModTime, _ = GetModTime(b.Path)
+ } else {
+ // Load new changes
+ b.ReOpen()
+ }
+ }
+ }
}
-func (b *Buffer) String() string {
- if b.r.Len() != 0 {
- return b.r.String()
+// ReOpen reloads the current buffer from disk
+func (b *Buffer) ReOpen() {
+ data, err := ioutil.ReadFile(b.Path)
+ txt := string(data)
+
+ if err != nil {
+ messenger.Error(err.Error())
+ return
}
- return ""
+ b.EventHandler.ApplyDiff(txt)
+
+ b.ModTime, _ = GetModTime(b.Path)
+ b.IsModified = false
+ b.Update()
+ b.Cursor.Relocate()
}
// Update fetches the string from the rope and updates the `text` and `lines` in the buffer
func (b *Buffer) Update() {
- b.Lines = strings.Split(b.String(), "\n")
- b.NumLines = len(b.Lines)
+ b.NumLines = len(b.lines)
}
// Save saves the buffer to its default path
return b.SaveAs(b.Path)
}
+// SaveWithSudo saves the buffer to the default path with sudo
+func (b *Buffer) SaveWithSudo() error {
+ return b.SaveAsWithSudo(b.Path)
+}
+
// Serialize serializes the buffer to configDir/buffers
func (b *Buffer) Serialize() error {
- if settings["savecursor"].(bool) {
- absPath, _ := filepath.Abs(b.Path)
- file, err := os.Create(configDir + "/buffers/" + EscapePath(absPath))
+ if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
+ file, err := os.Create(configDir + "/buffers/" + EscapePath(b.AbsPath))
if err == nil {
enc := gob.NewEncoder(file)
- err = enc.Encode(b.Cursor)
+ gob.Register(TextEvent{})
+ err = enc.Encode(SerializedBuffer{
+ b.EventHandler,
+ b.Cursor,
+ b.ModTime,
+ })
}
file.Close()
return err
// 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()
- data := []byte(b.String())
+ 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) {
+ 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.IsModified = false
- err = b.Serialize()
+ 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.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
+ }
+
+ // Set up everything for the command
+ cmd := exec.Command("sudo", "tee", filename)
+ cmd.Stdin = bytes.NewBufferString(b.String())
+
+ // 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
+ c := make(chan os.Signal, 1)
+ signal.Notify(c, os.Interrupt)
+ go func() {
+ for range c {
+ cmd.Process.Kill()
+ }
+ }()
+
+ // Start the command
+ cmd.Start()
+ 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()
+ }
+ if err == nil {
+ b.IsModified = false
+ b.ModTime, _ = GetModTime(filename)
+ b.Serialize()
}
return err
}
-// This directly inserts value at idx, bypassing all undo/redo
-func (b *Buffer) insert(idx int, value string) {
+func (b *Buffer) insert(pos Loc, value []byte) {
b.IsModified = true
- b.r = b.r.Insert(idx, value)
+ b.LineArray.insert(pos, value)
b.Update()
}
-
-// Remove a slice of the rope from start to end (exclusive)
-// Returns the string that was removed
-// This directly removes from start to end from the buffer, bypassing all undo/redo
-func (b *Buffer) remove(start, end int) string {
+func (b *Buffer) remove(start, end Loc) string {
b.IsModified = true
- if start < 0 {
- start = 0
- }
- if end > b.Len() {
- end = b.Len()
+ sub := b.LineArray.remove(start, end)
+ 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 {
+ return Loc{0, 0}
+}
+
+// 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}
+}
+
+// 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]
}
- if start == end {
+ return '\n'
+}
+
+// Line returns a single line
+func (b *Buffer) Line(n int) string {
+ if n >= len(b.lines) {
return ""
}
- removed := b.Substr(start, end)
- // The rope implenentation I am using wants indicies starting at 1 instead of 0
- start++
- end++
- b.r = b.r.Delete(start, end-start)
- b.Update()
- return removed
+ return string(b.lines[n])
}
-// Substr returns the substring of the rope from start to end
-func (b *Buffer) Substr(start, end int) string {
- return b.r.Substr(start+1, end-start).String()
+// Lines returns an array of strings containing the lines from start to end
+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))
+ }
+ return slice
}
// Len gives the length of the buffer
func (b *Buffer) Len() int {
- return b.r.Len()
+ 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]),
+ 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},
+ )
}