Skip to content

Commit 3e13e81

Browse files
committed
Add blink completion support
1 parent cdc0d36 commit 3e13e81

File tree

7 files changed

+223
-116
lines changed

7 files changed

+223
-116
lines changed

after/plugin/eca-nvim.lua

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,20 @@ if has_cmp then
1010
},
1111
})
1212
end
13+
14+
local has_blink, blink = pcall(require, "blink.cmp")
15+
if has_blink then
16+
blink.add_source_provider("eca_commands", {
17+
name = "eca_commands",
18+
module = "eca.completion.blink.commands",
19+
enabled = true,
20+
})
21+
blink.add_filetype_source("eca-input", "eca_commands")
22+
23+
blink.add_source_provider("eca_contexts", {
24+
name = "eca_contexts",
25+
module = "eca.completion.blink.context",
26+
enabled = true,
27+
})
28+
blink.add_filetype_source("eca-input", "eca_contexts")
29+
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
local source = {}
2+
3+
-- `opts` table comes from `sources.providers.your_provider.opts`
4+
-- You may also accept a second argument `config`, to get the full
5+
-- `sources.providers.your_provider` table
6+
function source.new(opts)
7+
-- vim.validate("your-source.opts.some_option", opts.some_option, { "string" })
8+
-- vim.validate("your-source.opts.optional_option", opts.optional_option, { "string" }, true)
9+
10+
local self = setmetatable({}, { __index = source })
11+
self.opts = opts
12+
return self
13+
end
14+
15+
function source:enabled()
16+
return vim.bo.filetype == "eca-input"
17+
end
18+
19+
---@return lsp.CompletionItem
20+
---@param command eca.ChatCommand
21+
local function as_completion_item(command)
22+
---@type lsp.CompletionItem
23+
return {
24+
label = command.name,
25+
detail = command.description or ("ECA command: " .. command.name),
26+
documentation = command.help and {
27+
kind = "markdown",
28+
value = command.help,
29+
} or nil,
30+
}
31+
end
32+
33+
-- (Optional) Non-alphanumeric characters that trigger the source
34+
function source:get_trigger_characters()
35+
return { "/" }
36+
end
37+
38+
---@module 'blink.cmp'
39+
---@param ctx blink.cmp.Context
40+
---@param callback fun(response?: blink.cmp.CompletionResponse)
41+
function source:get_completions(ctx, callback)
42+
local commands = require("eca.completion.commands")
43+
local q = commands.get_query(ctx.line)
44+
if q then
45+
commands.get_completion_candidates(q, as_completion_item, callback)
46+
end
47+
end
48+
49+
return source
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---@module 'blink.cmp'
2+
---@class blink.cmp.Source
3+
local source = {}
4+
5+
-- `opts` table comes from `sources.providers.your_provider.opts`
6+
-- You may also accept a second argument `config`, to get the full
7+
-- `sources.providers.your_provider` table
8+
function source.new(opts)
9+
-- vim.validate("your-source.opts.some_option", opts.some_option, { "string" })
10+
-- vim.validate("your-source.opts.optional_option", opts.optional_option, { "string" }, true)
11+
12+
local self = setmetatable({}, { __index = source })
13+
self.opts = opts
14+
return self
15+
end
16+
17+
function source:enabled()
18+
return vim.bo.filetype == "eca-input"
19+
end
20+
21+
-- (Optional) Non-alphanumeric characters that trigger the source
22+
function source:get_trigger_characters()
23+
return { "@" }
24+
end
25+
26+
---@param context eca.ChatContext
27+
---@return lsp.CompletionItem
28+
local function as_completion_item(context)
29+
local kinds = require("blink.cmp.types").CompletionItemKind
30+
---@type lsp.CompletionItem
31+
---@diagnostic disable-next-line: missing-fields
32+
local item = {}
33+
if context.type == "file" then
34+
item.label = string.format("@%s", vim.fn.fnamemodify(context.path, ":."))
35+
item.kind = kinds.File
36+
item.data = {
37+
context_item = context,
38+
}
39+
elseif context.type == "directory" then
40+
item.label = string.format("@%s", vim.fn.fnamemodify(context.path, ":."))
41+
item.kind = kinds.Folder
42+
elseif context.type == "web" then
43+
item.label = context.url
44+
item.kind = kinds.File
45+
elseif context.type == "repoMap" then
46+
item.label = "repoMap"
47+
item.kind = kinds.Module
48+
item.detail = "Summary view of workspace files."
49+
elseif context.type == "mcpResource" then
50+
item.label = string.format("%s:%s", context.server, context.name)
51+
item.kind = kinds.Struct
52+
item.detail = context.description
53+
end
54+
if not item.label then
55+
return {}
56+
end
57+
return item
58+
end
59+
60+
---@param ctx blink.cmp.Context
61+
---@param callback fun(response?: blink.cmp.CompletionResponse)
62+
function source:get_completions(ctx, callback)
63+
local context = require("eca.completion.context")
64+
local query = context.get_query(ctx.line, ctx.cursor)
65+
if query then
66+
context.get_completion_candidates(query, as_completion_item, callback)
67+
end
68+
end
69+
70+
return source
Lines changed: 20 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,17 @@
1-
---@param commands eca.ChatCommand
2-
---@return lsp.CompletionItem[]
3-
local function create_completion_items(commands)
4-
---@type lsp.CompletionItem[]
5-
local items = {}
6-
7-
if commands then
8-
for _, command in ipairs(commands) do
9-
---@type lsp.CompletionItem
10-
local item = {
11-
label = command.name or command.command,
12-
kind = vim.lsp.protocol.CompletionItemKind.Function,
13-
detail = command.description or ("ECA command: " .. (command.name or command.command)),
14-
documentation = command.help and {
15-
kind = "markdown",
16-
value = command.help,
17-
} or nil,
18-
insertText = command.name or command.command,
19-
}
20-
table.insert(items, item)
21-
end
22-
end
23-
24-
return items
25-
end
26-
27-
---@param s string
28-
---@return string
29-
local function get_query(s)
30-
local match = s:match("^>?%s*/(.*)$")
31-
return match
32-
end
33-
34-
-- Query server for available commands
35-
local function query_server_commands(query, callback)
36-
local eca = require("eca")
37-
if not eca.server or not eca.server:is_running() then
38-
callback({})
39-
return
40-
end
41-
42-
eca.server:send_request("chat/queryCommands", { query = query }, function(err, result)
43-
if err then
44-
callback({})
45-
return
46-
end
47-
48-
local items = create_completion_items(result.commands)
49-
callback(items)
50-
end)
1+
---@param command eca.ChatCommand
2+
---@return lsp.CompletionItem
3+
local function as_completion_item(command)
4+
local cmp = require("cmp")
5+
---@type lsp.CompletionItem
6+
return {
7+
label = command.name,
8+
kind = cmp.lsp.CompletionItemKind.Function,
9+
detail = command.description or ("ECA command: " .. command.name),
10+
documentation = command.help and {
11+
kind = "markdown",
12+
value = command.help,
13+
} or nil,
14+
}
5115
end
5216

