From ab54329570b349a57ca2bb832b7ebd63174e3a02 Mon Sep 17 00:00:00 2001 From: Joe Lim <50560759+joelim-work@users.noreply.github.com> Date: Mon, 12 Feb 2024 10:19:17 +1100 Subject: [PATCH] Dynamically generate list of completions (#1595) * Dynamically generate list of completions * Update contribution guide * Add unit tests --- CONTRIBUTING.md | 1 - complete.go | 174 ++++++++++------------------------------------- complete_test.go | 38 +++++++++++ 3 files changed, 75 insertions(+), 138 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e720b8fe..dac133f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,6 @@ Adding a new option usually requires the following steps: - Add default option value to `init` function in `opts.go` - Add option evaluation logic to `setExpr.eval` in `eval.go` - Implement the option somewhere in the code -- Add option name to `gOptWords` in `complete.go` for tab completion - Add option name and its default value to `Quick Reference` and `Options` sections in `doc.md` - Run `gen/doc-with-docker.sh` to update the documentation - Commit your changes and send a pull request diff --git a/complete.go b/complete.go index e0055dad..cd962a5a 100644 --- a/complete.go +++ b/complete.go @@ -4,6 +4,7 @@ import ( "log" "os" "path/filepath" + "reflect" "sort" "strings" ) @@ -109,146 +110,45 @@ var ( "cmd-lowercase-word", } - gOptWords = []string{ - "anchorfind", - "noanchorfind", - "anchorfind!", - "autoquit", - "noautoquit", - "autoquit!", - "borderfmt", - "copyfmt", - "cursoractivefmt", - "cursorparentfmt", - "cursorpreviewfmt", - "cutfmt", - "hidecursorinactive", - "nohidecursorinactive", - "hidecursorinactive!", - "dircache", - "nodircache", - "dircache!", - "dircounts", - "nodircounts", - "dircounts!", - "dirfirst", - "nodirfirst", - "dirfirst!", - "dironly", - "nodironly", - "dironly!", - "dirpreviews", - "nodirpreviews", - "dirpreviews!", - "drawbox", - "nodrawbox", - "drawbox!", - "dupfilefmt", - "globsearch", - "noglobsearch", - "globsearch!", - "hidden", - "nohidden", - "hidden!", - "history", - "nohistory", - "history!", - "icons", - "noicons", - "icons!", - "ignorecase", - "noignorecase", - "ignorecase!", - "ignoredia", - "noignoredia", - "ignoredia!", - "incsearch", - "noincsearch", - "incsearch!", - "incfilter", - "noincfilter", - "incfilter!", - "mouse", - "nomouse", - "mouse!", - "number", - "nonumber", - "number!", - "preview", - "nopreview", - "preview!", - "relativenumber", - "norelativenumber", - "relativenumber!", - "reverse", - "noreverse", - "reverse!", - "ruler", - "rulerfmt", - "preserve", - "selectfmt", - "sixel", - "nosixel", - "sixel!", - "smartcase", - "nosmartcase", - "smartcase!", - "smartdia", - "nosmartdia", - "smartdia!", - "waitmsg", - "wrapscan", - "nowrapscan", - "wrapscan!", - "wrapscroll", - "nowrapscroll", - "wrapscroll!", - "findlen", - "period", - "scrolloff", - "tabstop", - "errorfmt", - "filesep", - "hiddenfiles", - "ifs", - "info", - "numberfmt", - "previewer", - "cleaner", - "promptfmt", - "ratios", - "selmode", - "shell", - "shellflag", - "shellopts", - "sortby", - "statfmt", - "timefmt", - "tempmarks", - "tagfmt", - "infotimefmtnew", - "infotimefmtold", - "truncatechar", - "truncatepct", + gOptWords = getOptWords(gOpts) + gLocalOptWords = getLocalOptWords(gLocalOpts) +) + +func getOptWords(opts any) (optWords []string) { + t := reflect.TypeOf(opts) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + switch field.Type.Kind() { + case reflect.Map: + continue + case reflect.Bool: + name := field.Name + optWords = append(optWords, name, "no"+name, name+"!") + default: + optWords = append(optWords, field.Name) + } } + sort.Strings(optWords) + return +} - gLocalOptWords = []string{ - "dirfirst", - "nodirfirst", - "dirfirst!", - "dironly", - "nodironly", - "dironly!", - "hidden", - "nohidden", - "hidden!", - "info", - "reverse", - "noreverse", - "reverse!", - "sortby", +func getLocalOptWords(localOpts any) (localOptWords []string) { + t := reflect.TypeOf(localOpts) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + name := strings.TrimSuffix(field.Name, "s") + if field.Type.Kind() != reflect.Map { + continue + } + if field.Type.Elem().Kind() == reflect.Bool { + localOptWords = append(localOptWords, name, "no"+name, name+"!") + } else { + localOptWords = append(localOptWords, name) + } } -) + sort.Strings(localOptWords) + return +} func matchLongest(s1, s2 []rune) []rune { i := 0 diff --git a/complete_test.go b/complete_test.go index 36994ddb..51896b97 100644 --- a/complete_test.go +++ b/complete_test.go @@ -54,3 +54,41 @@ func TestMatchWord(t *testing.T) { } } } + +func TestGetOptWords(t *testing.T) { + tests := []struct { + opts any + exp []string + }{ + {struct{ feature bool }{}, []string{"feature", "feature!", "nofeature"}}, + {struct{ feature int }{}, []string{"feature"}}, + {struct{ feature string }{}, []string{"feature"}}, + {struct{ feature []string }{}, []string{"feature"}}, + } + + for _, test := range tests { + result := getOptWords(test.opts) + if !reflect.DeepEqual(result, test.exp) { + t.Errorf("at input '%#v' expected '%s' but got '%s'", test.opts, test.exp, result) + } + } +} + +func TestGetLocalOptWords(t *testing.T) { + tests := []struct { + localOpts any + exp []string + }{ + {struct{ features map[string]bool }{}, []string{"feature", "feature!", "nofeature"}}, + {struct{ features map[string]int }{}, []string{"feature"}}, + {struct{ features map[string]string }{}, []string{"feature"}}, + {struct{ features map[string][]string }{}, []string{"feature"}}, + } + + for _, test := range tests { + result := getLocalOptWords(test.localOpts) + if !reflect.DeepEqual(result, test.exp) { + t.Errorf("at input '%#v' expected '%s' but got '%s'", test.localOpts, test.exp, result) + } + } +}