Skip to content

Commit 3f1c8bd

Browse files
committed
feat: smarter caching, misc fixes
1 parent a9ff243 commit 3f1c8bd

File tree

12 files changed

+123
-63
lines changed

12 files changed

+123
-63
lines changed

lua/blink/cmp/sources/lib/context.lua

+18-10
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ function sources_context.new(context, sources_groups, on_completions_callback)
1212
self.active_request = nil
1313
self.queued_request_context = nil
1414
self.last_successful_completions = nil
15+
self.last_sources_group_idx = nil
1516
--- @type fun(context: blink.cmp.Context, items: blink.cmp.CompletionItem[])
1617
self.on_completions_callback = on_completions_callback
1718

@@ -32,21 +33,25 @@ function sources_context:get_completions(context)
3233

3334
-- Create a task to get the completions for the first sources group,
3435
-- falling back to the next sources group iteratively if there are no items
35-
local request = self:get_completions_for_group(self.sources_groups[1], context)
36+
local request = self:get_completions_for_group(1, self.sources_groups[1], context)
3637
for idx, sources_group in ipairs(self.sources_groups) do
3738
if idx > 1 then
38-
request = request:map(function(items)
39-
if #items > 0 then return items end
40-
return self:get_completions_for_group(sources_group, context)
39+
request = request:map(function(res)
40+
if #res.items > 0 then return res end
41+
return self:get_completions_for_group(idx, sources_group, context)
4142
end)
4243
end
4344
end
4445

4546
-- Send response upstream and run the queued request, if it exists
46-
self.active_request = request:map(function(items)
47+
self.active_request = request:map(function(response)
4748
self.active_request = nil
48-
self.last_successful_completions = items
49-
self.on_completions_callback(context, items)
49+
self.last_successful_completions = response.items
50+
-- only send upstream if the response contains something new
51+
if not response.is_cached or response.sources_group_idx ~= self.last_sources_group_idx then
52+
self.on_completions_callback(context, response.items)
53+
end
54+
self.last_sources_group_idx = response.sources_group_idx
5055

5156
-- todo: when the queued request results in 100% cached content, we end up
5257
-- calling the on_completions_callback with the same data, which triggers
@@ -59,10 +64,11 @@ function sources_context:get_completions(context)
5964
end)
6065
end
6166

67+
--- @param sources_group_idx number
6268
--- @param sources_group blink.cmp.Source[]
6369
--- @param context blink.cmp.Context
6470
--- @return blink.cmp.Task
65-
function sources_context:get_completions_for_group(sources_group, context)
71+
function sources_context:get_completions_for_group(sources_group_idx, sources_group, context)
6672
-- get completions for each source in the group
6773
local tasks = vim.tbl_map(function(source)
6874
-- the source indicates we should refetch when this character is typed
@@ -85,23 +91,25 @@ function sources_context:get_completions_for_group(sources_group, context)
8591
return async.task
8692
.await_all(tasks)
8793
:map(function(tasks_results)
94+
local is_cached = true
8895
local items = {}
8996
-- for each task, filter the items and add them to the list
9097
-- if the source should show the completions
9198
for idx, task_result in ipairs(tasks_results) do
9299
if task_result.status == async.STATUS.COMPLETED then
100+
is_cached = is_cached and (task_result.result.is_cached or false)
93101
local source = sources_group[idx]
94102
--- @type blink.cmp.CompletionResponse
95103
local response = task_result.result
96104
response.items = source:filter_completions(response)
97105
if source:should_show_completions(response) then vim.list_extend(items, response.items) end
98106
end
99107
end
100-
return items
108+
return { sources_group_idx = sources_group_idx, is_cached = is_cached, items = items }
101109
end)
102110
:catch(function(err)
103111
vim.print('failed to get completions for group with error: ' .. err)
104-
return { is_incomplete_forward = false, is_incomplete_backward = false, items = {} }
112+
return { sources_group_idx = sources_group_idx, is_cached = false, items = {} }
105113
end)
106114
end
107115

lua/blink/cmp/sources/lib/init.lua

+5-5
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ end
3838

3939
function sources.listen_on_completions(callback) sources.on_completions_callback = callback end
4040

41-
--- @param context blink.cmp.ShowContext
41+
--- @param context blink.cmp.Context
4242
function sources.request_completions(context)
4343
-- a new context means we should refetch everything
4444
local is_new_context = sources.current_context == nil or context.id ~= sources.current_context.id
@@ -79,10 +79,10 @@ function sources.resolve(item, callback)
7979
callback(nil)
8080
return function() end
8181
end
82-
return item_source
83-
:resolve(item)
84-
:map(function(resolved_item) callback(resolved_item) end)
85-
:catch(function() callback(nil) end)
82+
return item_source:resolve(item):map(function(resolved_item) callback(resolved_item) end):catch(function(err)
83+
vim.print('failed to resolve item with error: ' .. err)
84+
callback(nil)
85+
end)
8686
end
8787

8888
return sources

lua/blink/cmp/sources/lib/source.lua

+7-4
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function source:get_completions(context)
2828
-- and the data doesn't need to be updated
2929
if self.last_response ~= nil and self.last_response.context.id == context.id then
3030
if utils.should_run_request(context, self.last_response) == false then
31-
return async.task.new(function(resolve) resolve(self.last_response) end)
31+
return async.task.new(function(resolve) resolve(require('blink.cmp.utils').shallow_copy(self.last_response)) end)
3232
end
3333
end
3434

@@ -37,15 +37,16 @@ function source:get_completions(context)
3737
:map(function(response)
3838
if response == nil then response = { is_incomplete_forward = true, is_incomplete_backward = true, items = {} } end
3939
response.context = context
40-
self.last_response = response
4140

4241
-- add score offset if configured
4342
for _, item in ipairs(response.items) do
4443
item.score_offset = (item.score_offset or 0) + (self.config.score_offset or 0)
45-
item.cursor_column = context.bounds.end_col -- todo: is this correct?
44+
item.cursor_column = context.cursor[2]
4645
item.source = self.config[1]
4746
end
4847

48+
self.last_response = require('blink.cmp.utils').shallow_copy(response)
49+
self.last_response.is_cached = true
4950
return response
5051
end)
5152
end
@@ -69,7 +70,9 @@ end
6970
function source:resolve(item)
7071
return async.task.new(function(resolve)
7172
if self.module.resolve == nil then return resolve(nil) end
72-
return self.module:resolve(item, resolve)
73+
return self.module:resolve(item, function(resolved_item)
74+
vim.schedule(function() resolve(resolved_item) end)
75+
end)
7376
end)
7477
end
7578

lua/blink/cmp/sources/lib/types.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
--- @class blink.cmp.CompletionResponse
66
--- @field is_incomplete_forward boolean
77
--- @field is_incomplete_backward boolean
8-
--- @field context blink.cmp.CompletionContext
8+
--- @field context blink.cmp.Context
99
--- @field items blink.cmp.CompletionItem[]
1010
---
1111
--- @class blink.cmp.Source

lua/blink/cmp/sources/lib/utils.lua

+5-6
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,19 @@ local utils = {}
33
--- Checks if a request should be made, based on the previous response/context
44
--- and the new context
55
---
6-
--- @param context blink.cmp.ShowContext | blink.cmp.TriggerContext
7-
--- @param new_context blink.cmp.ShowContext | blink.cmp.TriggerContext
6+
--- @param new_context blink.cmp.Context
87
--- @param response blink.cmp.CompletionResponse
98
---
109
--- @return false | 'forward' | 'backward' | 'unknown'
1110
function utils.should_run_request(new_context, response)
1211
local old_context = response.context
1312
-- get the text for the current and queued context
14-
local context_query = old_context.line:sub(old_context.bounds.start_col, old_context.bounds.end_col)
15-
local queued_context_query = new_context.line:sub(new_context.bounds.start_col, new_context.bounds.end_col)
13+
local old_context_query = old_context.line:sub(old_context.bounds.start_col, old_context.cursor[2])
14+
local new_context_query = new_context.line:sub(new_context.bounds.start_col, new_context.cursor[2])
1615

1716
-- check if the texts are overlapping
18-
local is_before = vim.startswith(context_query, queued_context_query)
19-
local is_after = vim.startswith(queued_context_query, context_query)
17+
local is_before = vim.startswith(old_context_query, new_context_query)
18+
local is_after = vim.startswith(new_context_query, old_context_query)
2019

2120
if is_before and response.is_incomplete_backward then return 'forward' end
2221
if is_after and response.is_incomplete_forward then return 'backward' end

lua/blink/cmp/sources/lsp.lua

+15-15
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,19 @@ function lsp:get_completions(context, callback)
7070
-- for these special cases
7171
-- i.e. hello.wor| would be sent as hello.|wor
7272
-- todo: should we still make two calls to the LSP server and merge?
73-
local trigger_characters = self:get_trigger_characters()
74-
local trigger_character_block_list = { ' ', '\n', '\t' }
75-
local bounds = context.bounds
76-
local trigger_character_before_context = context.line:sub(bounds.start_col - 1, bounds.start_col - 1)
77-
if
78-
vim.tbl_contains(trigger_characters, trigger_character_before_context)
79-
and not vim.tbl_contains(trigger_character_block_list, trigger_character_before_context)
80-
then
81-
local offset_encoding = vim.lsp.get_clients({ bufnr = 0 })[1].offset_encoding
82-
params.position.character =
83-
vim.lsp.util.character_offset(0, params.position.line, bounds.start_col - 1, offset_encoding)
84-
end
73+
-- todo: breaks the textEdit resolver since it assumes the request was made from the cursor
74+
-- local trigger_characters = self:get_trigger_characters()
75+
-- local trigger_character_block_list = { ' ', '\n', '\t' }
76+
-- local bounds = context.bounds
77+
-- local trigger_character_before_context = context.line:sub(bounds.start_col - 1, bounds.start_col - 1)
78+
-- if
79+
-- vim.tbl_contains(trigger_characters, trigger_character_before_context)
80+
-- and not vim.tbl_contains(trigger_character_block_list, trigger_character_before_context)
81+
-- then
82+
-- local offset_encoding = vim.lsp.get_clients({ bufnr = 0 })[1].offset_encoding
83+
-- params.position.character =
84+
-- vim.lsp.util.character_offset(0, params.position.line, bounds.start_col - 1, offset_encoding)
85+
-- end
8586

8687
-- request from each of the clients
8788
-- todo: refactor
@@ -118,9 +119,8 @@ function lsp:get_completions(context, callback)
118119
item.kind = item.kind or vim.lsp.protocol.CompletionItemKind.Text
119120
item.client_id = client_id
120121

121-
if item.deprecated or (item.tags and vim.tbl_contains(item.tags, 1)) then
122-
item.score_offset = (item.score_for_deprecated or -2)
123-
end
122+
-- todo: make configurable
123+
if item.deprecated or (item.tags and vim.tbl_contains(item.tags, 1)) then item.score_offset = -2 end
124124
end
125125
end
126126

lua/blink/cmp/sources/path/fs.lua

+16
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,20 @@ function fs.fs_stat_all(cwd, entries)
5555
end)
5656
end
5757

58+
--- @param path string
59+
--- @param byte_limit number
60+
--- @return blink.cmp.Task
61+
function fs.read_file(path, byte_limit)
62+
return async.task.new(function(resolve, reject)
63+
uv.fs_open(path, 'r', 438, function(open_err, fd)
64+
if open_err or fd == nil then return reject(open_err) end
65+
uv.fs_read(fd, byte_limit, 0, function(read_err, data)
66+
uv.fs_close(fd, function() end)
67+
if read_err or data == nil then return reject(read_err) end
68+
resolve(data)
69+
end)
70+
end)
71+
end)
72+
end
73+
5874
return fs

lua/blink/cmp/sources/path/init.lua

+29-2
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,45 @@ function path:get_completions(context, callback)
3232
local lib = require('blink.cmp.sources.path.lib')
3333

3434
local dirname = lib.dirname(PATH_REGEX, self.opts.get_cwd, context)
35-
if not dirname then return callback() end
35+
if not dirname then return callback({ is_incomplete_forward = false, is_incomplete_backward = false, items = {} }) end
3636

3737
local include_hidden = self.opts.show_hidden_files_by_default
3838
or string.sub(context.line, context.bounds.start_col - 1, context.bounds.start_col - 1) == '.'
3939
lib
4040
.candidates(dirname, include_hidden, self.opts)
4141
:map(
4242
function(candidates)
43-
callback({ is_incomplete_forward = false, is_incomplete_backward = true, items = candidates })
43+
callback({ is_incomplete_forward = false, is_incomplete_backward = false, items = candidates })
4444
end
4545
)
4646
:catch(function() callback() end)
4747
end
4848

49+
function path:resolve(item, callback)
50+
require('blink.cmp.sources.path.fs')
51+
.read_file(item.data.full_path, 1024)
52+
:map(function(content)
53+
local is_binary = content:find('\0')
54+
55+
-- binary file
56+
if is_binary then
57+
item.documentation = {
58+
kind = 'plaintext',
59+
value = 'Binary file',
60+
}
61+
-- highlight with markdown
62+
else
63+
local ext = vim.fn.fnamemodify(item.data.path, ':e')
64+
item.documentation = {
65+
kind = 'markdown',
66+
value = '```' .. ext .. '\n' .. content .. '```',
67+
}
68+
end
69+
70+
return item
71+
end)
72+
:map(function(resolved_item) callback(resolved_item) end)
73+
:catch(function() callback(item) end)
74+
end
75+
4976
return path

lua/blink/cmp/sources/path/lib.lua

+4-3
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function lib.candidates(dirname, include_hidden, opts)
5151
return vim.tbl_filter(function(entry) return include_hidden or entry.name ~= '.' end, entries)
5252
end)
5353
:map(function(entries)
54-
return vim.tbl_map(function(entry) return lib.entry_to_completion_item(entry, opts) end, entries)
54+
return vim.tbl_map(function(entry) return lib.entry_to_completion_item(entry, dirname, opts) end, entries)
5555
end)
5656
end
5757

@@ -65,16 +65,17 @@ function lib.is_slash_comment(_)
6565
end
6666

6767
--- @param entry { name: string, type: string, stat: table }
68+
--- @param dirname string
6869
--- @param opts table
6970
--- @return blink.cmp.CompletionItem[]
70-
function lib.entry_to_completion_item(entry, opts)
71+
function lib.entry_to_completion_item(entry, dirname, opts)
7172
local is_dir = entry.type == 'directory'
7273
return {
7374
label = (opts.label_trailing_slash and is_dir) and entry.name .. '/' or entry.name,
7475
kind = is_dir and vim.lsp.protocol.CompletionItemKind.Folder or vim.lsp.protocol.CompletionItemKind.File,
7576
insertText = is_dir and entry.name .. '/' or entry.name,
7677
word = opts.trailing_slash and entry.name or nil,
77-
data = { path = entry.name, type = entry.type, stat = entry.stat },
78+
data = { path = entry.name, full_path = dirname .. '/' .. entry.name, type = entry.type, stat = entry.stat },
7879
}
7980
end
8081

lua/blink/cmp/trigger.lua

+4-14
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
---
1111
--- @class blink.cmp.Context
1212
--- @field id number
13-
--- @field bounds blink.cmp.TriggerBounds
1413
--- @field bufnr number
15-
--- @field treesitter_node table | nil
14+
--- @field cursor number[]
15+
--- @field line string
16+
--- @field bounds blink.cmp.TriggerBounds
1617
--- @field trigger { kind: number, character: string | nil }
1718
---
1819
--- @class blink.cmp.TriggerEventTargets
@@ -138,7 +139,7 @@ end
138139

139140
function trigger.listen_on_hide(callback) trigger.event_targets.on_hide = callback end
140141

141-
--- @param context blink.cmp.ShowContext | nil
142+
--- @param context blink.cmp.Context | nil
142143
--- @param cursor number[]
143144
--- @return boolean
144145
function trigger.within_query_bounds(cursor)
@@ -184,15 +185,4 @@ function helpers.get_context_bounds(regex)
184185
return { line_number = cursor_line, start_col = start_col, end_col = end_col }
185186
end
186187

187-
--- @return TSNode | nil
188-
function helpers.get_treesitter_node_at_cursor()
189-
local ts = vim.treesitter
190-
local parser = ts.get_parser(0) -- Adjust language as needed
191-
if not parser then return end
192-
parser:parse()
193-
194-
local cursor = vim.api.nvim_win_get_cursor(0)
195-
return ts.get_node({ bufnr = 0, pos = { cursor[1] - 1, math.max(0, cursor[2] - 1) } })
196-
end
197-
198188
return trigger

lua/blink/cmp/util.lua renamed to lua/blink/cmp/utils.lua

+18-2
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@ function utils.keymap(mode, key, callback)
1717
end
1818

1919
--- Gets the text under the cursor to be used for fuzzy matching
20-
function utils.get_query()
20+
--- @param regex string | nil
21+
--- @return string
22+
function utils.get_query(regex)
23+
if regex == nil then regex = '[%w_\\-]+$' end
24+
2125
local bufnr = vim.api.nvim_get_current_buf()
2226

2327
local current_line = vim.api.nvim_win_get_cursor(0)[1] - 1
2428
local current_col = vim.api.nvim_win_get_cursor(0)[2] - 1
2529
local line = vim.api.nvim_buf_get_lines(bufnr, current_line, current_line + 1, false)[1]
2630

27-
return string.sub(line, 1, current_col + 1):match('[%w_\\-]+$') or ''
31+
return string.sub(line, 1, current_col + 1):match(regex) or ''
2832
end
2933

3034
--- Debounces a function on the trailing edge. Automatically
@@ -47,4 +51,16 @@ function utils.debounce(fn, timeout)
4751
return wrapped_fn, timer
4852
end
4953

54+
--- Shallow copy table
55+
--- @generic T
56+
--- @param t T
57+
--- @return T
58+
function utils.shallow_copy(t)
59+
local t2 = {}
60+
for k, v in pairs(t) do
61+
t2[k] = v
62+
end
63+
return t2
64+
end
65+
5066
return utils

0 commit comments

Comments
 (0)