5317
local source = {}
@@ -56,41 +20,21 @@ source.new = function()
5620
return setmetatable({ cache = {} }, { __index = source })
5721
end
5822

59-
source.get_trigger_characters = function()
23+
function source:get_trigger_characters()
6024
return { "/" }
6125
end
6226

63-
source.is_available = function()
27+
function source:is_available()
6428
return vim.bo.filetype == "eca-input"
6529
end
6630

6731
---@diagnostic disable-next-line: unused-local
68-
source.complete = function(self, _, callback)
32+
function source:complete(params, callback)
6933
-- Only complete if we're typing a command (starts with /)
70-
local line = vim.api.nvim_get_current_line()
71-
local query = get_query(line)
72-
73-
-- Only provide command completions when we have / followed by word characters or at word boundary
34+
local commands = require("eca.completion.commands")
35+
local query = commands.get_query(params.context.cursor_line)
7436
if query then
75-
local bufnr = vim.api.nvim_get_current_buf()
76-
77-
if self.cache[bufnr] and self.cache[bufnr][query] then
78-
callback({ items = self.cache[bufnr][query], isIncomplete = false })
79-
else
80-
query_server_commands(query, function(items)
81-
callback({
82-
items = items,
83-
isIncomplete = false,
84-
})
85-
self.cache[bufnr] = {}
86-
self.cache[bufnr][query] = items
87-
end)
88-
end
89-
else
90-
callback({
91-
items = {},
92-
isIncomplete = false,
93-
})
37+
commands.get_completion_candidates(query, as_completion_item, callback)
9438
end
9539
end
9640
return source

lua/eca/completion/cmp/context.lua

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
local cmp = require("cmp")
2+
---@module 'cmp'
23
---@param context eca.ChatContext
3-
---@return lsp.CompletionItem
4+
---@return cmp.CompletionItem
45
local function as_completion_item(context)
56
---@type lsp.CompletionItem
67
---@diagnostic disable-next-line: missing-fields
@@ -32,27 +33,6 @@ local function as_completion_item(context)
3233
return item
3334
end
3435

