micro/cmd/micro/buffer.go

310 lines
7.4 KiB
Go
Raw Normal View History

2016-03-17 21:27:57 +00:00
package main
import (
"bytes"
"encoding/gob"
2016-03-17 21:27:57 +00:00
"io/ioutil"
"os"
"os/exec"
"os/signal"
"path/filepath"
2016-03-17 21:27:57 +00:00
"strings"
"time"
"github.com/vinzmay/go-rope"
2016-03-17 21:27:57 +00:00
)
2016-03-19 00:40:00 +00:00
// 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
2016-03-17 21:27:57 +00:00
type Buffer struct {
// The eventhandler for undo/redo
*EventHandler
2016-03-19 00:40:00 +00:00
// Stores the text of the buffer
2016-03-27 19:22:57 +00:00
r *rope.Rope
2016-03-17 21:27:57 +00:00
Cursor Cursor
2016-03-17 21:27:57 +00:00
// Path to the file on disk
2016-04-25 16:48:43 +00:00
Path string
2016-03-17 21:27:57 +00:00
// Name of the buffer on the status line
2016-04-25 16:48:43 +00:00
Name string
2016-03-17 21:27:57 +00:00
2016-05-14 16:04:13 +00:00
IsModified bool
2016-03-17 21:27:57 +00:00
// Stores the last modification time of the file the buffer is pointing to
ModTime time.Time
2016-03-19 01:25:45 +00:00
// Provide efficient and easy access to text and lines so the rope String does not
2016-03-19 00:40:00 +00:00
// need to be constantly recalculated
// These variables are updated in the update() function
2016-04-25 16:48:43 +00:00
Lines []string
NumLines int
// Syntax highlighting rules
2016-03-23 20:36:17 +00:00
rules []SyntaxRule
// The buffer's filetype
2016-05-14 16:04:13 +00:00
FileType string
2016-03-17 21:27:57 +00:00
}
2016-05-29 15:02:56 +00:00
// The SerializedBuffer holds the types that get serialized when a buffer is saved
type SerializedBuffer struct {
EventHandler *EventHandler
Cursor Cursor
ModTime time.Time
2016-05-29 15:02:56 +00:00
}
2016-03-19 00:40:00 +00:00
// NewBuffer creates a new buffer from `txt` with path and name `path`
func NewBuffer(txt, path string) *Buffer {
2016-03-17 21:27:57 +00:00
b := new(Buffer)
2016-03-27 19:22:57 +00:00
if txt == "" {
b.r = new(rope.Rope)
} else {
b.r = rope.New(txt)
}
2016-04-25 16:48:43 +00:00
b.Path = path
b.Name = path
2016-03-17 21:27:57 +00:00
b.ModTime, _ = GetModTime(b.Path)
b.EventHandler = NewEventHandler(b)
2016-03-19 00:40:00 +00:00
b.Update()
2016-03-26 14:54:18 +00:00
b.UpdateRules()
if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
os.Mkdir(configDir+"/buffers/", os.ModePerm)
}
2016-05-29 15:02:56 +00:00
// Put the cursor at the first spot
b.Cursor = Cursor{
X: 0,
Y: 0,
buf: b,
}
if settings["savecursor"].(bool) || settings["saveundo"].(bool) {
absPath, _ := filepath.Abs(b.Path)
file, err := os.Open(configDir + "/buffers/" + EscapePath(absPath))
if err == nil {
2016-05-29 15:02:56 +00:00
var buffer SerializedBuffer
decoder := gob.NewDecoder(file)
2016-05-29 15:02:56 +00:00
gob.Register(TextEvent{})
err = decoder.Decode(&buffer)
if err != nil {
TermMessage(err.Error())
}
2016-05-29 15:02:56 +00:00
if settings["savecursor"].(bool) {
b.Cursor = buffer.Cursor
b.Cursor.buf = b
b.Cursor.Relocate()
2016-05-29 15:02:56 +00:00
}
if 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()
}
2016-03-17 21:27:57 +00:00
return b
}
2016-03-26 14:54:18 +00:00
// UpdateRules updates the syntax rules and filetype for this buffer
// This is called when the colorscheme changes
func (b *Buffer) UpdateRules() {
2016-05-14 16:04:13 +00:00
b.rules, b.FileType = GetRules(b)
2016-03-26 14:54:18 +00:00
}
func (b *Buffer) String() string {
2016-05-07 14:57:40 +00:00
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
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
2016-05-30 21:48:33 +00:00
b.ReOpen()
}
}
}
}
2016-05-30 21:48:33 +00:00
// 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
}
b.EventHandler.ApplyDiff(txt)
2016-05-30 21:48:33 +00:00
b.ModTime, _ = GetModTime(b.Path)
b.IsModified = false
b.Update()
2016-05-30 22:22:10 +00:00
b.Cursor.Relocate()
2016-05-30 21:48:33 +00:00
}
2016-03-19 00:40:00 +00:00
// Update fetches the string from the rope and updates the `text` and `lines` in the buffer
func (b *Buffer) Update() {
2016-05-07 14:57:40 +00:00
b.Lines = strings.Split(b.String(), "\n")
2016-04-25 16:48:43 +00:00
b.NumLines = len(b.Lines)
2016-03-17 21:27:57 +00:00
}
2016-03-19 00:40:00 +00:00
// Save saves the buffer to its default path
func (b *Buffer) Save() error {
2016-04-25 16:48:43 +00:00
return b.SaveAs(b.Path)
2016-03-17 21:27:57 +00:00
}
// 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 {
2016-05-29 15:02:56 +00:00
if settings["savecursor"].(bool) || settings["saveundo"].(bool) {
absPath, _ := filepath.Abs(b.Path)
file, err := os.Create(configDir + "/buffers/" + EscapePath(absPath))
if err == nil {
enc := gob.NewEncoder(file)
2016-05-29 15:02:56 +00:00
gob.Register(TextEvent{})
err = enc.Encode(SerializedBuffer{
b.EventHandler,
b.Cursor,
b.ModTime,
2016-05-29 15:02:56 +00:00
})
// err = enc.Encode(b.Cursor)
}
file.Close()
return err
}
return nil
}
2016-03-19 00:40:00 +00:00
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
func (b *Buffer) SaveAs(filename string) error {
2016-04-04 22:05:34 +00:00
b.UpdateRules()
b.Name = filename
b.Path = filename
data := []byte(b.String())
err := ioutil.WriteFile(filename, data, 0644)
2016-03-23 14:28:12 +00:00
if err == nil {
b.IsModified = false
b.ModTime, _ = GetModTime(filename)
2016-05-29 15:02:56 +00:00
return b.Serialize()
2016-03-23 14:28:12 +00:00
}
2016-03-17 21:27:57 +00:00
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.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
}
// 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) {
2016-05-14 16:04:13 +00:00
b.IsModified = true
2016-03-27 19:22:57 +00:00
b.r = b.r.Insert(idx, value)
2016-03-19 00:40:00 +00:00
b.Update()
2016-03-17 21:27:57 +00:00
}
2016-03-19 00:40:00 +00:00
// Remove a slice of the rope from start to end (exclusive)
2016-03-19 22:16:10 +00:00
// 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 {
2016-05-14 16:04:13 +00:00
b.IsModified = true
if start < 0 {
start = 0
}
if end > b.Len() {
end = b.Len()
}
if start == end {
return ""
}
2016-05-07 14:57:40 +00:00
removed := b.Substr(start, end)
2016-03-27 19:22:57 +00:00
// The rope implenentation I am using wants indicies starting at 1 instead of 0
start++
end++
b.r = b.r.Delete(start, end-start)
2016-03-19 00:40:00 +00:00
b.Update()
2016-03-19 22:16:10 +00:00
return removed
2016-03-17 21:27:57 +00:00
}
// Substr returns the substring of the rope from start to end
2016-05-07 14:57:40 +00:00
func (b *Buffer) Substr(start, end int) string {
return b.r.Substr(start+1, end-start).String()
}
2016-03-19 00:40:00 +00:00
// Len gives the length of the buffer
func (b *Buffer) Len() int {
2016-03-27 19:22:57 +00:00
return b.r.Len()
2016-03-17 21:27:57 +00:00
}