diff --git a/CHANGELOG.md b/CHANGELOG.md index e856546..e25a863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,7 @@ random errors introduced with the new Lua runtime (see [#197]) - `exec`, `clear` and `cat` builtin commands - `hilbish.cancel` hook - 1st item on history is now inserted when history search menu is opened ([#148]) +- Case insensitive tab completion! It can be enabled with the `insensitive` opt. [#148]: https://github.com/Rosettea/Hilbish/issues/148 [#197]: https://github.com/Rosettea/Hilbish/issues/197 diff --git a/complete.go b/complete.go index c2a107c..0458a27 100644 --- a/complete.go +++ b/complete.go @@ -72,13 +72,13 @@ func splitForFile(str string) []string { return split } -func fileComplete(query, ctx string, fields []string) ([]string, string) { +func fileComplete(query, ctx string, fields []string, caseInsensitive bool) ([]string, string) { q := splitForFile(ctx) - return matchPath(q[len(q) - 1]) + return matchPath(q[len(q) - 1], caseInsensitive) } -func binaryComplete(query, ctx string, fields []string) ([]string, string) { +func binaryComplete(query, ctx string, fields []string, caseInsensitive bool) ([]string, string) { q := splitForFile(ctx) query = q[len(q) - 1] @@ -87,7 +87,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) { prefixes := []string{"./", "../", "/", "~/"} for _, prefix := range prefixes { if strings.HasPrefix(query, prefix) { - fileCompletions, filePref := matchPath(query) + fileCompletions, filePref := matchPath(query, caseInsensitive) if len(fileCompletions) != 0 { for _, f := range fileCompletions { fullPath, _ := filepath.Abs(util.ExpandHome(query + strings.TrimPrefix(f, filePref))) @@ -132,7 +132,7 @@ func binaryComplete(query, ctx string, fields []string) ([]string, string) { return completions, query } -func matchPath(query string) ([]string, string) { +func matchPath(query string, caseInsensitive bool) ([]string, string) { oldQuery := query query = strings.TrimPrefix(query, "\"") var entries []string @@ -148,9 +148,17 @@ func matchPath(query string) ([]string, string) { baseName = filepath.Base(query) } + if caseInsensitive { + baseName = strings.ToLower(baseName) + } + files, _ := os.ReadDir(path) for _, file := range files { - if strings.HasPrefix(file.Name(), baseName) { + fname := file.Name() + if caseInsensitive { + fname = strings.ToLower(fname) + } + if strings.HasPrefix(fname, baseName) { entry := file.Name() if file.IsDir() { entry = entry + string(os.PathSeparator) @@ -174,8 +182,8 @@ func escapeFilename(fname string) string { func completionLoader(rtm *rt.Runtime) *rt.Table { exports := map[string]util.LuaExport{ - "files": {luaFileComplete, 3, false}, - "bins": {luaBinaryComplete, 3, false}, + "files": {luaFileComplete, 3, true}, + "bins": {luaBinaryComplete, 3, true}, "call": {callLuaCompleter, 4, false}, "handler": {completionHandler, 2, false}, } @@ -231,12 +239,12 @@ func callLuaCompleter(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } func luaFileComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - query, ctx, fds, err := getCompleteParams(t, c) + query, ctx, fds, insensitive, err := getCompleteParams(t, c) if err != nil { return nil, err } - completions, pfx := fileComplete(query, ctx, fds) + completions, pfx := fileComplete(query, ctx, fds, insensitive) luaComps := rt.NewTable() for i, comp := range completions { @@ -247,12 +255,12 @@ func luaFileComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { } func luaBinaryComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { - query, ctx, fds, err := getCompleteParams(t, c) + query, ctx, fds, insensitive, err := getCompleteParams(t, c) if err != nil { return nil, err } - completions, pfx := binaryComplete(query, ctx, fds) + completions, pfx := binaryComplete(query, ctx, fds, insensitive) luaComps := rt.NewTable() for i, comp := range completions { @@ -262,21 +270,30 @@ func luaBinaryComplete(t *rt.Thread, c *rt.GoCont) (rt.Cont, error) { return c.PushingNext(t.Runtime, rt.TableValue(luaComps), rt.StringValue(pfx)), nil } -func getCompleteParams(t *rt.Thread, c *rt.GoCont) (string, string, []string, error) { +func getCompleteParams(t *rt.Thread, c *rt.GoCont) (string, string, []string, bool, error) { if err := c.CheckNArgs(3); err != nil { - return "", "", []string{}, err + return "", "", []string{}, false, err } query, err := c.StringArg(0) if err != nil { - return "", "", []string{}, err + return "", "", []string{}, false, err } ctx, err := c.StringArg(1) if err != nil { - return "", "", []string{}, err + return "", "", []string{}, false, err } fields, err := c.TableArg(2) if err != nil { - return "", "", []string{}, err + return "", "", []string{}, false, err + } + var insensitive bool + if len(c.Etc()) != 0 { + typ := c.Etc()[0] + var ok bool + insensitive, ok = typ.TryBool() + if !ok { + return "", "", []string{}, false, errors.New("bad argument #4 (expected bool, got " + typ.TypeName() + ")") + } } var fds []string @@ -286,5 +303,5 @@ func getCompleteParams(t *rt.Thread, c *rt.GoCont) (string, string, []string, er } }) - return query, ctx, fds, err + return query, ctx, fds, insensitive, err } diff --git a/docs/completions.txt b/docs/completions.txt index 1354dc0..8b0b3ed 100644 --- a/docs/completions.txt +++ b/docs/completions.txt @@ -36,9 +36,9 @@ then there is the `files` function, which is mentioned below. # Completion Interface ## Functions -- `files(query, ctx, fields)` -> table, prefix: get file completions, based +- `files(query, ctx, fields, caseInsensitive)` -> table, prefix: get file completions, based on the user's query. -- `bins(query, ctx, fields)` -> table, prefix: get binary/executable +- `bins(query, ctx, fields, caseInsensitive)` -> table, prefix: get binary/executable completions, based on user query. - `call(scope, query, ctx, fields)` -> table, prefix: call a completion handler with `scope`, usually being in the form of `command.` diff --git a/nature/completions.lua b/nature/completions.lua index f8127a1..579d659 100644 --- a/nature/completions.lua +++ b/nature/completions.lua @@ -15,7 +15,7 @@ function hilbish.completion.handler(line, pos) local query = fields[#fields] if #fields == 1 then - local comps, pfx = hilbish.completion.bins(query, ctx, fields) + local comps, pfx = hilbish.completion.bins(query, ctx, fields, hilbish.opts.insensitive) local compGroup = { items = comps, type = 'grid' @@ -29,7 +29,7 @@ function hilbish.completion.handler(line, pos) return compGroups, pfx end - local comps, pfx = hilbish.completion.files(query, ctx, fields) + local comps, pfx = hilbish.completion.files(query, ctx, fields, hilbish.opts.insensitive) local compGroup = { items = comps, type = 'grid' diff --git a/nature/opts/init.lua b/nature/opts/init.lua index ae95ee1..067801b 100644 --- a/nature/opts/init.lua +++ b/nature/opts/init.lua @@ -25,7 +25,8 @@ local defaultOpts = { greeting = string.format([[Welcome to {magenta}Hilbish{reset}, {cyan}%s{reset}. The nice lil shell for {blue}Lua{reset} fanatics! ]], hilbish.user), - motd = true + motd = true, + insensitive = false } for optsName, default in pairs(defaultOpts) do diff --git a/nature/opts/insensitive.lua b/nature/opts/insensitive.lua new file mode 100644 index 0000000..1fcd659 --- /dev/null +++ b/nature/opts/insensitive.lua @@ -0,0 +1 @@ +-- noop diff --git a/readline/tab-virtual.go b/readline/tab-virtual.go index d1e1d76..37df4d5 100644 --- a/readline/tab-virtual.go +++ b/readline/tab-virtual.go @@ -74,7 +74,9 @@ func (rl *Instance) insertCandidate() { // Ensure no indexing error happens with prefix if len(completion) >= prefix { - rl.insert([]rune(completion[prefix:])) + rl.viDeleteByAdjust(-prefix) + + rl.insert([]rune(completion)) if !cur.TrimSlash && !cur.NoSpace { rl.insert([]rune(" ")) } @@ -86,7 +88,6 @@ func (rl *Instance) insertCandidate() { func (rl *Instance) updateVirtualComp() { cur := rl.getCurrentGroup() if cur != nil { - completion := cur.getCurrentCell(rl) prefix := len(rl.tcPrefix) @@ -99,6 +100,9 @@ func (rl *Instance) updateVirtualComp() { rl.viUndoSkipAppend = true rl.resetTabCompletion() } else { + if strings.HasSuffix(string(rl.line), rl.tcPrefix) && (!rl.modeAutoFind || rl.searchMode != HistoryFind) { + rl.viDeleteByAdjust(-prefix) + } // Special case for the only special escape, which // if not handled, will make us insert the first @@ -109,7 +113,12 @@ func (rl *Instance) updateVirtualComp() { // Or insert it virtually. if len(completion) >= prefix { - rl.insertCandidateVirtual([]rune(completion[prefix:])) + comp := completion + if rl.modeAutoFind && rl.searchMode == HistoryFind { + comp = completion[prefix:] + } + + rl.insertCandidateVirtual([]rune(comp)) } } } @@ -173,7 +182,7 @@ func (rl *Instance) resetVirtualComp(drop bool) { completion = completion + " " } } - rl.insertCandidateVirtual([]rune(completion[prefix:])) + rl.insertCandidateVirtual([]rune(completion)) } // Reset virtual