278 lines
6.2 KiB
Go
278 lines
6.2 KiB
Go
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
|
|
|
|
package readline
|
|
|
|
import (
|
|
"container/list"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"kitty/tools/cli"
|
|
"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 CompleterFunction = func(before_cursor, after_cursor string) *cli.Completions
|
|
|
|
type RlInit struct {
|
|
Prompt string
|
|
HistoryPath string
|
|
HistoryCount int
|
|
ContinuationPrompt string
|
|
EmptyContinuationPrompt bool
|
|
DontMarkPrompts bool
|
|
SyntaxHighlighter SyntaxHighlightFunction
|
|
Completer CompleterFunction
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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, screen_height 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
|
|
completions completions
|
|
}
|
|
|
|
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},
|
|
completions: completions{completer: r.Completer},
|
|
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) SetPrompt(prompt string) {
|
|
self.prompt = self.make_prompt(prompt, false)
|
|
}
|
|
|
|
func (self *Readline) Shutdown() {
|
|
self.history.Shutdown()
|
|
}
|
|
|
|
func (self *Readline) AddHistoryItem(hi HistoryItem) {
|
|
self.history.merge_items(hi)
|
|
}
|
|
|
|
func (self *Readline) ResetText() {
|
|
self.input_state = InputState{lines: []string{""}}
|
|
self.last_action = ActionNil
|
|
self.keyboard_state = KeyboardState{}
|
|
self.history_search = nil
|
|
self.completions.current = completion{}
|
|
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) SetText(text string) {
|
|
self.set_text(text)
|
|
}
|
|
|
|
func (self *Readline) MoveCursorToEnd() bool {
|
|
return self.move_to_end()
|
|
}
|
|
|
|
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, self.screen_height = 0, 0
|
|
self.Redraw()
|
|
return nil
|
|
}
|