Skip to content

Add sweep.nvim: AI autocomplete plugin using Sweep's next-edit model#3

Open
Kabilan108 wants to merge 19 commits intonixosfrom
claude/neovim-sweep-autocomplete-IY2GC
Open

Add sweep.nvim: AI autocomplete plugin using Sweep's next-edit model#3
Kabilan108 wants to merge 19 commits intonixosfrom
claude/neovim-sweep-autocomplete-IY2GC

Conversation

@Kabilan108
Copy link
Owner

@Kabilan108 Kabilan108 commented Jan 24, 2026

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

  • Ghost text completions via extmarks
  • Rich context gathering: LSP definitions (async), treesitter scope, ring buffer, edit tracking
  • Performance: LRU cache, prompt caching, debouncing, race condition handling
  • Configurable keymaps matching llama.vim setup
  • Debug pane for inspecting stats

Architecture (14 modules)

init, config, completion, http, fim, parser, ui, ring, context, cache, edits, autocmds, keymaps, debug

Prompt Structure

  1. Edit history (<edit><original>...</updated></edit> format)
  2. Ring buffer (cross-file context)
  3. LSP/Treesitter (definitions, scope, imports)
  4. File prefix → [CURSOR] → File suffix

Code Quality

  • Test-driven development with plenary.test
  • Multiple code reviews fixing: race conditions, deduplication bugs, dead code

Test Plan

  • Run llama-server -m sweep-next-edit-1.5B.Q4_K_M.gguf --port 8012
  • Verify completions appear on typing
  • Test accept keybindings (full/line/word)
  • Check :SweepDebug output

Summary by CodeRabbit

  • New Features

    • Added sweep.nvim, an AI autocomplete plugin offering intelligent code completions with ghost-text display.
    • Supports configurable keymaps for triggering, accepting, and dismissing suggestions (full line, single word).
    • Integrated caching and context extraction using editor history and language features.
    • Includes debugging interface for monitoring completion metrics and plugin status.
  • Documentation

    • Added comprehensive README and technical analysis for sweep.nvim setup and usage.

✏️ Tip: You can customize this high-level summary in your review settings.


Open with Devin

claude added 19 commits January 24, 2026 01:29
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
@coderabbitai
Copy link

coderabbitai bot commented Jan 24, 2026

📝 Walkthrough

Walkthrough

A complete new Neovim plugin sweep.nvim is added as a replacement for llama.vim, featuring AI-powered code autocomplete with context extraction, HTTP backend integration, caching, edit tracking, LSP support, and comprehensive testing.

Changes

