Files
zyedidia.micro/cmd/micro/bindings.go
Zachary Yedidia a92a7dc4e6 Add savecursor option
This adds the `savecursor` option which will remember where the cursor
was when the file was closed and put it back when the file is opened
again. The option is off by default so that people aren't confused as to
why the cursor isn't at the start of a file when they open it.

This commit also adds a more general ability to serialize a buffer so
various components can be saved (which could also be useful for persistent
undo).

Fixes #107
2016-05-28 17:29:49 -04:00

968 lines
28 KiB
Go

package main
import (
"encoding/json"
"io/ioutil"
"os"
"strconv"
"strings"
"time"
"github.com/mitchellh/go-homedir"
"github.com/zyedidia/clipboard"
"github.com/zyedidia/tcell"
)
var bindings map[Key]func(*View) bool
var helpBinding string
// The Key struct holds the data for a keypress (keycode + modifiers)
type Key struct {
keyCode tcell.Key
modifiers tcell.ModMask
r rune
}
// InitBindings initializes the keybindings for micro
func InitBindings() {
bindings = make(map[Key]func(*View) bool)
actions := map[string]func(*View) bool{
"CursorUp": (*View).CursorUp,
"CursorDown": (*View).CursorDown,
"CursorLeft": (*View).CursorLeft,
"CursorRight": (*View).CursorRight,
"CursorStart": (*View).CursorStart,
"CursorEnd": (*View).CursorEnd,
"SelectToStart": (*View).SelectToStart,
"SelectToEnd": (*View).SelectToEnd,
"SelectUp": (*View).SelectUp,
"SelectDown": (*View).SelectDown,
"SelectLeft": (*View).SelectLeft,
"SelectRight": (*View).SelectRight,
"WordRight": (*View).WordRight,
"WordLeft": (*View).WordLeft,
"SelectWordRight": (*View).SelectWordRight,
"SelectWordLeft": (*View).SelectWordLeft,
"SelectToStartOfLine": (*View).SelectToStartOfLine,
"SelectToEndOfLine": (*View).SelectToEndOfLine,
"InsertEnter": (*View).InsertEnter,
"InsertSpace": (*View).InsertSpace,
"Backspace": (*View).Backspace,
"Delete": (*View).Delete,
"InsertTab": (*View).InsertTab,
"Save": (*View).Save,
"Find": (*View).Find,
"FindNext": (*View).FindNext,
"FindPrevious": (*View).FindPrevious,
"Undo": (*View).Undo,
"Redo": (*View).Redo,
"Copy": (*View).Copy,
"Cut": (*View).Cut,
"CutLine": (*View).CutLine,
"DuplicateLine": (*View).DuplicateLine,
"Paste": (*View).Paste,
"SelectAll": (*View).SelectAll,
"OpenFile": (*View).OpenFile,
"Start": (*View).Start,
"End": (*View).End,
"PageUp": (*View).PageUp,
"PageDown": (*View).PageDown,
"HalfPageUp": (*View).HalfPageUp,
"HalfPageDown": (*View).HalfPageDown,
"StartOfLine": (*View).StartOfLine,
"EndOfLine": (*View).EndOfLine,
"ToggleHelp": (*View).ToggleHelp,
"ToggleRuler": (*View).ToggleRuler,
"JumpLine": (*View).JumpLine,
"ClearStatus": (*View).ClearStatus,
"ShellMode": (*View).ShellMode,
"CommandMode": (*View).CommandMode,
"Quit": (*View).Quit,
}
keys := map[string]Key{
"Up": Key{tcell.KeyUp, tcell.ModNone, 0},
"Down": Key{tcell.KeyDown, tcell.ModNone, 0},
"Right": Key{tcell.KeyRight, tcell.ModNone, 0},
"Left": Key{tcell.KeyLeft, tcell.ModNone, 0},
"AltUp": Key{tcell.KeyUp, tcell.ModAlt, 0},
"AltDown": Key{tcell.KeyDown, tcell.ModAlt, 0},
"AltLeft": Key{tcell.KeyLeft, tcell.ModAlt, 0},
"AltRight": Key{tcell.KeyRight, tcell.ModAlt, 0},
"CtrlUp": Key{tcell.KeyUp, tcell.ModCtrl, 0},
"CtrlDown": Key{tcell.KeyDown, tcell.ModCtrl, 0},
"CtrlLeft": Key{tcell.KeyLeft, tcell.ModCtrl, 0},
"CtrlRight": Key{tcell.KeyRight, tcell.ModCtrl, 0},
"ShiftUp": Key{tcell.KeyUp, tcell.ModShift, 0},
"ShiftDown": Key{tcell.KeyDown, tcell.ModShift, 0},
"ShiftLeft": Key{tcell.KeyLeft, tcell.ModShift, 0},
"ShiftRight": Key{tcell.KeyRight, tcell.ModShift, 0},
"AltShiftUp": Key{tcell.KeyUp, tcell.ModShift | tcell.ModAlt, 0},
"AltShiftDown": Key{tcell.KeyDown, tcell.ModShift | tcell.ModAlt, 0},
"AltShiftLeft": Key{tcell.KeyLeft, tcell.ModShift | tcell.ModAlt, 0},
"AltShiftRight": Key{tcell.KeyRight, tcell.ModShift | tcell.ModAlt, 0},
"CtrlShiftUp": Key{tcell.KeyUp, tcell.ModShift | tcell.ModCtrl, 0},
"CtrlShiftDown": Key{tcell.KeyDown, tcell.ModShift | tcell.ModCtrl, 0},
"CtrlShiftLeft": Key{tcell.KeyLeft, tcell.ModShift | tcell.ModCtrl, 0},
"CtrlShiftRight": Key{tcell.KeyRight, tcell.ModShift | tcell.ModCtrl, 0},
"UpLeft": Key{tcell.KeyUpLeft, tcell.ModNone, 0},
"UpRight": Key{tcell.KeyUpRight, tcell.ModNone, 0},
"DownLeft": Key{tcell.KeyDownLeft, tcell.ModNone, 0},
"DownRight": Key{tcell.KeyDownRight, tcell.ModNone, 0},
"Center": Key{tcell.KeyCenter, tcell.ModNone, 0},
"PgUp": Key{tcell.KeyPgUp, tcell.ModNone, 0},
"PgDn": Key{tcell.KeyPgDn, tcell.ModNone, 0},
"Home": Key{tcell.KeyHome, tcell.ModNone, 0},
"End": Key{tcell.KeyEnd, tcell.ModNone, 0},
"Insert": Key{tcell.KeyInsert, tcell.ModNone, 0},
"Delete": Key{tcell.KeyDelete, tcell.ModNone, 0},
"Help": Key{tcell.KeyHelp, tcell.ModNone, 0},
"Exit": Key{tcell.KeyExit, tcell.ModNone, 0},
"Clear": Key{tcell.KeyClear, tcell.ModNone, 0},
"Cancel": Key{tcell.KeyCancel, tcell.ModNone, 0},
"Print": Key{tcell.KeyPrint, tcell.ModNone, 0},
"Pause": Key{tcell.KeyPause, tcell.ModNone, 0},
"Backtab": Key{tcell.KeyBacktab, tcell.ModNone, 0},
"F1": Key{tcell.KeyF1, tcell.ModNone, 0},
"F2": Key{tcell.KeyF2, tcell.ModNone, 0},
"F3": Key{tcell.KeyF3, tcell.ModNone, 0},
"F4": Key{tcell.KeyF4, tcell.ModNone, 0},
"F5": Key{tcell.KeyF5, tcell.ModNone, 0},
"F6": Key{tcell.KeyF6, tcell.ModNone, 0},
"F7": Key{tcell.KeyF7, tcell.ModNone, 0},
"F8": Key{tcell.KeyF8, tcell.ModNone, 0},
"F9": Key{tcell.KeyF9, tcell.ModNone, 0},
"F10": Key{tcell.KeyF10, tcell.ModNone, 0},
"F11": Key{tcell.KeyF11, tcell.ModNone, 0},
"F12": Key{tcell.KeyF12, tcell.ModNone, 0},
"F13": Key{tcell.KeyF13, tcell.ModNone, 0},
"F14": Key{tcell.KeyF14, tcell.ModNone, 0},
"F15": Key{tcell.KeyF15, tcell.ModNone, 0},
"F16": Key{tcell.KeyF16, tcell.ModNone, 0},
"F17": Key{tcell.KeyF17, tcell.ModNone, 0},
"F18": Key{tcell.KeyF18, tcell.ModNone, 0},
"F19": Key{tcell.KeyF19, tcell.ModNone, 0},
"F20": Key{tcell.KeyF20, tcell.ModNone, 0},
"F21": Key{tcell.KeyF21, tcell.ModNone, 0},
"F22": Key{tcell.KeyF22, tcell.ModNone, 0},
"F23": Key{tcell.KeyF23, tcell.ModNone, 0},
"F24": Key{tcell.KeyF24, tcell.ModNone, 0},
"F25": Key{tcell.KeyF25, tcell.ModNone, 0},
"F26": Key{tcell.KeyF26, tcell.ModNone, 0},
"F27": Key{tcell.KeyF27, tcell.ModNone, 0},
"F28": Key{tcell.KeyF28, tcell.ModNone, 0},
"F29": Key{tcell.KeyF29, tcell.ModNone, 0},
"F30": Key{tcell.KeyF30, tcell.ModNone, 0},
"F31": Key{tcell.KeyF31, tcell.ModNone, 0},
"F32": Key{tcell.KeyF32, tcell.ModNone, 0},
"F33": Key{tcell.KeyF33, tcell.ModNone, 0},
"F34": Key{tcell.KeyF34, tcell.ModNone, 0},
"F35": Key{tcell.KeyF35, tcell.ModNone, 0},
"F36": Key{tcell.KeyF36, tcell.ModNone, 0},
"F37": Key{tcell.KeyF37, tcell.ModNone, 0},
"F38": Key{tcell.KeyF38, tcell.ModNone, 0},
"F39": Key{tcell.KeyF39, tcell.ModNone, 0},
"F40": Key{tcell.KeyF40, tcell.ModNone, 0},
"F41": Key{tcell.KeyF41, tcell.ModNone, 0},
"F42": Key{tcell.KeyF42, tcell.ModNone, 0},
"F43": Key{tcell.KeyF43, tcell.ModNone, 0},
"F44": Key{tcell.KeyF44, tcell.ModNone, 0},
"F45": Key{tcell.KeyF45, tcell.ModNone, 0},
"F46": Key{tcell.KeyF46, tcell.ModNone, 0},
"F47": Key{tcell.KeyF47, tcell.ModNone, 0},
"F48": Key{tcell.KeyF48, tcell.ModNone, 0},
"F49": Key{tcell.KeyF49, tcell.ModNone, 0},
"F50": Key{tcell.KeyF50, tcell.ModNone, 0},
"F51": Key{tcell.KeyF51, tcell.ModNone, 0},
"F52": Key{tcell.KeyF52, tcell.ModNone, 0},
"F53": Key{tcell.KeyF53, tcell.ModNone, 0},
"F54": Key{tcell.KeyF54, tcell.ModNone, 0},
"F55": Key{tcell.KeyF55, tcell.ModNone, 0},
"F56": Key{tcell.KeyF56, tcell.ModNone, 0},
"F57": Key{tcell.KeyF57, tcell.ModNone, 0},
"F58": Key{tcell.KeyF58, tcell.ModNone, 0},
"F59": Key{tcell.KeyF59, tcell.ModNone, 0},
"F60": Key{tcell.KeyF60, tcell.ModNone, 0},
"F61": Key{tcell.KeyF61, tcell.ModNone, 0},
"F62": Key{tcell.KeyF62, tcell.ModNone, 0},
"F63": Key{tcell.KeyF63, tcell.ModNone, 0},
"F64": Key{tcell.KeyF64, tcell.ModNone, 0},
"CtrlSpace": Key{tcell.KeyCtrlSpace, tcell.ModCtrl, 0},
"CtrlA": Key{tcell.KeyCtrlA, tcell.ModCtrl, 0},
"CtrlB": Key{tcell.KeyCtrlB, tcell.ModCtrl, 0},
"CtrlC": Key{tcell.KeyCtrlC, tcell.ModCtrl, 0},
"CtrlD": Key{tcell.KeyCtrlD, tcell.ModCtrl, 0},
"CtrlE": Key{tcell.KeyCtrlE, tcell.ModCtrl, 0},
"CtrlF": Key{tcell.KeyCtrlF, tcell.ModCtrl, 0},
"CtrlG": Key{tcell.KeyCtrlG, tcell.ModCtrl, 0},
"CtrlH": Key{tcell.KeyCtrlH, tcell.ModCtrl, 0},
"CtrlI": Key{tcell.KeyCtrlI, tcell.ModCtrl, 0},
"CtrlJ": Key{tcell.KeyCtrlJ, tcell.ModCtrl, 0},
"CtrlK": Key{tcell.KeyCtrlK, tcell.ModCtrl, 0},
"CtrlL": Key{tcell.KeyCtrlL, tcell.ModCtrl, 0},
"CtrlM": Key{tcell.KeyCtrlM, tcell.ModCtrl, 0},
"CtrlN": Key{tcell.KeyCtrlN, tcell.ModCtrl, 0},
"CtrlO": Key{tcell.KeyCtrlO, tcell.ModCtrl, 0},
"CtrlP": Key{tcell.KeyCtrlP, tcell.ModCtrl, 0},
"CtrlQ": Key{tcell.KeyCtrlQ, tcell.ModCtrl, 0},
"CtrlR": Key{tcell.KeyCtrlR, tcell.ModCtrl, 0},
"CtrlS": Key{tcell.KeyCtrlS, tcell.ModCtrl, 0},
"CtrlT": Key{tcell.KeyCtrlT, tcell.ModCtrl, 0},
"CtrlU": Key{tcell.KeyCtrlU, tcell.ModCtrl, 0},
"CtrlV": Key{tcell.KeyCtrlV, tcell.ModCtrl, 0},
"CtrlW": Key{tcell.KeyCtrlW, tcell.ModCtrl, 0},
"CtrlX": Key{tcell.KeyCtrlX, tcell.ModCtrl, 0},
"CtrlY": Key{tcell.KeyCtrlY, tcell.ModCtrl, 0},
"CtrlZ": Key{tcell.KeyCtrlZ, tcell.ModCtrl, 0},
"CtrlLeftSq": Key{tcell.KeyCtrlLeftSq, tcell.ModCtrl, 0},
"CtrlBackslash": Key{tcell.KeyCtrlBackslash, tcell.ModCtrl, 0},
"CtrlRightSq": Key{tcell.KeyCtrlRightSq, tcell.ModCtrl, 0},
"CtrlCarat": Key{tcell.KeyCtrlCarat, tcell.ModCtrl, 0},
"CtrlUnderscore": Key{tcell.KeyCtrlUnderscore, tcell.ModCtrl, 0},
"Backspace": Key{tcell.KeyBackspace, tcell.ModNone, 0},
"Tab": Key{tcell.KeyTab, tcell.ModNone, 0},
"Esc": Key{tcell.KeyEsc, tcell.ModNone, 0},
"Escape": Key{tcell.KeyEscape, tcell.ModNone, 0},
"Enter": Key{tcell.KeyEnter, tcell.ModNone, 0},
"Backspace2": Key{tcell.KeyBackspace2, tcell.ModNone, 0},
}
var parsed map[string]string
defaults := DefaultBindings()
filename := configDir + "/bindings.json"
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if err != nil {
TermMessage("Error reading bindings.json file: " + err.Error())
return
}
err = json.Unmarshal(input, &parsed)
if err != nil {
TermMessage("Error reading bindings.json:", err.Error())
}
}
for k, v := range defaults {
if strings.Contains(k, "Alt-") {
key := Key{tcell.KeyRune, tcell.ModAlt, rune(k[len(k)-1])}
bindings[key] = actions[v]
} else {
bindings[keys[k]] = actions[v]
}
if v == "ToggleHelp" {
helpBinding = k
}
}
for k, v := range parsed {
if strings.Contains(k, "Alt-") {
key := Key{tcell.KeyRune, tcell.ModAlt, rune(k[len(k)-1])}
bindings[key] = actions[v]
} else {
bindings[keys[k]] = actions[v]
}
if v == "ToggleHelp" {
helpBinding = k
}
}
}
// DefaultBindings returns a map containing micro's default keybindings
func DefaultBindings() map[string]string {
return map[string]string{
"Up": "CursorUp",
"Down": "CursorDown",
"Right": "CursorRight",
"Left": "CursorLeft",
"ShiftUp": "SelectUp",
"ShiftDown": "SelectDown",
"ShiftLeft": "SelectLeft",
"ShiftRight": "SelectRight",
"AltLeft": "WordLeft",
"AltRight": "WordRight",
"AltShiftRight": "SelectWordRight",
"AltShiftLeft": "SelectWordLeft",
"CtrlLeft": "StartOfLine",
"CtrlRight": "EndOfLine",
"CtrlShiftLeft": "SelectToStartOfLine",
"CtrlShiftRight": "SelectToEndOfLine",
"CtrlUp": "CursorStart",
"CtrlDown": "CursorEnd",
"CtrlShiftUp": "SelectToStart",
"CtrlShiftDown": "SelectToEnd",
"Enter": "InsertEnter",
"Space": "InsertSpace",
"Backspace": "Backspace",
"Backspace2": "Backspace",
"Tab": "InsertTab",
"CtrlO": "OpenFile",
"CtrlS": "Save",
"CtrlF": "Find",
"CtrlN": "FindNext",
"CtrlP": "FindPrevious",
"CtrlZ": "Undo",
"CtrlY": "Redo",
"CtrlC": "Copy",
"CtrlX": "Cut",
"CtrlK": "CutLine",
"CtrlD": "DuplicateLine",
"CtrlV": "Paste",
"CtrlA": "SelectAll",
"Home": "Start",
"End": "End",
"PgUp": "PageUp",
"PgDn": "PageDown",
"CtrlG": "ToggleHelp",
"CtrlR": "ToggleRuler",
"CtrlL": "JumpLine",
"Delete": "Delete",
"Esc": "ClearStatus",
"CtrlB": "ShellMode",
"CtrlQ": "Quit",
"CtrlE": "CommandMode",
// Emacs-style keybindings
"Alt-f": "WordRight",
"Alt-b": "WordLeft",
"Alt-a": "StartOfLine",
"Alt-e": "EndOfLine",
"Alt-p": "CursorUp",
"Alt-n": "CursorDown",
}
}
// CursorUp moves the cursor up
func (v *View) CursorUp() bool {
if v.Cursor.HasSelection() {
v.Cursor.SetLoc(v.Cursor.CurSelection[0])
v.Cursor.ResetSelection()
}
v.Cursor.Up()
return true
}
// CursorDown moves the cursor down
func (v *View) CursorDown() bool {
if v.Cursor.HasSelection() {
v.Cursor.SetLoc(v.Cursor.CurSelection[1])
v.Cursor.ResetSelection()
}
v.Cursor.Down()
return true
}
// CursorLeft moves the cursor left
func (v *View) CursorLeft() bool {
if v.Cursor.HasSelection() {
v.Cursor.SetLoc(v.Cursor.CurSelection[0])
v.Cursor.ResetSelection()
} else {
v.Cursor.Left()
}
return true
}
// CursorRight moves the cursor right
func (v *View) CursorRight() bool {
if v.Cursor.HasSelection() {
v.Cursor.SetLoc(v.Cursor.CurSelection[1] - 1)
v.Cursor.ResetSelection()
} else {
v.Cursor.Right()
}
return true
}
// WordRight moves the cursor one word to the right
func (v *View) WordRight() bool {
v.Cursor.WordRight()
return true
}
// WordLeft moves the cursor one word to the left
func (v *View) WordLeft() bool {
v.Cursor.WordLeft()
return true
}
// SelectUp selects up one line
func (v *View) SelectUp() bool {
loc := v.Cursor.Loc()
if !v.Cursor.HasSelection() {
v.Cursor.OrigSelection[0] = loc
}
v.Cursor.Up()
v.Cursor.SelectTo(v.Cursor.Loc())
return true
}
// SelectDown selects down one line
func (v *View) SelectDown() bool {
loc := v.Cursor.Loc()
if !v.Cursor.HasSelection() {
v.Cursor.OrigSelection[0] = loc
}
v.Cursor.Down()
v.Cursor.SelectTo(v.Cursor.Loc())
return true
}
// SelectLeft selects the character to the left of the cursor
func (v *View) SelectLeft() bool {
loc := v.Cursor.Loc()
count := v.Buf.Len() - 1
if loc > count {
loc = count
}
if !v.Cursor.HasSelection() {
v.Cursor.OrigSelection[0] = loc
}
v.Cursor.Left()
v.Cursor.SelectTo(v.Cursor.Loc())
return true
}
// SelectRight selects the character to the right of the cursor
func (v *View) SelectRight() bool {
loc := v.Cursor.Loc()
count := v.Buf.Len() - 1
if loc > count {
loc = count
}
if !v.Cursor.HasSelection() {
v.Cursor.OrigSelection[0] = loc
}
v.Cursor.Right()
v.Cursor.SelectTo(v.Cursor.Loc())
return true
}
// SelectWordRight selects the word to the right of the cursor
func (v *View) SelectWordRight() bool {
loc := v.Cursor.Loc()
if !v.Cursor.HasSelection() {
v.Cursor.OrigSelection[0] = loc
}
v.Cursor.WordRight()
v.Cursor.SelectTo(v.Cursor.Loc())
return true
}
// SelectWordLeft selects the word to the left of the cursor
func (v *View) SelectWordLeft() bool {
loc := v.Cursor.Loc()
if !v.Cursor.HasSelection() {
v.Cursor.OrigSelection[0] = loc
}
v.Cursor.WordLeft()
v.Cursor.SelectTo(v.Cursor.Loc())
return true
}
// StartOfLine moves the cursor to the start of the line
func (v *View) StartOfLine() bool {
v.Cursor.Start()
return true
}
// EndOfLine moves the cursor to the end of the line
func (v *View) EndOfLine() bool {
v.Cursor.End()
return true
}
// SelectToStartOfLine selects to the start of the current line
func (v *View) SelectToStartOfLine() bool {
loc := v.Cursor.Loc()
if !v.Cursor.HasSelection() {
v.Cursor.OrigSelection[0] = loc
}
v.Cursor.Start()
v.Cursor.SelectTo(v.Cursor.Loc())
return true
}
// SelectToEndOfLine selects to the end of the current line
func (v *View) SelectToEndOfLine() bool {
loc := v.Cursor.Loc()
if !v.Cursor.HasSelection() {
v.Cursor.OrigSelection[0] = loc
}
v.Cursor.End()
v.Cursor.SelectTo(v.Cursor.Loc())
return true
}
// CursorStart moves the cursor to the start of the buffer
func (v *View) CursorStart() bool {
v.Cursor.X = 0
v.Cursor.Y = 0
return true
}
// CursorEnd moves the cursor to the end of the buffer
func (v *View) CursorEnd() bool {
v.Cursor.SetLoc(v.Buf.Len())
return true
}
// SelectToStart selects the text from the cursor to the start of the buffer
func (v *View) SelectToStart() bool {
loc := v.Cursor.Loc()
if !v.Cursor.HasSelection() {
v.Cursor.OrigSelection[0] = loc
}
v.CursorStart()
v.Cursor.SelectTo(0)
return true
}
// SelectToEnd selects the text from the cursor to the end of the buffer
func (v *View) SelectToEnd() bool {
loc := v.Cursor.Loc()
if !v.Cursor.HasSelection() {
v.Cursor.OrigSelection[0] = loc
}
v.CursorEnd()
v.Cursor.SelectTo(v.Buf.Len())
return true
}
// InsertSpace inserts a space
func (v *View) InsertSpace() bool {
if v.Cursor.HasSelection() {
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
}
v.Buf.Insert(v.Cursor.Loc(), " ")
v.Cursor.Right()
return true
}
// InsertEnter inserts a newline plus possible some whitespace if autoindent is on
func (v *View) InsertEnter() bool {
// Insert a newline
if v.Cursor.HasSelection() {
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
}
v.Buf.Insert(v.Cursor.Loc(), "\n")
ws := GetLeadingWhitespace(v.Buf.Lines[v.Cursor.Y])
v.Cursor.Right()
if settings["autoindent"].(bool) {
v.Buf.Insert(v.Cursor.Loc(), ws)
for i := 0; i < len(ws); i++ {
v.Cursor.Right()
}
}
v.Cursor.LastVisualX = v.Cursor.GetVisualX()
return true
}
// Backspace deletes the previous character
func (v *View) Backspace() bool {
// Delete a character
if v.Cursor.HasSelection() {
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
} else if v.Cursor.Loc() > 0 {
// We have to do something a bit hacky here because we want to
// delete the line by first moving left and then deleting backwards
// but the undo redo would place the cursor in the wrong place
// So instead we move left, save the position, move back, delete
// and restore the position
// If the user is using spaces instead of tabs and they are deleting
// whitespace at the start of the line, we should delete as if its a
// tab (tabSize number of spaces)
lineStart := v.Buf.Lines[v.Cursor.Y][:v.Cursor.X]
tabSize := int(settings["tabsize"].(float64))
if settings["tabstospaces"].(bool) && IsSpaces(lineStart) && len(lineStart) != 0 && len(lineStart)%tabSize == 0 {
loc := v.Cursor.Loc()
v.Cursor.SetLoc(loc - tabSize)
cx, cy := v.Cursor.X, v.Cursor.Y
v.Cursor.SetLoc(loc)
v.Buf.Remove(loc-tabSize, loc)
v.Cursor.X, v.Cursor.Y = cx, cy
} else {
v.Cursor.Left()
cx, cy := v.Cursor.X, v.Cursor.Y
v.Cursor.Right()
loc := v.Cursor.Loc()
v.Buf.Remove(loc-1, loc)
v.Cursor.X, v.Cursor.Y = cx, cy
}
}
v.Cursor.LastVisualX = v.Cursor.GetVisualX()
return true
}
// Delete deletes the next character
func (v *View) Delete() bool {
if v.Cursor.HasSelection() {
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
} else {
loc := v.Cursor.Loc()
if loc < v.Buf.Len() {
v.Buf.Remove(loc, loc+1)
}
}
return true
}
// InsertTab inserts a tab or spaces
func (v *View) InsertTab() bool {
// Insert a tab
if v.Cursor.HasSelection() {
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
}
if settings["tabstospaces"].(bool) {
tabSize := int(settings["tabsize"].(float64))
v.Buf.Insert(v.Cursor.Loc(), Spaces(tabSize))
for i := 0; i < tabSize; i++ {
v.Cursor.Right()
}
} else {
v.Buf.Insert(v.Cursor.Loc(), "\t")
v.Cursor.Right()
}
return true
}
// Save the buffer to disk
func (v *View) Save() bool {
if v.helpOpen {
// We can't save the help text
return false
}
// If this is an empty buffer, ask for a filename
if v.Buf.Path == "" {
filename, canceled := messenger.Prompt("Filename: ")
if !canceled {
v.Buf.Path = filename
v.Buf.Name = filename
} else {
return true
}
}
err := v.Buf.Save()
if err != nil {
messenger.Error(err.Error())
} else {
messenger.Message("Saved " + v.Buf.Path)
}
return true
}
// Find opens a prompt and searches forward for the input
func (v *View) Find() bool {
if v.Cursor.HasSelection() {
searchStart = v.Cursor.CurSelection[1]
} else {
searchStart = ToCharPos(v.Cursor.X, v.Cursor.Y, v.Buf)
}
BeginSearch()
return true
}
// FindNext searches forwards for the last used search term
func (v *View) FindNext() bool {
if v.Cursor.HasSelection() {
searchStart = v.Cursor.CurSelection[1]
} else {
searchStart = ToCharPos(v.Cursor.X, v.Cursor.Y, v.Buf)
}
messenger.Message("Finding: " + lastSearch)
Search(lastSearch, v, true)
return true
}
// FindPrevious searches backwards for the last used search term
func (v *View) FindPrevious() bool {
if v.Cursor.HasSelection() {
searchStart = v.Cursor.CurSelection[0]
} else {
searchStart = ToCharPos(v.Cursor.X, v.Cursor.Y, v.Buf)
}
messenger.Message("Finding: " + lastSearch)
Search(lastSearch, v, false)
return true
}
// Undo undoes the last action
func (v *View) Undo() bool {
v.Buf.Undo()
messenger.Message("Undid action")
return true
}
// Redo redoes the last action
func (v *View) Redo() bool {
v.Buf.Redo()
messenger.Message("Redid action")
return true
}
// Copy the selection to the system clipboard
func (v *View) Copy() bool {
if v.Cursor.HasSelection() {
clipboard.WriteAll(v.Cursor.GetSelection())
v.freshClip = true
messenger.Message("Copied selection")
}
return true
}
// CutLine cuts the current line to the clipboard
func (v *View) CutLine() bool {
v.Cursor.SelectLine()
if !v.Cursor.HasSelection() {
return false
}
if v.freshClip == true {
if v.Cursor.HasSelection() {
if clip, err := clipboard.ReadAll(); err != nil {
messenger.Error(err)
} else {
clipboard.WriteAll(clip + v.Cursor.GetSelection())
}
}
} else if time.Since(v.lastCutTime)/time.Second > 10*time.Second || v.freshClip == false {
v.Copy()
}
v.freshClip = true
v.lastCutTime = time.Now()
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
messenger.Message("Cut line")
return true
}
// Cut the selection to the system clipboard
func (v *View) Cut() bool {
if v.Cursor.HasSelection() {
clipboard.WriteAll(v.Cursor.GetSelection())
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
v.freshClip = true
messenger.Message("Cut selection")
}
return true
}
// DuplicateLine duplicates the current line
func (v *View) DuplicateLine() bool {
v.Cursor.End()
v.Buf.Insert(v.Cursor.Loc(), "\n"+v.Buf.Lines[v.Cursor.Y])
v.Cursor.Right()
messenger.Message("Duplicated line")
return true
}
// Paste whatever is in the system clipboard into the buffer
// Delete and paste if the user has a selection
func (v *View) Paste() bool {
if v.Cursor.HasSelection() {
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
}
clip, _ := clipboard.ReadAll()
v.Buf.Insert(v.Cursor.Loc(), clip)
v.Cursor.SetLoc(v.Cursor.Loc() + Count(clip))
v.freshClip = false
messenger.Message("Pasted clipboard")
return true
}
// SelectAll selects the entire buffer
func (v *View) SelectAll() bool {
v.Cursor.CurSelection[0] = 0
v.Cursor.CurSelection[1] = v.Buf.Len()
// Put the cursor at the beginning
v.Cursor.X = 0
v.Cursor.Y = 0
return true
}
// OpenFile opens a new file in the buffer
func (v *View) OpenFile() bool {
if v.CanClose("Continue? (yes, no, save) ") {
filename, canceled := messenger.Prompt("File to open: ")
if canceled {
return true
}
home, _ := homedir.Dir()
filename = strings.Replace(filename, "~", home, 1)
file, err := ioutil.ReadFile(filename)
if err != nil {
messenger.Error(err.Error())
return true
}
buf := NewBuffer(string(file), filename)
v.OpenBuffer(buf)
}
return true
}
// Start moves the viewport to the start of the buffer
func (v *View) Start() bool {
v.Topline = 0
return false
}
// End moves the viewport to the end of the buffer
func (v *View) End() bool {
if v.height > v.Buf.NumLines {
v.Topline = 0
} else {
v.Topline = v.Buf.NumLines - v.height
}
return false
}
// PageUp scrolls the view up a page
func (v *View) PageUp() bool {
if v.Topline > v.height {
v.ScrollUp(v.height)
} else {
v.Topline = 0
}
return false
}
// PageDown scrolls the view down a page
func (v *View) PageDown() bool {
if v.Buf.NumLines-(v.Topline+v.height) > v.height {
v.ScrollDown(v.height)
} else if v.Buf.NumLines >= v.height {
v.Topline = v.Buf.NumLines - v.height
}
return false
}
// HalfPageUp scrolls the view up half a page
func (v *View) HalfPageUp() bool {
if v.Topline > v.height/2 {
v.ScrollUp(v.height / 2)
} else {
v.Topline = 0
}
return false
}
// HalfPageDown scrolls the view down half a page
func (v *View) HalfPageDown() bool {
if v.Buf.NumLines-(v.Topline+v.height) > v.height/2 {
v.ScrollDown(v.height / 2)
} else {
if v.Buf.NumLines >= v.height {
v.Topline = v.Buf.NumLines - v.height
}
}
return false
}
// ToggleRuler turns line numbers off and on
func (v *View) ToggleRuler() bool {
if settings["ruler"] == false {
settings["ruler"] = true
messenger.Message("Enabled ruler")
} else {
settings["ruler"] = false
messenger.Message("Disabled ruler")
}
return false
}
// JumpLine jumps to a line and moves the view accordingly.
func (v *View) JumpLine() bool {
// Prompt for line number
linestring, canceled := messenger.Prompt("Jump to line # ")
if canceled {
return false
}
lineint, err := strconv.Atoi(linestring)
lineint = lineint - 1 // fix offset
if err != nil {
messenger.Error(err) // return errors
return false
}
// Move cursor and view if possible.
if lineint < v.Buf.NumLines {
v.Cursor.X = 0
v.Cursor.Y = lineint
return true
}
messenger.Error("Only ", v.Buf.NumLines, " lines to jump")
return false
}
// ClearStatus clears the messenger bar
func (v *View) ClearStatus() bool {
messenger.Message("")
return false
}
// ToggleHelp toggles the help screen
func (v *View) ToggleHelp() bool {
if !v.helpOpen {
v.lastBuffer = v.Buf
helpBuffer := NewBuffer(helpTxt, "help.md")
helpBuffer.Name = "Help"
v.helpOpen = true
v.OpenBuffer(helpBuffer)
} else {
v.OpenBuffer(v.lastBuffer)
v.helpOpen = false
}
return true
}
// ShellMode opens a terminal to run a shell command
func (v *View) ShellMode() bool {
input, canceled := messenger.Prompt("$ ")
if !canceled {
// The true here is for openTerm to make the command interactive
HandleShellCommand(input, true)
}
return false
}
// CommandMode lets the user enter a command
func (v *View) CommandMode() bool {
input, canceled := messenger.Prompt("> ")
if !canceled {
HandleCommand(input)
}
return false
}
// Quit quits the editor
// This behavior needs to be changed and should really only quit the editor if this
// is the last view
// However, since micro only supports one view for now, it doesn't really matter
func (v *View) Quit() bool {
if v.helpOpen {
return v.ToggleHelp()
}
// Make sure not to quit if there are unsaved changes
if views[mainView].CanClose("Quit anyway? (yes, no, save) ") {
views[mainView].CloseBuffer()
screen.Fini()
os.Exit(0)
}
return false
}
// None is no action
func None() bool {
return false
}