35-
-- Query server for available contexts
36-
-- @param query string
37-
--
38-
local function query_server_contexts(query, callback)
39-
local eca = require("eca")
40-
if not eca.server or not eca.server:is_running() then
41-
callback({})
42-
return
43-
end
44-
45-
eca.server:send_request("chat/queryContext", { query = query }, function(err, result)
46-
if err then
47-
callback({})
48-
return
49-
end
50-
51-
local items = vim.iter(result.contexts):map(as_completion_item):totable()
52-
callback({ items = items, isIncomplete = true })
53-
end)
54-
end
55-
5636
local source = {}
5737

5838
function source.new()
@@ -71,28 +51,13 @@ function source:is_available()
7151
return vim.bo.filetype == "eca-input"
7252
end
7353

74-
---@param cursor_line string
75-
---@param cursor_position lsp.Position|vim.Position
76-
---@return string
77-
local function get_query(cursor_line, cursor_position)
78-
local before_cursor = cursor_line:sub(1, cursor_position.col)
79-
---@type string[]
80-
local matches = {}
81-
local it = before_cursor:gmatch("@([%w%./_\\%-~]*)")
82-
for match in it do
83-
table.insert(matches, match)
84-
end
85-
return matches[#matches]
86-
end
87-
8854
---@param params cmp.SourceCompletionApiParams
8955
---@diagnostic disable-next-line: unused-local
9056
function source:complete(params, callback)
91-
local query = get_query(params.context.cursor_line, params.context.cursor)
57+
local context = require("eca.completion.context")
58+
local query = context.get_query(params.context.cursor_line, params.context.cursor)
9259
if query then
93-
query_server_contexts(query, function(items)
94-
callback(items)
95-
end)
60+
context.get_completion_candidates(query, as_completion_item, callback)
9661
end
9762
end
9863

lua/eca/completion/commands.lua

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
local M = {}
2+
3+
---@param s string
4+
---@return string?
5+
function M.get_query(s)
6+
return s:match("^>?%s*/(.*)$") -- Cursor character followed by slash
7+
end
8+
9+
---@param query string
10+
---@param as_completion_item fun(eca.ChatCommand): lsp.CompletionItem
11+
---@param callback fun(resp: {items: lsp.CompletionItem[], isIncomplete?: boolean, is_incomplete_forward?: boolean, is_incomplete_backward?: boolean})
12+
function M.get_completion_candidates(query, as_completion_item, callback)
13+
local server = require("eca").server
14+
server:send_request("chat/queryCommands", { query = query }, function(err, result)
15+
if err then
16+
---@diagnostic disable-next-line: missing-fields
17+
callback({ items = {} })
18+
end
19+
20+
if not result and not result.commands then
21+
---@diagnostic disable-next-line: missing-fields
22+
callback({ items = {} })
23+
end
24+
25+
local items = vim.iter(result.commands):map(as_completion_item):totable()
26+
callback({ items = items })
27+
end)
28+
end
29+
30+
return M

lua/eca/completion/context.lua

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
local M = {}
2+
3+
---@param cursor_line string
4+
---@param cursor_position lsp.Position|vim.Position
5+
---@return string
6+
function M.get_query(cursor_line, cursor_position)
7+
local before_cursor = cursor_line:sub(1, cursor_position.col)
8+
---@type string[]
9+
local matches = {}
10+
local it = before_cursor:gmatch("@([%w%./_\\%-~]*)")
11+
for match in it do
12+
table.insert(matches, match)
13+
end
14+
return matches[#matches]
15+
end
16+
17+
---@param query string
18+
---@param as_completion_item fun(eca.ChatContext): lsp.CompletionItem
19+
---@param callback fun(resp: {items: lsp.CompletionItem[], isIncomplete?: boolean, is_incomplete_forward?: boolean, is_incomplete_backward?: boolean})
20+
function M.get_completion_candidates(query, as_completion_item, callback)
21+
local server = require("eca").server
22+
server:send_request("chat/queryContext", { query = query }, function(err, result)
23+
if err then
24+
callback({ items = { items = {} } })
25+
return
26+
end
27+
28+
local items = vim.iter(result.contexts):map(as_completion_item):totable()
29+
callback({ items = items })
30+
end)
31+
end
32+
return M

0 commit comments

Comments
 (0)