Add sweep.nvim: AI autocomplete plugin using Sweep's next-edit model#3
Add sweep.nvim: AI autocomplete plugin using Sweep's next-edit model#3Kabilan108 wants to merge 19 commits intonixosfrom
Conversation
Research notes from analyzing the JetBrains Sweep AI plugin architecture, including context building strategies, prompt formats, and LSP integration patterns for designing a Neovim equivalent.
- Plugin entry point with commands (enable/disable/toggle/status/debug) - Configuration module with typed defaults for keymaps, context, server, UI - Test harness setup with plenary.test - Initial config_spec tests Targeting Neovim 0.10+ with plenary.nvim dependency.
Implement context module for sweep.nvim that extracts rich context: - LSP integration for definitions and type info via textDocument/definition and hover - Treesitter integration for scope detection (function, class, method) - Import/require statement extraction for multiple languages - Formatted output with prioritized sections for prompts - Async API with callbacks for LSP operations - Graceful handling of missing LSP/treesitter - Content truncation to configurable limits
This is the core orchestration module for sweep.nvim that ties together the FIM, HTTP, parser, and UI modules. Implements: - Debounced trigger mechanism using vim.loop timers - Request cancellation for in-flight HTTP requests - accept_full/accept_line/accept_word for partial completion acceptance - dismiss for clearing without insertion - Filetype exclusion support - Enable/disable/toggle functionality Tests cover debouncing, request management, text insertion, and error handling.
Implements keymaps for sweep.nvim that allow users to: - Trigger completion manually (insert mode) - Accept full, line, or word completions (insert mode, only when visible) - Dismiss completions (insert mode, only when visible) Uses expr mappings for conditional behavior that passes through the key when no completion is visible. Includes comprehensive TDD tests.
Configure sweep.nvim with keybindings matching previous llama.vim setup: - <C-c> trigger, <C-s> accept full, <C-l> accept line, <C-w> accept word - Same highlight color (#f2cdcd) for ghost text - Uses LLAMA_SERVER_URL env var with localhost:8012 default
- Fix SCOPE_NODE_TYPES duplicate keys bug (Lua table overwrites) - Remove unnecessary JSON encode/decode round-trip in completion - Make parser.parse() accept both string and table input - Remove dead code: unreachable cancellation check in http callback - Remove dead code: unused get_treesitter_parser function - Remove dead code: unused in_import_section variable - Remove dead code: empty vim.schedule wrapper in error handler - Simplify redundant nil check in parser - Add entry counter to cache for O(1) count operations - Reuse parser.first_word() in completion accept_word - Clean up unused variable captures in treesitter range extraction
- Fix 4: Attach edit tracking to buffers on BufEnter (autocmds.lua) - Skip special buffers and excluded filetypes - edits.attach() now called for normal file buffers - Fix 1: Wire edit tracking into completion flow (completion.lua) - Add edits module to lazy-loaded dependencies - Include edits.get_context() in prompt (first, as most relevant) - Fix 3: Implement completion deduplication (completion.lua) - Add deduplicate_completion() helper - Removes text that already exists after cursor - Prevents suggesting duplicate content - Fix 2: Fix LSP context async/sync mismatch (completion.lua) - Refactor make_request() to use context.get_async() when LSP enabled - Extract send_completion_request() helper for cleaner flow - Extract add_sync_context() for ring buffer and edit context - LSP definitions and type_info now properly included in prompts
Code review fixes: 1. Race condition in async LSP callbacks - Add request_id counter to state - Increment on each make_request() call - Check in async callback to ignore stale responses 2. Missing deduplication on cached results - Apply deduplicate_completion() to cached results before display - Cursor context may differ from when result was cached
📝 WalkthroughWalkthroughA complete new Neovim plugin Changes
Sequence Diagram(s)sequenceDiagram
participant User as User / Editor
participant Autocmd as Autocmds
participant Completion as Completion
participant Context as Context (Ring, Edits, LSP)
participant HTTP as HTTP Client
participant Server as llama.cpp Server
participant Parser as Parser
participant UI as UI
User->>Autocmd: CursorMovedI
Autocmd->>Completion: trigger() [debounced]
Completion->>Context: gather context (ring, edits, LSP)
Context-->>Completion: context data
Completion->>Completion: build FIM prompt
Completion->>HTTP: POST /infill request
HTTP->>Server: JSON POST (prefix, suffix, context)
Server-->>HTTP: JSON response (content, tokens, timings)
HTTP-->>Completion: parsed response
Completion->>Parser: parse(response)
Parser-->>Completion: structured result
Completion->>UI: show(lines, row, col)
UI-->>User: render ghost text
User->>Completion: accept / dismiss
Completion->>UI: clear()
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Fix all issues with AI agents
In `@sweep-ai-autocomplete-analysis.md`:
- Around line 496-528: The build_prompt function uses a non-existent
vim.list_slice API (calls in prefix_lines and suffix_lines) which will break;
replace those calls with standard Lua slicing or fetch ranges directly via
vim.api.nvim_buf_get_lines: either compute prefix_lines and suffix_lines by
iterating/copying the lines table (or using table.move/table.unpack) to extract
1..row and row..#lines, or call nvim_buf_get_lines twice with proper start/end
indices to get the prefix and suffix segments, then proceed to trim the
last/first element with string.sub as before (references: build_prompt,
prefix_lines, suffix_lines).
- Around line 442-470: The example function get_definitions_at_cursor uses
blocking vim.lsp.buf_request_sync calls; change it to the async LSP pattern by
replacing each buf_request_sync invocation (for 'textDocument/typeDefinition',
'textDocument/hover', and 'textDocument/documentSymbol') with non-blocking
vim.lsp.buf_request calls that supply callbacks (or use coroutine/await
wrappers) and aggregate results before invoking a final callback or returning a
promise; ensure you reference the same result keys (type_definition, hover,
symbols) and preserve the timeout handling by moving any timeout logic into the
async callbacks or promise rejection handling so the function no longer blocks
the UI.
In `@sweep.nvim/lua/sweep/config.lua`:
- Around line 41-87: The SweepConfig type annotation is missing the root-level
show_info field used by M.defaults and referenced in init.lua; update the
`@class/`@field block for SweepConfig to include "@field show_info boolean" so the
type matches the defaults (note that ui.show_info remains its own nested field),
ensuring SweepConfig, M.defaults, and init.lua references are consistent.
In `@sweep.nvim/lua/sweep/context.lua`:
- Around line 284-369: M.get_definitions currently calls vim.lsp.buf_request
which invokes the handler per LSP client and only checks location.uri (ignoring
LocationLink fields), so switch to vim.lsp.buf_request_all with the same params
and a single aggregated handler, iterate over each client response and each
entry in its result list, and for each location accept either location.uri or
location.targetUri and use location.range or location.targetSelectionRange or
location.targetRange; Preserve existing buffer loading, slicing, name/filename
extraction and ensure you call callback once with the deduplicated/aggregated
definitions list.
- Around line 371-424: The get_type_info function currently calls
vim.lsp.buf_request which invokes the handler per LSP client and doesn't handle
MarkedString[] arrays; change M.get_type_info to use vim.lsp.buf_request_all
(passing the same params) so you receive a single table of responses, iterate
responses to aggregate successful results from all clients, normalize hover
contents for each response by handling string, MarkedString (table with .value
or {language,value}), and MarkedString[] (array of strings or tables) by
concatenating their textual parts into one type_info string per response, then
combine/choose the best non-empty aggregated type_info (e.g., join with "\n" or
take first non-empty) and call callback exactly once with that final string or
nil.
In `@sweep.nvim/lua/sweep/edits.lua`:
- Around line 241-248: The detach implementation currently only clears local
tables (attached_buffers and buffer_content) but doesn't stop Neovim's buffer
callbacks, so change M.detach to explicitly detach the buffer via
vim.api.nvim_buf_detach(bufnr) (after checking vim.api.nvim_buf_is_loaded or
that attached_buffers[bufnr] exists) and then clear attached_buffers[bufnr] and
buffer_content[bufnr]; alternatively, if you prefer the existing on_lines-based
approach, set a detach flag on the attached_buffers[bufnr] entry that the
on_lines callback checks and returns true to stop, but ensure the code path that
calls nvim_buf_attach (the attach function) and the on_lines handler respect
that flag to avoid duplicate recordings.
In `@sweep.nvim/lua/sweep/ui.lua`:
- Around line 136-140: The is_visible() function currently returns true if
state.mark_id and state.bufnr are non-nil even when the buffer has been wiped;
update M.is_visible to also verify the buffer is still valid by checking
vim.api.nvim_buf_is_valid(state.bufnr) (or equivalent) before returning true so
that it returns false for destroyed buffers; ensure callers like get_current()
relying on M.is_visible() will no longer receive stale state from
state.bufnr/state.mark_id when the underlying buffer is invalid.
🧹 Nitpick comments (11)
sweep-ai-autocomplete-analysis.md (1)
15-556: Consider adding language specifiers to fenced code blocks.Multiple fenced code blocks lack language specifiers, which markdownlint flags. While some blocks are ASCII diagrams (where this is acceptable), adding
textor appropriate language identifiers improves syntax highlighting and accessibility.For example:
- Lines 15-32: ASCII diagram → use
```text- Lines 64-77: ASCII diagram → use
```text- Lines 147-152: FIM format example → use
```textor```python- Lines 222-256: ASCII diagram → use
```textThis is a documentation quality improvement and can be deferred.
sweep.nvim/tests/minimal_init.lua (1)
8-27: Fail fast whenplenary.nvimis missing.
Right now a missing dependency surfaces as a generic runtime error. Consider an explicit check to provide a clear CI failure message.✅ Suggested change
local plenary_path = vim.fn.stdpath('data') .. '/lazy/plenary.nvim' +local found_plenary = false if vim.fn.isdirectory(plenary_path) == 1 then vim.opt.runtimepath:prepend(plenary_path) + found_plenary = true else -- Try common alternative paths local alt_paths = { vim.fn.expand('~/.local/share/nvim/lazy/plenary.nvim'), vim.fn.expand('~/.local/share/nvim/site/pack/packer/start/plenary.nvim'), '/usr/share/nvim/site/pack/packer/start/plenary.nvim', } for _, path in ipairs(alt_paths) do if vim.fn.isdirectory(path) == 1 then vim.opt.runtimepath:prepend(path) + found_plenary = true break end end end + +if not found_plenary then + error('plenary.nvim not found on runtimepath; install it before running tests') +end -- Basic settings for testing vim.cmd('runtime plugin/plenary.vim')sweep.nvim/lua/sweep/fim.lua (1)
80-83: Avoid pulling the entire buffer on every request.
nvim_buf_get_lines(bufnr, 0, -1, false)loads the whole buffer each time; in a hot completion path this can be expensive on large files. Consider fetching only the needed ranges.♻️ Suggested refactor
- -- Get all buffer lines - local total_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - local line_count = `#total_lines` + -- Get line count without loading the full buffer + local line_count = vim.api.nvim_buf_line_count(bufnr) -- Handle empty buffer if line_count == 0 then local metadata = { cursor_line = '', @@ - local current_line = total_lines[row + 1] or '' + local current_line = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1]) or '' col = math.max(0, math.min(col, `#current_line`)) @@ - for i = prefix_start, row - 1 do - local line = total_lines[i + 1] - if line then - table.insert(prefix_parts, line) - end - end + if prefix_start < row then + local before_lines = vim.api.nvim_buf_get_lines(bufnr, prefix_start, row, false) + for _, line in ipairs(before_lines) do + table.insert(prefix_parts, line) + end + end @@ - for i = row + 1, suffix_end do - local line = total_lines[i + 1] - if line then - table.insert(suffix_parts, line) - end - end + if row + 1 <= suffix_end then + local after_lines = vim.api.nvim_buf_get_lines(bufnr, row + 1, suffix_end + 1, false) + for _, line in ipairs(after_lines) do + table.insert(suffix_parts, line) + end + endAlso applies to: 110-155
sweep.nvim/tests/sweep/autocmds_spec.lua (1)
12-66: Consider clearing mocked modules inafter_eachto avoid cross-test leakage.
Mocks remain inpackage.loadedafter each test; if other specs run in the same Neovim instance, they may accidentally reuse the mocks.🧹 Suggested cleanup
after_each(function() -- Always teardown after tests pcall(function() autocmds.teardown() end) + + package.loaded['sweep.autocmds'] = nil + package.loaded['sweep.config'] = nil + package.loaded['sweep.completion'] = nil + package.loaded['sweep.ring'] = nil + package.loaded['sweep.ui'] = nil end)sweep.nvim/lua/sweep/parser.lua (1)
8-23: Potential edge case insplit_lineswith consecutive newlines.The condition on line 14
line ~= '' or#lines== 0discards empty lines except for the first line of an empty string input. This means"a\n\nb"(with consecutive newlines) would return{ "a", "b" }instead of{ "a", "", "b" }, losing the blank line between them.If preserving blank lines within content is intentional (e.g., for multi-line completions with spacing), consider revising the logic. If the current behavior is intended to collapse blank lines in completions, this is acceptable but worth documenting.
sweep.nvim/lua/sweep/autocmds.lua (1)
80-92: Unusedbufnrparameter — consider passing it toadd_visible_to_ring.The
on_buf_leavefunction receivesbufnrfromargs.bufbut doesn't use it. Whilenvim_get_current_buf()returns the correct buffer duringBufLeave, explicitly passingbufnrwould make the code more robust and self-documenting.♻️ Suggested improvement
--- Handle BufLeave event - cancel completion and add to ring ----@param bufnr number -local function on_buf_leave(bufnr) +---@param bufnr number Buffer being left +local function on_buf_leave(bufnr) local completion = require('sweep.completion') local ui = require('sweep.ui') -- Cancel any pending completion completion.cancel() ui.clear() -- Add visible content to ring buffer - add_visible_to_ring('buffer_leave') + add_visible_to_ring('buffer_leave', bufnr) endThen update
add_visible_to_ringto accept an optionalbufnrparameter:local function add_visible_to_ring(source, bufnr) bufnr = bufnr or vim.api.nvim_get_current_buf() -- ...rest of function endsweep.nvim/lua/sweep/ring.lua (1)
32-54: Fingerprint length may cause false positives for files with similar headers.The 100-character prefix for duplicate detection could cause false positives when adding chunks from files that share similar import headers or boilerplate. This is a reasonable performance trade-off, but consider documenting this limitation or making
SIMILARITY_PREFIX_LENconfigurable if users report issues.sweep.nvim/lua/sweep/keymaps.lua (1)
159-167: Potential issue: Modifying table during iteration.Iterating over
buffer_keymapswithpairs()whileteardown_buffer_keymapssets entries tonilcan cause undefined behavior in Lua. The subsequentbuffer_keymaps = {}on line 165 makes this safe in practice, but the iteration pattern is fragile.♻️ Suggested safer iteration pattern
function M.teardown() -- Remove keymaps from all tracked buffers - for bufnr, _ in pairs(buffer_keymaps) do - teardown_buffer_keymaps(bufnr) - end - - buffer_keymaps = {} + local buffers_to_teardown = vim.tbl_keys(buffer_keymaps) + for _, bufnr in ipairs(buffers_to_teardown) do + teardown_buffer_keymaps(bufnr) + end is_setup_flag = false endsweep.nvim/lua/sweep/init.lua (1)
127-139: Inconsistent notification behavior withshow_infoconfig.The
clear_cacheandclear_ringfunctions always emit notifications, whileenable/disablerespect theconfig.options.show_infosetting. Consider making this consistent.♻️ Suggested fix for consistent notification behavior
function M.clear_cache() local cache = require('sweep.cache') cache.clear() - vim.notify('Sweep cache cleared', vim.log.levels.INFO) + if config.options.show_info then + vim.notify('Sweep cache cleared', vim.log.levels.INFO) + end end function M.clear_ring() local ring = require('sweep.ring') ring.clear() - vim.notify('Sweep ring buffer cleared', vim.log.levels.INFO) + if config.options.show_info then + vim.notify('Sweep ring buffer cleared', vim.log.levels.INFO) + end endsweep.nvim/lua/sweep/edits.lua (1)
188-232: Consider optimizing buffer content cache updates for large files.The current implementation refreshes the entire buffer content cache on every edit (lines 222-225). For large files with frequent edits, this could impact performance. Consider updating only the changed range.
♻️ Suggested incremental cache update
-- Update our content cache - local full_ok, full_lines = pcall(vim.api.nvim_buf_get_lines, buf, 0, -1, false) - if full_ok then - buffer_content[buf] = full_lines + if buffer_content[buf] then + -- Remove old lines from cache + for _ = first_line + 1, last_line do + table.remove(buffer_content[buf], first_line + 1) + end + -- Insert new lines into cache + for i, line in ipairs(new_lines) do + table.insert(buffer_content[buf], first_line + i, line) + end + else + -- Fallback: fetch full buffer if cache missing + local full_ok, full_lines = pcall(vim.api.nvim_buf_get_lines, buf, 0, -1, false) + if full_ok then + buffer_content[buf] = full_lines + end endNote: The incremental approach is more complex and may have edge cases. Verify the current approach causes measurable performance issues before applying this optimization.
sweep.nvim/lua/sweep/cache.lua (1)
46-60: O(n) access order management may impact performance at scale.Both
remove_from_access_orderandtouchare O(n) operations. With the defaultmax_entries = 100, this is likely acceptable, but if increased significantly, this could become a bottleneck on cache hits.For a more scalable implementation, consider using a doubly-linked list for O(1) removal, or accept the current complexity as appropriate for the expected cache size.
| ```lua | ||
| -- Replicate PSI behavior with LSP | ||
| local function get_definitions_at_cursor() | ||
| local params = vim.lsp.util.make_position_params() | ||
|
|
||
| -- Get type definition | ||
| local type_def = vim.lsp.buf_request_sync( | ||
| 0, 'textDocument/typeDefinition', params, 1000 | ||
| ) | ||
|
|
||
| -- Get hover info for signatures | ||
| local hover = vim.lsp.buf_request_sync( | ||
| 0, 'textDocument/hover', params, 1000 | ||
| ) | ||
|
|
||
| -- Get document symbols for context | ||
| local symbols = vim.lsp.buf_request_sync( | ||
| 0, 'textDocument/documentSymbol', { | ||
| textDocument = vim.lsp.util.make_text_document_params() | ||
| }, 1000 | ||
| ) | ||
|
|
||
| return { | ||
| type_definition = type_def, | ||
| hover = hover, | ||
| symbols = symbols, | ||
| } | ||
| end | ||
| ``` |
There was a problem hiding this comment.
Sync LSP calls block the UI and contradict the async implementation.
The example uses vim.lsp.buf_request_sync(), which blocks the editor UI during LSP requests. According to the PR summary, the actual plugin implementation uses async LSP calls. This documentation could mislead developers into implementing a blocking version.
♻️ Update example to use async LSP pattern
--- Replicate PSI behavior with LSP
local function get_definitions_at_cursor()
local params = vim.lsp.util.make_position_params()
-
- -- Get type definition
- local type_def = vim.lsp.buf_request_sync(
- 0, 'textDocument/typeDefinition', params, 1000
- )
-
- -- Get hover info for signatures
- local hover = vim.lsp.buf_request_sync(
- 0, 'textDocument/hover', params, 1000
- )
-
- -- Get document symbols for context
- local symbols = vim.lsp.buf_request_sync(
- 0, 'textDocument/documentSymbol', {
- textDocument = vim.lsp.util.make_text_document_params()
- }, 1000
- )
-
- return {
- type_definition = type_def,
- hover = hover,
- symbols = symbols,
- }
+ local results = {}
+
+ -- Get type definition (async)
+ vim.lsp.buf_request(0, 'textDocument/typeDefinition', params, function(err, result)
+ if not err then
+ results.type_definition = result
+ end
+ end)
+
+ -- Get hover info (async)
+ vim.lsp.buf_request(0, 'textDocument/hover', params, function(err, result)
+ if not err then
+ results.hover = result
+ end
+ end)
+
+ -- Note: Actual implementation should use callbacks or async/await pattern
+ -- See the plugin's context.lua module for the complete async implementation
endNote: The actual plugin implementation (mentioned in PR summary) handles async LSP correctly. This documentation example should be updated to reflect that pattern or include a note that it's simplified pseudocode.
🤖 Prompt for AI Agents
In `@sweep-ai-autocomplete-analysis.md` around lines 442 - 470, The example
function get_definitions_at_cursor uses blocking vim.lsp.buf_request_sync calls;
change it to the async LSP pattern by replacing each buf_request_sync invocation
(for 'textDocument/typeDefinition', 'textDocument/hover', and
'textDocument/documentSymbol') with non-blocking vim.lsp.buf_request calls that
supply callbacks (or use coroutine/await wrappers) and aggregate results before
invoking a final callback or returning a promise; ensure you reference the same
result keys (type_definition, hover, symbols) and preserve the timeout handling
by moving any timeout logic into the async callbacks or promise rejection
handling so the function no longer blocks the UI.
| ```lua | ||
| local function build_prompt(cursor_pos) | ||
| local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) | ||
| local row, col = cursor_pos[1], cursor_pos[2] | ||
|
|
||
| -- Split into prefix and suffix | ||
| local prefix_lines = vim.list_slice(lines, 1, row) | ||
| prefix_lines[#prefix_lines] = string.sub(prefix_lines[#prefix_lines], 1, col) | ||
|
|
||
| local suffix_lines = vim.list_slice(lines, row, #lines) | ||
| suffix_lines[1] = string.sub(suffix_lines[1], col + 1) | ||
|
|
||
| local prefix = table.concat(prefix_lines, "\n") | ||
| local suffix = table.concat(suffix_lines, "\n") | ||
|
|
||
| -- Get definitions via LSP | ||
| local definitions = get_definitions_at_cursor() | ||
|
|
||
| -- Get recent edits | ||
| local recent_diffs = format_recent_edits(edit_history) | ||
|
|
||
| return string.format([[ | ||
| [DEFINITIONS] | ||
| %s | ||
|
|
||
| [RECENT EDITS] | ||
| %s | ||
|
|
||
| [CURRENT FILE] | ||
| <|prefix|>%s<|suffix|>%s<|middle|> | ||
| ]], definitions, recent_diffs, prefix, suffix) | ||
| end | ||
| ``` |
There was a problem hiding this comment.
vim.list_slice does not exist in the Neovim API.
Lines 502 and 505 use vim.list_slice(), which is not a standard Neovim API function. This code will fail if copy-pasted.
🔧 Fix using standard Lua array slicing
local function build_prompt(cursor_pos)
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
local row, col = cursor_pos[1], cursor_pos[2]
-- Split into prefix and suffix
- local prefix_lines = vim.list_slice(lines, 1, row)
+ local prefix_lines = {unpack(lines, 1, row)}
prefix_lines[`#prefix_lines`] = string.sub(prefix_lines[`#prefix_lines`], 1, col)
- local suffix_lines = vim.list_slice(lines, row, `#lines`)
+ local suffix_lines = {unpack(lines, row, `#lines`)}
suffix_lines[1] = string.sub(suffix_lines[1], col + 1)
local prefix = table.concat(prefix_lines, "\n")
local suffix = table.concat(suffix_lines, "\n")Alternatively, use vim.api.nvim_buf_get_lines() directly with the appropriate line ranges instead of slicing after fetching all lines.
🤖 Prompt for AI Agents
In `@sweep-ai-autocomplete-analysis.md` around lines 496 - 528, The build_prompt
function uses a non-existent vim.list_slice API (calls in prefix_lines and
suffix_lines) which will break; replace those calls with standard Lua slicing or
fetch ranges directly via vim.api.nvim_buf_get_lines: either compute
prefix_lines and suffix_lines by iterating/copying the lines table (or using
table.move/table.unpack) to extract 1..row and row..#lines, or call
nvim_buf_get_lines twice with proper start/end indices to get the prefix and
suffix segments, then proceed to trim the last/first element with string.sub as
before (references: build_prompt, prefix_lines, suffix_lines).
| ---@type SweepConfig | ||
| M.defaults = { | ||
| auto_enable = true, | ||
| debounce_ms = 100, | ||
| show_info = true, | ||
|
|
||
| keymaps = { | ||
| trigger = '<C-Space>', | ||
| accept_full = '<Tab>', | ||
| accept_line = '<C-l>', | ||
| accept_word = '<C-Right>', | ||
| dismiss = '<C-]>', | ||
| }, | ||
|
|
||
| context = { | ||
| prefix_lines = 100, | ||
| suffix_lines = 50, | ||
| max_ring_chunks = 16, | ||
| chunk_size = 64, | ||
| use_lsp = true, | ||
| use_treesitter = true, | ||
| }, | ||
|
|
||
| server = { | ||
| endpoint = os.getenv('LLAMA_SERVER_URL') or 'http://localhost:8080', | ||
| timeout = 5000, | ||
| n_predict = 128, | ||
| temperature = 0.1, | ||
| cache_prompt = true, | ||
| }, | ||
|
|
||
| ui = { | ||
| show_info = true, | ||
| hl_group = 'SweepGhostText', | ||
| info_hl_group = 'SweepInfo', | ||
| }, | ||
|
|
||
| filetypes_exclude = { | ||
| 'help', | ||
| 'TelescopePrompt', | ||
| 'lazy', | ||
| 'mason', | ||
| 'neo-tree', | ||
| 'NvimTree', | ||
| 'toggleterm', | ||
| }, | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for all show_info usage in the codebase
echo "=== All show_info references ==="
rg -n 'show_info' --type luaRepository: Kabilan108/dotfiles
Length of output: 832
🏁 Script executed:
#!/bin/bash
# Check if SweepConfig type is defined and what it contains
echo "=== Searching for SweepConfig type definition ==="
rg -n 'SweepConfig' --type lua -A 20Repository: Kabilan108/dotfiles
Length of output: 3171
🏁 Script executed:
#!/bin/bash
# Get the full config.lua file to understand structure
echo "=== Full config.lua content ==="
cat -n lua/sweep/config.luaRepository: Kabilan108/dotfiles
Length of output: 164
🏁 Script executed:
#!/bin/bash
cat -n sweep.nvim/lua/sweep/config.luaRepository: Kabilan108/dotfiles
Length of output: 3805
🏁 Script executed:
#!/bin/bash
# Check the usage in init.lua more closely
echo "=== Context around show_info usage in init.lua ==="
sed -n '60,95p' sweep.nvim/lua/sweep/init.luaRepository: Kabilan108/dotfiles
Length of output: 891
🏁 Script executed:
#!/bin/bash
# Check the usage in ui.lua more closely
echo "=== Context around show_info usage in ui.lua ==="
sed -n '75,95p' sweep.nvim/lua/sweep/ui.luaRepository: Kabilan108/dotfiles
Length of output: 690
Fix incomplete SweepConfig type annotation—show_info field is missing from type definition.
The root-level show_info at line 45 is actively used in init.lua (lines 66, 90) to control notification messages. However, the SweepConfig type annotation (lines 32–39) omits this field, creating a mismatch between the type definition and the actual defaults object. Add @field show_info boolean to the SweepConfig class annotation to match the implementation.
Note: show_info serves two separate purposes—root level for notifications, nested under ui for inline info display—which is valid but worth documenting.
🤖 Prompt for AI Agents
In `@sweep.nvim/lua/sweep/config.lua` around lines 41 - 87, The SweepConfig type
annotation is missing the root-level show_info field used by M.defaults and
referenced in init.lua; update the `@class/`@field block for SweepConfig to
include "@field show_info boolean" so the type matches the defaults (note that
ui.show_info remains its own nested field), ensuring SweepConfig, M.defaults,
and init.lua references are consistent.
| --- Get definitions at cursor position via LSP (async) | ||
| ---@param opts {bufnr: number, row: number, col: number, max_lines?: number} Options | ||
| ---@param callback fun(definitions: SweepDefinition[]) Callback with results | ||
| function M.get_definitions(opts, callback) | ||
| local bufnr = opts.bufnr | ||
| local row = opts.row | ||
| local col = opts.col | ||
| local max_lines = opts.max_lines or DEFAULT_MAX_DEFINITION_LINES | ||
|
|
||
| -- Check for LSP clients | ||
| local clients = vim.lsp.get_clients({ bufnr = bufnr }) | ||
| if not clients or #clients == 0 then | ||
| callback({}) | ||
| return | ||
| end | ||
|
|
||
| -- Create LSP position params | ||
| local params = { | ||
| textDocument = { | ||
| uri = vim.uri_from_bufnr(bufnr), | ||
| }, | ||
| position = { | ||
| line = row, | ||
| character = col, | ||
| }, | ||
| } | ||
|
|
||
| -- Request definitions from LSP | ||
| local success, _ = vim.lsp.buf_request(bufnr, 'textDocument/definition', params, function(err, result, ctx) | ||
| if err or not result then | ||
| callback({}) | ||
| return | ||
| end | ||
|
|
||
| local definitions = {} | ||
|
|
||
| -- Normalize result to array | ||
| local locations = vim.islist(result) and result or { result } | ||
|
|
||
| for _, location in ipairs(locations) do | ||
| if location and location.uri then | ||
| local def_bufnr = vim.uri_to_bufnr(location.uri) | ||
| local range = location.range or location.targetSelectionRange or location.targetRange | ||
|
|
||
| if range then | ||
| local start_line = range.start.line | ||
| local end_line = range['end'].line | ||
|
|
||
| -- Limit content size | ||
| local content_end = math.min(end_line + 1, start_line + max_lines) | ||
|
|
||
| -- Try to load buffer if not loaded | ||
| local loaded = vim.api.nvim_buf_is_loaded(def_bufnr) | ||
| if not loaded then | ||
| pcall(vim.fn.bufload, def_bufnr) | ||
| end | ||
|
|
||
| local lines = get_buffer_lines(def_bufnr, start_line, content_end) | ||
| local content = table.concat(lines, '\n') | ||
|
|
||
| -- Extract name from first line if possible | ||
| local name = 'unknown' | ||
| if lines[1] then | ||
| -- Try to extract identifier from first line | ||
| name = lines[1]:match('[%w_]+') or 'unknown' | ||
| end | ||
|
|
||
| local filename = vim.fn.fnamemodify(vim.uri_to_fname(location.uri), ':t') | ||
|
|
||
| table.insert(definitions, { | ||
| name = name, | ||
| kind = 'definition', | ||
| content = content, | ||
| filename = filename, | ||
| }) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| callback(definitions) | ||
| end) | ||
|
|
||
| if not success then | ||
| callback({}) | ||
| end | ||
| end |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Neovim vim.lsp.buf_request vs vim.lsp.buf_request_all callback behavior documentation
💡 Result:
vim.lsp.buf_request (per-client callback)
buf_request sends the request to (matching) attached client(s) and invokes the handler once per client response (so your handler may run multiple times).
The handler you pass is an LSP response handler (“lsp-handler”), with signature:
function(err, result, ctx, config) (older docs often omit config). Here ctx includes e.g. ctx.client_id, ctx.method, ctx.bufnr, etc. [1]
vim.lsp.buf_request_all (aggregated callback)
buf_request_all(bufnr, method, params, handler) also sends to all attached clients, but waits for all responses and then calls your handler exactly once with an aggregated table. The docs describe this as a client_id:result map. [2]
In current Neovim, each entry is a “request_result” object that includes both the LSP result and any error, i.e. conceptually:
handler(results_by_client_id)
-- results_by_client_id[client_id] = { result = ..., err = ... }Note: the field inside each entry was renamed from error → err. [3]
Where this behavior is documented
- LSP handler signature (
err, result, ctx, config): Neovim:help lsp-handler[1] buf_request_all“called after all requests are completed” and passes a client-id keyed map: Neovim:help vim.lsp.buf_request_all()[2]error→errrename forbuf_request_allaggregated results: Neovim:help deprecated[3]
Sources:
[1] (git.api.net.tw)
[2] (git.api.net.tw)
[3] (neovim.io)
Citations:
- 1: https://git.api.net.tw/neovim/neovim/src/commit/72a6643b1380cdf6f1153d70eeaffb90bdca30d6/runtime/doc/lsp.txt
- 2: https://git.api.net.tw/neovim/neovim/src/commit/72a6643b1380cdf6f1153d70eeaffb90bdca30d6/runtime/doc/lsp.txt
- 3: https://neovim.io/doc/user/deprecated.html?utm_source=openai
🏁 Script executed:
# Search for get_async function in the codebase to understand the pending counter impact
rg -i "get_async|pending" --type luaRepository: Kabilan108/dotfiles
Length of output: 2827
🌐 Web query:
LSP LocationLink targetUri field textDocument/definition response specification
💡 Result:
In LSP, textDocument/definition may return “definition links” (LocationLink[]) when the client indicates support for them.
LocationLink.targetUri
targetUri is a required field on LocationLink:
targetUri: DocumentUri— “The target resource identifier of this link.”- Related required fields:
targetRange: Range,targetSelectionRange: Range(and optionaloriginSelectionRange?: Range).
[1]
So in a textDocument/definition response, each LocationLink’s targetUri is the URI of the document you should navigate to (for example, a file://... URI).
textDocument/definition response type
Per the spec, the result for textDocument/definition is:
Location | Location[] | LocationLink[] | null
AndLocationLink[]is available (introduced in LSP 3.14.0) when the client capabilitytextDocument.definition.linkSupportistrue.
[2]
Sources:
[1] https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#locationLink
[2] https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition
🏁 Script executed:
# Get the full context.lua file or at least the relevant section
wc -l sweep.nvim/lua/sweep/context.luaRepository: Kabilan108/dotfiles
Length of output: 99
🏁 Script executed:
# Look at the get_async function
sed -n '1,100p' sweep.nvim/lua/sweep/context.lua | head -80Repository: Kabilan108/dotfiles
Length of output: 2966
🏁 Script executed:
# Search for get_async function definition and its surrounding context
rg -A 30 "function M.get_async" sweep.nvim/lua/sweep/context.luaRepository: Kabilan108/dotfiles
Length of output: 942
🏁 Script executed:
# Also check where get_definitions is called to see how pending counter is used
rg -B 5 -A 15 "pending = pending - 1" sweep.nvim/lua/sweep/context.luaRepository: Kabilan108/dotfiles
Length of output: 546
🏁 Script executed:
# Check if there are more calls to check_complete to understand the full flow
rg -B 3 -A 3 "check_complete" sweep.nvim/lua/sweep/context.luaRepository: Kabilan108/dotfiles
Length of output: 569
Handle multi-client LSP responses and LocationLink URIs.
vim.lsp.buf_request invokes the handler once per attached client, so with multiple clients the callback fires multiple times and breaks the pending counter in get_async. Additionally, textDocument/definition can return LocationLink entries with targetUri (required per LSP 3.14.0+), which are currently ignored since only location.uri is checked. Use vim.lsp.buf_request_all to aggregate all client responses into a single callback, and accept location.uri or location.targetUri.
🐛 Proposed fix
- local success, _ = vim.lsp.buf_request(bufnr, 'textDocument/definition', params, function(err, result, ctx)
- if err or not result then
- callback({})
- return
- end
-
- local definitions = {}
-
- -- Normalize result to array
- local locations = vim.islist(result) and result or { result }
-
- for _, location in ipairs(locations) do
- if location and location.uri then
- local def_bufnr = vim.uri_to_bufnr(location.uri)
- local range = location.range or location.targetSelectionRange or location.targetRange
-
- if range then
- local start_line = range.start.line
- local end_line = range['end'].line
-
- -- Limit content size
- local content_end = math.min(end_line + 1, start_line + max_lines)
-
- -- Try to load buffer if not loaded
- local loaded = vim.api.nvim_buf_is_loaded(def_bufnr)
- if not loaded then
- pcall(vim.fn.bufload, def_bufnr)
- end
-
- local lines = get_buffer_lines(def_bufnr, start_line, content_end)
- local content = table.concat(lines, '\n')
-
- -- Extract name from first line if possible
- local name = 'unknown'
- if lines[1] then
- -- Try to extract identifier from first line
- name = lines[1]:match('[%w_]+') or 'unknown'
- end
-
- local filename = vim.fn.fnamemodify(vim.uri_to_fname(location.uri), ':t')
-
- table.insert(definitions, {
- name = name,
- kind = 'definition',
- content = content,
- filename = filename,
- })
- end
- end
- end
-
- callback(definitions)
- end)
-
- if not success then
- callback({})
- end
+ local ok = vim.lsp.buf_request_all(bufnr, 'textDocument/definition', params, function(results)
+ local definitions = {}
+ if results then
+ for _, res in pairs(results) do
+ local result = res.result
+ if result then
+ local locations = vim.islist(result) and result or { result }
+ for _, location in ipairs(locations) do
+ local uri = location.uri or location.targetUri
+ local range = location.range or location.targetSelectionRange or location.targetRange
+
+ if uri and range then
+ local def_bufnr = vim.uri_to_bufnr(uri)
+ local start_line = range.start.line
+ local end_line = range['end'].line
+
+ -- Limit content size
+ local content_end = math.min(end_line + 1, start_line + max_lines)
+
+ -- Try to load buffer if not loaded
+ local loaded = vim.api.nvim_buf_is_loaded(def_bufnr)
+ if not loaded then
+ pcall(vim.fn.bufload, def_bufnr)
+ end
+
+ local lines = get_buffer_lines(def_bufnr, start_line, content_end)
+ local content = table.concat(lines, '\n')
+
+ -- Extract name from first line if possible
+ local name = 'unknown'
+ if lines[1] then
+ -- Try to extract identifier from first line
+ name = lines[1]:match('[%w_]+') or 'unknown'
+ end
+
+ local filename = vim.fn.fnamemodify(vim.uri_to_fname(uri), ':t')
+
+ table.insert(definitions, {
+ name = name,
+ kind = 'definition',
+ content = content,
+ filename = filename,
+ })
+ end
+ end
+ end
+ end
+ end
+ callback(definitions)
+ end)
+
+ if not ok then
+ callback({})
+ end🤖 Prompt for AI Agents
In `@sweep.nvim/lua/sweep/context.lua` around lines 284 - 369, M.get_definitions
currently calls vim.lsp.buf_request which invokes the handler per LSP client and
only checks location.uri (ignoring LocationLink fields), so switch to
vim.lsp.buf_request_all with the same params and a single aggregated handler,
iterate over each client response and each entry in its result list, and for
each location accept either location.uri or location.targetUri and use
location.range or location.targetSelectionRange or location.targetRange;
Preserve existing buffer loading, slicing, name/filename extraction and ensure
you call callback once with the deduplicated/aggregated definitions list.
| --- Get type information at cursor via LSP (async) | ||
| ---@param opts {bufnr: number, row: number, col: number} Options | ||
| ---@param callback fun(type_info: string|nil) Callback with result | ||
| function M.get_type_info(opts, callback) | ||
| local bufnr = opts.bufnr | ||
| local row = opts.row | ||
| local col = opts.col | ||
|
|
||
| -- Check for LSP clients | ||
| local clients = vim.lsp.get_clients({ bufnr = bufnr }) | ||
| if not clients or #clients == 0 then | ||
| callback(nil) | ||
| return | ||
| end | ||
|
|
||
| -- Create LSP position params | ||
| local params = { | ||
| textDocument = { | ||
| uri = vim.uri_from_bufnr(bufnr), | ||
| }, | ||
| position = { | ||
| line = row, | ||
| character = col, | ||
| }, | ||
| } | ||
|
|
||
| -- Request hover info from LSP | ||
| local success, _ = vim.lsp.buf_request(bufnr, 'textDocument/hover', params, function(err, result, ctx) | ||
| if err or not result or not result.contents then | ||
| callback(nil) | ||
| return | ||
| end | ||
|
|
||
| -- Extract type from hover contents | ||
| local contents = result.contents | ||
| local type_info = nil | ||
|
|
||
| if type(contents) == 'string' then | ||
| type_info = contents | ||
| elseif type(contents) == 'table' then | ||
| if contents.value then | ||
| type_info = contents.value | ||
| elseif contents.kind and contents.value then | ||
| type_info = contents.value | ||
| end | ||
| end | ||
|
|
||
| callback(type_info) | ||
| end) | ||
|
|
||
| if not success then | ||
| callback(nil) | ||
| end | ||
| end |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
LSP textDocument/hover response format MarkedString MarkupContent specification
💡 Result:
In the LSP (v3.17 “current”) textDocument/hover response is:
export interface Hover {
contents: MarkedString | MarkedString[] | MarkupContent;
range?: Range;
}[1]
MarkedString (deprecated)
type MarkedString = string | { language: string; value: string };It can be a plain/markdown string, or a {language, value} pair equivalent to a fenced code block. It is deprecated: “use MarkupContent instead.” [1][2]
MarkupContent (preferred)
export type MarkupKind = 'plaintext' | 'markdown';
export interface MarkupContent {
kind: MarkupKind;
value: string;
}If kind is markdown, the content should follow GitHub Flavored Markdown, and clients may sanitize markdown (e.g., strip HTML). [2]
🌐 Web query:
Neovim vim.lsp.buf_request vs buf_request_all multiple clients behavior difference
💡 Result:
vim.lsp.buf_request() and vim.lsp.buf_request_all() both fan out a request to all LSP clients attached to the buffer (unless you explicitly pick a client by calling client:request() yourself). What differs is how the responses are delivered.
vim.lsp.buf_request(bufnr, method, params, handler)
- Sends the request to all attached clients.
- Calls
handler(err, result, ctx)once per client response (so it may run multiple times). - You identify which client replied via
ctx.client_id(the handler context includesclient_id). (neovim.io) - If you need to merge/deduplicate results across clients, you do it yourself inside the handler (tracking how many replies you’ve seen, etc.).
(Neovim’s higher-level vim.lsp.buf.* features historically use this pattern: get attached clients, then send requests via the client request API.) (zignar.net)
vim.lsp.buf_request_all(bufnr, method, params, handler)
- Sends the request to all attached clients.
- Waits for all replies, then calls
handleronce with a combined result:- “Server results are passed as a
client_id:resultmap.” (neovim.io)
- “Server results are passed as a
- The docs explicitly note: parameters are the same as
buf_request(), but the callback/return behavior differs. (cmgriffing.github.io)
Practical implication with multiple clients
- Use
buf_requestwhen you want streaming / per-client handling (e.g., “first reply wins”, or show partial results ASAP). - Use
buf_request_allwhen you want one merged decision point (e.g., combine results, prefer one client, compare responses, etc.).
Citations:
- 1: https://neovim.io/doc/user/lsp?utm_source=openai
- 2: https://zignar.net/2020/10/27/neovim-under-the-hood-understanding-the-language-server-client/?utm_source=openai
- 3: https://neovim.io/doc/user/lsp?utm_source=openai
- 4: https://cmgriffing.github.io/neovim-docs-web/en/lsp/?utm_source=openai
🏁 Script executed:
# Check the actual file in the repository
cd sweep.nvim && find . -name "context.lua" -type fRepository: Kabilan108/dotfiles
Length of output: 86
🏁 Script executed:
# Check the actual context.lua file to see current implementation details
cd sweep.nvim && cat -n lua/sweep/context.lua | sed -n '371,424p'Repository: Kabilan108/dotfiles
Length of output: 1781
🏁 Script executed:
# Search for any usage of get_type_info to understand context
cd sweep.nvim && rg -n "get_type_info" lua/Repository: Kabilan108/dotfiles
Length of output: 175
🏁 Script executed:
# Verify vim.islist exists in Neovim and check context of proposed fix
cd sweep.nvim && rg -i "islist" lua/ || echo "Not found in codebase"Repository: Kabilan108/dotfiles
Length of output: 150
🌐 Web query:
Neovim vim.islist function Lua tables array detection
💡 Result:
In Neovim Lua, the built-in helpers for “array/list detection” are:
vim.isarray(t): true iftis indexed only by integers (may be non-contiguous, e.g.{[1]=1,[3]=3}); empty{}counts as array unless it’s a dict-like table fromvim.empty_dict()/ some API results. [1][2]vim.islist(t): true iftis indexed only by contiguous integers starting at 1 (a “regular array”); empty{}counts as list with the same dict-like exceptions. [1][2]
Example:
vim.islist({1,2,3}) -- true
vim.isarray({[1]=1,[3]=3}) -- true
vim.islist({[1]=1,[3]=3}) -- false
vim.islist({a=1, [1]=1}) -- falseCompatibility note: older configs/plugins often used vim.tbl_islist() (now deprecated in favor of vim.islist() in newer Neovim). A common shim is: [3]
local islist = vim.islist or vim.tbl_islistSources: [1] Neovim :help lua (lua.html) docs for vim.isarray() / vim.islist(); [2] vimhelp mirror of lua.txt; [3] community discussion showing the vim.islist vs vim.tbl_islist compatibility pattern.
🌐 Web query:
Neovim vim.lsp.buf_request_all results table structure format client_id err result
💡 Result:
vim.lsp.buf_request_all(bufnr, method, params, handler) calls your handler once, after all attached clients have replied, with a single argument:
handler(results)Where results is a map keyed by client id:
---@type table<integer, { err: lsp.ResponseError?, result: any }>
results = {
[client_id] = { err = <ResponseError|nil>, result = <any> },
...
}So you iterate it like:
vim.lsp.buf_request_all(0, method, params, function(results)
for client_id, resp in pairs(results) do
local err = resp.err
local result = resp.result
end
end)Notes:
- The per-client error field inside the handler result was renamed from
error→err(update old code accordingly). [2] - The docs describe this as “a
client_id:resultmap” forbuf_request_all(). [1]
Sources: Neovim :help lsp / vim.lsp.buf_request_all() docs [1], Neovim :help deprecated (LSP section) [2].
Aggregate responses from multiple LSP clients and handle MarkedString[] array format in hover contents.
vim.lsp.buf_request invokes the handler once per attached client, which causes the callback to fire multiple times. Additionally, LSP textDocument/hover responses can contain MarkedString[] (array of strings or {language, value} pairs), which the current code doesn't handle—it returns nil instead of aggregating the contents. Use vim.lsp.buf_request_all to collect all client responses into a single handler invocation, and normalize array-style contents by concatenating parts.
🐛 Proposed fix
- local success, _ = vim.lsp.buf_request(bufnr, 'textDocument/hover', params, function(err, result, ctx)
- if err or not result or not result.contents then
- callback(nil)
- return
- end
-
- -- Extract type from hover contents
- local contents = result.contents
- local type_info = nil
-
- if type(contents) == 'string' then
- type_info = contents
- elseif type(contents) == 'table' then
- if contents.value then
- type_info = contents.value
- elseif contents.kind and contents.value then
- type_info = contents.value
- end
- end
-
- callback(type_info)
- end)
-
- if not success then
- callback(nil)
- end
+ local ok = vim.lsp.buf_request_all(bufnr, 'textDocument/hover', params, function(results)
+ local type_info
+ if results then
+ for _, res in pairs(results) do
+ local result = res.result
+ local contents = result and result.contents
+ if contents then
+ if type(contents) == 'string' then
+ type_info = contents
+ elseif type(contents) == 'table' then
+ if contents.value then
+ type_info = contents.value
+ elseif vim.islist(contents) then
+ local parts = {}
+ for _, item in ipairs(contents) do
+ if type(item) == 'string' then
+ table.insert(parts, item)
+ elseif type(item) == 'table' and item.value then
+ table.insert(parts, item.value)
+ end
+ end
+ if `#parts` > 0 then
+ type_info = table.concat(parts, '\n')
+ end
+ end
+ end
+ end
+ if type_info and type_info ~= '' then
+ break
+ end
+ end
+ end
+ callback(type_info)
+ end)
+
+ if not ok then
+ callback(nil)
+ end🤖 Prompt for AI Agents
In `@sweep.nvim/lua/sweep/context.lua` around lines 371 - 424, The get_type_info
function currently calls vim.lsp.buf_request which invokes the handler per LSP
client and doesn't handle MarkedString[] arrays; change M.get_type_info to use
vim.lsp.buf_request_all (passing the same params) so you receive a single table
of responses, iterate responses to aggregate successful results from all
clients, normalize hover contents for each response by handling string,
MarkedString (table with .value or {language,value}), and MarkedString[] (array
of strings or tables) by concatenating their textual parts into one type_info
string per response, then combine/choose the best non-empty aggregated type_info
(e.g., join with "\n" or take first non-empty) and call callback exactly once
with that final string or nil.
| --- Stop tracking a buffer | ||
| ---@param bufnr number | ||
| function M.detach(bufnr) | ||
| attached_buffers[bufnr] = nil | ||
| buffer_content[bufnr] = nil | ||
| -- Note: nvim_buf_attach with on_detach handles cleanup automatically | ||
| -- when buffer is deleted. Manual detach is for explicit stop tracking. | ||
| end |
There was a problem hiding this comment.
Incomplete detachment: buffer attachment callback continues running.
The detach function clears local state but doesn't actually stop the nvim_buf_attach callback from firing. Neovim's buffer attach API requires returning true from on_lines to detach. This means if detach is called and then attach is called again on the same buffer, edits may be recorded twice.
🔧 Suggested fix with detach flag
---@type table<number, boolean> Track which buffers are attached
-local attached_buffers = {}
+local attached_buffers = {} -- true = attached, 'detaching' = pending detach
...
on_lines = function(_, buf, _, first_line, last_line, new_last_line, _, _, _)
+ -- Check if we should detach
+ if attached_buffers[buf] == 'detaching' then
+ attached_buffers[buf] = nil
+ buffer_content[buf] = nil
+ return true -- Return true to detach
+ end
+
-- Get the old content from our cache
...
function M.detach(bufnr)
- attached_buffers[bufnr] = nil
- buffer_content[bufnr] = nil
+ if attached_buffers[bufnr] then
+ attached_buffers[bufnr] = 'detaching'
+ end
-- Note: nvim_buf_attach with on_detach handles cleanup automatically
-- when buffer is deleted. Manual detach is for explicit stop tracking.
end🤖 Prompt for AI Agents
In `@sweep.nvim/lua/sweep/edits.lua` around lines 241 - 248, The detach
implementation currently only clears local tables (attached_buffers and
buffer_content) but doesn't stop Neovim's buffer callbacks, so change M.detach
to explicitly detach the buffer via vim.api.nvim_buf_detach(bufnr) (after
checking vim.api.nvim_buf_is_loaded or that attached_buffers[bufnr] exists) and
then clear attached_buffers[bufnr] and buffer_content[bufnr]; alternatively, if
you prefer the existing on_lines-based approach, set a detach flag on the
attached_buffers[bufnr] entry that the on_lines callback checks and returns true
to stop, but ensure the code path that calls nvim_buf_attach (the attach
function) and the on_lines handler respect that flag to avoid duplicate
recordings.
| --- Check if completion is currently visible | ||
| ---@return boolean | ||
| function M.is_visible() | ||
| return state.mark_id ~= nil and state.bufnr ~= nil | ||
| end |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's locate the file and read the relevant section
find . -name "ui.lua" -path "*/sweep/*" | head -5Repository: Kabilan108/dotfiles
Length of output: 92
🏁 Script executed:
# Read the ui.lua file to see the function and surrounding context
cat -n ./sweep.nvim/lua/sweep/ui.lua | head -160Repository: Kabilan108/dotfiles
Length of output: 4783
🏁 Script executed:
# Search for any buffer event handlers or cleanup routines
rg -n "BufWipeout\|BufDelete\|BufUnload\|nvim_create_autocmd\|nvim_buf_attach" ./sweep.nvim --type luaRepository: Kabilan108/dotfiles
Length of output: 45
🏁 Script executed:
# Search for all usages of is_visible function
rg -n "is_visible" ./sweep.nvim --type lua -B 2 -A 2Repository: Kabilan108/dotfiles
Length of output: 4928
🏁 Script executed:
# Let's check the test file to understand the expected behavior better
cat -n ./sweep.nvim/tests/sweep/ui_spec.lua | sed -n '228,260p'Repository: Kabilan108/dotfiles
Length of output: 1062
🏁 Script executed:
# Check how get_current is used after is_visible check
rg -n "get_current" ./sweep.nvim --type lua -B 3 -A 3Repository: Kabilan108/dotfiles
Length of output: 11126
🏁 Script executed:
# Check if there are any tests for buffer wipe/delete scenarios
rg -n "BufWipeout\|BufDelete\|nvim_buf_is_valid" ./sweep.nvim/tests --type luaRepository: Kabilan108/dotfiles
Length of output: 45
🏁 Script executed:
# Check the complete test file to see if buffer validity is tested
cat -n ./sweep.nvim/tests/sweep/ui_spec.lua | grep -A 5 -B 5 "valid\|wipe\|delete"Repository: Kabilan108/dotfiles
Length of output: 441
Guard is_visible against invalid buffers.
If a buffer is wiped while a completion is shown, state.bufnr and state.mark_id remain set, causing is_visible() to return true even though the buffer is no longer valid. This affects get_current(), which depends on is_visible() to determine if state data is safe to return. Add a buffer-validity check to prevent returning data for destroyed buffers.
🛠️ Suggested fix
function M.is_visible()
- return state.mark_id ~= nil and state.bufnr ~= nil
+ if state.mark_id == nil or state.bufnr == nil then
+ return false
+ end
+ return vim.api.nvim_buf_is_valid(state.bufnr)
end🤖 Prompt for AI Agents
In `@sweep.nvim/lua/sweep/ui.lua` around lines 136 - 140, The is_visible()
function currently returns true if state.mark_id and state.bufnr are non-nil
even when the buffer has been wiped; update M.is_visible to also verify the
buffer is still valid by checking vim.api.nvim_buf_is_valid(state.bufnr) (or
equivalent) before returning true so that it returns false for destroyed
buffers; ensure callers like get_current() relying on M.is_visible() will no
longer receive stale state from state.bufnr/state.mark_id when the underlying
buffer is invalid.
| if line ~= '' or #lines == 0 then | ||
| table.insert(lines, line) | ||
| end |
There was a problem hiding this comment.
🔴 split_lines function drops empty lines from middle of content
The split_lines function in parser.lua incorrectly discards empty lines from the middle of completion content.
Click to expand
Mechanism
The function at line 14 uses the condition if line ~= '' or #lines == 0. This means:
- Empty strings are only inserted if the
linesarray is empty (first iteration) - Subsequent empty strings (blank lines in the middle of content) are skipped
For input "line1\n\nline2":
- gmatch captures:
"line1","","line2" "line1"is inserted (line ~= '')""is NOT inserted (line == '' AND #lines > 0)"line2"is inserted (line ~= '')- Result:
{"line1", "line2"}— the blank line is lost
Impact
Multi-line completions from the LLM that contain intentional blank lines (e.g., between function definitions, paragraph breaks in docstrings) will have those blank lines stripped, resulting in malformed/incorrect code being inserted.
Recommendation: Change the condition to always insert lines, or use a simpler split approach like vim.split(str, '\n', { plain = true }) which preserves empty lines.
Was this helpful? React with 👍 or 👎 to provide feedback.
| add_visible_to_ring('buffer_enter') | ||
|
|
||
| -- Attach edit tracking for next-edit prediction | ||
| local edits = require('sweep.edits') | ||
| edits.attach(bufnr) |
There was a problem hiding this comment.
🔴 Keymaps not set up for buffers entered after plugin is enabled
Completion keymaps (Tab to accept, C-l for line, etc.) only work in the buffer that was active when sweep.enable() was called.
Click to expand
Mechanism
In sweep.nvim/lua/sweep/init.lua:63-64, enable() calls:
require('sweep.autocmds').setup()
require('sweep.keymaps').setup()The keymaps.setup() function (sweep.nvim/lua/sweep/keymaps.lua:145-155) only sets up keymaps for the current buffer:
function M.setup()
local bufnr = vim.api.nvim_get_current_buf()
teardown_buffer_keymaps(bufnr)
setup_buffer_keymaps(bufnr)
is_setup_flag = true
endThe autocmds module (sweep.nvim/lua/sweep/autocmds.lua:94-114) has a BufEnter handler that attaches edit tracking but does NOT call keymaps.setup_buffer().
Impact
When a user enables sweep in one buffer then switches to another file, the Tab key and other keymaps won't work to accept completions. Completions will appear but cannot be accepted via keyboard shortcuts, severely impacting usability.
Recommendation: Add require('sweep.keymaps').setup_buffer(bufnr) to the on_buf_enter() function after the edit tracking attachment, so keymaps are set up for each new buffer entered.
Was this helpful? React with 👍 or 👎 to provide feedback.
3dc9eb5 to
459925b
Compare
Summary
This PR introduces sweep.nvim, a new Neovim plugin for AI-powered autocomplete using Sweep AI's 1.5B parameter next-edit model via llama.cpp.
Key Features
Architecture (14 modules)
init, config, completion, http, fim, parser, ui, ring, context, cache, edits, autocmds, keymaps, debug
Prompt Structure
<edit><original>...</updated></edit>format)Code Quality
Test Plan
llama-server -m sweep-next-edit-1.5B.Q4_K_M.gguf --port 8012:SweepDebugoutputSummary by CodeRabbit
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.