mirror of
https://github.com/zyedidia/micro.git
synced 2026-02-06 07:00:24 +09:00
This commit adds beta support for running a shell or other program within a micro view. Use the `> term` command. With no arguments, `term` will open your shell in interactive mode. You can also run an arbitrary command with `> term cmd` and the command with be executed and output shown. One issue at the moment is the terminal window will close immediately after the process dies. No mouse events are sent to programs running within micro. Ref #243
633 lines
15 KiB
Go
633 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/md5"
|
|
"encoding/gob"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/zyedidia/micro/cmd/micro/highlight"
|
|
)
|
|
|
|
var (
|
|
// 0 - no line type detected
|
|
// 1 - lf detected
|
|
// 2 - crlf detected
|
|
fileformat = 0
|
|
)
|
|
|
|
// 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
|
|
type Buffer struct {
|
|
// The eventhandler for undo/redo
|
|
*EventHandler
|
|
// This stores all the text in the buffer as an array of lines
|
|
*LineArray
|
|
|
|
Cursor Cursor
|
|
cursors []*Cursor // for multiple cursors
|
|
curCursor int // the current 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
|
|
|
|
// 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
|
|
|
|
// 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{}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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 {
|
|
if path != "" {
|
|
for _, tab := range tabs {
|
|
for _, view := range tab.views {
|
|
if view.Buf.Path == path {
|
|
return view.Buf
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
b := new(Buffer)
|
|
b.LineArray = NewLineArray(size, reader)
|
|
|
|
b.Settings = DefaultLocalSettings()
|
|
for k, v := range globalSettings {
|
|
if _, ok := b.Settings[k]; ok {
|
|
b.Settings[k] = v
|
|
}
|
|
}
|
|
|
|
if fileformat == 1 {
|
|
b.Settings["fileformat"] = "unix"
|
|
} else if fileformat == 2 {
|
|
b.Settings["fileformat"] = "dos"
|
|
}
|
|
|
|
absPath, _ := filepath.Abs(path)
|
|
|
|
b.Path = path
|
|
b.AbsPath = absPath
|
|
|
|
// The last time this file was modified
|
|
b.ModTime, _ = GetModTime(b.Path)
|
|
|
|
b.EventHandler = NewEventHandler(b)
|
|
|
|
b.Update()
|
|
b.UpdateRules()
|
|
|
|
if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
|
|
os.Mkdir(configDir+"/buffers/", os.ModePerm)
|
|
}
|
|
|
|
// 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 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 err == nil {
|
|
var buffer SerializedBuffer
|
|
decoder := gob.NewDecoder(file)
|
|
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.")
|
|
}
|
|
if b.Settings["savecursor"].(bool) {
|
|
b.Cursor = buffer.Cursor
|
|
b.Cursor.buf = b
|
|
b.Cursor.Relocate()
|
|
}
|
|
|
|
if b.Settings["saveundo"].(bool) {
|
|
// 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 == "" {
|
|
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() {
|
|
rehighlight := false
|
|
var files []*highlight.File
|
|
for _, f := range ListRuntimeFiles(RTSyntax) {
|
|
data, err := f.Data()
|
|
if err != nil {
|
|
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
|
} else {
|
|
file, err := highlight.ParseFile(data)
|
|
if err != nil {
|
|
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
|
continue
|
|
}
|
|
ftdetect, err := highlight.ParseFtDetect(file)
|
|
if err != nil {
|
|
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
|
continue
|
|
}
|
|
|
|
ft := b.Settings["filetype"].(string)
|
|
if (ft == "Unknown" || ft == "") && !rehighlight {
|
|
if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
|
|
header := new(highlight.Header)
|
|
header.FileType = file.FileType
|
|
header.FtDetect = ftdetect
|
|
b.syntaxDef, err = highlight.ParseDef(file, header)
|
|
if err != nil {
|
|
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
|
continue
|
|
}
|
|
rehighlight = true
|
|
}
|
|
} else {
|
|
if file.FileType == ft && !rehighlight {
|
|
header := new(highlight.Header)
|
|
header.FileType = file.FileType
|
|
header.FtDetect = ftdetect
|
|
b.syntaxDef, err = highlight.ParseDef(file, header)
|
|
if err != nil {
|
|
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
|
|
continue
|
|
}
|
|
rehighlight = true
|
|
}
|
|
}
|
|
files = append(files, file)
|
|
}
|
|
}
|
|
|
|
if b.syntaxDef != nil {
|
|
highlight.ResolveIncludes(b.syntaxDef, files)
|
|
}
|
|
|
|
if b.highlighter == nil || rehighlight {
|
|
if b.syntaxDef != nil {
|
|
b.Settings["filetype"] = b.syntaxDef.FileType
|
|
b.highlighter = highlight.NewHighlighter(b.syntaxDef)
|
|
if b.Settings["syntax"].(bool) {
|
|
b.highlighter.HighlightStates(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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
|
|
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.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++ {
|
|
c1 := b.cursors[i]
|
|
if c1 != nil {
|
|
for j := 0; j < len(b.cursors); j++ {
|
|
c2 := b.cursors[j]
|
|
if c2 != nil && i != j && c1.Loc == c2.Loc {
|
|
b.cursors[j] = nil
|
|
}
|
|
}
|
|
cursors = append(cursors, c1)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// Save saves the buffer to its default path
|
|
func (b *Buffer) Save() error {
|
|
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 b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
|
|
file, err := os.Create(configDir + "/buffers/" + EscapePath(b.AbsPath))
|
|
if err == nil {
|
|
enc := gob.NewEncoder(file)
|
|
gob.Register(TextEvent{})
|
|
err = enc.Encode(SerializedBuffer{
|
|
b.EventHandler,
|
|
b.Cursor,
|
|
b.ModTime,
|
|
})
|
|
}
|
|
err = file.Close()
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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()
|
|
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")
|
|
}
|
|
}
|
|
|
|
defer func() {
|
|
b.ModTime, _ = GetModTime(filename)
|
|
}()
|
|
|
|
f, err := os.OpenFile(ReplaceHome(filename), 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
|
|
// 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
|
|
|
|
// 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(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
|
|
// 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()
|
|
|
|
// Start the screen back up
|
|
InitScreen()
|
|
if err == nil {
|
|
b.IsModified = false
|
|
b.ModTime, _ = GetModTime(filename)
|
|
b.Serialize()
|
|
}
|
|
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)
|
|
b.Update()
|
|
}
|
|
func (b *Buffer) remove(start, end Loc) string {
|
|
b.IsModified = true
|
|
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].data), 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]
|
|
}
|
|
return '\n'
|
|
}
|
|
|
|
// Line returns a single line
|
|
func (b *Buffer) Line(n int) string {
|
|
if n >= len(b.lines) {
|
|
return ""
|
|
}
|
|
return string(b.lines[n].data)
|
|
}
|
|
|
|
// LinesNum returns the number of lines in the buffer
|
|
func (b *Buffer) LinesNum() int {
|
|
return len(b.lines)
|
|
}
|
|
|
|
// 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.data))
|
|
}
|
|
return slice
|
|
}
|
|
|
|
// Len gives the length of the buffer
|
|
func (b *Buffer) Len() int {
|
|
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].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},
|
|
)
|
|
}
|
|
|
|
// 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},
|
|
)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|