323 lines
6.9 KiB
Go
323 lines
6.9 KiB
Go
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
|
|
|
|
package readline
|
|
|
|
import (
|
|
"container/list"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"kitty/tools/cli/markup"
|
|
"kitty/tools/tui/loop"
|
|
"kitty/tools/wcswidth"
|
|
)
|
|
|
|
var _ = fmt.Print
|
|
|
|
const ST = "\x1b\\"
|
|
const PROMPT_MARK = "\x1b]133;"
|
|
|
|
type SyntaxHighlightFunction func(text string, x, y int) string
|
|
|
|
type RlInit struct {
|
|
Prompt string
|
|
HistoryPath string
|
|
HistoryCount int
|
|
ContinuationPrompt string
|
|
EmptyContinuationPrompt bool
|
|
DontMarkPrompts bool
|
|
SyntaxHighlighter SyntaxHighlightFunction
|
|
}
|
|
|
|
type Position struct {
|
|
X int
|
|
Y int
|
|
}
|
|
|
|
func (self Position) Less(other Position) bool {
|
|
return self.Y < other.Y || (self.Y == other.Y && self.X < other.X)
|
|
}
|
|
|
|
// Actions {{{
|
|
type Action uint
|
|
|
|
const (
|
|
ActionNil Action = iota
|
|
ActionIgnored
|
|
ActionBackspace
|
|
ActionDelete
|
|
ActionMoveToStartOfLine
|
|
ActionMoveToEndOfLine
|
|
ActionMoveToStartOfDocument
|
|
ActionMoveToEndOfDocument
|
|
ActionMoveToEndOfWord
|
|
ActionMoveToStartOfWord
|
|
ActionCursorLeft
|
|
ActionCursorRight
|
|
ActionEndInput
|
|
ActionAcceptInput
|
|
ActionCursorUp
|
|
ActionHistoryPreviousOrCursorUp
|
|
ActionCursorDown
|
|
ActionHistoryNextOrCursorDown
|
|
ActionHistoryNext
|
|
ActionHistoryPrevious
|
|
ActionHistoryFirst
|
|
ActionHistoryLast
|
|
ActionHistoryIncrementalSearchBackwards
|
|
ActionHistoryIncrementalSearchForwards
|
|
ActionTerminateHistorySearchAndApply
|
|
ActionTerminateHistorySearchAndRestore
|
|
ActionClearScreen
|
|
ActionAddText
|
|
ActionAbortCurrentLine
|
|
|
|
ActionStartKillActions
|
|
ActionKillToEndOfLine
|
|
ActionKillToStartOfLine
|
|
ActionKillNextWord
|
|
ActionKillPreviousWord
|
|
ActionKillPreviousSpaceDelimitedWord
|
|
ActionEndKillActions
|
|
|
|
ActionYank
|
|
ActionPopYank
|
|
|
|
ActionNumericArgumentDigit0
|
|
ActionNumericArgumentDigit1
|
|
ActionNumericArgumentDigit2
|
|
ActionNumericArgumentDigit3
|
|
ActionNumericArgumentDigit4
|
|
ActionNumericArgumentDigit5
|
|
ActionNumericArgumentDigit6
|
|
ActionNumericArgumentDigit7
|
|
ActionNumericArgumentDigit8
|
|
ActionNumericArgumentDigit9
|
|
ActionNumericArgumentDigitMinus
|
|
)
|
|
|
|
// }}}
|
|
|
|
type kill_ring struct {
|
|
items *list.List
|
|
}
|
|
|
|
func (self *kill_ring) append_to_existing_item(text string) {
|
|
e := self.items.Front()
|
|
if e == nil {
|
|
self.add_new_item(text)
|
|
}
|
|
e.Value = e.Value.(string) + text
|
|
}
|
|
|
|
func (self *kill_ring) add_new_item(text string) {
|
|
if text != "" {
|
|
self.items.PushFront(text)
|
|
}
|
|
}
|
|
|
|
func (self *kill_ring) yank() string {
|
|
e := self.items.Front()
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
return e.Value.(string)
|
|
}
|
|
|
|
func (self *kill_ring) pop_yank() string {
|
|
e := self.items.Front()
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
self.items.MoveToBack(e)
|
|
return self.yank()
|
|
}
|
|
|
|
func (self *kill_ring) clear() {
|
|
self.items = self.items.Init()
|
|
}
|
|
|
|
type Prompt struct {
|
|
Text string
|
|
Length int
|
|
}
|
|
|
|
type InputState struct {
|
|
// Input lines
|
|
lines []string
|
|
// The cursor position in the text
|
|
cursor Position
|
|
}
|
|
|
|
func (self InputState) copy() InputState {
|
|
ans := self
|
|
l := make([]string, len(self.lines))
|
|
copy(l, self.lines)
|
|
ans.lines = l
|
|
return ans
|
|
}
|
|
|
|
type syntax_highlighted struct {
|
|
lines []string
|
|
src_for_last_highlight string
|
|
highlighter SyntaxHighlightFunction
|
|
last_highlighter_name string
|
|
}
|
|
|
|
type Readline struct {
|
|
prompt, continuation_prompt Prompt
|
|
|
|
mark_prompts bool
|
|
loop *loop.Loop
|
|
history *History
|
|
kill_ring kill_ring
|
|
|
|
input_state InputState
|
|
// The number of lines after the initial line on the screen
|
|
cursor_y int
|
|
screen_width int
|
|
last_yank_extent struct {
|
|
start, end Position
|
|
}
|
|
bracketed_paste_buffer strings.Builder
|
|
last_action Action
|
|
history_matches *HistoryMatches
|
|
history_search *HistorySearch
|
|
keyboard_state KeyboardState
|
|
fmt_ctx *markup.Context
|
|
text_to_be_added string
|
|
syntax_highlighted syntax_highlighted
|
|
}
|
|
|
|
func (self *Readline) make_prompt(text string, is_secondary bool) Prompt {
|
|
if self.mark_prompts {
|
|
m := PROMPT_MARK + "A"
|
|
if is_secondary {
|
|
m += ";k=s"
|
|
}
|
|
text = m + ST + text
|
|
}
|
|
return Prompt{Text: text, Length: wcswidth.Stringwidth(text)}
|
|
}
|
|
|
|
func New(loop *loop.Loop, r RlInit) *Readline {
|
|
hc := r.HistoryCount
|
|
if hc == 0 {
|
|
hc = 8192
|
|
}
|
|
ans := &Readline{
|
|
mark_prompts: !r.DontMarkPrompts, fmt_ctx: markup.New(true),
|
|
loop: loop, input_state: InputState{lines: []string{""}}, history: NewHistory(r.HistoryPath, hc),
|
|
syntax_highlighted: syntax_highlighted{highlighter: r.SyntaxHighlighter},
|
|
kill_ring: kill_ring{items: list.New().Init()},
|
|
}
|
|
ans.prompt = ans.make_prompt(r.Prompt, false)
|
|
t := ""
|
|
if r.ContinuationPrompt != "" || !r.EmptyContinuationPrompt {
|
|
t = r.ContinuationPrompt
|
|
if t == "" {
|
|
t = ans.fmt_ctx.Yellow(">") + " "
|
|
}
|
|
}
|
|
ans.continuation_prompt = ans.make_prompt(t, true)
|
|
return ans
|
|
}
|
|
|
|
func (self *Readline) Shutdown() {
|
|
self.history.Shutdown()
|
|
}
|
|
|
|
func (self *Readline) AddHistoryItem(hi HistoryItem) {
|
|
self.history.add_item(hi)
|
|
}
|
|
|
|
func (self *Readline) ResetText() {
|
|
self.input_state = InputState{lines: []string{""}}
|
|
self.last_action = ActionNil
|
|
self.keyboard_state = KeyboardState{}
|
|
self.history_search = nil
|
|
self.cursor_y = 0
|
|
}
|
|
|
|
func (self *Readline) ChangeLoopAndResetText(lp *loop.Loop) {
|
|
self.loop = lp
|
|
self.ResetText()
|
|
}
|
|
|
|
func (self *Readline) Start() {
|
|
self.loop.SetCursorShape(loop.BAR_CURSOR, true)
|
|
self.loop.StartBracketedPaste()
|
|
self.Redraw()
|
|
}
|
|
|
|
func (self *Readline) End() {
|
|
self.loop.SetCursorShape(loop.BLOCK_CURSOR, true)
|
|
self.loop.EndBracketedPaste()
|
|
self.loop.QueueWriteString("\r\n")
|
|
if self.mark_prompts {
|
|
self.loop.QueueWriteString(PROMPT_MARK + "C" + ST)
|
|
}
|
|
}
|
|
|
|
func MarkOutputStart() string {
|
|
return PROMPT_MARK + "C" + ST
|
|
}
|
|
|
|
func (self *Readline) Redraw() {
|
|
self.loop.StartAtomicUpdate()
|
|
self.RedrawNonAtomic()
|
|
self.loop.EndAtomicUpdate()
|
|
}
|
|
|
|
func (self *Readline) RedrawNonAtomic() {
|
|
self.redraw()
|
|
}
|
|
|
|
func (self *Readline) OnKeyEvent(event *loop.KeyEvent) error {
|
|
err := self.handle_key_event(event)
|
|
if err == ErrCouldNotPerformAction {
|
|
err = nil
|
|
self.loop.Beep()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (self *Readline) OnText(text string, from_key_event bool, in_bracketed_paste bool) error {
|
|
if in_bracketed_paste {
|
|
self.bracketed_paste_buffer.WriteString(text)
|
|
return nil
|
|
}
|
|
if self.bracketed_paste_buffer.Len() > 0 {
|
|
self.bracketed_paste_buffer.WriteString(text)
|
|
text = self.bracketed_paste_buffer.String()
|
|
self.bracketed_paste_buffer.Reset()
|
|
}
|
|
self.text_to_be_added = text
|
|
return self.dispatch_key_action(ActionAddText)
|
|
}
|
|
|
|
func (self *Readline) TextBeforeCursor() string {
|
|
return self.text_upto_cursor_pos()
|
|
}
|
|
|
|
func (self *Readline) TextAfterCursor() string {
|
|
return self.text_after_cursor_pos()
|
|
}
|
|
|
|
func (self *Readline) AllText() string {
|
|
return self.all_text()
|
|
}
|
|
|
|
func (self *Readline) CursorAtEndOfLine() bool {
|
|
return self.input_state.cursor.X >= len(self.input_state.lines[self.input_state.cursor.Y])
|
|
}
|
|
|
|
func (self *Readline) OnResize(old_size loop.ScreenSize, new_size loop.ScreenSize) error {
|
|
self.screen_width = int(new_size.CellWidth)
|
|
if self.screen_width < 1 {
|
|
self.screen_width = 1
|
|
}
|
|
self.Redraw()
|
|
return nil
|
|
}
|