diff --git a/commands/view/bat.go b/commands/view/bat.go index 8b73388..4c5c90e 100644 --- a/commands/view/bat.go +++ b/commands/view/bat.go @@ -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"}, diff --git a/commands/view/code.go b/commands/view/code.go index 9392bef..b23cd69 100644 --- a/commands/view/code.go +++ b/commands/view/code.go @@ -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"}, diff --git a/commands/view/cot.go b/commands/view/cot.go index d205a12..03f5c27 100644 --- a/commands/view/cot.go +++ b/commands/view/cot.go @@ -8,5 +8,6 @@ func init() { spec.Register(&spec.Spec{ Name: "cot", Description: "Command-line utility for CotEditor", + Generator: spec.FileGenerator(), }) } diff --git a/commands/view/emacs.go b/commands/view/emacs.go index c356984..a1bf2c0 100644 --- a/commands/view/emacs.go +++ b/commands/view/emacs.go @@ -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(), }) } diff --git a/commands/view/glow.go b/commands/view/glow.go index 4c85820..9526afa 100644 --- a/commands/view/glow.go +++ b/commands/view/glow.go @@ -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"}, diff --git a/commands/view/idea.go b/commands/view/idea.go index 6b6ce46..bfb12c1 100644 --- a/commands/view/idea.go +++ b/commands/view/idea.go @@ -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"}, diff --git a/commands/view/less.go b/commands/view/less.go index 94b57d5..06067d5 100644 --- a/commands/view/less.go +++ b/commands/view/less.go @@ -8,6 +8,7 @@ func init() { spec.Register(&spec.Spec{ Name: "less", Description: "view file contents (scrollable)", + MaxArgs: 1, Generator: spec.FileGenerator(), }) } diff --git a/commands/view/lvim.go b/commands/view/lvim.go index 7872ecd..7e7ec0a 100644 --- a/commands/view/lvim.go +++ b/commands/view/lvim.go @@ -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 before any config"}, {Name: "-c", Description: "Execute after config and first file"}, diff --git a/commands/view/micro.go b/commands/view/micro.go index fd9bd2e..325a55b 100644 --- a/commands/view/micro.go +++ b/commands/view/micro.go @@ -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)"}, diff --git a/commands/view/more.go b/commands/view/more.go index 96519e3..0449888 100644 --- a/commands/view/more.go +++ b/commands/view/more.go @@ -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"}, diff --git a/commands/view/nano.go b/commands/view/nano.go index c3ffe51..2744fd5 100644 --- a/commands/view/nano.go +++ b/commands/view/nano.go @@ -8,5 +8,6 @@ func init() { spec.Register(&spec.Spec{ Name: "nano", Description: "Nano", + Generator: spec.FileGenerator(), }) } diff --git a/commands/view/nvim.go b/commands/view/nvim.go index 3f47496..1ce5be2 100644 --- a/commands/view/nvim.go +++ b/commands/view/nvim.go @@ -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 before any config"}, {Name: "-c", Description: "Execute after config and first file"}, diff --git a/commands/view/rich.go b/commands/view/rich.go index e580253..504a6f0 100644 --- a/commands/view/rich.go +++ b/commands/view/rich.go @@ -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"}, diff --git a/commands/view/subl.go b/commands/view/subl.go index 4a6e5a8..a2b8cf2 100644 --- a/commands/view/subl.go +++ b/commands/view/subl.go @@ -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"}, diff --git a/commands/view/vi.go b/commands/view/vi.go index 0083900..af62a00 100644 --- a/commands/view/vi.go +++ b/commands/view/vi.go @@ -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"}, }, diff --git a/commands/view/vim.go b/commands/view/vim.go index c982241..5fb672f 100644 --- a/commands/view/vim.go +++ b/commands/view/vim.go @@ -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')"}, diff --git a/commands/view/vimr.go b/commands/view/vimr.go index 323a8a5..a1d610e 100644 --- a/commands/view/vimr.go +++ b/commands/view/vimr.go @@ -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"}, diff --git a/commands/view/xed.go b/commands/view/xed.go index a619bd5..3ecf842 100644 --- a/commands/view/xed.go +++ b/commands/view/xed.go @@ -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"}, diff --git a/commands/view/xxd.go b/commands/view/xxd.go index 4d8842c..c163e43 100644 --- a/commands/view/xxd.go +++ b/commands/view/xxd.go @@ -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"}, diff --git a/commands/view/zed.go b/commands/view/zed.go index c9f022b..7084475 100644 --- a/commands/view/zed.go +++ b/commands/view/zed.go @@ -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"}, diff --git a/root/wrapper.go b/root/wrapper.go index c51421a..209111d 100644 --- a/root/wrapper.go +++ b/root/wrapper.go @@ -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() diff --git a/spec/filegen.go b/spec/filegen.go index 7805f0c..3b7e156 100644 --- a/spec/filegen.go +++ b/spec/filegen.go @@ -104,8 +104,9 @@ func FileGenerator(filters ...string) GeneratorFunc { if entry.IsDir() { if dirOnly || len(filterSet) == 0 { results = append(results, Suggestion{ - Cmd: fullPath + "/", - Desc: "directory", + Cmd: fullPath + "/", + Desc: "directory", + Priority: 50, }) } else { // scan only 1 level deeper if there is a filter @@ -122,8 +123,9 @@ func FileGenerator(filters ...string) GeneratorFunc { ext := strings.ToLower(filepath.Ext(subName)) if filterSet[ext] { results = append(results, Suggestion{ - Cmd: fullPath + "/" + subName, - Desc: "file", + 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", + Cmd: fullPath, + Desc: desc, + Priority: 50, }) } diff --git a/spec/lookup.go b/spec/lookup.go index 9e10f00..55c3fa4 100644 --- a/spec/lookup.go +++ b/spec/lookup.go @@ -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, }) } } diff --git a/spec/lookup_test.go b/spec/lookup_test.go index 47cbae6..e7143d0 100644 --- a/spec/lookup_test.go +++ b/spec/lookup_test.go @@ -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) + } +}