Tracker: make command completion non-blocking #744

Closed
opened 2026-05-13 18:21:44 +00:00 by barrettruth · 1 comment
Owner

Problem

:Forge pr <Tab> can block for seconds even when the visible completion result is only the static-looking list:

open
browse
ci
create
edit
refresh
repo=
head=

The command-line completion path currently tries to prove whether hidden/contextual PR verbs should be shown before returning that list. On a branch without a current PR, that work produces no extra visible candidates but still blocks the cmdline.

This is not a Forgejo-only problem. The root issue is that Ex command completion can call synchronous forge/backend commands while Neovim is waiting for completion candidates.

Confirmed evidence

A traced vim.fn.getcompletion('Forge pr ', 'cmdline') on this checkout returned:

{ 'open', 'browse', 'ci', 'create', 'edit', 'refresh', 'repo=', 'head=' }

but took about 2.1 to 2.4 seconds and invoked:

tea pr list --state open   ...
tea pr list --state closed ...
tea pr list --state closed ...

The duplicate closed lookup happens because the completion path checks closed and then merged; for Forgejo, merged lookup maps to the closed list internally.

Relevant current path:

  • plugin/forge.lua registers :Forge completion.
  • lua/forge/cmd/init.lua delegates to forge.cmd.complete.
  • lua/forge/cmd/complete.lua enters the family/verb slot for Forge pr .
  • filter_family_verb_completion_items() calls implicit_pr_completion_target() for implicit pr open.
  • implicit_pr_completion_target() calls synchronous current/open PR resolution, then closed and merged branch PR resolution.
  • lua/forge/resolve.lua performs backend PR list/detail commands with vim.system(...):wait().

80f07ab already improved this by avoiding cold pr_state / repo_info fetches during completion, but current/branch PR discovery is still synchronous.

Desired invariant

Cmdline completion must be fast and should not synchronously call forge CLIs or network-backed backend commands.

In particular, completion should not call synchronous backend PR/issue/release/CI list or detail commands just to decide which candidates to show.

Allowed in cmdline completion:

  • static command grammar
  • already-cached state
  • cheap local-only data, ideally with short-lived caching where repeated Tab calls would otherwise rerun git work
  • async cache warming that does not block candidate return

Not allowed in cmdline completion:

  • gh, glab, tea, or backend-equivalent forge CLI fetches on the critical path
  • synchronous branch PR discovery just to hide/show contextual verbs
  • backend-specific fixes that only make Forgejo faster while leaving GitHub/GitLab with the same blocking model

Backend scope

GitHub, GitLab, and Forgejo all need to satisfy the same completion contract.

Backend-specific behavior still matters:

  • GitHub uses gh PR list/detail commands.
  • GitLab uses glab/API-style MR commands and project-specific scope behavior.
  • Forgejo uses tea and currently maps merged branch lookup through closed PR listing.

The fix should define the shared cmdline-completion boundary first, then adapt each backend-facing path to that boundary rather than optimizing only one backend command.

Topological order

  1. Add completion call-budget regression coverage.

    Capture repeated vim.fn.getcompletion() calls for the hot commandlines and assert external command budgets, not wall-clock timing. Cover at least:

    • Forge pr
    • Forge pr m
    • Forge pr re
    • Forge pr merge method=
    • release completion on cold and warm cache
    • local ref/target completion repeated calls

    The key assertion should be that PR family completion does not call synchronous backend PR lookup commands.

  2. Define a cmdline completion data-source contract.

    Make the code distinguish completion sources by cost and side effect:

    • static grammar
    • local git/filesystem
    • in-memory or persisted cache
    • async cache warming
    • forbidden synchronous backend fetch

    This should be documented in code/tests so future completions do not regress into blocking fetches.

  3. Make implicit PR verb completion cache-only.

    Replace the synchronous implicit_pr_completion_target() path in cmdline completion with a cache-only target lookup.

    If a current PR and enough PR state are already cached, contextual verbs like approve, merge, close, draft, ready, and reopen may be shown.

    If no cached target exists, return the static candidates immediately. Do not call current_pr() / branch_pr() synchronously from completion.

  4. Add non-blocking cache warming for contextual PR state.

    If contextual PR verbs are still desirable, warm the current-branch PR/status cache asynchronously outside the completion critical path. Reuse existing status/cache primitives where possible, but keep command execution semantics unchanged.

  5. Audit and fix release completion.

    Release tag completion is currently allowed to cold-fetch synchronously. Decide whether releases should become cache-only in cmdline completion, or whether an async warm/stale-cache model should be used. Apply the same backend-neutral contract to GitHub/GitLab/Forgejo release listing.

  6. Add short-lived local completion caches for git-backed values.

    branch=, commit=, head=, base=, and target= rerun local git discovery on repeated completion calls. Add root-scoped short-lived caching for local git/ref completion so repeated Tab presses do not repeatedly run git for-each-ref and git rev-list.

  7. Clean up backend-specific branch lookup duplication where still relevant.

    After cmdline completion no longer blocks on branch PR lookup, review resolve.branch_pr() behavior separately. In particular, avoid duplicated closed-list work for Forgejo merged lookup when the caller genuinely needs synchronous closed/merged resolution.