Cohort / File(s) Summary
Plugin Configuration
config/nvim/lua/plugins.lua
Replaces llama.vim plugin declaration with sweep.nvim, removing llama highlight setup (llama_hl_hint) and adding sweep-specific config including debounce, keymaps, server endpoints, context options, and UI highlight groups (SweepGhostText, SweepInfo).
Documentation
sweep-ai-autocomplete-analysis.md, sweep.nvim/README.md
Adds comprehensive technical analysis document covering architecture, PSI context building, prompt formats, model training, inference optimizations, and Neovim integration guidance. Also adds user-facing README with installation, configuration, commands, keymaps, and llama.cpp setup instructions.
Core Modules: Configuration & Setup
sweep.nvim/lua/sweep/init.lua, sweep.nvim/lua/sweep/config.lua, sweep.nvim/lua/sweep/plugin/sweep.lua
Implements main plugin entry point with lifecycle methods (setup, enable, disable, toggle), configuration module with typed defaults and deep-merge, and plugin loader defining five user commands (SweepEnable, SweepDisable, SweepToggle, SweepStatus, SweepDebug).
Core Modules: Completion Logic
sweep.nvim/lua/sweep/completion.lua, sweep.nvim/lua/sweep/fim.lua, sweep.nvim/lua/sweep/parser.lua
Orchestrates completion requests with debouncing and cancellation; builds Fill-in-Middle prompts with context extraction; parses llama.cpp server responses with stop-token handling, whitespace trimming, and latency tracking.
Core Modules: Context & Integration
sweep.nvim/lua/sweep/context.lua, sweep.nvim/lua/sweep/edits.lua, sweep.nvim/lua/sweep/ring.lua
Provides LSP/treesitter-based context extraction (definitions, type info, scope, imports) via async API; tracks buffer edits with structured history and diff formatting; maintains cross-file context ring buffer with duplicate detection and LRU eviction.
Core Modules: Networking & Caching
sweep.nvim/lua/sweep/http.lua, sweep.nvim/lua/sweep/cache.lua
Implements async HTTP POST client with timeout handling, request cancellation, and Neovim event-loop integration; provides LRU cache with configurable TTL, expiry logic, and statistics tracking.
Core Modules: UI & Interactions
sweep.nvim/lua/sweep/ui.lua, sweep.nvim/lua/sweep/autocmds.lua, sweep.nvim/lua/sweep/keymaps.lua
Renders ghost text completions via extmarks with optional latency/token info; manages autocmds for completion triggers (CursorMovedI, InsertLeave, BufEnter, TextYankPost) and buffer lifecycle; provides insert-mode keymaps for trigger and completion acceptance (full/line/word).
Core Modules: Debugging
sweep.nvim/lua/sweep/debug.lua
Provides floating debug pane aggregating plugin state (enabled, completion, cache stats, ring stats, config) with formatted human-friendly output and logging support.
Tests: Unit Tests
sweep.nvim/tests/sweep/*_spec.lua (12 test modules)
Comprehensive test suite covering all modules with mocked Neovim APIs, LSP, treesitter, HTTP, and timers. Validates setup/teardown, lifecycle, caching, context extraction, parsing, UI rendering, keymap behavior, and edge cases.
Tests: Test Infrastructure
sweep.nvim/tests/minimal_init.lua
Minimal Neovim initialization script for tests, configuring runtimepath, locating plenary.nvim dependency, and disabling swapfiles.

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()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A hop, a skip, through code we'd lept,
Where Sweep autocomplete now steps ahead.
With context gathered, prompts held tight,
Ghost text glimmers in the editor's light! ✨
Llama's gone, but the magic's not—
Sweep.nvim brings the brains we sought!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main addition: a new AI autocomplete plugin (sweep.nvim) for Neovim using Sweep's next-edit model.
Docstring Coverage ✅ Passed Docstring coverage is 98.57% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 text or 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 ```text or ```python
  • Lines 222-256: ASCII diagram → use ```text

This is a documentation quality improvement and can be deferred.

sweep.nvim/tests/minimal_init.lua (1)

8-27: Fail fast when plenary.nvim is 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
+  end

Also applies to: 110-155

sweep.nvim/tests/sweep/autocmds_spec.lua (1)

12-66: Consider clearing mocked modules in after_each to avoid cross-test leakage.
Mocks remain in package.loaded after 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 in split_lines with consecutive newlines.

The condition on line 14 line ~= '' or #lines == 0 discards 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: Unused bufnr parameter — consider passing it to add_visible_to_ring.

The on_buf_leave function receives bufnr from args.buf but doesn't use it. While nvim_get_current_buf() returns the correct buffer during BufLeave, explicitly passing bufnr would 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)
 end

Then update add_visible_to_ring to accept an optional bufnr parameter:

local function add_visible_to_ring(source, bufnr)
  bufnr = bufnr or vim.api.nvim_get_current_buf()
  -- ...rest of function
end
sweep.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_LEN configurable if users report issues.

sweep.nvim/lua/sweep/keymaps.lua (1)

159-167: Potential issue: Modifying table during iteration.

Iterating over buffer_keymaps with pairs() while teardown_buffer_keymaps sets entries to nil can cause undefined behavior in Lua. The subsequent buffer_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
 end
sweep.nvim/lua/sweep/init.lua (1)

127-139: Inconsistent notification behavior with show_info config.

The clear_cache and clear_ring functions always emit notifications, while enable/disable respect the config.options.show_info setting. 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
 end
sweep.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
         end

Note: 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_order and touch are O(n) operations. With the default max_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.

Comment on lines +442 to +470
```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
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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
 end

Note: 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.

Comment on lines +496 to +528
```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
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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).

Comment on lines +41 to +87
---@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',
},
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 lua

Repository: 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 20

Repository: 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.lua

Repository: Kabilan108/dotfiles

Length of output: 164


🏁 Script executed:

#!/bin/bash
cat -n sweep.nvim/lua/sweep/config.lua

Repository: 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.lua

Repository: 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.lua

Repository: 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.

Comment on lines +284 to +369
--- 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 errorerr. [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]
  • errorerr rename for buf_request_all aggregated results: Neovim :help deprecated [3]

Sources:
[1] (git.api.net.tw)
[2] (git.api.net.tw)
[3] (neovim.io)

Citations:


🏁 Script executed:

# Search for get_async function in the codebase to understand the pending counter impact
rg -i "get_async|pending" --type lua

Repository: 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 optional originSelectionRange?: 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
    And LocationLink[] is available (introduced in LSP 3.14.0) when the client capability textDocument.definition.linkSupport is true.
    [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.lua

Repository: 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 -80

Repository: 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.lua

Repository: 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.lua

Repository: 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.lua

Repository: 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.

Comment on lines +371 to +424
--- 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 includes client_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 handler once with a combined result:
    • “Server results are passed as a client_id:result map.” (neovim.io)
  • 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_request when you want streaming / per-client handling (e.g., “first reply wins”, or show partial results ASAP).
  • Use buf_request_all when you want one merged decision point (e.g., combine results, prefer one client, compare responses, etc.).

Citations:


🏁 Script executed:

# Check the actual file in the repository
cd sweep.nvim && find . -name "context.lua" -type f

Repository: 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 if t is 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 from vim.empty_dict() / some API results. [1][2]
  • vim.islist(t): true if t is 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})         -- false

Compatibility 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_islist

Sources: [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 errorerr (update old code accordingly). [2]
  • The docs describe this as “a client_id:result map” for buf_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.

Comment on lines +241 to +248
--- 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +136 to +140
--- Check if completion is currently visible
---@return boolean
function M.is_visible()
return state.mark_id ~= nil and state.bufnr ~= nil
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's locate the file and read the relevant section
find . -name "ui.lua" -path "*/sweep/*" | head -5

Repository: 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 -160

Repository: 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 lua

Repository: 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 2

Repository: 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 3

Repository: 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 lua

Repository: 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.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View issues and 6 additional flags in Devin Review.

Open in Devin Review

Comment on lines +14 to +16
if line ~= '' or #lines == 0 then
table.insert(lines, line)
end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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 lines array 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +109 to +113
add_visible_to_ring('buffer_enter')

-- Attach edit tracking for next-edit prediction
local edits = require('sweep.edits')
edits.attach(bufnr)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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
end

The 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@Kabilan108 Kabilan108 force-pushed the nixos branch 2 times, most recently from 3dc9eb5 to 459925b Compare February 2, 2026 04:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants