autoterm.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. /*
  2. * Public Domain Software
  3. *
  4. * I (Matthias Ladkau) am the author of the source code in this file.
  5. * I have placed the source code in this file in the public domain.
  6. *
  7. * For further information see: http://creativecommons.org/publicdomain/zero/1.0/
  8. */
  9. package termutil
  10. import (
  11. "fmt"
  12. "sort"
  13. "strings"
  14. "devt.de/krotik/common/stringutil"
  15. "devt.de/krotik/common/termutil/getch"
  16. )
  17. /*
  18. Dict is a dictionary object used by the AutoCompleteMixin
  19. */
  20. type Dict interface {
  21. /*
  22. Suggest returns dictionary suggestions based on a given prefix. Returns if there
  23. is a direct match and a list of suggestions.
  24. */
  25. Suggest(prefix string) ([]string, error)
  26. }
  27. /*
  28. autocompleteLineTerminalMixin adds auto-complete functionality to a given ConsoleLineTerminals
  29. */
  30. type autocompleteLineTerminalMixin struct {
  31. ConsoleLineTerminal // Terminal which is being extended
  32. dict Dict // Dictionary to use for suggestions
  33. tabCount int // Counter for tab presses
  34. }
  35. /*
  36. AddAutoCompleteMixin adds auto-complete support for a given ConsoleLineTerminal.
  37. The auto-complete function operates on a given Dict object which suggests either
  38. a direct match or a list of matches. A single tab auto-completes if there is a
  39. direct match. Two tabs and the console outputs all suggestions.
  40. */
  41. func AddAutoCompleteMixin(term ConsoleLineTerminal, dict Dict) (ConsoleLineTerminal, error) {
  42. autoterm := &autocompleteLineTerminalMixin{term, dict, 0}
  43. // Add key handler
  44. autoterm.AddKeyHandler(autoterm.handleKeyInput)
  45. return autoterm, nil
  46. }
  47. /*
  48. handleKeyInput handles the key input for the history mixin.
  49. */
  50. func (at *autocompleteLineTerminalMixin) handleKeyInput(e *getch.KeyEvent, buf []rune) (bool, []rune, error) {
  51. var err error
  52. var ret []rune
  53. if e.Code == getch.KeyTab {
  54. var suggestions []string
  55. at.tabCount++
  56. currentLine := stringutil.RuneSliceToString(buf)
  57. words := strings.Split(currentLine, " ")
  58. prefix := strings.Join(words[:len(words)-1], " ")
  59. lastWord := words[len(words)-1]
  60. if suggestions, err = at.dict.Suggest(currentLine); err == nil {
  61. num := len(suggestions)
  62. if num == 1 {
  63. var newline string
  64. if suggestions[0] == lastWord {
  65. // Nothing more to auto-complete insert a space for next level suggestions
  66. newline = fmt.Sprintf("%v ", currentLine)
  67. } else {
  68. // If there is only one suggestion we can use it
  69. if prefix != "" {
  70. newline = fmt.Sprintf("%v ", prefix)
  71. }
  72. newline = fmt.Sprintf("%v%v ", newline, suggestions[0])
  73. }
  74. ret = stringutil.StringToRuneSlice(newline)
  75. } else if len(suggestions) > 1 {
  76. cp := stringutil.LongestCommonPrefix(suggestions)
  77. if len(cp) > len(lastWord) {
  78. var newline string
  79. if prefix != "" {
  80. newline = fmt.Sprintf("%v ", prefix)
  81. }
  82. ret = stringutil.StringToRuneSlice(fmt.Sprintf("%v%v", newline, cp))
  83. }
  84. if at.tabCount > 1 || ret == nil {
  85. // There are multiple suggestions and tab was pressed more than once
  86. at.WriteString(fmt.Sprintln())
  87. at.WriteString(stringutil.PrintStringTable(suggestions, 4))
  88. if at.tabCount == 2 {
  89. // Check if at least on suggestion is the full string
  90. for _, s := range suggestions {
  91. if s == lastWord {
  92. ret = stringutil.StringToRuneSlice(currentLine + " ")
  93. break
  94. }
  95. }
  96. }
  97. }
  98. }
  99. }
  100. if ret != nil {
  101. at.tabCount = 0
  102. }
  103. }
  104. return ret != nil, ret, err
  105. }
  106. // Dictionaries
  107. // ============
  108. /*
  109. MultiWordDict models a dictionary which can present suggestions based on multiple
  110. words. Only suggestions for the last word are returned. However, these suggestions
  111. may depend on the preceding words.
  112. */
  113. type MultiWordDict struct {
  114. chooser DictChooser
  115. dicts map[string]Dict
  116. }
  117. /*
  118. DictChooser chooses a WordListDict based on given prefix words. The function
  119. also gets a presisted map of WordListDicts which can be used as a cache.
  120. */
  121. type DictChooser func([]string, map[string]Dict) (Dict, error)
  122. /*
  123. NewMultiWordDict returns a new MultiWordDict. The client code needs to specify a
  124. function to retrieve WordListDicts for given prefix words and can optionally
  125. supply an initial map of WordListDicts.
  126. */
  127. func NewMultiWordDict(chooser DictChooser, dicts map[string]Dict) *MultiWordDict {
  128. if dicts == nil {
  129. dicts = make(map[string]Dict)
  130. }
  131. return &MultiWordDict{chooser, dicts}
  132. }
  133. /*
  134. Suggest returns dictionary suggestions based on a given prefix. Returns if there
  135. is a direct match and a list of suggestions.
  136. */
  137. func (md *MultiWordDict) Suggest(prefix string) ([]string, error) {
  138. // Split prefix into words
  139. prefixWords := strings.Split(prefix, " ")
  140. dict, err := md.chooser(prefixWords, md.dicts)
  141. if err == nil && dict != nil {
  142. return dict.Suggest(prefixWords[len(prefixWords)-1])
  143. }
  144. return nil, err
  145. }
  146. /*
  147. WordListDict is a simple dictionary which looks up suggstions based on an
  148. internal word list
  149. */
  150. type WordListDict struct {
  151. words []string
  152. }
  153. /*
  154. NewWordListDict returns a new WordListDict from a given list of words. The list
  155. of words will be sorted.
  156. */
  157. func NewWordListDict(words []string) *WordListDict {
  158. sort.Strings(words)
  159. return &WordListDict{words}
  160. }
  161. /*
  162. Suggest returns dictionary suggestions based on a given prefix. Returns if there
  163. is a direct match and a list of suggestions.
  164. */
  165. func (wd *WordListDict) Suggest(prefix string) ([]string, error) {
  166. var suggestions []string
  167. // Do a binary search on the word list
  168. index := sort.SearchStrings(wd.words, prefix)
  169. if index < len(wd.words) {
  170. // Check the found word
  171. foundWord := wd.words[index]
  172. if strings.HasPrefix(foundWord, prefix) {
  173. // Build up suggestions
  174. suggestions = append(suggestions, foundWord)
  175. // Look for further matching words
  176. for i := index + 1; i < len(wd.words); i++ {
  177. if nextWord := wd.words[i]; strings.HasPrefix(nextWord, prefix) {
  178. suggestions = append(suggestions, nextWord)
  179. }
  180. }
  181. }
  182. }
  183. return suggestions, nil
  184. }