Non-goals

  • Do not make Ex-mode commands require a picker backend.
  • Do not add numeric PR/issue/CI subject completion back into stock cmdline completion.
  • Do not change command execution semantics for :Forge pr merge, :Forge pr close, :Forge pr reopen, etc.
  • Do not hide real backend differences behind a fake shared abstraction.
  • Do not solve this only for Forgejo.

Acceptance criteria

  • :Forge pr <Tab> returns immediately from static/cache state and does not invoke gh, glab, tea, or backend PR list/detail commands.
  • :Forge pr m<Tab> and :Forge pr re<Tab> do not block on backend PR discovery when no cached contextual PR target exists.
  • GitHub, GitLab, and Forgejo completion tests enforce the same no-synchronous-backend-fetch rule.
  • Existing picker-backed workflows may continue to fetch asynchronously and show stale cached rows while revalidating.
  • Command execution paths remain allowed to resolve current PRs synchronously when the user actually runs a command.
## Problem `:Forge pr <Tab>` can block for seconds even when the visible completion result is only the static-looking list: ```text open browse ci create edit refresh repo= head= ``` The command-line completion path currently tries to prove whether hidden/contextual PR verbs should be shown before returning that list. On a branch without a current PR, that work produces no extra visible candidates but still blocks the cmdline. This is not a Forgejo-only problem. The root issue is that Ex command completion can call synchronous forge/backend commands while Neovim is waiting for completion candidates. ## Confirmed evidence A traced `vim.fn.getcompletion('Forge pr ', 'cmdline')` on this checkout returned: ```lua { 'open', 'browse', 'ci', 'create', 'edit', 'refresh', 'repo=', 'head=' } ``` but took about 2.1 to 2.4 seconds and invoked: ```text tea pr list --state open ... tea pr list --state closed ... tea pr list --state closed ... ``` The duplicate closed lookup happens because the completion path checks closed and then merged; for Forgejo, merged lookup maps to the closed list internally. Relevant current path: - `plugin/forge.lua` registers `:Forge` completion. - `lua/forge/cmd/init.lua` delegates to `forge.cmd.complete`. - `lua/forge/cmd/complete.lua` enters the family/verb slot for `Forge pr `. - `filter_family_verb_completion_items()` calls `implicit_pr_completion_target()` for implicit `pr open`. - `implicit_pr_completion_target()` calls synchronous current/open PR resolution, then closed and merged branch PR resolution. - `lua/forge/resolve.lua` performs backend PR list/detail commands with `vim.system(...):wait()`. `80f07ab` already improved this by avoiding cold `pr_state` / `repo_info` fetches during completion, but current/branch PR discovery is still synchronous. ## Desired invariant Cmdline completion must be fast and should not synchronously call forge CLIs or network-backed backend commands. In particular, completion should not call synchronous backend PR/issue/release/CI list or detail commands just to decide which candidates to show. Allowed in cmdline completion: - static command grammar - already-cached state - cheap local-only data, ideally with short-lived caching where repeated Tab calls would otherwise rerun git work - async cache warming that does not block candidate return Not allowed in cmdline completion: - `gh`, `glab`, `tea`, or backend-equivalent forge CLI fetches on the critical path - synchronous branch PR discovery just to hide/show contextual verbs - backend-specific fixes that only make Forgejo faster while leaving GitHub/GitLab with the same blocking model ## Backend scope GitHub, GitLab, and Forgejo all need to satisfy the same completion contract. Backend-specific behavior still matters: - GitHub uses `gh` PR list/detail commands. - GitLab uses `glab`/API-style MR commands and project-specific scope behavior. - Forgejo uses `tea` and currently maps merged branch lookup through closed PR listing. The fix should define the shared cmdline-completion boundary first, then adapt each backend-facing path to that boundary rather than optimizing only one backend command. ## Topological order 1. Add completion call-budget regression coverage. Capture repeated `vim.fn.getcompletion()` calls for the hot commandlines and assert external command budgets, not wall-clock timing. Cover at least: - `Forge pr ` - `Forge pr m` - `Forge pr re` - `Forge pr merge method=` - release completion on cold and warm cache - local ref/target completion repeated calls The key assertion should be that PR family completion does not call synchronous backend PR lookup commands. 2. Define a cmdline completion data-source contract. Make the code distinguish completion sources by cost and side effect: - static grammar - local git/filesystem - in-memory or persisted cache - async cache warming - forbidden synchronous backend fetch This should be documented in code/tests so future completions do not regress into blocking fetches. 3. Make implicit PR verb completion cache-only. Replace the synchronous `implicit_pr_completion_target()` path in cmdline completion with a cache-only target lookup. If a current PR and enough PR state are already cached, contextual verbs like `approve`, `merge`, `close`, `draft`, `ready`, and `reopen` may be shown. If no cached target exists, return the static candidates immediately. Do not call `current_pr()` / `branch_pr()` synchronously from completion. 4. Add non-blocking cache warming for contextual PR state. If contextual PR verbs are still desirable, warm the current-branch PR/status cache asynchronously outside the completion critical path. Reuse existing status/cache primitives where possible, but keep command execution semantics unchanged. 5. Audit and fix release completion. Release tag completion is currently allowed to cold-fetch synchronously. Decide whether releases should become cache-only in cmdline completion, or whether an async warm/stale-cache model should be used. Apply the same backend-neutral contract to GitHub/GitLab/Forgejo release listing. 6. Add short-lived local completion caches for git-backed values. `branch=`, `commit=`, `head=`, `base=`, and `target=` rerun local git discovery on repeated completion calls. Add root-scoped short-lived caching for local git/ref completion so repeated Tab presses do not repeatedly run `git for-each-ref` and `git rev-list`. 7. Clean up backend-specific branch lookup duplication where still relevant. After cmdline completion no longer blocks on branch PR lookup, review `resolve.branch_pr()` behavior separately. In particular, avoid duplicated closed-list work for Forgejo merged lookup when the caller genuinely needs synchronous closed/merged resolution. ## Non-goals - Do not make Ex-mode commands require a picker backend. - Do not add numeric PR/issue/CI subject completion back into stock cmdline completion. - Do not change command execution semantics for `:Forge pr merge`, `:Forge pr close`, `:Forge pr reopen`, etc. - Do not hide real backend differences behind a fake shared abstraction. - Do not solve this only for Forgejo. ## Acceptance criteria - `:Forge pr <Tab>` returns immediately from static/cache state and does not invoke `gh`, `glab`, `tea`, or backend PR list/detail commands. - `:Forge pr m<Tab>` and `:Forge pr re<Tab>` do not block on backend PR discovery when no cached contextual PR target exists. - GitHub, GitLab, and Forgejo completion tests enforce the same no-synchronous-backend-fetch rule. - Existing picker-backed workflows may continue to fetch asynchronously and show stale cached rows while revalidating. - Command execution paths remain allowed to resolve current PRs synchronously when the user actually runs a command.
Author
Owner

Tracker complete. The subissue stack has landed on origin/main:

All corresponding commits are present in origin/main, so this tracker can be closed.

Tracker complete. The subissue stack has landed on origin/main: - #745 via #746 - #747 via #763 - #748 via #764 - #749 via #765 - #750 via #766 - #751 via #767 - #752 via #768 All corresponding commits are present in origin/main, so this tracker can be closed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
barrettruth/forge.nvim#744
No description provided.