Skip to content

Commit 1bde817

Browse files
authored
Merge pull request #23 from tomgeorge/completion/blink
Add blink completion support
2 parents cdc0d36 + 6d4a020 commit 1bde817

File tree

7 files changed

+281
-158
lines changed

7 files changed

+281
-158
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: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
---@param item lsp.CompletionItem
71+
---@param callback fun(any)
72+
function source:resolve(item, callback)
73+
require("eca.completion.context").resolve_completion_item(item, callback)
74+
end
75+
76+
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: 6 additions & 82 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,75 +51,19 @@ 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

99-
--- Taken from https://github.com/hrsh7th/cmp-path/blob/9a16c8e5d0be845f1d1b64a0331b155a9fe6db4d/lua/cmp_path/init.lua
100-
--- Show a small preview of file contexft items in the documentation window.
101-
---@param data eca.ChatContext
102-
---@return lsp.MarkupContent
103-
source._get_documentation = function(_, data, count)
104-
if data and data.path then
105-
local filename = data.path
106-
local binary = assert(io.open(data.path, "rb"))
107-
local first_kb = binary:read(1024)
108-
if first_kb and first_kb:find("\0") then
109-
return { kind = vim.lsp.protocol.MarkupKind.PlainText, value = "binary file" }
110-
end
111-
112-
local content = io.lines(data.path)
113-
114-
--- Try to support line ranges, I don't know if this works or not yet
115-
local start = data.lines_range and data.lines_range.start or 1
116-
local last = data.lines_range and data.lines_range["end"] or count
117-
local skip_lines = start - 1
118-
local take_lines = last - start
119-
local contents = vim.iter(content):skip(skip_lines):take(take_lines):totable()
120-
121-
local filetype = vim.filetype.match({ filename = filename })
122-
if not filetype then
123-
return { kind = vim.lsp.protocol.MarkupKind.PlainText, value = table.concat(contents, "\n") }
124-
end
125-
126-
table.insert(contents, 1, "```" .. filetype)
127-
table.insert(contents, "```")
128-
return { kind = vim.lsp.protocol.MarkupKind.Markdown, value = table.concat(contents, "\n") }
129-
end
130-
return {}
131-
end
132-
13364
---@param completion_item lsp.CompletionItem
13465
function source:resolve(completion_item, callback)
135-
if completion_item.data then
136-
local context_item = completion_item.data.context_item
137-
---@cast context_item eca.ChatContext
138-
if context_item.type == "file" then
139-
completion_item.documentation = self:_get_documentation(context_item, 20)
140-
end
141-
callback(completion_item)
142-
end
66+
require("eca.completion.context").resolve_completion_item(completion_item, callback)
14367
end
14468

14569
return source

0 commit comments

Comments
 (0)