Skip to content

Commit ecc0141

Browse files
committed
refactor: migrate cursor handling to vim.Pos
This migrates internal cursor handling from the legacy (line, col) tuple representation to Neovim's experimental `vim.Pos` API, discussed in #2574 (comment). Using it removes many of the recurring +1/-1 conversions and comparisons between the different coordinate systems, making the code easier to read and should reduce off-by-one mistakes. For compatibility with existing community sources, `context.cursor` and `context.get_cursor()` are retained for now, while the implementation use `context.pos` and `context.get_pos()`.
1 parent 338f643 commit ecc0141

30 files changed

Lines changed: 191 additions & 223 deletions

File tree

lua/blink/cmp/completion/accept/init.lua

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,9 @@ local function apply_item(ctx, item)
6464

6565
-- OR Normal: Apply the text edit and move the cursor
6666
else
67-
local new_cursor = text_edits_lib.get_apply_end_position(item.textEdit, all_text_edits)
68-
new_cursor[2] = new_cursor[2]
69-
67+
local new_pos = text_edits_lib.get_apply_end_position(item.textEdit, all_text_edits)
7068
text_edits_lib.apply(item.textEdit, all_text_edits)
71-
72-
ctx.set_cursor(new_cursor)
69+
ctx.set_cursor(new_pos)
7370
text_edits_lib.move_cursor_in_dot_repeat(offset)
7471
end
7572

lua/blink/cmp/completion/accept/preview.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
local nvim = require('blink.lib.nvim')
22

33
--- @param item blink.cmp.CompletionItem
4-
--- @return lsp.TextEdit, blink.cmp.CursorPos?
4+
--- @return lsp.TextEdit, vim.Pos?
55
local function preview(item)
66
local text_edits_lib = require('blink.cmp.lib.text_edits')
77
local text_edit = text_edits_lib.get_from_item(item)
@@ -16,7 +16,7 @@ local function preview(item)
1616
-- only keep the first line.
1717
text_edit.newText = vim.split(text_edit.newText, '\n', { plain = true, trimempty = true })[1] or '' -- vim.split returns an empty list if the string was empty, making [1] nil
1818

