mirror of
https://github.com/go-sylixos/elvish.git
synced 2024-12-04 10:57:50 +08:00
7209b48bf8
This fixes #27.
423 lines
9.0 KiB
Go
423 lines
9.0 KiB
Go
package edit
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/elves/elvish/util"
|
|
)
|
|
|
|
// Line editor builtins.
|
|
|
|
// Builtin records an editor builtin.
|
|
type Builtin struct {
|
|
name string
|
|
impl func(ed *Editor)
|
|
}
|
|
|
|
var builtins = []Builtin{
|
|
// Command and insert mode
|
|
{"start-insert", startInsert},
|
|
{"start-command", startCommand},
|
|
{"kill-line-left", killLineLeft},
|
|
{"kill-line-right", killLineRight},
|
|
{"kill-word-left", killWordLeft},
|
|
{"kill-rune-left", killRuneLeft},
|
|
{"kill-rune-right", killRuneRight},
|
|
{"move-dot-left", moveDotLeft},
|
|
{"move-dot-right", moveDotRight},
|
|
{"move-dot-left-word", moveDotLeftWord},
|
|
{"move-dot-right-word", moveDotRightWord},
|
|
{"move-dot-sol", moveDotSOL},
|
|
{"move-dot-eol", moveDotEOL},
|
|
{"move-dot-up", moveDotUp},
|
|
{"move-dot-down", moveDotDown},
|
|
{"insert-last-word", insertLastWord},
|
|
{"insert-key", insertKey},
|
|
{"return-line", returnLine},
|
|
{"smart-enter", smartEnter},
|
|
{"return-eof", returnEOF},
|
|
{"default-command", defaultCommand},
|
|
{"default-insert", defaultInsert},
|
|
|
|
// Completion mode
|
|
{"complete-prefix-or-start-completion", completePrefixOrStartCompletion},
|
|
{"start-completion", startCompletion},
|
|
{"cancel-completion", cancelCompletion},
|
|
{"select-cand-up", selectCandUp},
|
|
{"select-cand-down", selectCandDown},
|
|
{"select-cand-left", selectCandLeft},
|
|
{"select-cand-right", selectCandRight},
|
|
{"cycle-cand-right", cycleCandRight},
|
|
{"accept-completion", acceptCompletion},
|
|
{"default-completion", defaultCompletion},
|
|
|
|
// Navigation mode
|
|
{"start-navigation", startNavigation},
|
|
{"select-nav-up", selectNavUp},
|
|
{"select-nav-down", selectNavDown},
|
|
{"ascend-nav", ascendNav},
|
|
{"descend-nav", descendNav},
|
|
{"trigger-nav-show-hidden", triggerNavShowHidden},
|
|
{"default-navigation", defaultNavigation},
|
|
|
|
// History mode
|
|
{"start-history", startHistory},
|
|
{"select-history-prev", selectHistoryPrev},
|
|
{"select-history-next", selectHistoryNext},
|
|
{"select-history-next-or-quit", selectHistoryNextOrQuit},
|
|
{"default-history", defaultHistory},
|
|
|
|
// History listing mode
|
|
{"start-history-listing", startHistoryListing},
|
|
{"default-history-listing", defaultHistoryListing},
|
|
|
|
// Location mode
|
|
{"start-location", startLocation},
|
|
{"location-prev", locationPrev},
|
|
{"location-next", locationNext},
|
|
{"accept-location", acceptLocation},
|
|
{"cancel-location", cancelLocation},
|
|
{"location-default", locationDefault},
|
|
|
|
// Misc
|
|
{"redraw", redraw},
|
|
}
|
|
|
|
var builtinMap = map[string]Builtin{}
|
|
|
|
func init() {
|
|
for _, b := range builtins {
|
|
builtinMap[b.name] = b
|
|
}
|
|
for mode, table := range defaultBindings {
|
|
keyBindings[mode] = map[Key]Caller{}
|
|
for key, name := range table {
|
|
caller, ok := builtinMap[name]
|
|
if !ok {
|
|
fmt.Println("bad name " + name)
|
|
} else {
|
|
keyBindings[mode][key] = caller
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type action struct {
|
|
actionType
|
|
returnValue LineRead
|
|
}
|
|
|
|
func startInsert(ed *Editor) {
|
|
ed.mode = modeInsert
|
|
}
|
|
|
|
func defaultCommand(ed *Editor) {
|
|
k := ed.lastKey
|
|
ed.addTip("Unbound: %s", k)
|
|
}
|
|
|
|
func startCommand(ed *Editor) {
|
|
ed.mode = modeCommand
|
|
}
|
|
|
|
func killLineLeft(ed *Editor) {
|
|
sol := util.FindLastSOL(ed.line[:ed.dot])
|
|
ed.line = ed.line[:sol] + ed.line[ed.dot:]
|
|
ed.dot = sol
|
|
}
|
|
|
|
func killLineRight(ed *Editor) {
|
|
eol := util.FindFirstEOL(ed.line[ed.dot:]) + ed.dot
|
|
ed.line = ed.line[:ed.dot] + ed.line[eol:]
|
|
}
|
|
|
|
// NOTE(xiaq): A word is now defined as a series of non-whitespace chars.
|
|
func killWordLeft(ed *Editor) {
|
|
if ed.dot == 0 {
|
|
return
|
|
}
|
|
space := strings.LastIndexFunc(
|
|
strings.TrimRightFunc(ed.line[:ed.dot], unicode.IsSpace),
|
|
unicode.IsSpace) + 1
|
|
ed.line = ed.line[:space] + ed.line[ed.dot:]
|
|
ed.dot = space
|
|
}
|
|
|
|
func killRuneLeft(ed *Editor) {
|
|
if ed.dot > 0 {
|
|
_, w := utf8.DecodeLastRuneInString(ed.line[:ed.dot])
|
|
ed.line = ed.line[:ed.dot-w] + ed.line[ed.dot:]
|
|
ed.dot -= w
|
|
} else {
|
|
ed.flash()
|
|
}
|
|
}
|
|
|
|
func killRuneRight(ed *Editor) {
|
|
if ed.dot < len(ed.line) {
|
|
_, w := utf8.DecodeRuneInString(ed.line[ed.dot:])
|
|
ed.line = ed.line[:ed.dot] + ed.line[ed.dot+w:]
|
|
} else {
|
|
ed.flash()
|
|
}
|
|
}
|
|
|
|
func moveDotLeft(ed *Editor) {
|
|
_, w := utf8.DecodeLastRuneInString(ed.line[:ed.dot])
|
|
ed.dot -= w
|
|
}
|
|
|
|
func moveDotRight(ed *Editor) {
|
|
_, w := utf8.DecodeRuneInString(ed.line[ed.dot:])
|
|
ed.dot += w
|
|
}
|
|
|
|
func moveDotLeftWord(ed *Editor) {
|
|
if ed.dot == 0 {
|
|
return
|
|
}
|
|
space := strings.LastIndexFunc(
|
|
strings.TrimRightFunc(ed.line[:ed.dot], unicode.IsSpace),
|
|
unicode.IsSpace) + 1
|
|
ed.dot = space
|
|
}
|
|
|
|
func moveDotRightWord(ed *Editor) {
|
|
// Move to first space
|
|
p := strings.IndexFunc(ed.line[ed.dot:], unicode.IsSpace)
|
|
if p == -1 {
|
|
ed.dot = len(ed.line)
|
|
return
|
|
}
|
|
ed.dot += p
|
|
// Move to first nonspace
|
|
p = strings.IndexFunc(ed.line[ed.dot:], notSpace)
|
|
if p == -1 {
|
|
ed.dot = len(ed.line)
|
|
return
|
|
}
|
|
ed.dot += p
|
|
}
|
|
|
|
func notSpace(r rune) bool {
|
|
return !unicode.IsSpace(r)
|
|
}
|
|
|
|
func moveDotSOL(ed *Editor) {
|
|
sol := util.FindLastSOL(ed.line[:ed.dot])
|
|
ed.dot = sol
|
|
}
|
|
|
|
func moveDotEOL(ed *Editor) {
|
|
eol := util.FindFirstEOL(ed.line[ed.dot:]) + ed.dot
|
|
ed.dot = eol
|
|
}
|
|
|
|
func moveDotUp(ed *Editor) {
|
|
sol := util.FindLastSOL(ed.line[:ed.dot])
|
|
if sol == 0 {
|
|
ed.flash()
|
|
return
|
|
}
|
|
prevEOL := sol - 1
|
|
prevSOL := util.FindLastSOL(ed.line[:prevEOL])
|
|
width := WcWidths(ed.line[sol:ed.dot])
|
|
ed.dot = prevSOL + len(TrimWcWidth(ed.line[prevSOL:prevEOL], width))
|
|
}
|
|
|
|
func moveDotDown(ed *Editor) {
|
|
eol := util.FindFirstEOL(ed.line[ed.dot:]) + ed.dot
|
|
if eol == len(ed.line) {
|
|
ed.flash()
|
|
return
|
|
}
|
|
nextSOL := eol + 1
|
|
nextEOL := util.FindFirstEOL(ed.line[nextSOL:]) + nextSOL
|
|
sol := util.FindLastSOL(ed.line[:ed.dot])
|
|
width := WcWidths(ed.line[sol:ed.dot])
|
|
ed.dot = nextSOL + len(TrimWcWidth(ed.line[nextSOL:nextEOL], width))
|
|
}
|
|
|
|
func insertLastWord(ed *Editor) {
|
|
_, lastLine, err := ed.store.LastCmd(-1, "", true)
|
|
if err == nil {
|
|
ed.insertAtDot(lastWord(lastLine))
|
|
} else {
|
|
ed.addTip("db error: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func lastWord(s string) string {
|
|
s = strings.TrimRightFunc(s, unicode.IsSpace)
|
|
i := strings.LastIndexFunc(s, unicode.IsSpace) + 1
|
|
return s[i:]
|
|
}
|
|
|
|
func insertKey(ed *Editor) {
|
|
k := ed.lastKey
|
|
ed.insertAtDot(string(k.Rune))
|
|
}
|
|
|
|
func returnLine(ed *Editor) {
|
|
ed.nextAction = action{exitReadLine, LineRead{Line: ed.line}}
|
|
}
|
|
|
|
func smartEnter(ed *Editor) {
|
|
if ed.parseErrorAtEnd {
|
|
// There is a parsing error at the end. Insert a newline and copy
|
|
// indents from previous line.
|
|
indent := findLastIndent(ed.line[:ed.dot])
|
|
ed.insertAtDot("\n" + indent)
|
|
} else {
|
|
returnLine(ed)
|
|
}
|
|
}
|
|
|
|
func findLastIndent(s string) string {
|
|
line := s[util.FindLastSOL(s):]
|
|
trimmed := strings.TrimLeft(line, " \t")
|
|
return line[:len(line)-len(trimmed)]
|
|
}
|
|
|
|
func returnEOF(ed *Editor) {
|
|
if len(ed.line) == 0 {
|
|
ed.nextAction = action{exitReadLine, LineRead{EOF: true}}
|
|
}
|
|
}
|
|
|
|
func selectCandUp(ed *Editor) {
|
|
ed.completion.prev(false)
|
|
}
|
|
|
|
func selectCandDown(ed *Editor) {
|
|
ed.completion.next(false)
|
|
}
|
|
|
|
func selectCandLeft(ed *Editor) {
|
|
if c := ed.completion.current - ed.completionLines; c >= 0 {
|
|
ed.completion.current = c
|
|
}
|
|
}
|
|
|
|
func selectCandRight(ed *Editor) {
|
|
if c := ed.completion.current + ed.completionLines; c < len(ed.completion.candidates) {
|
|
ed.completion.current = c
|
|
}
|
|
}
|
|
|
|
func cycleCandRight(ed *Editor) {
|
|
ed.completion.next(true)
|
|
}
|
|
|
|
func cancelCompletion(ed *Editor) {
|
|
ed.completion = nil
|
|
ed.mode = modeInsert
|
|
}
|
|
|
|
func defaultInsert(ed *Editor) {
|
|
k := ed.lastKey
|
|
if k.Mod == 0 && k.Rune > 0 && unicode.IsGraphic(k.Rune) {
|
|
insertKey(ed)
|
|
} else {
|
|
ed.addTip("Unbound: %s", k)
|
|
}
|
|
}
|
|
|
|
func acceptCompletion(ed *Editor) {
|
|
ed.acceptCompletion()
|
|
ed.mode = modeInsert
|
|
}
|
|
|
|
func defaultCompletion(ed *Editor) {
|
|
acceptCompletion(ed)
|
|
ed.nextAction = action{actionType: reprocessKey}
|
|
}
|
|
|
|
func startNavigation(ed *Editor) {
|
|
ed.mode = modeNavigation
|
|
ed.navigation = newNavigation()
|
|
}
|
|
|
|
func selectNavUp(ed *Editor) {
|
|
ed.navigation.prev()
|
|
}
|
|
|
|
func selectNavDown(ed *Editor) {
|
|
ed.navigation.next()
|
|
}
|
|
|
|
func ascendNav(ed *Editor) {
|
|
ed.navigation.ascend()
|
|
}
|
|
|
|
func descendNav(ed *Editor) {
|
|
ed.navigation.descend()
|
|
}
|
|
|
|
func triggerNavShowHidden(ed *Editor) {
|
|
ed.navigation.showHidden = !ed.navigation.showHidden
|
|
ed.navigation.refresh()
|
|
}
|
|
|
|
func defaultNavigation(ed *Editor) {
|
|
ed.mode = modeInsert
|
|
ed.navigation = nil
|
|
}
|
|
|
|
func startHistory(ed *Editor) {
|
|
ed.history.prefix = ed.line[:ed.dot]
|
|
ed.history.current = -1
|
|
if ed.prevHistory() {
|
|
ed.mode = modeHistory
|
|
} else {
|
|
ed.addTip("no matching history item")
|
|
}
|
|
}
|
|
|
|
func selectHistoryPrev(ed *Editor) {
|
|
ed.prevHistory()
|
|
}
|
|
|
|
func selectHistoryNext(ed *Editor) {
|
|
ed.nextHistory()
|
|
}
|
|
|
|
func selectHistoryNextOrQuit(ed *Editor) {
|
|
if !ed.nextHistory() {
|
|
ed.mode = modeInsert
|
|
}
|
|
}
|
|
|
|
func defaultHistory(ed *Editor) {
|
|
ed.acceptHistory()
|
|
ed.mode = modeInsert
|
|
ed.nextAction = action{actionType: reprocessKey}
|
|
}
|
|
|
|
func startHistoryListing(ed *Editor) {
|
|
if ed.store == nil {
|
|
ed.notify("store not connected")
|
|
return
|
|
}
|
|
hl, err := newHistoryListing(ed.store)
|
|
if err != nil {
|
|
ed.notify("%s", err)
|
|
return
|
|
}
|
|
ed.historyListing = hl
|
|
ed.mode = modeHistoryListing
|
|
}
|
|
|
|
func defaultHistoryListing(ed *Editor) {
|
|
ed.mode = modeInsert
|
|
ed.historyListing = nil
|
|
ed.nextAction = action{actionType: reprocessKey}
|
|
}
|
|
|
|
func redraw(ed *Editor) {
|
|
ed.refresh(true, true)
|
|
}
|