timeouts for dynamic completions

This commit is contained in:
atagen 2026-05-23 20:10:51 +10:00
parent 8f92bb86db
commit 73904c036f
5 changed files with 122 additions and 8 deletions

View file

@ -73,6 +73,14 @@ programs.inshellah = {
# default of 200ms)
timeoutMs = null;
# timeout in ms for live dynamic completions at tab-completion time
# set to 0 to disable the runtime timeout
dynamicTimeoutMs = 5000;
# result cap requested from live providers that support native limits
# set to 0 to omit native result-limit flags
dynamicLimit = 200;
# worker-thread count for the parallel scrape
workers = null;
};
@ -109,6 +117,14 @@ $env.config.completions.external = { ... }
the snippet provides both static lookups against the system index and
runtime fallbacks for cases the static index can't cover:
runtime fallbacks have a default 5s timeout, controlled by
`programs.inshellah.dynamicTimeoutMs` or `INSHELLAH_DYNAMIC_TIMEOUT_MS`
when sourcing the snippet manually. providers with native result caps use
`programs.inshellah.dynamicLimit` or `INSHELLAH_DYNAMIC_LIMIT`, defaulting
to 200. set either value to 0 to disable that guard. on timeout the
completer returns `null` so nushell can fall back to its normal completion
behavior.
| command | dynamic source |
|---|---|
| `nix` | flake refs via `NIX_GET_COMPLETIONS`, with optional `meta.description` |
@ -120,6 +136,7 @@ runtime fallbacks for cases the static index can't cover:
| `docker` / `podman` | containers + image refs by subcommand |
| `kubectl` | resource names from the live cluster |
| `git` | refs + worktree paths |
| `jj` | revisions, operations, bookmarks, remotes, files, and workspaces |
| `npm` / `pnpm` / `yarn` | scripts from package.json |
| `make` / `just` | targets / recipes |
| `cargo` | workspace targets behind `--bin` / `--example` / etc. |

View file

@ -68,6 +68,9 @@
fakeNix = pkgs.writeShellScriptBin "nix" ''
if [ "''${1:-}" = eval ]; then
printf 'raw package description\n'
elif [ "''${1:-}" = slow ]; then
sleep 1
printf 'header\nslow-package\n'
else
printf 'header\nbuild\nflake#pkg\n'
fi
@ -101,6 +104,9 @@
printf 'origin\nupstream\n'
;;
for-each-ref)
if [ -n "''${INSHELLAH_GIT_ARGS_FILE:-}" ]; then
printf '%s\n' "$*" > "$INSHELLAH_GIT_ARGS_FILE"
fi
case "$*" in
*"refs/heads refs/remotes refs/tags"*)
printf 'main\tcommit\tMain branch\norigin/main\tcommit\tRemote main\nv1.0\tcommit\tRelease 1\n'
@ -210,9 +216,30 @@
echo "running nushell shim checks"
export PATH="${fakeCompletionBackends}/bin:$PATH"
export KUBECTL_ARGS_FILE="$TMPDIR/kubectl.args"
export INSHELLAH_GIT_ARGS_FILE="$TMPDIR/git.args"
export INSHELLAH_STATIC_FILE="$TMPDIR/inshellah-static.json"
export INSHELLAH_DYNAMIC_TIMEOUT_MS=50
: > "$INSHELLAH_STATIC_FILE"
nu --no-config-file -c 'source ${./nix/inshellah-completer.nu}; source ${./tests/nushell-completer.nu}'
INSHELLAH_DYNAMIC_TIMEOUT_MS=0 nu --no-config-file -c '
source ${./nix/inshellah-completer.nu}
"" | save --force $env.INSHELLAH_STATIC_FILE
let completer = $env.config.completions.external.completer
let slow = do $completer [nix slow ""]
if (($slow | get 0.value) != "slow-package") {
error make {msg: "dynamic timeout 0 should disable timeout"}
}
'
INSHELLAH_DYNAMIC_LIMIT=0 nu --no-config-file -c '
source ${./nix/inshellah-completer.nu}
"[]" | save --force $env.INSHELLAH_STATIC_FILE
"" | save --force $env.INSHELLAH_GIT_ARGS_FILE
let completer = $env.config.completions.external.completer
do $completer [git fetch origin ""]
if ((open $env.INSHELLAH_GIT_ARGS_FILE) | str contains "--count") {
error make {msg: "dynamic limit 0 should omit provider limit flags"}
}
'
cat > "$TMPDIR/config-load.nu" <<'EOF'
source ${./nix/inshellah-completer.nu}

