Get completion basically working

This commit is contained in:
Kovid Goyal 2022-11-13 19:50:55 +05:30
parent 9e2c96653f
commit 723a9c91b5
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 240 additions and 40 deletions

View File

@ -61,6 +61,7 @@ func shell_loop(rl *readline.Readline, kill_if_signaled bool) (int, error) {
rl.Redraw()
return nil
}
lp.ClearToEndOfScreen()
return ErrExec
}
return err
@ -176,7 +177,7 @@ func exec_command(rl *readline.Readline, cmdline string) bool {
}
func completions(before_cursor, after_cursor string) (ans *cli.Completions) {
const prefix = "kitty @ "
const prefix = "kitty-tool @ "
text := prefix + before_cursor
argv, position_of_last_arg := shlex.SplitForCompletion(text)
if len(argv) == 0 || position_of_last_arg < len(prefix) {

View File

@ -17,10 +17,17 @@ import (
var _ = fmt.Print
func new_rl() *Readline {
lp, _ := loop.New()
rl := New(lp, RlInit{Prompt: "$$ "})
rl.screen_width = 10
rl.screen_height = 100
return rl
}
func test_func(t *testing.T) func(string, func(*Readline), ...string) *Readline {
return func(initial string, prepare func(rl *Readline), expected ...string) *Readline {
lp, _ := loop.New()
rl := New(lp, RlInit{})
rl := new_rl()
rl.add_text(initial)
if prepare != nil {
prepare(rl)
@ -64,9 +71,7 @@ func TestAddText(t *testing.T) {
}
func TestGetScreenLines(t *testing.T) {
lp, _ := loop.New()
rl := New(lp, RlInit{Prompt: "$$ "})
rl.screen_width = 10
rl := new_rl()
p := func(primary bool) Prompt {
if primary {
@ -189,9 +194,7 @@ func TestCursorMovement(t *testing.T) {
right(rl, 1, 1, false)
}, "à", "b")
lp, _ := loop.New()
rl := New(lp, RlInit{Prompt: "$$ "})
rl.screen_width = 10
rl := new_rl()
vert := func(amt int, moved_amt int, text_upto_cursor_pos string, initials ...Position) {
initial := Position{}
@ -266,8 +269,7 @@ func TestCursorMovement(t *testing.T) {
}
func TestYanking(t *testing.T) {
lp, _ := loop.New()
rl := New(lp, RlInit{Prompt: "$$ "})
rl := new_rl()
as_slice := func(l *list.List) []string {
ans := make([]string, 0, l.Len())
@ -386,8 +388,9 @@ func TestEraseChars(t *testing.T) {
}
func TestNumberArgument(t *testing.T) {
lp, _ := loop.New()
rl := New(lp, RlInit{Prompt: "$$ "})
rl := new_rl()
rl.screen_width = 100
test := func(ac Action, before_cursor, after_cursor string) {
rl.dispatch_key_action(ac)
if diff := cmp.Diff(before_cursor, rl.text_upto_cursor_pos()); diff != "" {
@ -431,8 +434,7 @@ func TestNumberArgument(t *testing.T) {
}
func TestHistory(t *testing.T) {
lp, _ := loop.New()
rl := New(lp, RlInit{Prompt: "$$ "})
rl := new_rl()
add_item := func(x string) {
rl.history.AddItem(x, 0)
@ -516,8 +518,6 @@ func TestHistory(t *testing.T) {
}
func TestReadlineCompletion(t *testing.T) {
lp, _ := loop.New()
completer := func(before_cursor, after_cursor string) (ans *cli.Completions) {
root := cli.NewRootCommand()
c := root.AddSubCommand(&cli.Command{Name: "test-completion"})
@ -535,7 +535,8 @@ func TestReadlineCompletion(t *testing.T) {
return
}
rl := New(lp, RlInit{Prompt: "$$ ", Completer: completer})
rl := new_rl()
rl.completions.completer = completer
ah := func(before_cursor, after_cursor string) {
ab := rl.text_upto_cursor_pos()

View File

@ -117,9 +117,9 @@ type Readline struct {
input_state InputState
// The number of lines after the initial line on the screen
cursor_y int
screen_width int
last_yank_extent struct {
cursor_y int
screen_width, screen_height int
last_yank_extent struct {
start, end Position
}
bracketed_paste_buffer strings.Builder
@ -259,10 +259,7 @@ func (self *Readline) CursorAtEndOfLine() bool {
}
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.screen_width, self.screen_height = 0, 0
self.Redraw()
return nil
}

View File

@ -4,9 +4,11 @@ package readline
import (
"fmt"
"strings"
"kitty/tools/cli"
"kitty/tools/utils"
"kitty/tools/wcswidth"
)
var _ = fmt.Print
@ -16,6 +18,9 @@ type completion struct {
results *cli.Completions
results_displayed, forwards bool
num_of_matches, current_match int
rendered_at_screen_width int
rendered_lines []string
last_rendered_above bool
}
func (self *completion) initialize() {
@ -64,23 +69,29 @@ func (self *Readline) complete(forwards bool, repeat_count uint) bool {
return false
}
if self.last_action == ActionCompleteForward || self.last_action == ActionCompleteBackward {
if c.current.num_of_matches == 0 {
return false
}
delta := -1
if forwards {
delta = 1
}
repeat_count %= uint(c.current.num_of_matches)
delta *= int(repeat_count)
c.current.current_match = (c.current.current_match + delta + c.current.num_of_matches) % c.current.num_of_matches
repeat_count = 0
} else {
before, after := self.text_upto_cursor_pos(), self.text_after_cursor_pos()
if before == "" {
return false
}
c.current = completion{before_cursor: before, after_cursor: after, forwards: forwards, results: c.completer(before, after)}
c.current.initialize()
if repeat_count > 0 {
repeat_count--
}
if c.current.current_match != 0 {
if self.loop != nil {
self.loop.Beep()
}
}
}
c.current.forwards = forwards
if c.current.results == nil {
@ -107,3 +118,158 @@ func (self *Readline) complete(forwards bool, repeat_count uint) bool {
}
return true
}
func (self *Readline) screen_lines_for_match_group_with_descriptions(g *cli.MatchGroup, lines []string) []string {
maxw := 0
lengths := make(map[string]int)
for _, m := range g.Matches {
l := wcswidth.Stringwidth(m.Word)
lengths[m.Word] = l
if l > maxw {
maxw = l
}
}
for _, m := range g.Matches {
p := m.Word + strings.Repeat(" ", maxw-lengths[m.Word])
line, _, _ := utils.Cut(strings.TrimSpace(m.Description), "\n")
line = p + " - " + self.fmt_ctx.Prettify(line)
truncated := wcswidth.TruncateToVisualLength(line, self.screen_width-1) + "\x1b[m"
if len(truncated) < len(line) {
line = truncated + "…"
}
lines = append(lines, line)
}
return lines
}
type cell struct {
text string
length int
}
func (self cell) whitespace(desired_length int) string {
return strings.Repeat(" ", desired_length-self.length)
}
type column struct {
cells []cell
length int
is_last bool
}
func (self *column) update_length() int {
self.length = 0
for _, c := range self.cells {
if c.length > self.length {
self.length = c.length
}
}
if !self.is_last {
self.length++
}
return self.length
}
func layout_words_in_table(words []string, lengths map[string]int, num_cols int) ([]column, int) {
cols := make([]column, num_cols)
for i, col := range cols {
col.cells = make([]cell, 0, len(words))
if i == len(cols)-1 {
col.is_last = true
}
}
r, c := 0, 0
for _, word := range words {
cols[r].cells = append(cols[r].cells, cell{word, lengths[word]})
c++
if c > num_cols {
c = 0
r++
}
}
total_length := 0
for i, col := range cols {
total_length += col.update_length()
for i > 0 && len(col.cells) < len(cols[i-1].cells) {
col.cells = append(col.cells, cell{})
}
}
return cols, total_length
}
func (self *Readline) screen_lines_for_match_group_without_descriptions(g *cli.MatchGroup, lines []string) []string {
words := make([]string, len(g.Matches))
lengths := make(map[string]int, len(words))
max_length := 0
for i, m := range g.Matches {
words[i] = m.Word
l := wcswidth.Stringwidth(words[i])
lengths[words[i]] = l
if l > max_length {
max_length = l
}
}
var ans []column
ncols := utils.Max(1, self.screen_width/(max_length+1))
for {
cols, total_length := layout_words_in_table(words, lengths, ncols)
if total_length > self.screen_width {
break
}
ans = cols
ncols++
}
if ans == nil {
for _, w := range words {
if lengths[w] > self.screen_width {
lines = append(lines, wcswidth.TruncateToVisualLength(w, self.screen_width))
} else {
lines = append(lines, w)
}
}
} else {
for r := 0; r < len(ans[0].cells); r++ {
w := strings.Builder{}
w.Grow(self.screen_width)
for c := 0; c < len(ans); c++ {
cell := ans[c].cells[r]
w.WriteString(cell.text)
if !ans[c].is_last {
w.WriteString(cell.whitespace(ans[r].length))
}
}
lines = append(lines, w.String())
}
}
return lines
}
func (self *Readline) completion_screen_lines() ([]string, bool) {
if self.completions.current.results == nil || self.completions.current.num_of_matches < 2 {
return []string{}, false
}
if len(self.completions.current.rendered_lines) > 0 && self.completions.current.rendered_at_screen_width == self.screen_width {
return self.completions.current.rendered_lines, true
}
lines := make([]string, 0, self.completions.current.num_of_matches)
for _, g := range self.completions.current.results.Groups {
if g.Title != "" {
lines = append(lines, self.fmt_ctx.Title(g.Title))
}
has_descriptions := false
for _, m := range g.Matches {
if m.Description != "" {
has_descriptions = true
break
}
}
if has_descriptions {
lines = self.screen_lines_for_match_group_with_descriptions(g, lines)
} else {
lines = self.screen_lines_for_match_group_without_descriptions(g, lines)
}
}
self.completions.current.rendered_lines = lines
self.completions.current.rendered_at_screen_width = self.screen_width
return lines, false
}

View File

@ -4,6 +4,7 @@ package readline
import (
"fmt"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/wcswidth"
"strings"
@ -12,18 +13,20 @@ import (
var _ = fmt.Print
func (self *Readline) update_current_screen_size() {
screen_size, err := self.loop.ScreenSize()
if err != nil {
var screen_size loop.ScreenSize
var err error
if self.loop != nil {
screen_size, err = self.loop.ScreenSize()
if err != nil {
screen_size.WidthCells = 80
screen_size.HeightCells = 24
}
} else {
screen_size.WidthCells = 80
screen_size.HeightCells = 24
}
if screen_size.WidthCells < 1 {
screen_size.WidthCells = 1
}
if screen_size.HeightCells < 1 {
screen_size.HeightCells = 1
}
self.screen_width = int(screen_size.WidthCells)
self.screen_width = utils.Max(1, int(screen_size.WidthCells))
self.screen_height = utils.Max(1, int(screen_size.HeightCells))
}
type ScreenLine struct {
@ -82,7 +85,7 @@ func (self *Readline) apply_syntax_highlighting() (lines []string, cursor Positi
}
func (self *Readline) get_screen_lines() []*ScreenLine {
if self.screen_width == 0 {
if self.screen_width == 0 || self.screen_height == 0 {
self.update_current_screen_size()
}
lines, cursor := self.apply_syntax_highlighting()
@ -129,7 +132,7 @@ func (self *Readline) get_screen_lines() []*ScreenLine {
}
func (self *Readline) redraw() {
if self.screen_width == 0 {
if self.screen_width == 0 || self.screen_height == 0 {
self.update_current_screen_size()
}
if self.screen_width < 4 {
@ -140,11 +143,38 @@ func (self *Readline) redraw() {
}
self.loop.QueueWriteString("\r")
self.loop.ClearToEndOfScreen()
prompt_lines := self.get_screen_lines()
csl, csl_cached := self.completion_screen_lines()
render_completion_above := len(csl)+len(prompt_lines) > self.screen_height
completion_needs_render := len(csl) > 0 && (!render_completion_above || !self.completions.current.last_rendered_above || !csl_cached)
cursor_x := -1
cursor_y := 0
move_cursor_up_by := 0
render_completion_lines := func() int {
if completion_needs_render {
if render_completion_above {
self.loop.QueueWriteString("\r")
} else {
self.loop.QueueWriteString("\r\n")
}
for i, cl := range csl {
self.loop.QueueWriteString(cl)
if i < len(csl)-1 || render_completion_above {
self.loop.QueueWriteString("\n\r")
}
}
return len(csl)
}
return 0
}
self.loop.AllowLineWrapping(false)
for i, sl := range self.get_screen_lines() {
if render_completion_above {
render_completion_lines()
}
for i, sl := range prompt_lines {
self.loop.QueueWriteString("\r")
if i > 0 {
self.loop.QueueWriteString("\n")
@ -160,6 +190,9 @@ func (self *Readline) redraw() {
}
cursor_y++
}
if !render_completion_above {
move_cursor_up_by += render_completion_lines()
}
self.loop.AllowLineWrapping(true)
self.loop.MoveCursorVertically(-move_cursor_up_by)
self.loop.QueueWriteString("\r")

View File

@ -88,6 +88,8 @@ func default_shortcuts() *ShortcutMap {
sm.AddOrPanic(ActionNumericArgumentDigit9, "alt+9")
sm.AddOrPanic(ActionNumericArgumentDigitMinus, "alt+-")
sm.AddOrPanic(ActionCompleteForward, "Tab")
sm.AddOrPanic(ActionCompleteBackward, "Shift+Tab")
_default_shortcuts = sm
}
return _default_shortcuts