package main
import (
+ "bytes"
"encoding/gob"
"io/ioutil"
"os"
+ "os/exec"
+ "os/signal"
"path/filepath"
- "strings"
"time"
-
- "github.com/vinzmay/go-rope"
+ "unicode/utf8"
)
// 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
// Name of the buffer on the status line
Name string
+ // Whether or not the buffer has been modified since it was opened
IsModified bool
// Stores the last modification time of the file the buffer is pointing to
ModTime time.Time
- // 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
NumLines int
// Syntax highlighting rules
}
// 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
}
// NewBuffer creates a new buffer from `txt` with path and name `path`
-func NewBuffer(txt, path string) *Buffer {
+func NewBuffer(txt []byte, path string) *Buffer {
b := new(Buffer)
- if txt == "" {
- b.r = new(rope.Rope)
- } else {
- b.r = rope.New(txt)
- }
+ b.LineArray = NewLineArray(txt)
+
b.Path = path
b.Name = path
+ // If the file doesn't have a path to disk then we give it no name
+ if path == "" {
+ b.Name = "No name"
+ }
+
+ // The last time this file was modified
b.ModTime, _ = GetModTime(b.Path)
b.EventHandler = NewEventHandler(b)
// Put the cursor at the first spot
b.Cursor = Cursor{
- X: 0,
- Y: 0,
+ Loc: Loc{
+ X: 0,
+ Y: 0,
+ },
buf: b,
}
if settings["savecursor"].(bool) || 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))
if err == nil {
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 settings["savecursor"].(bool) {
b.Cursor = buffer.Cursor
b.rules, b.FileType = GetRules(b)
}
-func (b *Buffer) String() string {
- if b.r.Len() != 0 {
- return b.r.String()
- }
- return ""
-}
-
// 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
messenger.Error(err.Error())
return
}
- if txt == "" {
- b.r = new(rope.Rope)
- } else {
- b.r = rope.New(txt)
- }
+ b.EventHandler.ApplyDiff(txt)
b.ModTime, _ = GetModTime(b.Path)
b.IsModified = false
// 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) || settings["saveundo"].(bool) {
return err
}
-// This directly inserts value at idx, bypassing all undo/redo
-func (b *Buffer) insert(idx int, value string) {
- b.IsModified = true
- b.r = b.r.Insert(idx, value)
- b.Update()
-}
+// 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.UpdateRules()
+ b.Name = filename
+ b.Path = filename
-// 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 {
- b.IsModified = true
- if start < 0 {
- start = 0
+ // 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
}
- if end > b.Len() {
- end = b.Len()
+
+ // 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 start == end {
- return ""
+ if err == nil {
+ b.IsModified = false
+ b.ModTime, _ = GetModTime(filename)
+ b.Serialize()
}
- 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)
+ return err
+}
+
+func (b *Buffer) insert(pos Loc, value []byte) {
+ b.IsModified = true
+ b.LineArray.insert(pos, value)
b.Update()
- return removed
+}
+func (b *Buffer) remove(start, end Loc) string {
+ b.IsModified = true
+ sub := b.LineArray.remove(start, end)
+ b.Update()
+ return sub
+}
+
+// Start returns the location of the first character in the buffer
+func (b *Buffer) Start() Loc {
+ return Loc{0, 0}
}
-// 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()
+// 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}
+}
+
+// Line returns a single line
+func (b *Buffer) Line(n int) string {
+ return string(b.lines[n])
+}
+
+// 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())
}