micro/cmd/micro/buffer.go

478 lines
12 KiB
Go
Raw Normal View History

2016-03-17 21:27:57 +00:00
package main
import (
"bytes"
"encoding/gob"
"io"
2016-03-17 21:27:57 +00:00
"io/ioutil"
"os"
"os/exec"
"os/signal"
"path/filepath"
2016-12-15 07:54:30 +00:00
"regexp"
"strconv"
"strings"
"time"
2016-06-07 15:43:28 +00:00
"unicode/utf8"
2016-12-13 13:58:08 +00:00
"github.com/mitchellh/go-homedir"
"github.com/zyedidia/micro/cmd/micro/highlight"
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-07-10 17:26:05 +00:00
// This stores all the text in the buffer as an array of lines
2016-06-07 15:43:28 +00:00
*LineArray
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
// Absolute path to the file on disk
AbsPath string
2016-03-17 21:27:57 +00:00
// Name of the buffer on the status line
2016-11-20 00:07:51 +00:00
name string
2016-03-17 21:27:57 +00:00
2016-07-10 17:26:05 +00:00
// Whether or not the buffer has been modified since it was opened
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-04-25 16:48:43 +00:00
NumLines int
syntaxDef *highlight.Def
highlighter *highlight.Highlighter
2016-08-24 23:55:44 +00:00
// Buffer local settings
Settings map[string]interface{}
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
2016-07-10 17:26:05 +00:00
// These are used for the savecursor and saveundo options
2016-05-29 15:02:56 +00:00
type SerializedBuffer struct {
EventHandler *EventHandler
Cursor Cursor
ModTime time.Time
2016-05-29 15:02:56 +00:00
}
func NewBufferFromString(text, path string) *Buffer {
return NewBuffer(strings.NewReader(text), path)
}
// 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
}
}
}
}
2016-03-17 21:27:57 +00:00
b := new(Buffer)
b.LineArray = NewLineArray(reader)
b.Settings = DefaultLocalSettings()
2016-08-24 23:55:44 +00:00
for k, v := range globalSettings {
if _, ok := b.Settings[k]; ok {
b.Settings[k] = v
}
2016-08-24 23:55:44 +00:00
}
absPath, _ := filepath.Abs(path)
2016-04-25 16:48:43 +00:00
b.Path = path
b.AbsPath = absPath
2016-07-10 17:26:05 +00:00
// The last time this file was modified
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
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
}
}
}
}
2016-05-29 15:02:56 +00:00
b.Cursor = Cursor{
2016-06-07 15:43:28 +00:00
Loc: Loc{
X: cursorStartX,
Y: cursorStartY,
2016-06-07 15:43:28 +00:00
},
2016-05-29 15:02:56 +00:00
buf: b,
}
InitLocalSettings(b)
2016-08-24 23:55:44 +00:00
if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
2016-07-10 17:26:05 +00:00
// 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 {
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(), "\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.")
}
2016-08-24 23:55:44 +00:00
if b.Settings["savecursor"].(bool) {
2016-05-29 15:02:56 +00:00
b.Cursor = buffer.Cursor
b.Cursor.buf = b
b.Cursor.Relocate()
2016-05-29 15:02:56 +00:00
}
2016-08-24 23:55:44 +00:00
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()
}
2016-03-17 21:27:57 +00:00
return b
}
2016-11-20 00:07:51 +00:00
func (b *Buffer) GetName() string {
if b.name == "" {
if b.Path == "" {
return "No name"
}
2016-11-20 00:07:51 +00:00
return b.Path
}
return b.name
}
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() {
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)
2017-02-19 21:59:46 +00:00
if b.Settings["syntax"].(bool) {
b.highlighter.HighlightStates(b)
2017-02-19 21:59:46 +00:00
}
}
}
// FileType returns the buffer's filetype
func (b *Buffer) FileType() string {
return b.Settings["filetype"].(string)
2016-03-26 14:54:18 +00:00
}
// 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)))
}
2016-10-18 15:12:28 +00:00
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
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-06-07 15:43:28 +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-08-24 23:55:44 +00:00
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)
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
})
}
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()
2016-12-13 13:58:08 +00:00
dir, _ := homedir.Dir()
b.Path = strings.Replace(filename, "~", dir, 1)
2016-12-15 07:54:30 +00:00
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)
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
}
b.ModTime, _ = GetModTime(filename)
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.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
}
2016-06-07 15:43:28 +00:00
func (b *Buffer) insert(pos Loc, value []byte) {
2016-05-14 16:04:13 +00:00
b.IsModified = true
2016-06-07 15:43:28 +00:00
b.LineArray.insert(pos, value)
2016-03-19 00:40:00 +00:00
b.Update()
2016-03-17 21:27:57 +00:00
}
2016-06-07 15:43:28 +00:00
func (b *Buffer) remove(start, end Loc) string {
2016-05-14 16:04:13 +00:00
b.IsModified = true
2016-06-07 15:43:28 +00:00
sub := b.LineArray.remove(start, end)
2016-03-19 00:40:00 +00:00
b.Update()
2016-06-07 15:43:28 +00:00
return sub
}
2016-12-15 07:54:30 +00:00
func (b *Buffer) deleteToEnd(start Loc) {
b.IsModified = true
b.LineArray.DeleteToEnd(start)
b.Update()
}
2016-06-07 15:43:28 +00:00
// 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 {
2017-02-18 20:45:49 +00:00
return Loc{utf8.RuneCount(b.lines[b.NumLines-1].data), b.NumLines - 1}
2016-06-07 15:43:28 +00:00
}
// 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]
}
return '\n'
}
2016-06-07 15:43:28 +00:00
// Line returns a single line
func (b *Buffer) Line(n int) string {
if n >= len(b.lines) {
return ""
}
2017-02-18 20:45:49 +00:00
return string(b.lines[n].data)
2016-03-17 21:27:57 +00:00
}
2016-06-07 15:43:28 +00:00
// 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 {
2017-02-18 20:45:49 +00:00
slice = append(slice, string(line.data))
2016-06-07 15:43:28 +00:00
}
return slice
2016-05-07 14:57:40 +00:00
}
2016-03-19 00:40:00 +00:00
// Len gives the length of the buffer
func (b *Buffer) Len() int {
2016-06-07 15:43:28 +00:00
return Count(b.String())
2016-03-17 21:27:57 +00:00
}
2016-10-18 15:12:28 +00:00
// 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{
2017-02-18 20:45:49 +00:00
utf8.RuneCount(b.lines[end-1].data),
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},
)
}
2016-10-18 15:12:28 +00:00
// 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",
)
2016-10-18 15:12:28 +00:00
end++
b.Remove(
Loc{0, end},
Loc{0, end + 1},
)
}
// ClearMatches clears all of the syntax highlighting for this buffer
func (b *Buffer) ClearMatches() {
for i := range b.lines {
b.SetMatch(i, nil)
b.SetState(i, nil)
}
}