Get zsh completion working apart from delegation

This commit is contained in:
Kovid Goyal 2022-09-19 08:54:54 +05:30
parent cbbda23e01
commit 2cc359ccc8
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
7 changed files with 160 additions and 6 deletions

View File

@ -3,5 +3,5 @@
(( ${+commands[kitty]} )) || builtin return (( ${+commands[kitty]} )) || builtin return
builtin local src cmd=${(F)words:0:$CURRENT} builtin local src cmd=${(F)words:0:$CURRENT}
# Send all words up to the word the cursor is currently on. # Send all words up to the word the cursor is currently on.
src=$(builtin command kitty +complete zsh "_matcher=$_matcher" <<<$cmd) || builtin return src=$(builtin command kitty-tool __complete__ zsh "_matcher=$_matcher" <<<$cmd) || builtin return
builtin eval "$src" builtin eval "$src"

View File

@ -269,6 +269,7 @@ func directory_completer(title string, relative_to relative_to) completion_func
return func(completions *Completions, word string, arg_num int) { return func(completions *Completions, word string, arg_num int) {
mg := completions.add_match_group(title) mg := completions.add_match_group(title)
mg.NoTrailingSpace = true
mg.IsFiles = true mg.IsFiles = true
complete_files(word, func(entry *FileEntry) { complete_files(word, func(entry *FileEntry) {
if entry.mode.IsDir() { if entry.mode.IsDir() {

View File

@ -55,6 +55,7 @@ func complete_kitty(completions *Completions, word string, arg_num int) {
func complete_kitty_override(title string, names []string) completion_func { func complete_kitty_override(title string, names []string) completion_func {
return func(completions *Completions, word string, arg_num int) { return func(completions *Completions, word string, arg_num int) {
mg := completions.add_match_group(title) mg := completions.add_match_group(title)
mg.NoTrailingSpace = true
for _, q := range names { for _, q := range names {
if strings.HasPrefix(q, word) { if strings.HasPrefix(q, word) {
mg.add_match(q + "=") mg.add_match(q + "=")
@ -66,6 +67,7 @@ func complete_kitty_override(title string, names []string) completion_func {
func complete_kitty_listen_on(completions *Completions, word string, arg_num int) { func complete_kitty_listen_on(completions *Completions, word string, arg_num int) {
if !strings.Contains(word, ":") { if !strings.Contains(word, ":") {
mg := completions.add_match_group("Address family") mg := completions.add_match_group("Address family")
mg.NoTrailingSpace = true
for _, q := range []string{"unix:", "tcp:"} { for _, q := range []string{"unix:", "tcp:"} {
if strings.HasPrefix(q, word) { if strings.HasPrefix(q, word) {
mg.add_match(q) mg.add_match(q)

View File

@ -43,7 +43,11 @@ func main(args []string) error {
output_type = args[0] output_type = args[0]
args = args[1:] args = args[1:]
} }
shell_state := make(map[string]string, len(args)) n := len(args)
if n < 1 {
n = 1
}
shell_state := make(map[string]string, n)
for _, arg := range args { for _, arg := range args {
k, v, found := utils.Cut(arg, "=") k, v, found := utils.Cut(arg, "=")
if !found { if !found {

View File

@ -55,12 +55,17 @@ func (self *Completions) add_options_group(options []*Option, word string) {
if word == "-" { if word == "-" {
group.Matches = append(group.Matches, &Match{Word: "--", Description: "End of options"}) group.Matches = append(group.Matches, &Match{Word: "--", Description: "End of options"})
for _, opt := range options { for _, opt := range options {
has_single_letter_alias := false
for _, q := range opt.Aliases { for _, q := range opt.Aliases {
if len(q) == 1 { if len(q) == 1 {
group.add_match("-"+q, opt.Description) group.add_match("-"+q, opt.Description)
has_single_letter_alias = true
break break
} }
} }
if !has_single_letter_alias {
group.add_match("--"+opt.Aliases[0], opt.Description)
}
} }
} else { } else {
runes := []rune(word) runes := []rune(word)

View File

@ -2,7 +2,11 @@
package completion package completion
import "strings" import (
"kitty/tools/utils"
"kitty/tools/wcswidth"
"strings"
)
type Match struct { type Match struct {
Word string `json:"word,omitempty"` Word string `json:"word,omitempty"`
@ -28,6 +32,46 @@ func (self *MatchGroup) add_prefix_to_all_matches(prefix string) {
} }
} }
func (self *MatchGroup) remove_prefix_from_all_matches(prefix string) {
for _, m := range self.Matches {
m.Word = m.Word[len(prefix):]
}
}
func (self *MatchGroup) has_descriptions() bool {
for _, m := range self.Matches {
if m.Description != "" {
return true
}
}
return false
}
func (self *MatchGroup) max_visual_word_length(limit int) int {
ans := 0
for _, m := range self.Matches {
if q := wcswidth.Stringwidth(m.Word); q > ans {
ans = q
if ans > limit {
return limit
}
}
}
return ans
}
func (self *MatchGroup) longest_common_prefix() string {
limit := len(self.Matches)
i := 0
return utils.LongestCommon(func() (string, bool) {
if i < limit {
i++
return self.Matches[i-1].Word, false
}
return "", true
}, true)
}
type Completions struct { type Completions struct {
Groups []*MatchGroup `json:"groups,omitempty"` Groups []*MatchGroup `json:"groups,omitempty"`

View File

@ -5,6 +5,11 @@ package completion
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"kitty/tools/cli/markup"
"kitty/tools/tty"
"kitty/tools/utils"
"kitty/tools/wcswidth"
"path/filepath"
"strings" "strings"
) )
@ -12,8 +17,11 @@ var _ = fmt.Print
func zsh_input_parser(data []byte, shell_state map[string]string) ([][]string, error) { func zsh_input_parser(data []byte, shell_state map[string]string) ([][]string, error) {
matcher := shell_state["_matcher"] matcher := shell_state["_matcher"]
q := strings.Split(strings.ToLower(matcher), ":")[0][:1] q := ""
if strings.Contains("lrbe", q) { if matcher != "" {
q = strings.Split(strings.ToLower(matcher), ":")[0][:1]
}
if q != "" && strings.Contains("lrbe", q) {
// this is zsh anchor based matching // this is zsh anchor based matching
// https://zsh.sourceforge.io/Doc/Release/Completion-Widgets.html#Completion-Matching-Control // https://zsh.sourceforge.io/Doc/Release/Completion-Widgets.html#Completion-Matching-Control
// can be specified with matcher-list and some systems do it by default, // can be specified with matcher-list and some systems do it by default,
@ -38,8 +46,98 @@ func zsh_input_parser(data []byte, shell_state map[string]string) ([][]string, e
return [][]string{words}, nil return [][]string{words}, nil
} }
func fmt_desc(word, desc string, max_word_len int, f *markup.Context, screen_width int) string {
if desc == "" {
return word
}
line, _, _ := utils.Cut(strings.TrimSpace(desc), "\n")
desc = f.Prettify(line)
multiline := false
max_desc_len := screen_width - 2
word_len := wcswidth.Stringwidth(word)
if word_len > max_word_len {
multiline = true
} else {
word += strings.Repeat(" ", max_word_len-word_len)
max_desc_len = screen_width - max_word_len - 3
}
if wcswidth.Stringwidth(desc) > max_desc_len {
desc = wcswidth.TruncateToVisualLength(desc, max_desc_len-2) + "…"
}
if multiline {
return word + "\n " + desc
}
return word + " " + desc
}
func serialize(completions *Completions, f *markup.Context, screen_width int) ([]byte, error) {
cmd := strings.Builder{}
output := strings.Builder{}
for _, mg := range completions.Groups {
cmd.WriteString("compadd -U -J ")
cmd.WriteString(utils.QuoteStringForSH(mg.Title))
cmd.WriteString(" -X ")
cmd.WriteString(utils.QuoteStringForSH("%B" + mg.Title + "%b"))
if mg.NoTrailingSpace {
cmd.WriteString(" -S ''")
}
if mg.IsFiles {
cmd.WriteString(" -f")
if len(mg.Matches) > 1 {
lcp := mg.longest_common_prefix()
if strings.Contains(lcp, utils.Sep) {
lcp = strings.TrimRight(filepath.Dir(lcp), utils.Sep) + utils.Sep
cmd.WriteString(" -p ")
cmd.WriteString(utils.QuoteStringForSH(lcp))
mg.remove_prefix_from_all_matches(lcp)
}
}
} else if len(mg.Matches) > 1 && strings.HasPrefix(mg.Matches[0].Word, "--") && strings.Contains(mg.Matches[0].Word, "=") {
lcp, _, _ := utils.Cut(mg.longest_common_prefix(), "=")
lcp += "="
if len(lcp) > 3 {
cmd.WriteString(" -p ")
cmd.WriteString(utils.QuoteStringForSH(lcp))
mg.remove_prefix_from_all_matches(lcp)
}
}
if mg.has_descriptions() {
fmt.Fprintln(&output, "compdescriptions=(")
limit := mg.max_visual_word_length(16)
for _, m := range mg.Matches {
fmt.Fprintln(&output, utils.QuoteStringForSH(fmt_desc(m.Word, m.Description, limit, f, screen_width)))
}
fmt.Fprintln(&output, ")")
cmd.WriteString(" -l -d compdescriptions")
}
cmd.WriteString(" --")
for _, m := range mg.Matches {
cmd.WriteString(" ")
cmd.WriteString(utils.QuoteStringForSH(m.Word))
}
fmt.Fprintln(&output, cmd.String(), ";")
}
return []byte(output.String()), nil
}
func zsh_output_serializer(completions []*Completions, shell_state map[string]string) ([]byte, error) { func zsh_output_serializer(completions []*Completions, shell_state map[string]string) ([]byte, error) {
return nil, nil var f *markup.Context
screen_width := 80
ctty, err := tty.OpenControllingTerm()
if err == nil {
sz, err := ctty.GetSize()
ctty.Close()
if err == nil {
screen_width = int(sz.Col)
f = markup.New(true)
}
}
if f == nil {
f = markup.New(false)
}
return serialize(completions[0], f, screen_width)
} }
func init() { func init() {