Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fish,jit: Implement fallbacks #47

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ grammar. It compiles the grammar down to a standalone bash/fish/zsh shell scrip
its own. As a separate use case, it can also produce completions directly on stdout, which is meant to be
used in interactive shells (see below).

`complgen` takes flags to complete from grammar files. Ideally, the grammar files are meant to be developed
and versioned along with the completed command line tool to avoid version mismatches. There's nothing
stopping you however from writing the grammar file yourself, optionally tailoring it for your most-frequent
use cases, sort of like shell aliases on steroids.

## Demo

[![asciicast](https://asciinema.org/a/SAH1uGqgwBEhyRV7G6Zasu45y.svg)](https://asciinema.org/a/SAH1uGqgwBEhyRV7G6Zasu45y)
Expand Down Expand Up @@ -107,7 +112,8 @@ function _complgen_jit
else
set words $COMP_WORDS[2..$last]
end
complgen jit $usage_file_path fish --prefix="$prefix" -- $words
set --local code (complgen jit $usage_file_path fish --prefix="$prefix" -- $words)
eval $code
end

for path in ~/.config/complgen/*.usage
Expand Down
18 changes: 18 additions & 0 deletions e2e/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@ def get_sorted_fish_completions(completions_script_path: Path, input: str) -> li
return parsed


@contextlib.contextmanager
def gen_fish_jit_completion_script_path(complgen_binary_path: Path, grammar: str) -> Generator[Path, None, None]:
fish_script = subprocess.run([complgen_binary_path, 'jit', '--test', 'dummy', '-', 'fish'], input=grammar.encode(), stdout=subprocess.PIPE, stderr=sys.stderr, check=True).stdout
with tempfile.NamedTemporaryFile() as f:
f.write(fish_script)
f.flush()
yield Path(f.name)


@contextlib.contextmanager
def gen_fish_aot_completion_script_path(complgen_binary_path: Path, grammar: str) -> Generator[Path, None, None]:
fish_script = subprocess.run([complgen_binary_path, 'aot', '--fish-script', '-', '-'], input=grammar.encode(), stdout=subprocess.PIPE, stderr=sys.stderr, check=True).stdout
with tempfile.NamedTemporaryFile() as f:
f.write(fish_script)
f.flush()
yield Path(f.name)


@contextlib.contextmanager
def gen_zsh_capture_script_path(completion_script: str) -> Generator[Path, None, None]:
this_file = Path(__file__)
Expand Down
43 changes: 15 additions & 28 deletions e2e/fish/test_fish_aot.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
import os
import sys
import tempfile
import subprocess
import contextlib
from pathlib import Path
from typing import Generator

from conftest import get_sorted_fish_completions, set_working_dir, fish_completions_from_stdout
from conftest import get_sorted_fish_completions, set_working_dir, gen_fish_aot_completion_script_path
from common import LSOF_FILTER_GRAMMAR, STRACE_EXPR_GRAMMAR


@contextlib.contextmanager
def completion_script_path(complgen_binary_path: Path, grammar: str) -> Generator[Path, None, None]:
fish_script = subprocess.run([complgen_binary_path, 'aot', '--fish-script', '-', '-'], input=grammar.encode(), stdout=subprocess.PIPE, stderr=sys.stderr, check=True).stdout
with tempfile.NamedTemporaryFile() as f:
f.write(fish_script)
f.flush()
yield Path(f.name)


def test_fish_uses_correct_description_with_duplicated_literals(complgen_binary_path: Path):
GRAMMAR = '''
cmd <COMMAND> [--help];
Expand All @@ -30,7 +17,7 @@ def test_fish_uses_correct_description_with_duplicated_literals(complgen_binary_
<REMOTE-SUBCOMMAND> ::= rm <name>;
'''

with completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
with gen_fish_aot_completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
input = 'complete --command cmd --do-complete "cmd "'
assert get_sorted_fish_completions(completions_file_path, input) == sorted([('rm', "Remove a project"), ('remote', "Manage a project's remotes")], key=lambda pair: pair[0])

Expand All @@ -44,7 +31,7 @@ def test_fish_uses_correct_description_with_duplicated_descriptions(complgen_bin
;
'''

with completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
with gen_fish_aot_completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
input = 'complete --command cmd --do-complete "cmd "'
assert get_sorted_fish_completions(completions_file_path, input) == sorted([('--color', "use markers to highlight the matching strings"), ('--colour', "use markers to highlight the matching strings")], key=lambda pair: pair[0])

Expand All @@ -54,7 +41,7 @@ def test_fish_external_command_produces_description(complgen_binary_path: Path):
cmd {{{ echo -e "completion\tdescription" }}};
'''

with completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
with gen_fish_aot_completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
input = 'complete --command cmd --do-complete "cmd "'
assert get_sorted_fish_completions(completions_file_path, input) == [('completion', 'description')]

Expand All @@ -63,7 +50,7 @@ def test_fish_external_command_produces_description(complgen_binary_path: Path):


def test_completes_paths(complgen_binary_path: Path):
with completion_script_path(complgen_binary_path, '''cmd <PATH> [--help];''') as completions_file_path:
with gen_fish_aot_completion_script_path(complgen_binary_path, '''cmd <PATH> [--help];''') as completions_file_path:
with tempfile.TemporaryDirectory() as dir:
with set_working_dir(Path(dir)):
Path('filename with spaces').write_text('dummy')
Expand All @@ -75,7 +62,7 @@ def test_completes_paths(complgen_binary_path: Path):


def test_completes_directories(complgen_binary_path: Path):
with completion_script_path(complgen_binary_path, '''cmd <DIRECTORY> [--help];''') as completions_file_path:
with gen_fish_aot_completion_script_path(complgen_binary_path, '''cmd <DIRECTORY> [--help];''') as completions_file_path:
with tempfile.TemporaryDirectory() as dir:
with set_working_dir(Path(dir)):
os.mkdir('dir with spaces')
Expand All @@ -88,7 +75,7 @@ def test_completes_directories(complgen_binary_path: Path):

def test_specializes_for_fish(complgen_binary_path: Path):
GRAMMAR = '''cmd <FOO>; <FOO> ::= {{{ echo foo }}}; <FOO@fish> ::= {{{ echo fish }}};'''
with completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
with gen_fish_aot_completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
input = 'complete --command cmd --do-complete "cmd "'
assert get_sorted_fish_completions(completions_file_path, input) == [('fish', '')]

Expand All @@ -99,7 +86,7 @@ def test_matches_prefix(complgen_binary_path: Path):
cargo test --test testname;
<toolchain> ::= stable-aarch64-apple-darwin | stable-x86_64-apple-darwin;
'''
with completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
with gen_fish_aot_completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
input = 'complete --command cargo --do-complete "cargo +stable-aarch64-apple-darwin "'
completions = get_sorted_fish_completions(completions_file_path, input)
assert completions == sorted([('foo', '')])
Expand All @@ -110,35 +97,35 @@ def test_completes_prefix(complgen_binary_path: Path):
cargo +<toolchain>;
<toolchain> ::= stable-aarch64-apple-darwin | stable-x86_64-apple-darwin;
'''
with completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
with gen_fish_aot_completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
input = 'complete --command cargo --do-complete "cargo +"'
completions = get_sorted_fish_completions(completions_file_path, input)
assert completions == sorted([('+stable-aarch64-apple-darwin', ''), ('+stable-x86_64-apple-darwin', '')])


def test_completes_strace_expr(complgen_binary_path: Path):
with completion_script_path(complgen_binary_path, STRACE_EXPR_GRAMMAR) as completions_file_path:
with gen_fish_aot_completion_script_path(complgen_binary_path, STRACE_EXPR_GRAMMAR) as completions_file_path:
input = 'complete --command cargo --do-complete "strace -e "'
completions = get_sorted_fish_completions(completions_file_path, input)
assert completions == sorted([('!', ''), ('%file', ''), ('file', ''), ('all', ''), ('read', ''), ('trace', ''), ('write', ''), ('fault', '')])


def test_completes_lsof_filter(complgen_binary_path: Path):
with completion_script_path(complgen_binary_path, LSOF_FILTER_GRAMMAR) as completions_file_path:
with gen_fish_aot_completion_script_path(complgen_binary_path, LSOF_FILTER_GRAMMAR) as completions_file_path:
input = 'complete --command cargo --do-complete "lsf "'
completions = get_sorted_fish_completions(completions_file_path, input)
assert completions == sorted([('-s', '')])


def test_subword_descriptions(complgen_binary_path: Path):
GRAMMAR = r'''cmd --option=(arg1 "descr1" | arg2 "descr2");'''
with completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
with gen_fish_aot_completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
input = 'complete --command cmd --do-complete "cmd --option="'
assert get_sorted_fish_completions(completions_file_path, input) == [('--option=arg1', 'descr1'), ('--option=arg2', 'descr2')]

def test_completes_subword_external_command(complgen_binary_path: Path):
GRAMMAR = r'''cmd --option={{{ echo -e "argument\tdescription" }}};'''
with completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
with gen_fish_aot_completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
input = 'complete --command cmd --do-complete "cmd --option="'
assert get_sorted_fish_completions(completions_file_path, input) == [('--option=argument', 'description')]

Expand All @@ -149,7 +136,7 @@ def test_subword_specialization(complgen_binary_path: Path):
<FOO> ::= {{{ echo generic }}};
<FOO@fish> ::= {{{ echo fish }}};
'''
with completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
with gen_fish_aot_completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
input = 'complete --command cmd --do-complete "cmd --option="'
assert get_sorted_fish_completions(completions_file_path, input) == [('--option=fish', '')]

Expand All @@ -158,6 +145,6 @@ def test_description_special_characters(complgen_binary_path: Path):
GRAMMAR = r'''
cmd --option "$f\"\\";
'''
with completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
with gen_fish_aot_completion_script_path(complgen_binary_path, GRAMMAR) as completions_file_path:
input = 'complete --command cmd --do-complete "cmd --option"'
assert get_sorted_fish_completions(completions_file_path, input) == [('--option', '$f\"\\')]
34 changes: 20 additions & 14 deletions e2e/fish/test_fish_jit.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,58 @@
import os
import sys
import tempfile
import subprocess
from pathlib import Path
from typing import Optional

from conftest import set_working_dir, fish_completions_from_stdout
from conftest import set_working_dir, gen_fish_jit_completion_script_path, get_sorted_fish_completions
from common import LSOF_FILTER_GRAMMAR, STRACE_EXPR_GRAMMAR


SPECIAL_CHARACTERS = '?[^a]*{foo,*bar}'


def get_sorted_jit_fish_completions(complgen_binary_path: Path, grammar: str, words_before_cursor: list[str] = [], prefix: Optional[str] = None) -> list[tuple[str, str]]:
args = [complgen_binary_path, 'jit', '-', 'fish']
if prefix is not None:
args += ['--prefix={}'.format(prefix)]
args += ['--']
args += words_before_cursor
process = subprocess.run(args, input=grammar.encode(), stdout=subprocess.PIPE, stderr=sys.stderr, check=True)
parsed = fish_completions_from_stdout(process.stdout.decode())
return sorted(parsed, key=lambda pair: pair[0])
with gen_fish_jit_completion_script_path(complgen_binary_path, grammar) as completions_file_path:
if words_before_cursor and prefix:
args = '{} {}'.format(' '.join(words_before_cursor), prefix)
elif words_before_cursor:
args = '{} '.format(' '.join(words_before_cursor))
elif prefix:
args = f'{prefix}'
else:
args = ''
input = f'__complgen_jit {args}'
return get_sorted_fish_completions(completions_file_path, input)


def test_jit_completes_paths_fish(complgen_binary_path: Path):
GRAMMAR = '''cmd <PATH> [--help];'''
with tempfile.TemporaryDirectory() as dir:
with set_working_dir(Path(dir)):
Path('filename with spaces').write_text('dummy')
Path(SPECIAL_CHARACTERS).write_text('dummy')
os.mkdir('dir with spaces')
assert get_sorted_jit_fish_completions(complgen_binary_path, '''cmd <PATH> [--help];''') == sorted([(SPECIAL_CHARACTERS, ''), ('filename with spaces', ''), ('dir with spaces/', '')])
expected = sorted([(SPECIAL_CHARACTERS, ''), ('filename with spaces', ''), ('dir with spaces/', '')])
actual = get_sorted_jit_fish_completions(complgen_binary_path, GRAMMAR)
assert actual == expected


def test_jit_completes_subdirectory_files(complgen_binary_path: Path):
GRAMMAR = '''cmd <PATH>;'''
with tempfile.TemporaryDirectory() as dir:
with set_working_dir(Path(dir)):
os.mkdir('subdir')
(Path('subdir') / 'file.txt').write_text('dummy')
assert get_sorted_jit_fish_completions(complgen_binary_path, '''cmd <PATH>;''', prefix='subdir/') == sorted([('subdir/file.txt', '')])
assert get_sorted_jit_fish_completions(complgen_binary_path, GRAMMAR, prefix='subdir/') == sorted([('subdir/file.txt', '')])


def test_jit_completes_directories_fish(complgen_binary_path: Path):
GRAMMAR = '''cmd <DIRECTORY> [--help];'''
with tempfile.TemporaryDirectory() as dir:
with set_working_dir(Path(dir)):
os.mkdir('dir with spaces')
os.mkdir(SPECIAL_CHARACTERS)
Path('filename with spaces').write_text('dummy')
assert get_sorted_jit_fish_completions(complgen_binary_path, '''cmd <DIRECTORY> [--help];''') == sorted([(SPECIAL_CHARACTERS + '/', 'Directory'), ('dir with spaces/', 'Directory')])
assert get_sorted_jit_fish_completions(complgen_binary_path, GRAMMAR) == sorted([(SPECIAL_CHARACTERS + '/', 'Directory'), ('dir with spaces/', 'Directory')])


def test_jit_specializes_for_fish(complgen_binary_path: Path):
Expand Down
2 changes: 1 addition & 1 deletion src/aot/fish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ fn write_lookup_tables<W: Write>(buffer: &mut W, dfa: &DFA) -> Result<()> {
}


fn write_subword_fn<W: Write>(buffer: &mut W, command: &str, id: usize, dfa: &DFA, command_id_from_state: &HashMap<StateId, usize>, subword_spec_id_from_state: &HashMap<StateId, usize>) -> Result<()> {
pub fn write_subword_fn<W: Write>(buffer: &mut W, command: &str, id: usize, dfa: &DFA, command_id_from_state: &HashMap<StateId, usize>, subword_spec_id_from_state: &HashMap<StateId, usize>) -> Result<()> {
writeln!(buffer, r#"function _{command}_subword_{id}
set mode $argv[1]
set word $argv[2]
Expand Down
21 changes: 3 additions & 18 deletions src/bin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use anyhow::Context;
use bumpalo::Bump;
use clap::Parser;

use complgen::jit::fish::write_fish_completion_shell_code;
use complgen::jit::zsh::write_zsh_completion_shell_code;
use complgen::jit::get_completions;
use complgen::grammar::{ValidGrammar, Grammar, to_railroad_diagram, to_railroad_diagram_file};
Expand Down Expand Up @@ -231,24 +232,8 @@ fn complete(args: &JitArgs) -> anyhow::Result<()> {
}
},
Shell::Fish(_) => {
let matches = {
let mut matches: Vec<String> = Default::default();
for group in completions.linear_group_by(|left, right| left.fallback_level == right.fallback_level) {
for completion in group {
let comp = completion.get_completion();
if comp.starts_with(word) {
matches.push(format!("{}\t{}", comp, completion.description));
}
}
if !matches.is_empty() {
break;
}
}
matches
};
for m in matches {
println!("{}", m);
}
let mut stdout = std::io::stdout();
write_fish_completion_shell_code(&validated.command, &dfa, &words_before_cursor, word, &mut stdout, &args.test)?;
},
Shell::Zsh(_) => {
let mut stdout = std::io::stdout();
Expand Down