123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- /*
- * Public Domain Software
- *
- * I (Matthias Ladkau) am the author of the source code in this file.
- * I have placed the source code in this file in the public domain.
- *
- * For further information see: http://creativecommons.org/publicdomain/zero/1.0/
- */
- package termutil
- import (
- "fmt"
- "sort"
- "strings"
- "devt.de/krotik/common/stringutil"
- "devt.de/krotik/common/termutil/getch"
- )
- /*
- Dict is a dictionary object used by the AutoCompleteMixin
- */
- type Dict interface {
- /*
- Suggest returns dictionary suggestions based on a given prefix. Returns if there
- is a direct match and a list of suggestions.
- */
- Suggest(prefix string) ([]string, error)
- }
- /*
- autocompleteLineTerminalMixin adds auto-complete functionality to a given ConsoleLineTerminals
- */
- type autocompleteLineTerminalMixin struct {
- ConsoleLineTerminal // Terminal which is being extended
- dict Dict // Dictionary to use for suggestions
- tabCount int // Counter for tab presses
- }
- /*
- AddAutoCompleteMixin adds auto-complete support for a given ConsoleLineTerminal.
- The auto-complete function operates on a given Dict object which suggests either
- a direct match or a list of matches. A single tab auto-completes if there is a
- direct match. Two tabs and the console outputs all suggestions.
- */
- func AddAutoCompleteMixin(term ConsoleLineTerminal, dict Dict) (ConsoleLineTerminal, error) {
- autoterm := &autocompleteLineTerminalMixin{term, dict, 0}
- // Add key handler
- autoterm.AddKeyHandler(autoterm.handleKeyInput)
- return autoterm, nil
- }
- /*
- handleKeyInput handles the key input for the history mixin.
- */
- func (at *autocompleteLineTerminalMixin) handleKeyInput(e *getch.KeyEvent, buf []rune) (bool, []rune, error) {
- var err error
- var ret []rune
- if e.Code == getch.KeyTab {
- var suggestions []string
- at.tabCount++
- currentLine := stringutil.RuneSliceToString(buf)
- words := strings.Split(currentLine, " ")
- prefix := strings.Join(words[:len(words)-1], " ")
- lastWord := words[len(words)-1]
- if suggestions, err = at.dict.Suggest(currentLine); err == nil {
- num := len(suggestions)
- if num == 1 {
- var newline string
- if suggestions[0] == lastWord {
- // Nothing more to auto-complete insert a space for next level suggestions
- newline = fmt.Sprintf("%v ", currentLine)
- } else {
- // If there is only one suggestion we can use it
- if prefix != "" {
- newline = fmt.Sprintf("%v ", prefix)
- }
- newline = fmt.Sprintf("%v%v ", newline, suggestions[0])
- }
- ret = stringutil.StringToRuneSlice(newline)
- } else if len(suggestions) > 1 {
- cp := stringutil.LongestCommonPrefix(suggestions)
- if len(cp) > len(lastWord) {
- var newline string
- if prefix != "" {
- newline = fmt.Sprintf("%v ", prefix)
- }
- ret = stringutil.StringToRuneSlice(fmt.Sprintf("%v%v", newline, cp))
- }
- if at.tabCount > 1 || ret == nil {
- // There are multiple suggestions and tab was pressed more than once
- at.WriteString(fmt.Sprintln())
- at.WriteString(stringutil.PrintStringTable(suggestions, 4))
- if at.tabCount == 2 {
- // Check if at least on suggestion is the full string
- for _, s := range suggestions {
- if s == lastWord {
- ret = stringutil.StringToRuneSlice(currentLine + " ")
- break
- }
- }
- }
- }
- }
- }
- if ret != nil {
- at.tabCount = 0
- }
- }
- return ret != nil, ret, err
- }
- // Dictionaries
- // ============
- /*
- MultiWordDict models a dictionary which can present suggestions based on multiple
- words. Only suggestions for the last word are returned. However, these suggestions
- may depend on the preceding words.
- */
- type MultiWordDict struct {
- chooser DictChooser
- dicts map[string]Dict
- }
- /*
- DictChooser chooses a WordListDict based on given prefix words. The function
- also gets a presisted map of WordListDicts which can be used as a cache.
- */
- type DictChooser func([]string, map[string]Dict) (Dict, error)
- /*
- NewMultiWordDict returns a new MultiWordDict. The client code needs to specify a
- function to retrieve WordListDicts for given prefix words and can optionally
- supply an initial map of WordListDicts.
- */
- func NewMultiWordDict(chooser DictChooser, dicts map[string]Dict) *MultiWordDict {
- if dicts == nil {
- dicts = make(map[string]Dict)
- }
- return &MultiWordDict{chooser, dicts}
- }
- /*
- Suggest returns dictionary suggestions based on a given prefix. Returns if there
- is a direct match and a list of suggestions.
- */
- func (md *MultiWordDict) Suggest(prefix string) ([]string, error) {
- // Split prefix into words
- prefixWords := strings.Split(prefix, " ")
- dict, err := md.chooser(prefixWords, md.dicts)
- if err == nil && dict != nil {
- return dict.Suggest(prefixWords[len(prefixWords)-1])
- }
- return nil, err
- }
- /*
- WordListDict is a simple dictionary which looks up suggstions based on an
- internal word list
- */
- type WordListDict struct {
- words []string
- }
- /*
- NewWordListDict returns a new WordListDict from a given list of words. The list
- of words will be sorted.
- */
- func NewWordListDict(words []string) *WordListDict {
- sort.Strings(words)
- return &WordListDict{words}
- }
- /*
- Suggest returns dictionary suggestions based on a given prefix. Returns if there
- is a direct match and a list of suggestions.
- */
- func (wd *WordListDict) Suggest(prefix string) ([]string, error) {
- var suggestions []string
- // Do a binary search on the word list
- index := sort.SearchStrings(wd.words, prefix)
- if index < len(wd.words) {
- // Check the found word
- foundWord := wd.words[index]
- if strings.HasPrefix(foundWord, prefix) {
- // Build up suggestions
- suggestions = append(suggestions, foundWord)
- // Look for further matching words
- for i := index + 1; i < len(wd.words); i++ {
- if nextWord := wd.words[i]; strings.HasPrefix(nextWord, prefix) {
- suggestions = append(suggestions, nextWord)
- }
- }
- }
- }
- return suggestions, nil
- }
|