mirror of
https://github.com/neovim/neovim.git
synced 2025-02-25 18:55:25 -06:00
lsp: add incremental text synchronization
* Implementation derived from and validated by vim-lsc authored by Nate Bosch
This commit is contained in:
parent
c12ea02e0b
commit
e4e51c69d7
@ -751,8 +751,8 @@ start_client({config}) *vim.lsp.start_client()*
|
|||||||
table.
|
table.
|
||||||
>
|
>
|
||||||
|
|
||||||
-- In attach function for the client, you can do:
|
-- In init function for the client, you can do:
|
||||||
local custom_attach = function(client)
|
local custom_init = function(client)
|
||||||
if client.config.flags then
|
if client.config.flags then
|
||||||
client.config.flags.allow_incremental_sync = true
|
client.config.flags.allow_incremental_sync = true
|
||||||
end
|
end
|
||||||
|
@ -262,6 +262,13 @@ end
|
|||||||
--@param bufnr (Number) Number of the buffer, or 0 for current
|
--@param bufnr (Number) Number of the buffer, or 0 for current
|
||||||
--@param client Client object
|
--@param client Client object
|
||||||
local function text_document_did_open_handler(bufnr, client)
|
local function text_document_did_open_handler(bufnr, client)
|
||||||
|
local allow_incremental_sync = if_nil(client.config.flags.allow_incremental_sync, false)
|
||||||
|
if allow_incremental_sync then
|
||||||
|
if not client._cached_buffers then
|
||||||
|
client._cached_buffers = {}
|
||||||
|
end
|
||||||
|
client._cached_buffers[bufnr] = nvim_buf_get_lines(bufnr, 0, -1, true)
|
||||||
|
end
|
||||||
if not client.resolved_capabilities.text_document_open_close then
|
if not client.resolved_capabilities.text_document_open_close then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@ -808,7 +815,6 @@ end
|
|||||||
--- Notify all attached clients that a buffer has changed.
|
--- Notify all attached clients that a buffer has changed.
|
||||||
local text_document_did_change_handler
|
local text_document_did_change_handler
|
||||||
do
|
do
|
||||||
local encoding_index = { ["utf-8"] = 1; ["utf-16"] = 2; ["utf-32"] = 3; }
|
|
||||||
text_document_did_change_handler = function(_, bufnr, changedtick,
|
text_document_did_change_handler = function(_, bufnr, changedtick,
|
||||||
firstline, lastline, new_lastline, old_byte_size, old_utf32_size,
|
firstline, lastline, new_lastline, old_byte_size, old_utf32_size,
|
||||||
old_utf16_size)
|
old_utf16_size)
|
||||||
@ -827,23 +833,12 @@ do
|
|||||||
util.buf_versions[bufnr] = changedtick
|
util.buf_versions[bufnr] = changedtick
|
||||||
-- Lazy initialize these because clients may not even need them.
|
-- Lazy initialize these because clients may not even need them.
|
||||||
local incremental_changes = once(function(client)
|
local incremental_changes = once(function(client)
|
||||||
local size_index = encoding_index[client.offset_encoding]
|
local lines = nvim_buf_get_lines(bufnr, 0, -1, true)
|
||||||
local length = select(size_index, old_byte_size, old_utf16_size, old_utf32_size)
|
local startline = math.min(firstline + 1, math.min(#client._cached_buffers[bufnr], #lines))
|
||||||
local lines = nvim_buf_get_lines(bufnr, firstline, new_lastline, true)
|
local endline = math.min(-(#lines - new_lastline), 0)
|
||||||
|
local incremental_change = vim.lsp.util.compute_diff(client._cached_buffers[bufnr], lines, startline, endline)
|
||||||
-- This is necessary because we are specifying the full line including the
|
client._cached_buffers[bufnr] = lines
|
||||||
-- newline in range. Therefore, we must replace the newline as well.
|
return incremental_change
|
||||||
if #lines > 0 then
|
|
||||||
table.insert(lines, '')
|
|
||||||
end
|
|
||||||
return {
|
|
||||||
range = {
|
|
||||||
start = { line = firstline, character = 0 };
|
|
||||||
["end"] = { line = lastline, character = 0 };
|
|
||||||
};
|
|
||||||
rangeLength = length;
|
|
||||||
text = table.concat(lines, '\n');
|
|
||||||
};
|
|
||||||
end)
|
end)
|
||||||
local full_changes = once(function()
|
local full_changes = once(function()
|
||||||
return {
|
return {
|
||||||
@ -931,6 +926,9 @@ function lsp.buf_attach_client(bufnr, client_id)
|
|||||||
if client.resolved_capabilities.text_document_open_close then
|
if client.resolved_capabilities.text_document_open_close then
|
||||||
client.notify('textDocument/didClose', params)
|
client.notify('textDocument/didClose', params)
|
||||||
end
|
end
|
||||||
|
if client._cached_buffers then
|
||||||
|
client._cached_buffers[bufnr] = nil
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
util.buf_versions[bufnr] = nil
|
util.buf_versions[bufnr] = nil
|
||||||
all_buffer_active_clients[bufnr] = nil
|
all_buffer_active_clients[bufnr] = nil
|
||||||
|
@ -226,6 +226,165 @@ end
|
|||||||
-- function M.glob_to_regex(glob)
|
-- function M.glob_to_regex(glob)
|
||||||
-- end
|
-- end
|
||||||
|
|
||||||
|
--@private
|
||||||
|
--- Finds the first line and column of the difference between old and new lines
|
||||||
|
--@param old_lines table list of lines
|
||||||
|
--@param new_lines table list of lines
|
||||||
|
--@returns (int, int) start_line_idx and start_col_idx of range
|
||||||
|
local function first_difference(old_lines, new_lines, start_line_idx)
|
||||||
|
local line_count = math.min(#old_lines, #new_lines)
|
||||||
|
if line_count == 0 then return 1, 1 end
|
||||||
|
if not start_line_idx then
|
||||||
|
for i = 1, line_count do
|
||||||
|
start_line_idx = i
|
||||||
|
if old_lines[start_line_idx] ~= new_lines[start_line_idx] then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local old_line = old_lines[start_line_idx]
|
||||||
|
local new_line = new_lines[start_line_idx]
|
||||||
|
local length = math.min(#old_line, #new_line)
|
||||||
|
local start_col_idx = 1
|
||||||
|
while start_col_idx <= length do
|
||||||
|
if string.sub(old_line, start_col_idx, start_col_idx) ~= string.sub(new_line, start_col_idx, start_col_idx) then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
start_col_idx = start_col_idx + 1
|
||||||
|
end
|
||||||
|
return start_line_idx, start_col_idx
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
--@private
|
||||||
|
--- Finds the last line and column of the differences between old and new lines
|
||||||
|
--@param old_lines table list of lines
|
||||||
|
--@param new_lines table list of lines
|
||||||
|
--@param start_char integer First different character idx of range
|
||||||
|
--@returns (int, int) end_line_idx and end_col_idx of range
|
||||||
|
local function last_difference(old_lines, new_lines, start_char, end_line_idx)
|
||||||
|
local line_count = math.min(#old_lines, #new_lines)
|
||||||
|
if line_count == 0 then return 0,0 end
|
||||||
|
if not end_line_idx then
|
||||||
|
end_line_idx = -1
|
||||||
|
end
|
||||||
|
for i = end_line_idx, -line_count, -1 do
|
||||||
|
if old_lines[#old_lines + i + 1] ~= new_lines[#new_lines + i + 1] then
|
||||||
|
end_line_idx = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local old_line
|
||||||
|
local new_line
|
||||||
|
if end_line_idx <= -line_count then
|
||||||
|
end_line_idx = -line_count
|
||||||
|
old_line = string.sub(old_lines[#old_lines + end_line_idx + 1], start_char)
|
||||||
|
new_line = string.sub(new_lines[#new_lines + end_line_idx + 1], start_char)
|
||||||
|
else
|
||||||
|
old_line = old_lines[#old_lines + end_line_idx + 1]
|
||||||
|
new_line = new_lines[#new_lines + end_line_idx + 1]
|
||||||
|
end
|
||||||
|
local old_line_length = #old_line
|
||||||
|
local new_line_length = #new_line
|
||||||
|
local length = math.min(old_line_length, new_line_length)
|
||||||
|
local end_col_idx = -1
|
||||||
|
while end_col_idx >= -length do
|
||||||
|
local old_char = string.sub(old_line, old_line_length + end_col_idx + 1, old_line_length + end_col_idx + 1)
|
||||||
|
local new_char = string.sub(new_line, new_line_length + end_col_idx + 1, new_line_length + end_col_idx + 1)
|
||||||
|
if old_char ~= new_char then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end_col_idx = end_col_idx - 1
|
||||||
|
end
|
||||||
|
return end_line_idx, end_col_idx
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
--@private
|
||||||
|
--- Get the text of the range defined by start and end line/column
|
||||||
|
--@param lines table list of lines
|
||||||
|
--@param start_char integer First different character idx of range
|
||||||
|
--@param end_char integer Last different character idx of range
|
||||||
|
--@param start_line integer First different line idx of range
|
||||||
|
--@param end_line integer Last different line idx of range
|
||||||
|
--@returns string text extracted from defined region
|
||||||
|
local function extract_text(lines, start_line, start_char, end_line, end_char)
|
||||||
|
if start_line == #lines + end_line + 1 then
|
||||||
|
if end_line == 0 then return '' end
|
||||||
|
local line = lines[start_line]
|
||||||
|
local length = #line + end_char - start_char
|
||||||
|
return string.sub(line, start_char, start_char + length + 1)
|
||||||
|
end
|
||||||
|
local result = string.sub(lines[start_line], start_char) .. '\n'
|
||||||
|
for line_idx = start_line + 1, #lines + end_line do
|
||||||
|
result = result .. lines[line_idx] .. '\n'
|
||||||
|
end
|
||||||
|
if end_line ~= 0 then
|
||||||
|
local line = lines[#lines + end_line + 1]
|
||||||
|
local length = #line + end_char + 1
|
||||||
|
result = result .. string.sub(line, 1, length)
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
--@private
|
||||||
|
--- Compute the length of the substituted range
|
||||||
|
--@param lines table list of lines
|
||||||
|
--@param start_char integer First different character idx of range
|
||||||
|
--@param end_char integer Last different character idx of range
|
||||||
|
--@param start_line integer First different line idx of range
|
||||||
|
--@param end_line integer Last different line idx of range
|
||||||
|
--@returns (int, int) end_line_idx and end_col_idx of range
|
||||||
|
local function compute_length(lines, start_line, start_char, end_line, end_char)
|
||||||
|
local adj_end_line = #lines + end_line + 1
|
||||||
|
local adj_end_char
|
||||||
|
if adj_end_line > #lines then
|
||||||
|
adj_end_char = end_char - 1
|
||||||
|
else
|
||||||
|
adj_end_char = #lines[adj_end_line] + end_char
|
||||||
|
end
|
||||||
|
if start_line == adj_end_line then
|
||||||
|
return adj_end_char - start_char + 1
|
||||||
|
end
|
||||||
|
local result = #lines[start_line] - start_char + 1
|
||||||
|
for line = start_line + 1, adj_end_line -1 do
|
||||||
|
result = result + #lines[line] + 1
|
||||||
|
end
|
||||||
|
result = result + adj_end_char + 1
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns the range table for the difference between old and new lines
|
||||||
|
--@param old_lines table list of lines
|
||||||
|
--@param new_lines table list of lines
|
||||||
|
--@returns table start_line_idx and start_col_idx of range
|
||||||
|
function M.compute_diff(old_lines, new_lines, start_line_idx, end_line_idx)
|
||||||
|
local start_line, start_char = first_difference(old_lines, new_lines, start_line_idx)
|
||||||
|
local end_line, end_char = last_difference(vim.list_slice(old_lines, start_line, #old_lines),
|
||||||
|
vim.list_slice(new_lines, start_line, #new_lines), start_char, end_line_idx)
|
||||||
|
local text = extract_text(new_lines, start_line, start_char, end_line, end_char)
|
||||||
|
local length = compute_length(old_lines, start_line, start_char, end_line, end_char)
|
||||||
|
|
||||||
|
local adj_end_line = #old_lines + end_line
|
||||||
|
local adj_end_char
|
||||||
|
if end_line == 0 then
|
||||||
|
adj_end_char = 0
|
||||||
|
else
|
||||||
|
adj_end_char = #old_lines[#old_lines + end_line + 1] + end_char + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local result = {
|
||||||
|
range = {
|
||||||
|
start = { line = start_line - 1, character = start_char - 1},
|
||||||
|
["end"] = { line = adj_end_line, character = adj_end_char}
|
||||||
|
},
|
||||||
|
text = text,
|
||||||
|
rangeLength = length + 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
--- Can be used to extract the completion items from a
|
--- Can be used to extract the completion items from a
|
||||||
--- `textDocument/completion` request, which may return one of
|
--- `textDocument/completion` request, which may return one of
|
||||||
--- `CompletionItem[]`, `CompletionList` or null.
|
--- `CompletionItem[]`, `CompletionList` or null.
|
||||||
|
@ -400,6 +400,20 @@ function vim.tbl_count(t)
|
|||||||
return count
|
return count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Creates a copy of a table containing only elements from start to end (inclusive)
|
||||||
|
---
|
||||||
|
--@param list table table
|
||||||
|
--@param start integer Start range of slice
|
||||||
|
--@param finish integer End range of slice
|
||||||
|
--@returns Copy of table sliced from start to finish (inclusive)
|
||||||
|
function vim.list_slice(list, start, finish)
|
||||||
|
local new_list = {}
|
||||||
|
for i = start or 1, finish or #list do
|
||||||
|
new_list[#new_list+1] = list[i]
|
||||||
|
end
|
||||||
|
return new_list
|
||||||
|
end
|
||||||
|
|
||||||
--- Trim whitespace (Lua pattern "%s") from both sides of a string.
|
--- Trim whitespace (Lua pattern "%s") from both sides of a string.
|
||||||
---
|
---
|
||||||
--@see https://www.lua.org/pil/20.2.html
|
--@see https://www.lua.org/pil/20.2.html
|
||||||
|
@ -402,11 +402,11 @@ function tests.basic_check_buffer_open_and_change_incremental()
|
|||||||
contentChanges = {
|
contentChanges = {
|
||||||
{
|
{
|
||||||
range = {
|
range = {
|
||||||
start = { line = 1; character = 0; };
|
start = { line = 1; character = 3; };
|
||||||
["end"] = { line = 2; character = 0; };
|
["end"] = { line = 1; character = 3; };
|
||||||
};
|
};
|
||||||
rangeLength = 4;
|
rangeLength = 0;
|
||||||
text = "boop\n";
|
text = "boop";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -27,10 +27,10 @@ teardown(function()
|
|||||||
os.remove(fake_lsp_logfile)
|
os.remove(fake_lsp_logfile)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local function fake_lsp_server_setup(test_name, timeout_ms)
|
local function fake_lsp_server_setup(test_name, timeout_ms, options)
|
||||||
exec_lua([=[
|
exec_lua([=[
|
||||||
lsp = require('vim.lsp')
|
lsp = require('vim.lsp')
|
||||||
local test_name, fixture_filename, logfile, timeout = ...
|
local test_name, fixture_filename, logfile, timeout, options = ...
|
||||||
TEST_RPC_CLIENT_ID = lsp.start_client {
|
TEST_RPC_CLIENT_ID = lsp.start_client {
|
||||||
cmd_env = {
|
cmd_env = {
|
||||||
NVIM_LOG_FILE = logfile;
|
NVIM_LOG_FILE = logfile;
|
||||||
@ -52,18 +52,19 @@ local function fake_lsp_server_setup(test_name, timeout_ms)
|
|||||||
on_init = function(client, result)
|
on_init = function(client, result)
|
||||||
TEST_RPC_CLIENT = client
|
TEST_RPC_CLIENT = client
|
||||||
vim.rpcrequest(1, "init", result)
|
vim.rpcrequest(1, "init", result)
|
||||||
|
client.config.flags.allow_incremental_sync = options.allow_incremental_sync or false
|
||||||
end;
|
end;
|
||||||
on_exit = function(...)
|
on_exit = function(...)
|
||||||
vim.rpcnotify(1, "exit", ...)
|
vim.rpcnotify(1, "exit", ...)
|
||||||
end;
|
end;
|
||||||
}
|
}
|
||||||
]=], test_name, fake_lsp_code, fake_lsp_logfile, timeout_ms or 1e3)
|
]=], test_name, fake_lsp_code, fake_lsp_logfile, timeout_ms or 1e3, options or {})
|
||||||
end
|
end
|
||||||
|
|
||||||
local function test_rpc_server(config)
|
local function test_rpc_server(config)
|
||||||
if config.test_name then
|
if config.test_name then
|
||||||
clear()
|
clear()
|
||||||
fake_lsp_server_setup(config.test_name, config.timeout_ms or 1e3)
|
fake_lsp_server_setup(config.test_name, config.timeout_ms or 1e3, config.options)
|
||||||
end
|
end
|
||||||
local client = setmetatable({}, {
|
local client = setmetatable({}, {
|
||||||
__index = function(_, name)
|
__index = function(_, name)
|
||||||
@ -680,8 +681,7 @@ describe('LSP', function()
|
|||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
-- TODO(askhan) we don't support full for now, so we can disable these tests.
|
it('should check the body and didChange incremental', function()
|
||||||
pending('should check the body and didChange incremental', function()
|
|
||||||
local expected_callbacks = {
|
local expected_callbacks = {
|
||||||
{NIL, "shutdown", {}, 1};
|
{NIL, "shutdown", {}, 1};
|
||||||
{NIL, "finish", {}, 1};
|
{NIL, "finish", {}, 1};
|
||||||
@ -690,6 +690,7 @@ describe('LSP', function()
|
|||||||
local client
|
local client
|
||||||
test_rpc_server {
|
test_rpc_server {
|
||||||
test_name = "basic_check_buffer_open_and_change_incremental";
|
test_name = "basic_check_buffer_open_and_change_incremental";
|
||||||
|
options = { allow_incremental_sync = true };
|
||||||
on_setup = function()
|
on_setup = function()
|
||||||
exec_lua [[
|
exec_lua [[
|
||||||
BUFFER = vim.api.nvim_create_buf(false, true)
|
BUFFER = vim.api.nvim_create_buf(false, true)
|
||||||
@ -716,7 +717,7 @@ describe('LSP', function()
|
|||||||
if method == 'start' then
|
if method == 'start' then
|
||||||
exec_lua [[
|
exec_lua [[
|
||||||
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
|
vim.api.nvim_buf_set_lines(BUFFER, 1, 2, false, {
|
||||||
"boop";
|
"123boop";
|
||||||
})
|
})
|
||||||
]]
|
]]
|
||||||
client.notify('finish')
|
client.notify('finish')
|
||||||
|
Loading…
Reference in New Issue
Block a user