Skip to content

Conversation

@joaopluigi
Copy link
Contributor

@joaopluigi joaopluigi commented Oct 14, 2025

Adding Context

Current file

:EcaChatAddFile

Adds the current buffer as a file context for the active chat.

Specific file

:EcaChatAddFile src/main.lua
:EcaChatAddFile /full/path/to/file.js

Pass a path to add that file as context. Relative paths are resolved to absolute paths.

Code selection

  1. Select code in visual mode (v, V, or Ctrl+v)
  2. Run :EcaChatAddSelection
  3. The selected lines will be added as a file-range context (file + line range)

Web URLs

:EcaChatAddUrl

Prompts for a URL and adds it as a web context. The URL label in the input is truncated for display, but the full URL is sent to the server.

Listing and clearing contexts

:EcaChatListContexts      " show all active contexts
:EcaChatClearContexts     " remove all contexts from the current chat
:EcaChatRemoveFile        " remove the current file from contexts

Multiple files

:EcaChatAddFile
:EcaChatAddFile src/utils.lua
:EcaChatAddFile src/config.lua
:EcaChatAddFile tests/test_utils.lua

Context area in the input

When the sidebar is open, the chat input buffer has two parts:

  1. First line – context area: shows one label per active context (e.g. sidebar.lua , sidebar.lua:25-50 or a truncated URL).
  2. Below that – message input: your prompt, prefixed by > (configurable via windows.input.prefix).

You normally do not need to edit the first line manually, but you can:

  • Remove a single context: move the cursor to the corresponding label on the first line and delete it; the context is removed from the current chat while your message text is preserved.
  • Clear all contexts: delete the whole first line; ECA restores an empty context line and clears all contexts.

Examples

No contexts yet

@
> Explain this code

Single file context

@sidebar.lua @
> Explain this code

Two contexts (file + line range)

@sidebar.lua @sidebar.lua:25-50 @
> Explain this selection

If you now delete just the sidebar.lua:25-50 label on the first line, only that context is removed:

@sidebar.lua @
> Explain this selection

If instead you delete the entire first line, all contexts are cleared. ECA recreates an empty context line internally and keeps your input text:

@
> Explain this selection

When typing paths directly with @ to trigger completion, the input might briefly look like:

@lua/eca/sidebar.lua
> Input text

After confirming a completion item, that @... reference is turned into a context entry and shown as a short label (for example sidebar.lua ) in the context area.

Context completion and @ / # path shortcuts

Inside the input (filetype eca-input):

  • Typing @ or # followed by part of a path triggers context completion (via the provided cmp/blink sources).
  • Selecting a completion item in the context area line automatically adds that item as a context for the current chat and shows it as a label on the first line.

Semantics of the two prefixes:

  • @ prefixinline content:
    • @path/to/file.lua means: "resolve this to the file contents and send those contents to the model".
    • The server expands the @ reference to the actual file content before forming the prompt.
  • # prefixpath reference:
    • #path/to/file.lua means: "send the full absolute path; the model will fetch and read the file itself".
    • The server keeps it as a path reference in the prompt so the model can look up the file by path.

In both cases, when you send a message any occurrences like:

@relative/path/to/file.lua
#another/path

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


@joaopluigi joaopluigi marked this pull request as ready for review December 1, 2025 19:31
Copilot AI review requested due to automatic review settings December 1, 2025 19:31
Copy link
Contributor

Copilot AI left a 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()
Copy link

Copilot AI Dec 1, 2025

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.

Copilot uses AI. Check for mistakes.
local item = {}
if context.type == "file" then
item.label = string.format("@%s", vim.fn.fnamemodify(context.path, ":."))
item.label = vim.fn.fnamemodify(context.path, ":.")
Copy link

Copilot AI Dec 1, 2025

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

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Dec 1, 2025

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +10 to 12
local it = before_cursor:gmatch("[@#]([%w%./_\\%-~]*)")
for match in it do
table.insert(matches, match)
Copy link

Copilot AI Dec 1, 2025

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.

Suggested change
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 })

Copilot uses AI. Check for mistakes.
local contexts = self:get_contexts()
local replaced = message:gsub("([@#])([%w%._%-%/]+)", function(prefix, path)
-- expand ~
if path:sub(1,1) == "~" then
Copy link

Copilot AI Dec 1, 2025

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

Suggested change
if path:sub(1,1) == "~" then
if vim.startswith(path, "~") then

Copilot uses AI. Check for mistakes.
self:_add_message("user", message)

local contexts = self:get_contexts()
local replaced = message:gsub("([@#])([%w%._%-%/]+)", function(prefix, path)
Copy link

Copilot AI Dec 1, 2025

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%._%-%/\\]+).

Suggested change
local replaced = message:gsub("([@#])([%w%._%-%/]+)", function(prefix, path)
local replaced = message:gsub("([@#])([%w%._%-%/\\]+)", function(prefix, path)

Copilot uses AI. Check for mistakes.
self:_add_message("user", message)

local contexts = self:get_contexts()
local replaced = message:gsub("([@#])([%w%._%-%/]+)", function(prefix, path)
Copy link

Copilot AI Dec 1, 2025

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.

Suggested change
local replaced = message:gsub("([@#])([%w%._%-%/]+)", function(prefix, path)
local replaced = message:gsub("([@#])([%w%._%-%/\\~]+)", function(prefix, path)

Copilot uses AI. Check for mistakes.

-- Store the last user message to avoid duplication
self._last_user_message = message
if not message or not type(message) == "string" then
Copy link

Copilot AI Dec 1, 2025

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.

Suggested change
if not message or not type(message) == "string" then
if not message or type(message) ~= "string" then

Copilot uses AI. Check for mistakes.
Comment on lines 800 to 801
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)
Copy link

Copilot AI Dec 1, 2025

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

Suggested change
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)

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Dec 1, 2025

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

Suggested change
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)

Copilot uses AI. Check for mistakes.
@ericdallo ericdallo merged commit ff230d6 into main Dec 1, 2025
3 checks passed
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.

3 participants