feat(spec): better file path suggestions (#41)
Enhance file/folder suggestion
This commit is contained in:
@@ -8,6 +8,7 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "bat",
|
||||
Description: "A cat(1) clone with syntax highlighting and Git integration",
|
||||
Generator: spec.FileGenerator(),
|
||||
Options: []spec.Option{
|
||||
{Name: "-A", Description: "Show non-printable characters"},
|
||||
{Name: "-p", Description: "Show plain style, no decorations"},
|
||||
|
||||
@@ -8,6 +8,7 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "code",
|
||||
Description: "Read from stdin (e.g.",
|
||||
Generator: spec.FileGenerator(),
|
||||
Options: []spec.Option{
|
||||
{Name: "-d", Description: "Compare two files with each other"},
|
||||
{Name: "-m", Description: "Add folder(s) to the last active window"},
|
||||
|
||||
@@ -8,5 +8,6 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "cot",
|
||||
Description: "Command-line utility for CotEditor",
|
||||
Generator: spec.FileGenerator(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,5 +8,6 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "emacs",
|
||||
Description: "An extensible, customizable, free/libre text editor - and more",
|
||||
Generator: spec.FileGenerator(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "glow",
|
||||
Description: "Render markdown on the CLI, with pizzazz!",
|
||||
MaxArgs: 1,
|
||||
Generator: spec.FileGenerator(),
|
||||
Subcommands: []spec.Subcommand{
|
||||
{Name: "config", Description: "Edit the glow config file"},
|
||||
{Name: "help", Description: "Help about any command"},
|
||||
|
||||
@@ -8,6 +8,7 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "idea",
|
||||
Description: "IntelliJ IDEA CLI",
|
||||
Generator: spec.FileGenerator(),
|
||||
Subcommands: []spec.Subcommand{
|
||||
{Name: "diff", Description: "Open the diff viewer to see the differences between two specified files"},
|
||||
{Name: "merge", Description: "Open the Merge dialog to merge the specified files"},
|
||||
|
||||
@@ -8,6 +8,7 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "less",
|
||||
Description: "view file contents (scrollable)",
|
||||
MaxArgs: 1,
|
||||
Generator: spec.FileGenerator(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "lvim",
|
||||
Description: "Hyperextensible Vim-based text editor",
|
||||
Generator: spec.FileGenerator(),
|
||||
Options: []spec.Option{
|
||||
{Name: "--cmd", Description: "Execute <cmd> before any config"},
|
||||
{Name: "-c", Description: "Execute <cmd> after config and first file"},
|
||||
|
||||
@@ -8,6 +8,7 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "micro",
|
||||
Description: "True/false",
|
||||
Generator: spec.FileGenerator(),
|
||||
Subcommands: []spec.Subcommand{
|
||||
{Name: "remove", Description: "Remove plugin(s)"},
|
||||
{Name: "update", Description: "Update plugin(s) (if no argument is given, updates all plugins)"},
|
||||
|
||||
@@ -8,6 +8,8 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "more",
|
||||
Description: "Opposite of less",
|
||||
MaxArgs: 1,
|
||||
Generator: spec.FileGenerator(),
|
||||
Options: []spec.Option{
|
||||
{Name: "-d", Description: "Do not pause after any line containing a ^L (form feed)"},
|
||||
{Name: "-f", Description: "Count logical lines, rather than screen lines"},
|
||||
|
||||
@@ -8,5 +8,6 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "nano",
|
||||
Description: "Nano",
|
||||
Generator: spec.FileGenerator(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "nvim",
|
||||
Description: "Hyperextensible Vim-based text editor",
|
||||
Generator: spec.FileGenerator(),
|
||||
Options: []spec.Option{
|
||||
{Name: "--cmd", Description: "Execute <cmd> before any config"},
|
||||
{Name: "-c", Description: "Execute <cmd> after config and first file"},
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "rich",
|
||||
Description: "Defined by terminal, appearance may differ",
|
||||
Description: "Rich terminal text formatting",
|
||||
MaxArgs: 1,
|
||||
Generator: spec.FileGenerator(),
|
||||
Options: []spec.Option{
|
||||
{Name: "-p", Description: "Print console markup. See https://rich.readthedocs.io/en/latest/markup.html"},
|
||||
{Name: "-u", Description: "Display a horizontal rule"},
|
||||
|
||||
@@ -8,6 +8,7 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "subl",
|
||||
Description: "Sublime Text",
|
||||
Generator: spec.FileGenerator(),
|
||||
Options: []spec.Option{
|
||||
{Name: "--project", Description: "Load the given project"},
|
||||
{Name: "--command", Description: "Run the given command"},
|
||||
|
||||
@@ -8,6 +8,7 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "vi",
|
||||
Description: "Print help message for vi and exit",
|
||||
Generator: spec.FileGenerator(),
|
||||
Options: []spec.Option{
|
||||
{Name: "-h", Description: "Print help message for vi and exit"},
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "vim",
|
||||
Description: "Vi IMproved, a programmer",
|
||||
Generator: spec.FileGenerator(),
|
||||
Options: []spec.Option{
|
||||
{Name: "-v", Description: "Vi mode (like 'vi')"},
|
||||
{Name: "-e", Description: "Ex mode (like 'ex')"},
|
||||
|
||||
@@ -8,6 +8,7 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "vimr",
|
||||
Description: "VimR - Neovim GUI for macOS in Swift",
|
||||
Generator: spec.FileGenerator(),
|
||||
Options: []spec.Option{
|
||||
{Name: "-h", Description: "Show help"},
|
||||
{Name: "--dry-run", Description: "Just print the 'open' command"},
|
||||
|
||||
@@ -8,6 +8,7 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "xed",
|
||||
Description: "Xcode text editor invocation tool",
|
||||
Generator: spec.FileGenerator(),
|
||||
Options: []spec.Option{
|
||||
{Name: "--launch", Description: "Launches Xcode, opening a new empty unsaved file"},
|
||||
{Name: "--create", Description: "Selects the given line in the last file opened"},
|
||||
|
||||
@@ -8,6 +8,7 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "xxd",
|
||||
Description: "Make a hexdump or do the reverse",
|
||||
Generator: spec.FileGenerator(),
|
||||
Options: []spec.Option{
|
||||
{Name: "-help", Description: "Show help for xxd"},
|
||||
{Name: "-autoskip", Description: "Toggle autoskip: A single '*' replaces nul-lines. Default off"},
|
||||
|
||||
@@ -8,6 +8,7 @@ func init() {
|
||||
spec.Register(&spec.Spec{
|
||||
Name: "zed",
|
||||
Description: "A lightning-fast, collaborative code editor written in Rust",
|
||||
Generator: spec.FileGenerator(),
|
||||
Options: []spec.Option{
|
||||
{Name: "-h", Description: "Print help information"},
|
||||
{Name: "-v", Description: "Print Zed's version and the app path"},
|
||||
|
||||
+12
-2
@@ -850,7 +850,12 @@ func runWrapper() {
|
||||
currentMode := activeMode
|
||||
activeModeMu.RUnlock()
|
||||
if currentMode == "spec" {
|
||||
selected = strings.TrimSpace(selected) + " "
|
||||
s := strings.TrimSpace(selected)
|
||||
if strings.HasSuffix(s, "/") || strings.HasSuffix(s, "\\") {
|
||||
selected = s
|
||||
} else {
|
||||
selected = s + " "
|
||||
}
|
||||
}
|
||||
_, _ = ptmx.Write(append([]byte{0x15}, selected...))
|
||||
}
|
||||
@@ -911,7 +916,12 @@ func runWrapper() {
|
||||
currentMode := activeMode
|
||||
activeModeMu.RUnlock()
|
||||
if currentMode == "spec" {
|
||||
selected = strings.TrimSpace(selected) + " "
|
||||
s := strings.TrimSpace(selected)
|
||||
if strings.HasSuffix(s, "/") || strings.HasSuffix(s, "\\") {
|
||||
selected = s
|
||||
} else {
|
||||
selected = s + " "
|
||||
}
|
||||
}
|
||||
|
||||
bufferMu.Lock()
|
||||
|
||||
+8
-1
@@ -106,6 +106,7 @@ func FileGenerator(filters ...string) GeneratorFunc {
|
||||
results = append(results, Suggestion{
|
||||
Cmd: fullPath + "/",
|
||||
Desc: "directory",
|
||||
Priority: 50,
|
||||
})
|
||||
} else {
|
||||
// scan only 1 level deeper if there is a filter
|
||||
@@ -124,6 +125,7 @@ func FileGenerator(filters ...string) GeneratorFunc {
|
||||
results = append(results, Suggestion{
|
||||
Cmd: fullPath + "/" + subName,
|
||||
Desc: "file",
|
||||
Priority: 50,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -143,9 +145,14 @@ func FileGenerator(filters ...string) GeneratorFunc {
|
||||
continue
|
||||
}
|
||||
}
|
||||
desc := "file"
|
||||
if ext := strings.ToLower(filepath.Ext(name)); ext != "" {
|
||||
desc = strings.TrimPrefix(ext, ".")
|
||||
}
|
||||
results = append(results, Suggestion{
|
||||
Cmd: fullPath,
|
||||
Desc: "file",
|
||||
Desc: desc,
|
||||
Priority: 50,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+39
-7
@@ -61,9 +61,13 @@ func Lookup(input string) []Suggestion {
|
||||
|
||||
scanExternalCommands()
|
||||
|
||||
// if you have an alias in your shell config like: alias gca="git commit -a"
|
||||
// if the first word match it, IRIS will suggest "git commit -a"
|
||||
if len(tokens) > 1 {
|
||||
// if you have an alias in your shell config like: alias nv="nvim"
|
||||
// expand it even when there's only one token (e.g. "nv ") so that the
|
||||
// target spec's Generator (FileGenerator etc.) can still fire.
|
||||
// only expand when there's a trailing space — i.e. the user has committed
|
||||
// to the alias name and is now typing arguments (tokens last elem == "")
|
||||
hasTrailingSpace := len(tokens) > 0 && tokens[len(tokens)-1] == ""
|
||||
if hasTrailingSpace || len(tokens) > 1 {
|
||||
if target, ok := aliases[tokens[0]]; ok {
|
||||
aliasTokens := Tokenize(target)
|
||||
if len(aliasTokens) > 0 && aliasTokens[len(aliasTokens)-1] == "" {
|
||||
@@ -202,10 +206,28 @@ func Lookup(input string) []Suggestion {
|
||||
linePrefix := linePrefixBuilder.String()
|
||||
|
||||
if currentGen != nil && allowMoreArgs {
|
||||
genResults := currentGen(tokens, prefix, partial)
|
||||
genPartial := partial
|
||||
genLinePrefix := linePrefix
|
||||
|
||||
if partial == "" && len(tokens) >= 3 {
|
||||
prevToken := tokens[len(tokens)-2]
|
||||
if strings.HasSuffix(prevToken, "/") || strings.HasSuffix(prevToken, "\\") {
|
||||
genPartial = prevToken
|
||||
lpBuilder := strings.Builder{}
|
||||
for i := 0; i < len(tokens)-2; i++ {
|
||||
if i > 0 {
|
||||
lpBuilder.WriteByte(' ')
|
||||
}
|
||||
lpBuilder.WriteString(tokens[i])
|
||||
}
|
||||
genLinePrefix = lpBuilder.String()
|
||||
}
|
||||
}
|
||||
|
||||
genResults := currentGen(tokens, prefix, genPartial)
|
||||
|
||||
for _, g := range genResults {
|
||||
if partial != "" && !HasPrefix(g.Cmd, partial) && !strings.Contains(g.Cmd, partial) {
|
||||
if genPartial != "" && !HasPrefix(g.Cmd, genPartial) && !strings.Contains(g.Cmd, genPartial) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -220,8 +242,10 @@ func Lookup(input string) []Suggestion {
|
||||
finalCmd := ""
|
||||
if len(tokens) > depth+1 && strings.HasPrefix(g.Cmd, tokens[depth]) {
|
||||
finalCmd = prefix + " " + suggested
|
||||
} else if genLinePrefix != "" {
|
||||
finalCmd = genLinePrefix + " " + suggested
|
||||
} else {
|
||||
finalCmd = strings.TrimSpace(linePrefix) + " " + suggested
|
||||
finalCmd = suggested
|
||||
}
|
||||
|
||||
newTokens := Tokenize(finalCmd)
|
||||
@@ -267,8 +291,16 @@ func Lookup(input string) []Suggestion {
|
||||
for _, opt := range currentOpts {
|
||||
trimmedOpt := strings.TrimLeft(opt.Name, "-")
|
||||
if !usedOpts[opt.Name] && (partial == "" || HasPrefix(opt.Name, partial) || HasPrefix(trimmedOpt, partial)) {
|
||||
optPriority := opt.Priority
|
||||
if optPriority == 0 {
|
||||
if strings.HasPrefix(partial, "-") {
|
||||
optPriority = 80
|
||||
} else {
|
||||
optPriority = 10
|
||||
}
|
||||
}
|
||||
results = append(results, Suggestion{
|
||||
Cmd: linePrefix + " " + opt.Name, Desc: opt.Description, Icon: rootCmdName, Priority: opt.Priority,
|
||||
Cmd: linePrefix + " " + opt.Name, Desc: opt.Description, Icon: rootCmdName, Priority: optPriority,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package spec
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -136,3 +138,96 @@ func TestLookupConcurrent(t *testing.T) {
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestLookup_AliasFileGenerator(t *testing.T) {
|
||||
ResetRegistry()
|
||||
Register(&Spec{
|
||||
Name: "nvim",
|
||||
Generator: FileGenerator(),
|
||||
})
|
||||
// alias "nv" -> "nvim", single token with trailing space
|
||||
ShellAliases = map[string]string{"nv": "nvim"}
|
||||
|
||||
results := Lookup("nv ")
|
||||
if len(results) == 0 {
|
||||
t.Errorf("expected file suggestions for alias 'nv ' -> 'nvim', got none")
|
||||
}
|
||||
for _, r := range results {
|
||||
if !strings.HasPrefix(r.Cmd, "nvim ") {
|
||||
t.Errorf("expected suggestion to start with 'nvim ', got %q", r.Cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookup_NvimFileGenerator(t *testing.T) {
|
||||
ResetRegistry()
|
||||
Register(&Spec{
|
||||
Name: "nvim",
|
||||
Generator: FileGenerator(),
|
||||
})
|
||||
results := Lookup("nvim ")
|
||||
if len(results) == 0 {
|
||||
t.Errorf("expected file suggestions for 'nvim ', got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookup_OptionAndFilePriority(t *testing.T) {
|
||||
ResetRegistry()
|
||||
Register(&Spec{
|
||||
Name: "nvim",
|
||||
Generator: FileGenerator(),
|
||||
Options: []Option{
|
||||
{Name: "-c", Description: "Execute cmd"},
|
||||
{Name: "--cmd", Description: "Execute cmd before config"},
|
||||
},
|
||||
})
|
||||
|
||||
// When query is 'nvim ', files should be prioritized over flags
|
||||
resultsEmpty := Lookup("nvim ")
|
||||
if len(resultsEmpty) == 0 {
|
||||
t.Fatalf("expected results for 'nvim ', got 0")
|
||||
}
|
||||
// first result should be a file or dir (Priority 50), not option (Priority 10)
|
||||
if strings.HasPrefix(resultsEmpty[0].Cmd, "nvim -") {
|
||||
t.Errorf("expected file/dir as top result for 'nvim ', got %q", resultsEmpty[0].Cmd)
|
||||
}
|
||||
|
||||
// When query is 'nvim -', flags should be prioritized (Priority 80)
|
||||
resultsDash := Lookup("nvim -")
|
||||
if len(resultsDash) == 0 {
|
||||
t.Fatalf("expected results for 'nvim -', got 0")
|
||||
}
|
||||
if !strings.HasPrefix(resultsDash[0].Cmd, "nvim -") {
|
||||
t.Errorf("expected option as top result for 'nvim -', got %q", resultsDash[0].Cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookup_NestedDirectoryTrailingSpace(t *testing.T) {
|
||||
ResetRegistry()
|
||||
Register(&Spec{
|
||||
Name: "cat",
|
||||
Generator: FileGenerator(),
|
||||
})
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
subDir := filepath.Join(tmpDir, "subdir")
|
||||
_ = os.Mkdir(subDir, 0755)
|
||||
testFile := filepath.Join(subDir, "hello.txt")
|
||||
_ = os.WriteFile(testFile, []byte("hi"), 0644)
|
||||
|
||||
query := "cat " + subDir + "/ "
|
||||
results := Lookup(query)
|
||||
if len(results) == 0 {
|
||||
t.Fatalf("expected results for %q, got 0", query)
|
||||
}
|
||||
found := false
|
||||
for _, r := range results {
|
||||
if strings.Contains(r.Cmd, "hello.txt") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected hello.txt in results, got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user