completion.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package cli
  3. import (
  4. "fmt"
  5. "path/filepath"
  6. "strings"
  7. "kitty/tools/utils"
  8. "kitty/tools/wcswidth"
  9. )
  10. var _ = fmt.Print
  11. type Match struct {
  12. Word string `json:"word,omitempty"`
  13. Description string `json:"description,omitempty"`
  14. }
  15. type MatchGroup struct {
  16. Title string `json:"title,omitempty"`
  17. NoTrailingSpace bool `json:"no_trailing_space,omitempty"`
  18. IsFiles bool `json:"is_files,omitempty"`
  19. Matches []*Match `json:"matches,omitempty"`
  20. }
  21. func (self *MatchGroup) remove_common_prefix() string {
  22. if self.IsFiles {
  23. if len(self.Matches) > 1 {
  24. lcp := self.longest_common_prefix()
  25. if strings.Contains(lcp, utils.Sep) {
  26. lcp = strings.TrimRight(filepath.Dir(lcp), utils.Sep) + utils.Sep
  27. self.remove_prefix_from_all_matches(lcp)
  28. return lcp
  29. }
  30. }
  31. } else if len(self.Matches) > 1 && strings.HasPrefix(self.Matches[0].Word, "--") && strings.Contains(self.Matches[0].Word, "=") {
  32. lcp, _, _ := strings.Cut(self.longest_common_prefix(), "=")
  33. lcp += "="
  34. if len(lcp) > 3 {
  35. self.remove_prefix_from_all_matches(lcp)
  36. return lcp
  37. }
  38. }
  39. return ""
  40. }
  41. func (self *MatchGroup) AddMatch(word string, description ...string) *Match {
  42. ans := Match{Word: word, Description: strings.Join(description, " ")}
  43. self.Matches = append(self.Matches, &ans)
  44. return &ans
  45. }
  46. func (self *MatchGroup) AddPrefixToAllMatches(prefix string) {
  47. for _, m := range self.Matches {
  48. m.Word = prefix + m.Word
  49. }
  50. }
  51. func (self *MatchGroup) remove_prefix_from_all_matches(prefix string) {
  52. for _, m := range self.Matches {
  53. m.Word = m.Word[len(prefix):]
  54. }
  55. }
  56. func (self *MatchGroup) has_descriptions() bool {
  57. for _, m := range self.Matches {
  58. if m.Description != "" {
  59. return true
  60. }
  61. }
  62. return false
  63. }
  64. func (self *MatchGroup) max_visual_word_length(limit int) int {
  65. ans := 0
  66. for _, m := range self.Matches {
  67. if q := wcswidth.Stringwidth(m.Word); q > ans {
  68. ans = q
  69. if ans > limit {
  70. return limit
  71. }
  72. }
  73. }
  74. return ans
  75. }
  76. func (self *MatchGroup) longest_common_prefix() string {
  77. limit := len(self.Matches)
  78. i := 0
  79. return utils.LongestCommon(func() (string, bool) {
  80. if i < limit {
  81. i++
  82. return self.Matches[i-1].Word, false
  83. }
  84. return "", true
  85. }, true)
  86. }
  87. type Delegate struct {
  88. NumToRemove int `json:"num_to_remove,omitempty"`
  89. Command string `json:"command,omitempty"`
  90. }
  91. type Completions struct {
  92. Groups []*MatchGroup `json:"groups,omitempty"`
  93. Delegate Delegate `json:"delegate,omitempty"`
  94. CurrentCmd *Command `json:"-"`
  95. AllWords []string `json:"-"` // all words passed to parse_args()
  96. CurrentWordIdx int `json:"-"` // index of current word in all_words
  97. CurrentWordIdxInParent int `json:"-"` // index of current word in parents command line 1 for first word after parent
  98. split_on_equals bool // true if the cmdline is split on = (BASH does this because readline does this)
  99. }
  100. func NewCompletions() *Completions {
  101. return &Completions{Groups: make([]*MatchGroup, 0, 4)}
  102. }
  103. func (self *Completions) AddPrefixToAllMatches(prefix string) {
  104. for _, mg := range self.Groups {
  105. mg.AddPrefixToAllMatches(prefix)
  106. }
  107. }
  108. func (self *Completions) MergeMatchGroup(mg *MatchGroup) {
  109. if len(mg.Matches) == 0 {
  110. return
  111. }
  112. var dest *MatchGroup
  113. for _, q := range self.Groups {
  114. if q.Title == mg.Title {
  115. dest = q
  116. break
  117. }
  118. }
  119. if dest == nil {
  120. dest = self.AddMatchGroup(mg.Title)
  121. dest.NoTrailingSpace = mg.NoTrailingSpace
  122. dest.IsFiles = mg.IsFiles
  123. }
  124. seen := utils.NewSet[string](64)
  125. for _, q := range self.Groups {
  126. for _, m := range q.Matches {
  127. seen.Add(m.Word)
  128. }
  129. }
  130. for _, m := range mg.Matches {
  131. if !seen.Has(m.Word) {
  132. seen.Add(m.Word)
  133. dest.Matches = append(dest.Matches, m)
  134. }
  135. }
  136. }
  137. func (self *Completions) AddMatchGroup(title string) *MatchGroup {
  138. for _, q := range self.Groups {
  139. if q.Title == title {
  140. return q
  141. }
  142. }
  143. ans := MatchGroup{Title: title, Matches: make([]*Match, 0, 8)}
  144. self.Groups = append(self.Groups, &ans)
  145. return &ans
  146. }
  147. type CompletionFunc = func(completions *Completions, word string, arg_num int)
  148. func NamesCompleter(title string, names ...string) CompletionFunc {
  149. return func(completions *Completions, word string, arg_num int) {
  150. mg := completions.AddMatchGroup(title)
  151. for _, q := range names {
  152. if strings.HasPrefix(q, word) {
  153. mg.AddMatch(q)
  154. }
  155. }
  156. }
  157. }
  158. func ChainCompleters(completers ...CompletionFunc) CompletionFunc {
  159. return func(completions *Completions, word string, arg_num int) {
  160. for _, f := range completers {
  161. f(completions, word, arg_num)
  162. }
  163. }
  164. }
  165. func CompletionForWrapper(wrapped_cmd string) func(completions *Completions, word string, arg_num int) {
  166. return func(completions *Completions, word string, arg_num int) {
  167. completions.Delegate.NumToRemove = completions.CurrentWordIdx
  168. completions.Delegate.Command = wrapped_cmd
  169. }
  170. }