View file

@ -13,6 +13,47 @@ let inshellah_nonempty = { |items|
if ($result | is-empty) { null } else { $result }
}
let inshellah_default_dynamic_timeout_ms = 5000
let inshellah_dynamic_timeout_ms = do {
let raw = (try {
$env.INSHELLAH_DYNAMIC_TIMEOUT_MS? | default $inshellah_default_dynamic_timeout_ms | into int
} catch { $inshellah_default_dynamic_timeout_ms })
if $raw >= 0 { $raw } else { $inshellah_default_dynamic_timeout_ms }
}
let inshellah_dynamic_timeout = ($inshellah_dynamic_timeout_ms * 1ms)
let inshellah_default_dynamic_limit = 200
let inshellah_dynamic_limit = do {
let raw = (try {
$env.INSHELLAH_DYNAMIC_LIMIT? | default $inshellah_default_dynamic_limit | into int
} catch { $inshellah_default_dynamic_limit })
if $raw >= 0 { $raw } else { $inshellah_default_dynamic_limit }
}
let inshellah_limit_args = { |flag|
if $inshellah_dynamic_limit == 0 { [] } else { [$flag $inshellah_dynamic_limit] }
}
let inshellah_with_timeout = { |body|
if $inshellah_dynamic_timeout_ms == 0 {
try { do $body } catch { null }
} else {
let tag = random int 1..2147483647
let job_id = job spawn {
do $body | job send --tag $tag 0
}
try {
job recv --tag $tag --timeout $inshellah_dynamic_timeout
} catch {
try { job kill $job_id } catch { null }
null
}
}
}
let inshellah_fuzzy_score = { |needle, haystack|
let needle = $needle | default "" | into string
let haystack = $haystack | default "" | into string
@ -189,7 +230,7 @@ let inshellah_kubectl_names = { |kind, spans|
let inshellah_git_refs = { ||
try {
^git for-each-ref --format='%(refname:short)%09%(objecttype)%09%(contents:subject)' refs/heads refs/remotes refs/tags
^git for-each-ref ...(do $inshellah_limit_args "--count") --format='%(refname:short)%09%(objecttype)%09%(contents:subject)' refs/heads refs/remotes refs/tags
| lines
| each { |l|
let p = $l | split row "\t"
@ -200,7 +241,7 @@ let inshellah_git_refs = { ||
let inshellah_git_branches = { ||
try {
^git for-each-ref --format='%(refname:short)%09%(contents:subject)' refs/heads
^git for-each-ref ...(do $inshellah_limit_args "--count") --format='%(refname:short)%09%(contents:subject)' refs/heads
| lines
| each { |l|
let p = $l | split row "\t"
@ -211,7 +252,7 @@ let inshellah_git_branches = { ||
let inshellah_git_tags = { ||
try {
^git for-each-ref --format='%(refname:short)%09%(contents:subject)' refs/tags
^git for-each-ref ...(do $inshellah_limit_args "--count") --format='%(refname:short)%09%(contents:subject)' refs/tags
| lines
| each { |l|
let p = $l | split row "\t"
@ -232,7 +273,7 @@ let inshellah_git_remotes = { ||
let inshellah_git_stashes = { ||
try {
^git stash list
^git stash list ...(do $inshellah_limit_args "-n")
| lines
| each { |l|
let m = $l | parse -r '^(?P<stash>stash@\{[0-9]+\}):\s*(?P<desc>.*)$'
@ -289,7 +330,7 @@ let inshellah_git_worktrees = { ||
let inshellah_jj_revs = { ||
try {
^jj log --ignore-working-copy --no-graph -r 'all()' -T 'change_id.shortest() ++ "\t" ++ description.first_line() ++ "\n"' err> /dev/null
^jj log --ignore-working-copy --no-graph ...(do $inshellah_limit_args "-n") -r 'all()' -T 'change_id.shortest() ++ "\t" ++ description.first_line() ++ "\n"' err> /dev/null
| lines
| each { |l|
let p = $l | split row "\t"
@ -331,7 +372,7 @@ let inshellah_jj_remotes = { ||
let inshellah_jj_ops = { ||
try {
^jj op log --ignore-working-copy --no-graph -T 'id.short() ++ "\t" ++ description.first_line() ++ "\n"' err> /dev/null
^jj op log --ignore-working-copy --no-graph ...(do $inshellah_limit_args "-n") -T 'id.short() ++ "\t" ++ description.first_line() ++ "\n"' err> /dev/null
| lines
| each { |l|
let p = $l | split row "\t"
@ -368,6 +409,7 @@ let inshellah_complete = { |spans|
let sub = if $span_len >= 2 { $spans | get 1 } else { "" }
let additional = if ($completions == null and $span_len > 0) {
do $inshellah_with_timeout {
match $spans.0 {
"nix" => {
if $span_len < 2 {
@ -425,7 +467,7 @@ let inshellah_complete = { |spans|
if (($sub in $unit_verbs) and $span_len >= 3) {
let units = (do $inshellah_unit_candidates [] $last_span | default [])
let pids = (try {
^coredumpctl list --no-pager --no-legend
^coredumpctl list ...(do $inshellah_limit_args "-n") --no-pager --no-legend
| lines
| each { |l|
let p = $l | split row -r '\s+'
@ -502,7 +544,7 @@ let inshellah_complete = { |spans|
let need_image = ["run" "rmi" "tag" "push" "pull" "history" "save" "create"]
if ($sub in $need_container) {
try {
^($spans.0) ps -a --format '{{.Names}}\t{{.Image}}'
^($spans.0) ps ...(do $inshellah_limit_args "--last") --format '{{.Names}}\t{{.Image}}'
| lines | each { |l|
let p = $l | split row "\t"
if ($p | length) >= 2 { {value: $p.0, description: $p.1} }
@ -801,6 +843,7 @@ let inshellah_complete = { |spans|
}
_ => { null }
}
}
} else { null }
if $completions == null {

View file

@ -110,6 +110,28 @@ in
'';
};
dynamicTimeoutMs = lib.mkOption {
type = lib.types.int;
default = 5000;
example = 2000;
description = ''
timeout in milliseconds for live dynamic completions in the nushell
shim. this bounds runtime calls such as nix, jj, kubectl, and systemctl.
set to 0 to disable the runtime timeout.
'';
};
dynamicLimit = lib.mkOption {
type = lib.types.int;
default = 200;
example = 100;
description = ''
maximum number of results requested from live dynamic completion
providers when they expose a native result limit. set to 0 to omit
native result-limit flags.
'';
};
workers = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
@ -131,6 +153,9 @@ in
};
config = lib.mkIf cfg.enable {
environment.variables.INSHELLAH_DYNAMIC_TIMEOUT_MS = toString cfg.dynamicTimeoutMs;
environment.variables.INSHELLAH_DYNAMIC_LIMIT = toString cfg.dynamicLimit;
environment.systemPackages =
let
systemDir = "/run/current-system/sw${cfg.completionsPath}";

View file

@ -42,6 +42,8 @@ let nix_commands = do $completer [nix ""]
assert-eq ($nix_commands | get 0.value) "build" "nix command completion uses NIX_GET_COMPLETIONS"
let nix_pkg = do $completer [nix "flake#pkg"]
assert-eq ($nix_pkg | get 0.description) "raw package description" "nix descriptions are raw strings"
let nix_slow = do $completer [nix slow ""]
assert-eq $nix_slow null "slow dynamic completions time out"
let systemctl_empty = do $completer [systemctl daemon-reload ""]
assert-eq $systemctl_empty null "systemctl does not offer units for non-unit verbs"