Skip to content

feat: add terminal_command to the completion context #2001

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

Merged
merged 2 commits into from
Jul 22, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions lua/blink/cmp/completion/trigger/context.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
--- @field bufnr number
--- @field cursor number[]
--- @field line string
--- @field term blink.cmp.ContextTerm
--- @field bounds blink.cmp.ContextBounds
--- @field trigger blink.cmp.ContextTrigger
--- @field providers string[]
Expand All @@ -26,13 +27,22 @@
--- @field set_cursor fun(cursor: number[])
--- @field get_line fun(num?: number): string
--- @field get_bounds fun(range: blink.cmp.CompletionKeywordRange): blink.cmp.ContextBounds
--- @field get_term_command fun(): blink.cmp.ContextTermCommand?

--- @class blink.cmp.ContextTrigger
--- @field initial_kind blink.cmp.CompletionTriggerKind The trigger kind when the context was first created
--- @field initial_character? string The trigger character when initial_kind == 'trigger_character'
--- @field kind blink.cmp.CompletionTriggerKind The current trigger kind
--- @field character? string The trigger character when kind == 'trigger_character'

--- @class blink.cmp.ContextTerm
--- @field command blink.cmp.ContextTermCommand

--- @class blink.cmp.ContextTermCommand
--- @field found_escape_code boolean Whether the FTCS_COMMAND_START escape sequence was found when querying for the command on the current line. This will always be false when the cursor isn't in a prompt, such as when a command is running.
--- @field text string The command in the current line, without the shell prompt if found_escape_code = true, up to the cursor. Note that for multiline commands, it will always provide you with the content of the last line. This is because there is no way to distinguish the starting point of a single line command from a multiline one using terminal escape sequences
--- @field start_col number 0-indexed column of the command in the current line, or 0 if the terminal or shell does not support the FTCS_COMMAND_START escape sequence

--- @class blink.cmp.ContextOpts
--- @field id number
--- @field providers string[]
Expand All @@ -56,6 +66,7 @@ function context.new(opts)
bufnr = vim.api.nvim_get_current_buf(),
cursor = cursor,
line = line,
term = { command = context.get_term_command() },
bounds = context.get_bounds('full'),
trigger = {
initial_kind = opts.initial_trigger_kind,
Expand Down Expand Up @@ -139,4 +150,48 @@ function context.get_bounds(range)
return { line_number = cursor[1], start_col = start_col + 1, length = end_col - start_col }
end

--- Get the terminal command in the current line without the shell prompt.
---
--- If the terminal or shell does not support the FTCS_COMMAND_START escape sequence,
--- it will return the full line text, up to the cursor.
---
--- In the case of multiline commands, it will always provide you with the content
--- of the last line. This is because there is no way to distinguish the starting point of a
--- single line command from a multiline one using terminal escape sequences
---
--- @return blink.cmp.ContextTermCommand?
function context.get_term_command()
if context.get_mode() ~= 'term' then return end

local cursor = context.get_cursor()
local cursor_row = cursor[1]
local cursor_col = cursor[2] + 1
local line = string.sub(context.get_line(), 1, cursor_col - 1)

local extmarks = vim.api.nvim_buf_get_extmarks(
0,
vim.api.nvim_create_namespace('blink-term-command-start'),
{ cursor_row - 1, cursor_col - 1 },
{ cursor_row - 1, 0 },
{ limit = 1 }
)

--- If we find no mark for the start of the terminal command the terminal or shell
--- probably does not support the FTCS_COMMAND_START escape sequence. The best effort
--- we can do here is to return the full line text.
if #extmarks < 1 then return {
found_escape_code = false,
text = line,
start_col = 0,
} end

local command_start_mark = extmarks[1]
local command_start_col = command_start_mark[3] + 1
return {
found_escape_code = true,
text = string.sub(line, command_start_col, string.len(line)),
start_col = command_start_col - 1,
}
end

return context
Loading