Repository: gokcehan/lf
Pull request #1595 (2024-02-05 03:49:17)

Dynamically generate list of completions
From discussion in https://github.com/gokcehan/lf/pull/1569#issuecomment-1891179506


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e720b8f..dac133f 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 e0055da..cd962a5 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/eval.go b/eval.go
index 6979aed..17e927f 100644
--- a/eval.go
+++ b/eval.go
@@ -438,8 +438,8 @@ func (e *setExpr) eval(app *app, args []string) {
 }
 
 func (e *setLocalExpr) eval(app *app, args []string) {
-	path := replaceTilde(e.path)
-	if !filepath.IsAbs(path) {
+	e.path = replaceTilde(e.path)
+	if !filepath.IsAbs(e.path) {
 		app.ui.echoerr("setlocal: path should be absolute")
 		return
 	}
@@ -476,7 +476,7 @@ func (e *setLocalExpr) eval(app *app, args []string) {
 		}
 	case "info":
 		if e.val == "" {
-			gLocalOpts.infos[path] = nil
+			gLocalOpts.infos[e.path] = nil
 			return
 		}
 		toks := strings.Split(e.val, ":")
@@ -488,14 +488,14 @@ func (e *setLocalExpr) eval(app *app, args []string) {
 				return
 			}
 		}
-		gLocalOpts.infos[path] = toks
+		gLocalOpts.infos[e.path] = toks
 	case "sortby":
 		method := sortMethod(e.val)
 		if !isValidSortMethod(method) {
 			app.ui.echoerr(invalidSortErrorMessage)
 			return
 		}
-		gLocalOpts.sortbys[path] = method
+		gLocalOpts.sortbys[e.path] = method
 		app.nav.sort()
 		app.ui.sort()
 	default:
diff --git a/go.mod b/go.mod
index 30f480f..26e6564 100644
--- a/go.mod
+++ b/go.mod
@@ -6,8 +6,8 @@ require (
 	github.com/djherbis/times v1.6.0
 	github.com/gdamore/tcell/v2 v2.7.0
 	github.com/mattn/go-runewidth v0.0.15
-	golang.org/x/sys v0.16.0
-	golang.org/x/term v0.16.0
+	golang.org/x/sys v0.17.0
+	golang.org/x/term v0.17.0
 )
 
 require (
diff --git a/go.sum b/go.sum
index 4f1c012..0ec65b0 100644
--- a/go.sum
+++ b/go.sum
@@ -31,14 +31,14 @@ golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
-golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
-golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=


Test patch:
diff --git a/complete_test.go b/complete_test.go
index 36994dd..51896b9 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)
+		}
+	}
+}