-
Notifications
You must be signed in to change notification settings - Fork 2
Context improvements #52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…play together with input
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements comprehensive context management improvements for the ECA Neovim plugin. The changes introduce a new context system that allows users to add files, code selections, and web URLs as context for AI chat sessions, with an improved UI showing contexts inline using extmarks and support for context completion via @ and # prefixes.
Key Changes:
- Introduces new
EcaChat*command variants (e.g.,EcaChatAddFile,EcaChatAddSelection,EcaChatAddUrl) with deprecation warnings for old commands - Implements state-based context management with mediator pattern for better separation of concerns
- Adds context area UI in the input buffer using Neovim extmarks to display active contexts without modifying buffer text
- Supports
@(inline content) and#(path reference) prefixes for different context handling semantics
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_select_commands.lua | Adds sidebar to global Eca.sidebars table for test setup |
| tests/test_context_commands.lua | New comprehensive test suite for all context-related commands and deprecation warnings |
| tests/test_context_area.lua | New test suite for context area UI behavior including deletion, preservation of input text |
| plugin-spec.lua | Updates command list with new EcaChat* variants and removes old command references |
| lua/eca/types.lua | Adds type definitions for context structures (Cursor, Directory, File, Web, Context) |
| lua/eca/state.lua | Implements context storage and management methods in State class |
| lua/eca/sidebar.lua | Major refactor: removes old container-based context display, adds extmark-based inline context UI, implements input event handling for context manipulation |
| lua/eca/mediator.lua | Adds context adapter to transform internal context format to server protocol format, delegates context operations to State |
| lua/eca/config.lua | Adds web_context_max_len configuration for truncating URL display |
| lua/eca/completion/context.lua | Updates completion pattern to support both @ and # prefixes, adds execute callback for post-selection handling |
| lua/eca/completion/commands.lua | Updates to use mediator-based chat access pattern |
| lua/eca/completion/cmp/context.lua | Removes @ prefix from completion labels, adds # as trigger character, implements execute method |
| lua/eca/completion/blink/context.lua | Same changes as CMP source for Blink completion framework |
| lua/eca/commands.lua | Implements new EcaChat* commands with deprecation handling for old commands |
| lua/eca/api.lua | Refactors context API to use mediator pattern, adds web context support, removes old TODO/selected code functions |
| docs/usage.md | Comprehensive documentation update explaining new context commands, UI behavior, and @ vs # semantics |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| -- contexts area and input handler | ||
| vim.api.nvim_buf_attach(container.bufnr, false, { | ||
| on_lines = function(_, buf, _changedtick, first, _last, _new_last, _bytecount) | ||
| vim.schedule(function() |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The on_lines callback in vim.api.nvim_buf_attach wraps all logic in vim.schedule, but the callback is already executed asynchronously. This double-scheduling could cause race conditions or delayed updates. Consider whether the vim.schedule is necessary here, or if specific operations inside should be scheduled individually.
| local item = {} | ||
| if context.type == "file" then | ||
| item.label = string.format("@%s", vim.fn.fnamemodify(context.path, ":.")) | ||
| item.label = vim.fn.fnamemodify(context.path, ":.") |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The @ prefix is removed from the completion item label, but the documentation in the PR description states that @ and # have different semantics (inline content vs path reference). Without the prefix in the label, users cannot tell which type of context they're adding. Consider keeping the prefix in the label or using a different indicator (e.g., different completion item kinds or icons).
lua/eca/mediator.lua
Outdated
| if callback then | ||
| callback("Server is not running, please start the server", nil) | ||
| end | ||
| require("eca.logger").notify("Server is not rnning, please start the server", vim.log.levels.WARN) |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The typo "rnning" should be "running" in the error message.
| require("eca.logger").notify("Server is not rnning, please start the server", vim.log.levels.WARN) | |
| require("eca.logger").notify("Server is not running, please start the server", vim.log.levels.WARN) |
| local it = before_cursor:gmatch("[@#]([%w%./_\\%-~]*)") | ||
| for match in it do | ||
| table.insert(matches, match) |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pattern [@#]([%w%./_\\%-~]*) triggers completion for both @ and # prefixes, but the actual prefix character is not captured. This means downstream code cannot distinguish whether the user typed @ (inline content) or # (path reference). Consider capturing the prefix: ([@#])([%w%./_\\%-~]*) and passing it along with the query.
| local it = before_cursor:gmatch("[@#]([%w%./_\\%-~]*)") | |
| for match in it do | |
| table.insert(matches, match) | |
| local it = before_cursor:gmatch("([@#])([%w%./_\\%-~]*)") | |
| for prefix, match in it do | |
| table.insert(matches, { prefix = prefix, query = match }) |
lua/eca/sidebar.lua
Outdated
| local contexts = self:get_contexts() | ||
| local replaced = message:gsub("([@#])([%w%._%-%/]+)", function(prefix, path) | ||
| -- expand ~ | ||
| if path:sub(1,1) == "~" then |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The path:sub(1,1) check for ~ expansion is fragile. Use vim.startswith(path, "~") for better readability and consistency with other parts of the codebase (e.g., line 862 uses vim.startswith).
| if path:sub(1,1) == "~" then | |
| if vim.startswith(path, "~") then |
lua/eca/sidebar.lua
Outdated
| self:_add_message("user", message) | ||
|
|
||
| local contexts = self:get_contexts() | ||
| local replaced = message:gsub("([@#])([%w%._%-%/]+)", function(prefix, path) |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pattern ([@#])([%w%._%-%/]+) captures file paths but doesn't handle Windows-style paths with backslashes. The original pattern on line 10 of lua/eca/completion/context.lua includes \\ in the character class, but this regex only has /. For consistency and Windows compatibility, include \\ in this pattern as well: ([@#])([%w%._%-%/\\]+).
| local replaced = message:gsub("([@#])([%w%._%-%/]+)", function(prefix, path) | |
| local replaced = message:gsub("([@#])([%w%._%-%/\\]+)", function(prefix, path) |
lua/eca/sidebar.lua
Outdated
| self:_add_message("user", message) | ||
|
|
||
| local contexts = self:get_contexts() | ||
| local replaced = message:gsub("([@#])([%w%._%-%/]+)", function(prefix, path) |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The capture pattern doesn't match the tilde (~) character for home directory expansion, even though it's explicitly checked and expanded on lines 1061-1063. Add ~ to the pattern: ([@#])([%w%._%-%/\\~]+) to ensure paths starting with ~ are properly captured.
| local replaced = message:gsub("([@#])([%w%._%-%/]+)", function(prefix, path) | |
| local replaced = message:gsub("([@#])([%w%._%-%/\\~]+)", function(prefix, path) |
lua/eca/sidebar.lua
Outdated
|
|
||
| -- Store the last user message to avoid duplication | ||
| self._last_user_message = message | ||
| if not message or not type(message) == "string" then |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type guard type(message) == "string" will always be true when message is not nil because not has higher precedence than and. This condition should be if not message or type(message) ~= "string" then to properly validate the message parameter.
| if not message or not type(message) == "string" then | |
| if not message or type(message) ~= "string" then |
lua/eca/sidebar.lua
Outdated
| local row = 1 + (not clear and existing_lines and #existing_lines > 0 and #existing_lines or 1) | ||
| local col = #prefix + (not clear and existing_lines and #existing_lines > 0 and #existing_lines[#existing_lines] or 0) |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cursor row calculation 1 + (not clear and existing_lines and #existing_lines > 0 and #existing_lines or 1) has a logic error. When the condition is true, it evaluates to 1 + #existing_lines, but when false, the chain returns false (not 1), resulting in 1 + false. The proper fix is to add parentheses around the conditional: 1 + ((not clear and existing_lines and #existing_lines > 0) and #existing_lines or 1).
| local row = 1 + (not clear and existing_lines and #existing_lines > 0 and #existing_lines or 1) | |
| local col = #prefix + (not clear and existing_lines and #existing_lines > 0 and #existing_lines[#existing_lines] or 0) | |
| local row = 1 + ((not clear and existing_lines and #existing_lines > 0) and #existing_lines or 1) | |
| local col = #prefix + ((not clear and existing_lines and #existing_lines > 0) and #existing_lines[#existing_lines] or 0) |
lua/eca/sidebar.lua
Outdated
| if vim.api.nvim_win_is_valid(input.winid) then | ||
| vim.api.nvim_win_set_cursor(input.winid, { 1, #prefix }) | ||
| local row = 1 + (not clear and existing_lines and #existing_lines > 0 and #existing_lines or 1) | ||
| local col = #prefix + (not clear and existing_lines and #existing_lines > 0 and #existing_lines[#existing_lines] or 0) |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expression #prefix + (not clear and existing_lines and #existing_lines > 0 and #existing_lines[#existing_lines] or 0) uses and operators which will return the last truthy value, not a boolean. When not clear and existing_lines and #existing_lines > 0 is true, this evaluates to #prefix + #existing_lines[#existing_lines], but when false, the entire chain evaluates to false (not 0), resulting in attempting to compute #prefix + false which may cause unexpected behavior. Consider using proper conditional logic with parentheses: #prefix + ((not clear and existing_lines and #existing_lines > 0) and #existing_lines[#existing_lines] or 0).
| local col = #prefix + (not clear and existing_lines and #existing_lines > 0 and #existing_lines[#existing_lines] or 0) | |
| local col = #prefix + ((not clear and existing_lines and #existing_lines > 0 and #existing_lines[#existing_lines]) or 0) |
Adding Context
Current file
Adds the current buffer as a file context for the active chat.
Specific file
Pass a path to add that file as context. Relative paths are resolved to absolute paths.
Code selection
v,V, orCtrl+v):EcaChatAddSelectionWeb URLs
Prompts for a URL and adds it as a
webcontext. The URL label in the input is truncated for display, but the full URL is sent to the server.Listing and clearing contexts
Multiple files
Context area in the input
When the sidebar is open, the chat input buffer has two parts:
sidebar.lua,sidebar.lua:25-50or a truncated URL).>(configurable viawindows.input.prefix).You normally do not need to edit the first line manually, but you can:
Examples
No contexts yet
Single file context
Two contexts (file + line range)
If you now delete just the
sidebar.lua:25-50label on the first line, only that context is removed:If instead you delete the entire first line, all contexts are cleared. ECA recreates an empty context line internally and keeps your input text:
When typing paths directly with
@to trigger completion, the input might briefly look like:After confirming a completion item, that
@...reference is turned into a context entry and shown as a short label (for examplesidebar.lua) in the context area.Context completion and
@/#path shortcutsInside the input (filetype
eca-input):@or#followed by part of a path triggers context completion (via the providedcmp/blinksources).Semantics of the two prefixes:
@prefix – inline content:@path/to/file.luameans: "resolve this to the file contents and send those contents to the model".@reference to the actual file content before forming the prompt.#prefix – path reference:#path/to/file.luameans: "send the full absolute path; the model will fetch and read the file itself".In both cases, when you send a message any occurrences like:
are first expanded to absolute paths on the Neovim side (including
~expansion). The difference is how the server then interprets@(inline file contents) versus#(path-only reference that the model resolves).