19-
local original_cursor = nvim.win_get_cursor(0)
19+
local original_pos = vim.pos.cursor(0)
2020
local cursor_pos = {
2121
text_edit.range.start.line + 1,
2222
text_edit.range.start.character + #text_edit.newText,
@@ -33,7 +33,7 @@ local function preview(item)
3333
cursor_moved = true
3434
end
3535

36-
return undo_text_edit, cursor_moved and original_cursor or nil
36+
return undo_text_edit, cursor_moved and original_pos or nil
3737
end
3838

3939
return preview

lua/blink/cmp/completion/brackets/config.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
local css_exceptions = function(ctx)
2-
local str = string.sub(ctx.line, 1, ctx.cursor[2] or #ctx.line)
2+
local str = string.sub(ctx.line, 1, ctx.pos.col or #ctx.line)
33
return not str:find('[%w_-]*::?[%w-]*$')
44
end
55
local typescript_exceptions = function(ctx) return ctx.line:find('^%s*import%s') == nil end

lua/blink/cmp/completion/brackets/semantic.lua

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ local config = require('blink.cmp.config').completion.accept.auto_brackets
44
local utils = require('blink.cmp.completion.brackets.utils')
55

66
--- @class blink.cmp.SemanticRequest
7-
--- @field cursor blink.cmp.CursorPos
7+
--- @field pos vim.Pos
88
--- @field item blink.cmp.CompletionItem
99
--- @field filetype string
1010
--- @field callback fun(added: boolean)
@@ -34,17 +34,18 @@ function semantic.process_request(tokens)
3434
local request = semantic.request
3535
if request == nil then return end
3636

37-
local cursor = nvim.win_get_cursor(0)
37+
local pos = vim.pos.cursor(0)
38+
3839
-- cancel if the cursor moved
39-
if request.cursor[1] ~= cursor[1] or request.cursor[2] ~= cursor[2] then return semantic.finish_request() end
40+
if request.pos ~= pos then return semantic.finish_request() end
4041

4142
for _, token in ipairs(tokens) do
4243
if
4344
(token.type == 'function' or token.type == 'method')
44-
and cursor[1] - 1 == token.line
45-
and cursor[2] >= token.start_col
45+
and pos.row == token.line
46+
and pos.col >= token.start_col
4647
-- we do <= to check 1 character before the cursor (`bar|` would check `r`)
47-
and cursor[2] <= token.end_col
48+
and pos.col <= token.end_col
4849
then
4950
-- add the brackets
5051
-- TODO: make dot repeatable
@@ -55,12 +56,12 @@ function semantic.process_request(tokens)
5556
{
5657
newText = brackets_for_filetype[1] .. brackets_for_filetype[2],
5758
range = {
58-
start = { line = cursor[1] - 1, character = start_col },
59-
['end'] = { line = cursor[1] - 1, character = start_col },
59+
start = { line = pos.row, character = start_col },
60+
['end'] = { line = pos.row, character = start_col },
6061
},
6162
},
6263
}, nvim.get_current_buf(), 'utf-8')
63-
nvim.win_set_cursor(0, { cursor[1], start_col + #brackets_for_filetype[1] })
64+
nvim.win_set_cursor(0, { pos.row + 1, start_col + #brackets_for_filetype[1] })
6465
return semantic.finish_request()
6566
end
6667
end
@@ -89,9 +90,9 @@ function semantic.add_brackets_via_semantic_token(ctx, filetype, item)
8990
if highlighter == nil then return resolve(false) end
9091

9192
semantic.timer:stop()
92-
local cursor = nvim.win_get_cursor(0)
93+
local pos = vim.pos.cursor(0)
9394
semantic.request = {
94-
cursor = nvim.win_get_cursor(0),
95+
pos = pos,
9596
filetype = filetype,
9697
item = item,
9798
callback = resolve,
@@ -102,7 +103,7 @@ function semantic.add_brackets_via_semantic_token(ctx, filetype, item)
102103

103104
-- first check if a semantic token already exists at the current cursor position
104105
-- we get the token 1 character before the cursor (`bar|` would check `r`)
105-
local tokens = vim.lsp.semantic_tokens.get_at_pos(0, cursor[1] - 1, cursor[2] - 1)
106+
local tokens = vim.lsp.semantic_tokens.get_at_pos(0, pos.row, pos.col - 1)
106107
if tokens ~= nil then semantic.process_request(tokens) end
107108
if semantic.request == nil then
108109
-- a matching token exists, and brackets were added

lua/blink/cmp/completion/list.lua

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
--- @field items blink.cmp.CompletionItem[]
1111
--- @field selected_item_idx? integer
1212
--- @field is_explicitly_selected boolean
13-
--- @field preview_undo? { text_edit: lsp.TextEdit, cursor_before?: blink.cmp.CursorPos, cursor_after: blink.cmp.CursorPos }
13+
--- @field preview_undo? { text_edit: lsp.TextEdit, pos_before?: vim.Pos, pos_after: vim.Pos }
1414
---
1515
--- @field show fun(context: blink.cmp.Context, items: table<string, blink.cmp.CompletionItem[]>)
1616
--- @field fuzzy fun(context: blink.cmp.Context, items: table<string, blink.cmp.CompletionItem[]>): blink.cmp.CompletionItem[]
@@ -140,12 +140,9 @@ end
140140

141141
function list.fuzzy(ctx, items_by_source)
142142
local fuzzy = require('blink.cmp.fuzzy')
143-
local filtered_items = fuzzy.fuzzy(
144-
ctx.get_line(),
145-
ctx.get_cursor()[2],
146-
items_by_source,
147-
require('blink.cmp.config').completion.keyword.range
148-
)
143+
local pos = ctx.get_pos()
144+
local filtered_items =
145+
fuzzy.fuzzy(ctx.get_line(), pos.col, items_by_source, require('blink.cmp.config').completion.keyword.range)
149146

150147
-- apply the per source max_items
151148
filtered_items = require('blink.cmp.sources.lib').apply_max_items_for_completions(ctx, filtered_items)
@@ -328,13 +325,13 @@ function list.undo_preview()
328325
-- The text edit may be out of date due to the user typing more characters
329326
-- so we adjust the range to compensate
330327
-- TODO: Set offset_encoding
331-
local old_cursor_col = list.preview_undo.cursor_after[2]
332-
local new_cursor_col = context.get_cursor()[2]
333-
text_edit = text_edits_lib.compensate_for_cursor_movement(text_edit, old_cursor_col, new_cursor_col)
328+
local old_pos = list.preview_undo.pos_after
329+
local new_pos = context.get_pos()
330+
text_edit = text_edits_lib.compensate_for_cursor_movement(text_edit, old_pos, new_pos)
334331

335332
require('blink.cmp.lib.text_edits').apply(text_edit)
336-
if list.preview_undo.cursor_before ~= nil then
337-
require('blink.cmp.completion.trigger.context').set_cursor(list.preview_undo.cursor_before)
333+
if list.preview_undo.pos_before ~= nil then
334+
require('blink.cmp.completion.trigger.context').set_cursor(list.preview_undo.pos_before)
338335
end
339336

340337
list.preview_undo = nil
@@ -345,11 +342,12 @@ function list.apply_preview(item)
345342
list.undo_preview()
346343

347344
-- apply the new preview
348-
local undo_text_edit, undo_cursor = require('blink.cmp.completion.accept.preview')(item)
345+
local undo_text_edit, undo_pos = require('blink.cmp.completion.accept.preview')(item)
346+
349347
list.preview_undo = {
350348
text_edit = undo_text_edit,
351-
cursor_before = undo_cursor,
352-
cursor_after = context.get_cursor(),
349+
pos_before = undo_pos,
350+
pos_after = context.get_pos(),
353351
}
354352
end
355353

lua/blink/cmp/completion/trigger/context.lua

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ local nvim = require('blink.lib.nvim')
1212
--- @field mode blink.cmp.Mode
1313
--- @field id integer
1414
--- @field bufnr integer
15-
--- @field cursor blink.cmp.CursorPos
15+
--- @field cursor { [1]: integer, [2]: integer } Deprecated, use `pos` intead
16+
--- @field pos vim.Pos
1617
--- @field line string
1718
--- @field term blink.cmp.ContextTerm
1819
--- @field bounds blink.cmp.ContextBounds
@@ -23,11 +24,12 @@ local nvim = require('blink.lib.nvim')
2324
---
2425
--- @field new fun(opts: blink.cmp.ContextOpts): blink.cmp.Context
2526
--- @field get_keyword fun(): string
26-
--- @field within_query_bounds fun(self: blink.cmp.Context, cursor: blink.cmp.CursorPos, include_start_bound?: boolean): boolean
27+
--- @field within_query_bounds fun(self: blink.cmp.Context, include_start_bound?: boolean): boolean
2728
---
2829
--- @field get_mode fun(): blink.cmp.Mode
29-
--- @field get_cursor fun(): blink.cmp.CursorPos
30-
--- @field set_cursor fun(cursor: blink.cmp.CursorPos)
30+
--- @field get_pos fun(): vim.Pos
31+
--- @field get_cursor fun(): { [1]: integer, [2]: integer } Deprecated, use `get_pos` intead
32+
--- @field set_cursor fun(pos: vim.Pos)
3133
--- @field get_line fun(num?: integer): string
3234
--- @field get_bounds fun(range: blink.cmp.CompletionKeywordRange): blink.cmp.ContextBounds
3335
--- @field get_term_command fun(): blink.cmp.ContextTermCommand?
@@ -60,15 +62,15 @@ local nvim = require('blink.lib.nvim')
6062
local context = {}
6163

6264
function context.new(opts)
63-
local cursor = context.get_cursor()
64-
local line = context.get_line()
65+
local pos = context.get_pos()
6566

6667
return setmetatable({
6768
mode = context.get_mode(),
6869
id = opts.id,
6970
bufnr = nvim.get_current_buf(),
70-
cursor = cursor,
71-
line = line,
71+
pos = pos,
72+
cursor = pos:to_cursor(),
73+
line = context.get_line(),
7274
term = { command = context.get_term_command() },
7375
bounds = context.get_bounds('full'),
7476
trigger = {
@@ -89,18 +91,19 @@ function context.get_keyword()
8991
return string.sub(context.get_line(), range.start_col, range.start_col + range.length - 1)
9092
end
9193

92-
--- @param cursor blink.cmp.CursorPos
9394
--- @param include_start_bound? boolean Whether to include the start boundary as inside of the query. E.g. start_col = 1 (one indexed), cursor[2] = 0 (zero indexed) would be considered within the query bounds with this flag enabled.
9495
--- @return boolean
95-
function context:within_query_bounds(cursor, include_start_bound)
96-
local row, col = cursor[1], cursor[2]
97-
col = col + 1 -- Convert from 0-indexed to 1-indexed
96+
function context:within_query_bounds(include_start_bound)
97+
local pos, bounds = self.pos, self.bounds
98+
if pos.row + 1 ~= bounds.line_number then return false end
9899

99-
local bounds = self.bounds
100-
if include_start_bound then
101-
return row == bounds.line_number and col >= bounds.start_col and col <= (bounds.start_col + bounds.length)
102-
end
103-
return row == bounds.line_number and col > bounds.start_col and col <= (bounds.start_col + bounds.length)
100+
local cursor_col = pos.col + 1
101+
local end_col = bounds.start_col + bounds.length
102+
if cursor_col > end_col then return false end
103+
104+
if include_start_bound then return cursor_col >= bounds.start_col end
105+
106+
return cursor_col > bounds.start_col
104107
end
105108

106109
function context.get_mode()
@@ -117,20 +120,25 @@ function context.get_mode()
117120
or 'default'
118121
end
119122

120-
function context.get_cursor()
121-
return context.get_mode() == 'cmdline' and { 1, vim.fn.getcmdpos() - 1 } or nvim.win_get_cursor(0)
123+
function context.get_pos()
124+
local bufnr = context.bufnr or 0
125+
if context.get_mode() == 'cmdline' then return vim.pos(bufnr, 0, vim.fn.getcmdpos() - 1) end
126+
127+
return vim.pos.cursor(bufnr)
122128
end
123129

124-
function context.set_cursor(cursor)
130+
function context.get_cursor() return context.get_pos():to_cursor() end
131+
132+
function context.set_cursor(pos)
125133
local mode = context.get_mode()
126134
if vim.tbl_contains({ 'default', 'term', 'cmdwin' }, mode) then
127-
nvim.win_set_cursor(0, cursor)
135+
nvim.win_set_cursor(0, pos:to_cursor())
128136
return
129137
end
130138

131139
assert(mode == 'cmdline', 'Unsupported mode for setting cursor: ' .. mode)
132-
assert(cursor[1] == 1, 'Cursor must be on the first line in cmdline mode')
133-
vim.fn.setcmdpos(cursor[2])
140+
assert(pos.row == 0, 'Cursor must be on the first line in cmdline mode')
141+
vim.fn.setcmdpos(pos.col + 1)
134142
end
135143

136144
function context.get_line(num)
@@ -143,16 +151,18 @@ function context.get_line(num)
143151
end
144152

145153
-- This method works for normal buffers and the terminal prompt
146-
if num == nil then num = context.get_cursor()[1] - 1 end
154+
if not num then num = context.get_pos().row end
155+
147156
return nvim.buf_get_lines(0, num, num + 1, false)[1] or ''
148157
end
149158

150159
--- Gets characters around the cursor and returns the range, 0-indexed
151160
function context.get_bounds(range)
152161
local line = context.get_line()
153-
local cursor = context.get_cursor()
154-
local start_col, end_col = require('blink.cmp.fuzzy').get_keyword_range(line, cursor[2], range)
155-
return { line = line, line_number = cursor[1], start_col = start_col + 1, length = end_col - start_col }
162+
local pos = context.get_pos()
163+
local start_col, end_col = require('blink.cmp.fuzzy').get_keyword_range(line, pos.col, range)
164+
165+
return { line = line, line_number = pos.row + 1, start_col = start_col + 1, length = end_col - start_col }
156166
end
157167

158168
--- Get the terminal command in the current line without the shell prompt.
@@ -168,18 +178,10 @@ end
168178
function context.get_term_command()
169179
if context.get_mode() ~= 'term' then return end
170180

171-
local cursor = context.get_cursor()
172-
local cursor_row = cursor[1]
173-
local cursor_col = cursor[2] + 1
174-
local line = string.sub(context.get_line(), 1, cursor_col - 1)
175-
176-
local extmarks = nvim.buf_get_extmarks(
177-
0,
178-
nvim.create_namespace('blink_cmp_term_command_start'),
179-
{ cursor_row - 1, cursor_col - 1 },
180-
{ cursor_row - 1, 0 },
181-
{ limit = 1 }
182-
)
181+
local pos = context.get_pos()
182+
local line = string.sub(context.get_line(), 1, pos.col)
183+
local ns = nvim.create_namespace('blink_cmp_term_command_start')
184+
local extmarks = nvim.buf_get_extmarks(0, ns, { pos.row, pos.col }, { pos.row, 0 }, { limit = 1 })
183185

184186
--- If we find no mark for the start of the terminal command the terminal or shell
185187
--- probably does not support the FTCS_COMMAND_START escape sequence. The best effort

0 commit comments

Comments
 (0)