Complete ! shell escapes like a shell
A line starting with '!' was not in the completer tree at all, so Tab did nothing there -- just where the paths are longest. It now completes the way a shell does: the command word against the executables on PATH, the arguments against the filesystem, resolved relative to the active project because that is where forwardShell runs the line. Directories complete with their trailing slash, `~/` and absolute paths work, and dot entries stay hidden until the prefix asks for one. readline's completer is a tree of fixed words, which cannot express "a prefix that is not a word", so this is a small AutoCompleter that dispatches on the '!' and hands everything else to the existing tree. Its contract is easy to get subtly wrong -- candidates are the suffixes still missing, and the length is counted in runes, not bytes -- so the conversion has its own test, as does completing only the basename inside a directory, which is what keeps the candidate list readable. Only the basename is offered inside a directory, PATH is scanned once per session, and a bare '!' offers nothing rather than every executable on the machine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+41
-4
@@ -4,15 +4,52 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
)
|
||||
|
||||
// completer wires up Tab completion. Command names complete at the start of the
|
||||
// line; cd/open/view complete local project names; clone/show complete
|
||||
// completer wires up Tab completion. A line starting with '!' is completed the
|
||||
// way a shell would — executables for the command, paths for its arguments —
|
||||
// and everything else against the builtin command tree.
|
||||
func completer() readline.AutoCompleter {
|
||||
return &mgshCompleter{builtin: builtinCompleter()}
|
||||
}
|
||||
|
||||
// mgshCompleter dispatches between the two completion worlds.
|
||||
type mgshCompleter struct{ builtin *readline.PrefixCompleter }
|
||||
|
||||
func (c *mgshCompleter) Do(line []rune, pos int) ([][]rune, int) {
|
||||
if pos > len(line) {
|
||||
pos = len(line)
|
||||
}
|
||||
if strings.HasPrefix(strings.TrimLeft(string(line[:pos]), " \t"), "!") {
|
||||
cands, prefix := shellCandidates(string(line[:pos]))
|
||||
return runeSuffixes(cands, prefix)
|
||||
}
|
||||
return c.builtin.Do(line, pos)
|
||||
}
|
||||
|
||||
// runeSuffixes converts full candidate words into what readline wants: the part
|
||||
// still missing after the prefix already typed, plus that prefix's length.
|
||||
func runeSuffixes(cands []string, prefix string) ([][]rune, int) {
|
||||
n := utf8.RuneCountInString(prefix)
|
||||
out := make([][]rune, 0, len(cands))
|
||||
for _, c := range cands {
|
||||
r := []rune(c)
|
||||
if len(r) >= n {
|
||||
out = append(out, r[n:])
|
||||
}
|
||||
}
|
||||
return out, n
|
||||
}
|
||||
|
||||
// builtinCompleter is the command tree. Command names complete at the start of
|
||||
// the line; cd/open/view complete local project names; clone/show complete
|
||||
// repository names cached from the git server; checkout/tag complete branch and
|
||||
// tag names; dist completes filesystem paths.
|
||||
func completer() *readline.PrefixCompleter {
|
||||
// tag names; pushremote/release complete mirror targets; dist completes
|
||||
// filesystem paths.
|
||||
func builtinCompleter() *readline.PrefixCompleter {
|
||||
return readline.NewPrefixCompleter(
|
||||
readline.PcItem("cd", readline.PcItemDynamic(dynLocalProjects)),
|
||||
readline.PcItem("open", readline.PcItemDynamic(dynLocalProjects)),
|
||||
|
||||
Reference in New Issue
Block a user