lsp_spec.lua (230011B)
1 local t = require('test.testutil') 2 local n = require('test.functional.testnvim')() 3 4 local t_lsp = require('test.functional.plugin.lsp.testutil') 5 6 local buf_lines = n.buf_lines 7 local command = n.command 8 local dedent = t.dedent 9 local exec_lua = n.exec_lua 10 local eq = t.eq 11 local eval = n.eval 12 local matches = t.matches 13 local pcall_err = t.pcall_err 14 local pesc = vim.pesc 15 local insert = n.insert 16 local fn = n.fn 17 local retry = t.retry 18 local stop = n.stop 19 local NIL = vim.NIL 20 local read_file = t.read_file 21 local write_file = t.write_file 22 local is_ci = t.is_ci 23 local api = n.api 24 local is_os = t.is_os 25 local skip = t.skip 26 local mkdir = t.mkdir 27 local tmpname = t.tmpname 28 29 local clear_notrace = t_lsp.clear_notrace 30 local create_server_definition = t_lsp.create_server_definition 31 local fake_lsp_code = t_lsp.fake_lsp_code 32 local fake_lsp_logfile = t_lsp.fake_lsp_logfile 33 local test_rpc_server = t_lsp.test_rpc_server 34 local create_tcp_echo_server = t_lsp.create_tcp_echo_server 35 36 local function get_buf_option(name, bufnr) 37 return exec_lua(function() 38 bufnr = bufnr or _G.BUFFER 39 return vim.api.nvim_get_option_value(name, { buf = bufnr }) 40 end) 41 end 42 43 local function make_edit(y_0, x_0, y_1, x_1, text) 44 return { 45 range = { 46 start = { line = y_0, character = x_0 }, 47 ['end'] = { line = y_1, character = x_1 }, 48 }, 49 newText = type(text) == 'table' and table.concat(text, '\n') or (text or ''), 50 } 51 end 52 53 --- @param edits [integer, integer, integer, integer, string|string[]][] 54 --- @param encoding? string 55 local function apply_text_edits(edits, encoding) 56 local edits1 = vim.tbl_map( 57 --- @param edit [integer, integer, integer, integer, string|string[]] 58 function(edit) 59 return make_edit(unpack(edit)) 60 end, 61 edits 62 ) 63 exec_lua(function() 64 vim.lsp.util.apply_text_edits(edits1, 1, encoding or 'utf-16') 65 end) 66 end 67 68 --- @param notification_cb fun(method: 'body' | 'error', args: any) 69 local function verify_single_notification(notification_cb) 70 local called = false 71 n.run(nil, function(method, args) 72 notification_cb(method, args) 73 stop() 74 called = true 75 end, nil, 1000) 76 eq(true, called) 77 end 78 79 -- TODO(justinmk): hangs on Windows https://github.com/neovim/neovim/pull/11837 80 if skip(is_os('win')) then 81 return 82 end 83 84 describe('LSP', function() 85 before_each(function() 86 clear_notrace() 87 end) 88 89 after_each(function() 90 stop() 91 exec_lua(function() 92 vim.iter(vim.lsp.get_clients({ _uninitialized = true })):each(function(client) 93 client:stop(true) 94 end) 95 end) 96 api.nvim_exec_autocmds('VimLeavePre', { modeline = false }) 97 end) 98 99 teardown(function() 100 os.remove(fake_lsp_logfile) 101 end) 102 103 describe('server_name specified', function() 104 before_each(function() 105 -- Run an instance of nvim on the file which contains our "scripts". 106 -- Pass TEST_NAME to pick the script. 107 local test_name = 'basic_init' 108 exec_lua(function() 109 _G.lsp = require('vim.lsp') 110 function _G.test__start_client() 111 return vim.lsp.start({ 112 cmd_env = { 113 NVIM_LOG_FILE = fake_lsp_logfile, 114 NVIM_APPNAME = 'nvim_lsp_test', 115 }, 116 cmd = { 117 vim.v.progpath, 118 '-l', 119 fake_lsp_code, 120 test_name, 121 }, 122 workspace_folders = { 123 { 124 uri = 'file://' .. vim.uv.cwd(), 125 name = 'test_folder', 126 }, 127 }, 128 }, { attach = false }) 129 end 130 _G.TEST_CLIENT1 = _G.test__start_client() 131 end) 132 end) 133 134 it('start_client(), Client:stop()', function() 135 retry(nil, 4000, function() 136 eq( 137 1, 138 exec_lua(function() 139 return #vim.lsp.get_clients() 140 end) 141 ) 142 end) 143 eq( 144 2, 145 exec_lua(function() 146 _G.TEST_CLIENT2 = _G.test__start_client() 147 return _G.TEST_CLIENT2 148 end) 149 ) 150 eq( 151 3, 152 exec_lua(function() 153 _G.TEST_CLIENT3 = _G.test__start_client() 154 return _G.TEST_CLIENT3 155 end) 156 ) 157 retry(nil, 4000, function() 158 eq( 159 3, 160 exec_lua(function() 161 return #vim.lsp.get_clients() 162 end) 163 ) 164 end) 165 166 eq( 167 false, 168 exec_lua(function() 169 return vim.lsp.get_client_by_id(_G.TEST_CLIENT1) == nil 170 end) 171 ) 172 eq( 173 false, 174 exec_lua(function() 175 return vim.lsp.get_client_by_id(_G.TEST_CLIENT1).is_stopped() 176 end) 177 ) 178 exec_lua(function() 179 return vim.lsp.get_client_by_id(_G.TEST_CLIENT1).stop() 180 end) 181 retry(nil, 4000, function() 182 eq( 183 2, 184 exec_lua(function() 185 return #vim.lsp.get_clients() 186 end) 187 ) 188 end) 189 eq( 190 true, 191 exec_lua(function() 192 return vim.lsp.get_client_by_id(_G.TEST_CLIENT1) == nil 193 end) 194 ) 195 196 exec_lua(function() 197 vim.lsp.get_client_by_id(_G.TEST_CLIENT2):stop() 198 vim.lsp.get_client_by_id(_G.TEST_CLIENT3):stop() 199 end) 200 retry(nil, 4000, function() 201 eq( 202 0, 203 exec_lua(function() 204 return #vim.lsp.get_clients() 205 end) 206 ) 207 end) 208 end) 209 210 it('does not reuse an already-stopping client #33616', function() 211 -- we immediately try to start a second client with the same name/root 212 -- before the first one has finished shutting down; we must get a new id. 213 local clients = exec_lua(function() 214 local client1 = assert(vim.lsp.start({ 215 name = 'dup-test', 216 cmd = { vim.v.progpath, '-l', fake_lsp_code, 'basic_init' }, 217 }, { attach = false })) 218 vim.lsp.get_client_by_id(client1):stop() 219 local client2 = assert(vim.lsp.start({ 220 name = 'dup-test', 221 cmd = { vim.v.progpath, '-l', fake_lsp_code, 'basic_init' }, 222 }, { attach = false })) 223 return { client1, client2 } 224 end) 225 local c1, c2 = clients[1], clients[2] 226 eq(false, c1 == c2, 'Expected a fresh client while the old one is stopping') 227 end) 228 end) 229 230 describe('basic_init test', function() 231 it('should run correctly', function() 232 local expected_handlers = { 233 { NIL, {}, { method = 'test', client_id = 1 } }, 234 } 235 test_rpc_server { 236 test_name = 'basic_init', 237 on_init = function(client, _) 238 -- client is a dummy object which will queue up commands to be run 239 -- once the server initializes. It can't accept lua callbacks or 240 -- other types that may be unserializable for now. 241 client:stop() 242 end, 243 -- If the program timed out, then code will be nil. 244 on_exit = function(code, signal) 245 eq(0, code, 'exit code') 246 eq(0, signal, 'exit signal') 247 end, 248 -- Note that NIL must be used here. 249 -- on_handler(err, method, result, client_id) 250 on_handler = function(...) 251 eq(table.remove(expected_handlers), { ... }) 252 end, 253 } 254 end) 255 256 it('should fail', function() 257 local expected_handlers = { 258 { NIL, {}, { method = 'test', client_id = 1 } }, 259 } 260 test_rpc_server { 261 test_name = 'basic_init', 262 on_init = function(client) 263 client:notify('test') 264 client:stop() 265 end, 266 on_exit = function(code, signal) 267 eq(101, code, 'exit code') -- See fake-lsp-server.lua 268 eq(0, signal, 'exit signal') 269 t.assert_log( 270 pesc([[assert_eq failed: left == "\"shutdown\"", right == "\"test\""]]), 271 fake_lsp_logfile 272 ) 273 end, 274 on_handler = function(...) 275 eq(table.remove(expected_handlers), { ... }, 'expected handler') 276 end, 277 } 278 end) 279 280 it('should send didChangeConfiguration after initialize if there are settings', function() 281 test_rpc_server({ 282 test_name = 'basic_init_did_change_configuration', 283 on_init = function(client, _) 284 client:stop() 285 end, 286 on_exit = function(code, signal) 287 eq(0, code, 'exit code') 288 eq(0, signal, 'exit signal') 289 end, 290 settings = { 291 dummy = 1, 292 }, 293 }) 294 end) 295 296 it( 297 "should set the client's offset_encoding when positionEncoding capability is supported", 298 function() 299 exec_lua(create_server_definition) 300 local result = exec_lua(function() 301 local server = _G._create_server({ 302 capabilities = { 303 positionEncoding = 'utf-8', 304 }, 305 }) 306 307 local client_id = vim.lsp.start({ 308 name = 'dummy', 309 cmd = server.cmd, 310 }) 311 312 if not client_id then 313 return 'vim.lsp.start did not return client_id' 314 end 315 316 local client = vim.lsp.get_client_by_id(client_id) 317 if not client then 318 return 'No client found with id ' .. client_id 319 end 320 return client.offset_encoding 321 end) 322 eq('utf-8', result) 323 end 324 ) 325 326 it('should succeed with manual shutdown', function() 327 local expected_handlers = { 328 { NIL, {}, { method = 'shutdown', bufnr = 1, client_id = 1, request_id = 2, version = 0 } }, 329 { NIL, {}, { method = 'test', client_id = 1 } }, 330 } 331 test_rpc_server { 332 test_name = 'basic_init', 333 on_init = function(client) 334 eq(0, client.server_capabilities().textDocumentSync.change) 335 client:request('shutdown') 336 client:notify('exit') 337 client:stop() 338 end, 339 on_exit = function(code, signal) 340 eq(0, code, 'exit code') 341 eq(0, signal, 'exit signal') 342 end, 343 on_handler = function(...) 344 eq(table.remove(expected_handlers), { ... }, 'expected handler') 345 end, 346 } 347 end) 348 349 it('should detach buffer in response to nvim_buf_detach', function() 350 local expected_handlers = { 351 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 352 { NIL, {}, { method = 'finish', client_id = 1 } }, 353 } 354 local client --- @type vim.lsp.Client 355 test_rpc_server { 356 test_name = 'basic_finish', 357 on_setup = function() 358 exec_lua(function() 359 _G.BUFFER = vim.api.nvim_create_buf(false, true) 360 end) 361 eq( 362 true, 363 exec_lua(function() 364 return vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID) 365 end) 366 ) 367 eq( 368 true, 369 exec_lua(function() 370 return vim.lsp.buf_is_attached(_G.BUFFER, _G.TEST_RPC_CLIENT_ID) 371 end) 372 ) 373 exec_lua(function() 374 vim.cmd(_G.BUFFER .. 'bwipeout') 375 end) 376 end, 377 on_init = function(_client) 378 client = _client 379 client:notify('finish') 380 end, 381 on_exit = function(code, signal) 382 eq(0, code, 'exit code') 383 eq(0, signal, 'exit signal') 384 end, 385 on_handler = function(err, result, ctx) 386 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 387 if ctx.method == 'finish' then 388 exec_lua(function() 389 return vim.lsp.buf_detach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID) 390 end) 391 eq( 392 false, 393 exec_lua(function() 394 return vim.lsp.buf_is_attached(_G.BUFFER, _G.TEST_RPC_CLIENT_ID) 395 end) 396 ) 397 client:stop() 398 end 399 end, 400 } 401 end) 402 403 it('should fire autocommands on attach and detach', function() 404 local client --- @type vim.lsp.Client 405 test_rpc_server { 406 test_name = 'basic_init', 407 on_setup = function() 408 exec_lua(function() 409 _G.BUFFER = vim.api.nvim_create_buf(false, true) 410 vim.api.nvim_create_autocmd('LspAttach', { 411 callback = function(args) 412 local client0 = assert(vim.lsp.get_client_by_id(args.data.client_id)) 413 vim.g.lsp_attached = client0.name 414 end, 415 }) 416 vim.api.nvim_create_autocmd('LspDetach', { 417 callback = function(args) 418 local client0 = assert(vim.lsp.get_client_by_id(args.data.client_id)) 419 vim.g.lsp_detached = client0.name 420 end, 421 }) 422 end) 423 end, 424 on_init = function(_client) 425 client = _client 426 eq( 427 true, 428 exec_lua(function() 429 return vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID) 430 end) 431 ) 432 client:notify('finish') 433 end, 434 on_handler = function(_, _, ctx) 435 if ctx.method == 'finish' then 436 eq('basic_init', api.nvim_get_var('lsp_attached')) 437 exec_lua(function() 438 return vim.lsp.buf_detach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID) 439 end) 440 eq('basic_init', api.nvim_get_var('lsp_detached')) 441 client:stop() 442 end 443 end, 444 } 445 end) 446 447 it('should set default options on attach', function() 448 local client --- @type vim.lsp.Client 449 test_rpc_server { 450 test_name = 'set_defaults_all_capabilities', 451 on_init = function(_client) 452 client = _client 453 exec_lua(function() 454 _G.BUFFER = vim.api.nvim_create_buf(false, true) 455 vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID) 456 end) 457 end, 458 on_handler = function(_, _, ctx) 459 if ctx.method == 'test' then 460 eq('v:lua.vim.lsp.tagfunc', get_buf_option('tagfunc')) 461 eq('v:lua.vim.lsp.omnifunc', get_buf_option('omnifunc')) 462 eq('v:lua.vim.lsp.formatexpr()', get_buf_option('formatexpr')) 463 eq('', get_buf_option('keywordprg')) 464 eq( 465 true, 466 exec_lua(function() 467 local keymap --- @type table<string,any> 468 local called = false 469 local origin = vim.lsp.buf.hover 470 vim.lsp.buf.hover = function() 471 called = true 472 end 473 vim._with({ buf = _G.BUFFER }, function() 474 keymap = vim.fn.maparg('K', 'n', false, true) 475 end) 476 keymap.callback() 477 vim.lsp.buf.hover = origin 478 return called 479 end) 480 ) 481 client:stop() 482 end 483 end, 484 on_exit = function(_, _) 485 eq('', get_buf_option('tagfunc')) 486 eq('', get_buf_option('omnifunc')) 487 eq('', get_buf_option('formatexpr')) 488 eq( 489 true, 490 exec_lua(function() 491 local keymap --- @type string 492 vim._with({ buf = _G.BUFFER }, function() 493 keymap = vim.fn.maparg('K', 'n', false, false) 494 end) 495 return keymap:match('<Lua %d+: .*runtime/lua/vim/lsp%.lua:%d+>') ~= nil 496 end) 497 ) 498 end, 499 } 500 end) 501 502 it('should overwrite options set by ftplugins', function() 503 if t.is_zig_build() then 504 return pending('TODO: broken with zig build') 505 end 506 local client --- @type vim.lsp.Client 507 local BUFFER_1 --- @type integer 508 local BUFFER_2 --- @type integer 509 test_rpc_server { 510 test_name = 'set_defaults_all_capabilities', 511 on_init = function(_client) 512 client = _client 513 exec_lua(function() 514 vim.api.nvim_command('filetype plugin on') 515 BUFFER_1 = vim.api.nvim_create_buf(false, true) 516 BUFFER_2 = vim.api.nvim_create_buf(false, true) 517 vim.api.nvim_set_option_value('filetype', 'man', { buf = BUFFER_1 }) 518 vim.api.nvim_set_option_value('filetype', 'xml', { buf = BUFFER_2 }) 519 end) 520 521 -- Sanity check to ensure that some values are set after setting filetype. 522 eq("v:lua.require'man'.goto_tag", get_buf_option('tagfunc', BUFFER_1)) 523 eq('xmlcomplete#CompleteTags', get_buf_option('omnifunc', BUFFER_2)) 524 eq('xmlformat#Format()', get_buf_option('formatexpr', BUFFER_2)) 525 526 exec_lua(function() 527 vim.lsp.buf_attach_client(BUFFER_1, _G.TEST_RPC_CLIENT_ID) 528 vim.lsp.buf_attach_client(BUFFER_2, _G.TEST_RPC_CLIENT_ID) 529 end) 530 end, 531 on_handler = function(_, _, ctx) 532 if ctx.method == 'test' then 533 eq('v:lua.vim.lsp.tagfunc', get_buf_option('tagfunc', BUFFER_1)) 534 eq('v:lua.vim.lsp.omnifunc', get_buf_option('omnifunc', BUFFER_2)) 535 eq('v:lua.vim.lsp.formatexpr()', get_buf_option('formatexpr', BUFFER_2)) 536 client:stop() 537 end 538 end, 539 on_exit = function(_, _) 540 eq('', get_buf_option('tagfunc', BUFFER_1)) 541 eq('', get_buf_option('omnifunc', BUFFER_2)) 542 eq('', get_buf_option('formatexpr', BUFFER_2)) 543 end, 544 } 545 end) 546 547 it('should not overwrite user-defined options', function() 548 local client --- @type vim.lsp.Client 549 test_rpc_server { 550 test_name = 'set_defaults_all_capabilities', 551 on_init = function(_client) 552 client = _client 553 exec_lua(function() 554 _G.BUFFER = vim.api.nvim_create_buf(false, true) 555 vim.api.nvim_set_option_value('tagfunc', 'tfu', { buf = _G.BUFFER }) 556 vim.api.nvim_set_option_value('omnifunc', 'ofu', { buf = _G.BUFFER }) 557 vim.api.nvim_set_option_value('formatexpr', 'fex', { buf = _G.BUFFER }) 558 vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID) 559 end) 560 end, 561 on_handler = function(_, _, ctx) 562 if ctx.method == 'test' then 563 eq('tfu', get_buf_option('tagfunc')) 564 eq('ofu', get_buf_option('omnifunc')) 565 eq('fex', get_buf_option('formatexpr')) 566 client:stop() 567 end 568 end, 569 on_exit = function(_, _) 570 eq('tfu', get_buf_option('tagfunc')) 571 eq('ofu', get_buf_option('omnifunc')) 572 eq('fex', get_buf_option('formatexpr')) 573 end, 574 } 575 end) 576 577 it('should detach buffer on bufwipe', function() 578 exec_lua(create_server_definition) 579 local result = exec_lua(function() 580 local server = _G._create_server() 581 local bufnr1 = vim.api.nvim_create_buf(false, true) 582 local bufnr2 = vim.api.nvim_create_buf(false, true) 583 local detach_called1 = false 584 local detach_called2 = false 585 vim.api.nvim_create_autocmd('LspDetach', { 586 buffer = bufnr1, 587 callback = function() 588 detach_called1 = true 589 end, 590 }) 591 vim.api.nvim_create_autocmd('LspDetach', { 592 buffer = bufnr2, 593 callback = function() 594 detach_called2 = true 595 end, 596 }) 597 vim.api.nvim_set_current_buf(bufnr1) 598 local client_id = assert(vim.lsp.start({ name = 'detach-dummy', cmd = server.cmd })) 599 local client = assert(vim.lsp.get_client_by_id(client_id)) 600 vim.api.nvim_set_current_buf(bufnr2) 601 vim.lsp.start({ name = 'detach-dummy', cmd = server.cmd }) 602 assert(vim.tbl_count(client.attached_buffers) == 2) 603 vim.api.nvim_buf_delete(bufnr1, { force = true }) 604 assert(vim.tbl_count(client.attached_buffers) == 1) 605 vim.api.nvim_buf_delete(bufnr2, { force = true }) 606 assert(vim.tbl_count(client.attached_buffers) == 0) 607 return detach_called1 and detach_called2 608 end) 609 eq(true, result) 610 end) 611 612 it('should not re-attach buffer if it was deleted in on_init #28575', function() 613 exec_lua(create_server_definition) 614 exec_lua(function() 615 local server = _G._create_server({ 616 handlers = { 617 initialize = function(_, _, callback) 618 vim.schedule(function() 619 callback(nil, { capabilities = {} }) 620 end) 621 end, 622 }, 623 }) 624 local bufnr = vim.api.nvim_create_buf(false, true) 625 local on_init_called = false 626 local client_id = assert(vim.lsp.start({ 627 name = 'detach-dummy', 628 cmd = server.cmd, 629 on_init = function() 630 vim.api.nvim_buf_delete(bufnr, {}) 631 on_init_called = true 632 end, 633 })) 634 vim.lsp.buf_attach_client(bufnr, client_id) 635 local ok = vim.wait(1000, function() 636 return on_init_called 637 end) 638 assert(ok, 'on_init was not called') 639 end) 640 end) 641 642 it('should allow on_lines + nvim_buf_delete during LSP initialization #28575', function() 643 exec_lua(create_server_definition) 644 exec_lua(function() 645 local initialized = false 646 local server = _G._create_server({ 647 handlers = { 648 initialize = function(_, _, callback) 649 vim.schedule(function() 650 callback(nil, { capabilities = {} }) 651 initialized = true 652 end) 653 end, 654 }, 655 }) 656 local bufnr = vim.api.nvim_create_buf(false, true) 657 vim.api.nvim_set_current_buf(bufnr) 658 vim.lsp.start({ 659 name = 'detach-dummy', 660 cmd = server.cmd, 661 }) 662 vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 'hello' }) 663 vim.api.nvim_buf_delete(bufnr, {}) 664 local ok = vim.wait(1000, function() 665 return initialized 666 end) 667 assert(ok, 'lsp did not initialize') 668 end) 669 end) 670 671 it('client should return settings via workspace/configuration handler', function() 672 local expected_handlers = { 673 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 674 { 675 NIL, 676 { 677 items = { 678 { section = 'testSetting1' }, 679 { section = 'testSetting2' }, 680 { section = 'test.Setting3' }, 681 { section = 'test.Setting4' }, 682 {}, 683 { section = '' }, 684 }, 685 }, 686 { method = 'workspace/configuration', client_id = 1 }, 687 }, 688 { NIL, {}, { method = 'start', client_id = 1 } }, 689 } 690 local client --- @type vim.lsp.Client 691 test_rpc_server { 692 test_name = 'check_workspace_configuration', 693 on_init = function(_client) 694 client = _client 695 end, 696 on_exit = function(code, signal) 697 eq(0, code, 'exit code') 698 eq(0, signal, 'exit signal') 699 end, 700 on_handler = function(err, result, ctx) 701 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 702 if ctx.method == 'start' then 703 exec_lua(function() 704 local client0 = vim.lsp.get_client_by_id(_G.TEST_RPC_CLIENT_ID) 705 client0.settings = { 706 testSetting1 = true, 707 testSetting2 = false, 708 test = { Setting3 = 'nested' }, 709 } 710 end) 711 end 712 if ctx.method == 'workspace/configuration' then 713 local server_result = exec_lua( 714 [[ 715 local method, params = ... 716 return require 'vim.lsp.handlers'['workspace/configuration']( 717 err, 718 params, 719 { method = method, client_id = _G.TEST_RPC_CLIENT_ID } 720 ) 721 ]], 722 ctx.method, 723 result 724 ) 725 client:notify('workspace/configuration', server_result) 726 end 727 if ctx.method == 'shutdown' then 728 client:stop() 729 end 730 end, 731 } 732 end) 733 734 it( 735 'workspace/configuration returns NIL per section if client was started without config.settings', 736 function() 737 local result = nil 738 test_rpc_server { 739 test_name = 'basic_init', 740 on_init = function(c) 741 c.stop() 742 end, 743 on_setup = function() 744 result = exec_lua(function() 745 local result0 = { 746 items = { 747 { section = 'foo' }, 748 { section = 'bar' }, 749 }, 750 } 751 return vim.lsp.handlers['workspace/configuration']( 752 nil, 753 result0, 754 { client_id = _G.TEST_RPC_CLIENT_ID } 755 ) 756 end) 757 end, 758 } 759 eq({ NIL, NIL }, result) 760 end 761 ) 762 763 it('should verify capabilities sent', function() 764 local expected_handlers = { 765 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 766 } 767 test_rpc_server { 768 test_name = 'basic_check_capabilities', 769 on_init = function(client) 770 client:stop() 771 local full_kind = exec_lua(function() 772 return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full 773 end) 774 eq(full_kind, client.server_capabilities().textDocumentSync.change) 775 eq({ includeText = false }, client.server_capabilities().textDocumentSync.save) 776 eq(false, client.server_capabilities().codeLensProvider) 777 end, 778 on_exit = function(code, signal) 779 eq(0, code, 'exit code') 780 eq(0, signal, 'exit signal') 781 end, 782 on_handler = function(...) 783 eq(table.remove(expected_handlers), { ... }, 'expected handler') 784 end, 785 } 786 end) 787 788 it('BufWritePost sends didSave with bool textDocumentSync.save', function() 789 local expected_handlers = { 790 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 791 { NIL, {}, { method = 'start', client_id = 1 } }, 792 } 793 local client --- @type vim.lsp.Client 794 test_rpc_server { 795 test_name = 'text_document_sync_save_bool', 796 on_init = function(c) 797 client = c 798 end, 799 on_exit = function(code, signal) 800 eq(0, code, 'exit code') 801 eq(0, signal, 'exit signal') 802 end, 803 on_handler = function(err, result, ctx) 804 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 805 if ctx.method == 'start' then 806 exec_lua(function() 807 _G.BUFFER = vim.api.nvim_get_current_buf() 808 vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID) 809 vim.api.nvim_exec_autocmds('BufWritePost', { buffer = _G.BUFFER, modeline = false }) 810 end) 811 else 812 client:stop() 813 end 814 end, 815 } 816 end) 817 818 it('BufWritePre does not send notifications if server lacks willSave capabilities', function() 819 exec_lua(create_server_definition) 820 local messages = exec_lua(function() 821 local server = _G._create_server({ 822 capabilities = { 823 textDocumentSync = { 824 willSave = false, 825 willSaveWaitUntil = false, 826 }, 827 }, 828 }) 829 local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = server.cmd })) 830 local buf = vim.api.nvim_get_current_buf() 831 vim.api.nvim_exec_autocmds('BufWritePre', { buffer = buf, modeline = false }) 832 vim.lsp.get_client_by_id(client_id):stop() 833 return server.messages 834 end) 835 eq(4, #messages) 836 eq('initialize', messages[1].method) 837 eq('initialized', messages[2].method) 838 eq('shutdown', messages[3].method) 839 eq('exit', messages[4].method) 840 end) 841 842 it('BufWritePre sends willSave / willSaveWaitUntil, applies textEdits', function() 843 exec_lua(create_server_definition) 844 local result = exec_lua(function() 845 local server = _G._create_server({ 846 capabilities = { 847 textDocumentSync = { 848 willSave = true, 849 willSaveWaitUntil = true, 850 }, 851 }, 852 handlers = { 853 ['textDocument/willSaveWaitUntil'] = function(_, _, callback) 854 local text_edit = { 855 range = { 856 start = { line = 0, character = 0 }, 857 ['end'] = { line = 0, character = 0 }, 858 }, 859 newText = 'Hello', 860 } 861 callback(nil, { text_edit }) 862 end, 863 }, 864 }) 865 local buf = vim.api.nvim_get_current_buf() 866 local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = server.cmd })) 867 vim.api.nvim_exec_autocmds('BufWritePre', { buffer = buf, modeline = false }) 868 vim.lsp.get_client_by_id(client_id):stop() 869 return { 870 messages = server.messages, 871 lines = vim.api.nvim_buf_get_lines(buf, 0, -1, true), 872 } 873 end) 874 local messages = result.messages 875 eq('textDocument/willSave', messages[3].method) 876 eq('textDocument/willSaveWaitUntil', messages[4].method) 877 eq({ 'Hello' }, result.lines) 878 end) 879 880 it('saveas sends didOpen if filename changed', function() 881 local expected_handlers = { 882 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 883 { NIL, {}, { method = 'start', client_id = 1 } }, 884 } 885 local client --- @type vim.lsp.Client 886 test_rpc_server({ 887 test_name = 'text_document_save_did_open', 888 on_init = function(c) 889 client = c 890 end, 891 on_exit = function(code, signal) 892 eq(0, code, 'exit code') 893 eq(0, signal, 'exit signal') 894 end, 895 on_handler = function(err, result, ctx) 896 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 897 if ctx.method == 'start' then 898 local tmpfile_old = tmpname() 899 local tmpfile_new = tmpname(false) 900 exec_lua(function() 901 _G.BUFFER = vim.api.nvim_get_current_buf() 902 vim.api.nvim_buf_set_name(_G.BUFFER, tmpfile_old) 903 vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, true, { 'help me' }) 904 vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID) 905 vim._with({ buf = _G.BUFFER }, function() 906 vim.cmd('saveas ' .. tmpfile_new) 907 end) 908 end) 909 else 910 client:stop() 911 end 912 end, 913 }) 914 end) 915 916 it('BufWritePost sends didSave including text if server capability is set', function() 917 local expected_handlers = { 918 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 919 { NIL, {}, { method = 'start', client_id = 1 } }, 920 } 921 local client --- @type vim.lsp.Client 922 test_rpc_server { 923 test_name = 'text_document_sync_save_includeText', 924 on_init = function(c) 925 client = c 926 end, 927 on_exit = function(code, signal) 928 eq(0, code, 'exit code') 929 eq(0, signal, 'exit signal') 930 end, 931 on_handler = function(err, result, ctx) 932 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 933 if ctx.method == 'start' then 934 exec_lua(function() 935 _G.BUFFER = vim.api.nvim_get_current_buf() 936 vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, true, { 'help me' }) 937 vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID) 938 vim.api.nvim_exec_autocmds('BufWritePost', { buffer = _G.BUFFER, modeline = false }) 939 end) 940 else 941 client:stop() 942 end 943 end, 944 } 945 end) 946 947 it('client:supports_methods() should validate capabilities', function() 948 local expected_handlers = { 949 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 950 } 951 test_rpc_server { 952 test_name = 'capabilities_for_client_supports_method', 953 on_init = function(client) 954 client:stop() 955 local expected_sync_capabilities = { 956 change = 1, 957 openClose = true, 958 save = { includeText = false }, 959 willSave = false, 960 willSaveWaitUntil = false, 961 } 962 eq(expected_sync_capabilities, client.server_capabilities().textDocumentSync) 963 eq(true, client.server_capabilities().completionProvider) 964 eq(true, client.server_capabilities().hoverProvider) 965 eq(false, client.server_capabilities().definitionProvider) 966 eq(false, client.server_capabilities().renameProvider) 967 eq(true, client.server_capabilities().codeLensProvider.resolveProvider) 968 969 -- known methods for resolved capabilities 970 eq(true, client:supports_method('textDocument/hover')) 971 eq(false, client:supports_method('textDocument/definition')) 972 973 -- unknown methods are assumed to be supported. 974 eq(true, client:supports_method('unknown-method')) 975 end, 976 on_exit = function(code, signal) 977 eq(0, code, 'exit code') 978 eq(0, signal, 'exit signal') 979 end, 980 on_handler = function(...) 981 eq(table.remove(expected_handlers), { ... }, 'expected handler') 982 end, 983 } 984 end) 985 986 it('should not call unsupported_method when trying to call an unsupported method', function() 987 local expected_handlers = { 988 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 989 } 990 test_rpc_server { 991 test_name = 'capabilities_for_client_supports_method', 992 on_setup = function() 993 exec_lua(function() 994 _G.BUFFER = vim.api.nvim_get_current_buf() 995 vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID) 996 vim.lsp.handlers['textDocument/typeDefinition'] = function() end 997 vim.cmd(_G.BUFFER .. 'bwipeout') 998 end) 999 end, 1000 on_init = function(client) 1001 client:stop() 1002 exec_lua(function() 1003 vim.lsp.buf.type_definition() 1004 end) 1005 end, 1006 on_exit = function(code, signal) 1007 eq(0, code, 'exit code') 1008 eq(0, signal, 'exit signal') 1009 end, 1010 on_handler = function(...) 1011 eq(table.remove(expected_handlers), { ... }, 'expected handler') 1012 end, 1013 } 1014 end) 1015 1016 it( 1017 'should not call unsupported_method when no client and trying to call an unsupported method', 1018 function() 1019 local expected_handlers = { 1020 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 1021 } 1022 test_rpc_server { 1023 test_name = 'capabilities_for_client_supports_method', 1024 on_setup = function() 1025 exec_lua(function() 1026 vim.lsp.handlers['textDocument/typeDefinition'] = function() end 1027 end) 1028 end, 1029 on_init = function(client) 1030 client:stop() 1031 exec_lua(function() 1032 vim.lsp.buf.type_definition() 1033 end) 1034 end, 1035 on_exit = function(code, signal) 1036 eq(0, code, 'exit code') 1037 eq(0, signal, 'exit signal') 1038 end, 1039 on_handler = function(...) 1040 eq(table.remove(expected_handlers), { ... }, 'expected handler') 1041 end, 1042 } 1043 end 1044 ) 1045 1046 it('should not forward RequestCancelled to callback', function() 1047 local expected_handlers = { 1048 { NIL, {}, { method = 'finish', client_id = 1 } }, 1049 } 1050 local client --- @type vim.lsp.Client 1051 test_rpc_server { 1052 test_name = 'check_forward_request_cancelled', 1053 on_init = function(_client) 1054 _client:request('error_code_test') 1055 client = _client 1056 end, 1057 on_exit = function(code, signal) 1058 eq(0, code, 'exit code') 1059 eq(0, signal, 'exit signal') 1060 eq(0, #expected_handlers, 'did not call expected handler') 1061 end, 1062 on_handler = function(err, _, ctx) 1063 eq(table.remove(expected_handlers), { err, {}, ctx }, 'expected handler') 1064 if ctx.method == 'finish' then 1065 client:stop() 1066 end 1067 end, 1068 } 1069 end) 1070 1071 it('should forward ServerCancelled to callback', function() 1072 local expected_handlers = { 1073 { NIL, {}, { method = 'finish', client_id = 1 } }, 1074 { 1075 { code = -32802 }, 1076 NIL, 1077 { method = 'error_code_test', bufnr = 1, client_id = 1, request_id = 2, version = 0 }, 1078 }, 1079 } 1080 local client --- @type vim.lsp.Client 1081 test_rpc_server { 1082 test_name = 'check_forward_server_cancelled', 1083 on_init = function(_client) 1084 _client:request('error_code_test') 1085 client = _client 1086 end, 1087 on_exit = function(code, signal) 1088 eq(0, code, 'exit code') 1089 eq(0, signal, 'exit signal') 1090 eq(0, #expected_handlers, 'did not call expected handler') 1091 end, 1092 on_handler = function(err, _, ctx) 1093 eq(table.remove(expected_handlers), { err, _, ctx }, 'expected handler') 1094 if ctx.method ~= 'finish' then 1095 client:notify('finish') 1096 end 1097 if ctx.method == 'finish' then 1098 client:stop() 1099 end 1100 end, 1101 } 1102 end) 1103 1104 it('should forward ContentModified to callback', function() 1105 local expected_handlers = { 1106 { NIL, {}, { method = 'finish', client_id = 1 } }, 1107 { 1108 { code = -32801 }, 1109 NIL, 1110 { method = 'error_code_test', bufnr = 1, client_id = 1, request_id = 2, version = 0 }, 1111 }, 1112 } 1113 local client --- @type vim.lsp.Client 1114 test_rpc_server { 1115 test_name = 'check_forward_content_modified', 1116 on_init = function(_client) 1117 _client:request('error_code_test') 1118 client = _client 1119 end, 1120 on_exit = function(code, signal) 1121 eq(0, code, 'exit code') 1122 eq(0, signal, 'exit signal') 1123 eq(0, #expected_handlers, 'did not call expected handler') 1124 end, 1125 on_handler = function(err, _, ctx) 1126 eq(table.remove(expected_handlers), { err, _, ctx }, 'expected handler') 1127 if ctx.method ~= 'finish' then 1128 client:notify('finish') 1129 end 1130 if ctx.method == 'finish' then 1131 client:stop() 1132 end 1133 end, 1134 } 1135 end) 1136 1137 it('should track pending requests to the language server', function() 1138 local expected_handlers = { 1139 { NIL, {}, { method = 'finish', client_id = 1 } }, 1140 { 1141 NIL, 1142 {}, 1143 { method = 'slow_request', bufnr = 1, client_id = 1, request_id = 2, version = 0 }, 1144 }, 1145 } 1146 local client --- @type vim.lsp.Client 1147 test_rpc_server { 1148 test_name = 'check_pending_request_tracked', 1149 on_init = function(_client) 1150 client = _client 1151 client:request('slow_request') 1152 local request = exec_lua(function() 1153 return _G.TEST_RPC_CLIENT.requests[2] 1154 end) 1155 eq('slow_request', request.method) 1156 eq('pending', request.type) 1157 client:notify('release') 1158 end, 1159 on_exit = function(code, signal) 1160 eq(0, code, 'exit code') 1161 eq(0, signal, 'exit signal') 1162 eq(0, #expected_handlers, 'did not call expected handler') 1163 end, 1164 on_handler = function(err, _, ctx) 1165 eq(table.remove(expected_handlers), { err, {}, ctx }, 'expected handler') 1166 if ctx.method == 'slow_request' then 1167 local request = exec_lua(function() 1168 return _G.TEST_RPC_CLIENT.requests[2] 1169 end) 1170 eq(nil, request) 1171 client:notify('finish') 1172 end 1173 if ctx.method == 'finish' then 1174 client:stop() 1175 end 1176 end, 1177 } 1178 end) 1179 1180 it('should track cancel requests to the language server', function() 1181 local expected_handlers = { 1182 { NIL, {}, { method = 'finish', client_id = 1 } }, 1183 } 1184 local client --- @type vim.lsp.Client 1185 test_rpc_server { 1186 test_name = 'check_cancel_request_tracked', 1187 on_init = function(_client) 1188 client = _client 1189 client:request('slow_request') 1190 client:cancel_request(2) 1191 local request = exec_lua(function() 1192 return _G.TEST_RPC_CLIENT.requests[2] 1193 end) 1194 eq('slow_request', request.method) 1195 eq('cancel', request.type) 1196 client:notify('release') 1197 end, 1198 on_exit = function(code, signal) 1199 eq(0, code, 'exit code') 1200 eq(0, signal, 'exit signal') 1201 eq(0, #expected_handlers, 'did not call expected handler') 1202 end, 1203 on_handler = function(err, _, ctx) 1204 eq(table.remove(expected_handlers), { err, {}, ctx }, 'expected handler') 1205 local request = exec_lua(function() 1206 return _G.TEST_RPC_CLIENT.requests[2] 1207 end) 1208 eq(nil, request) 1209 if ctx.method == 'finish' then 1210 client:stop() 1211 end 1212 end, 1213 } 1214 end) 1215 1216 it('should clear pending and cancel requests on reply', function() 1217 local expected_handlers = { 1218 { NIL, {}, { method = 'finish', client_id = 1 } }, 1219 { 1220 NIL, 1221 {}, 1222 { method = 'slow_request', bufnr = 1, client_id = 1, request_id = 2, version = 0 }, 1223 }, 1224 } 1225 local client --- @type vim.lsp.Client 1226 test_rpc_server { 1227 test_name = 'check_tracked_requests_cleared', 1228 on_init = function(_client) 1229 client = _client 1230 client:request('slow_request') 1231 local request = exec_lua(function() 1232 return _G.TEST_RPC_CLIENT.requests[2] 1233 end) 1234 eq('slow_request', request.method) 1235 eq('pending', request.type) 1236 client:cancel_request(2) 1237 request = exec_lua(function() 1238 return _G.TEST_RPC_CLIENT.requests[2] 1239 end) 1240 eq('slow_request', request.method) 1241 eq('cancel', request.type) 1242 client:notify('release') 1243 end, 1244 on_exit = function(code, signal) 1245 eq(0, code, 'exit code') 1246 eq(0, signal, 'exit signal') 1247 eq(0, #expected_handlers, 'did not call expected handler') 1248 end, 1249 on_handler = function(err, _, ctx) 1250 eq(table.remove(expected_handlers), { err, {}, ctx }, 'expected handler') 1251 if ctx.method == 'slow_request' then 1252 local request = exec_lua(function() 1253 return _G.TEST_RPC_CLIENT.requests[2] 1254 end) 1255 eq(nil, request) 1256 client:notify('finish') 1257 end 1258 if ctx.method == 'finish' then 1259 client:stop() 1260 end 1261 end, 1262 } 1263 end) 1264 1265 it('request should not be pending for sync responses (in-process LS)', function() 1266 --- @type boolean 1267 local pending_request = exec_lua(function() 1268 local function server(dispatchers) 1269 local closing = false 1270 local srv = {} 1271 local request_id = 0 1272 1273 function srv.request(method, _params, callback, notify_reply_callback) 1274 if method == 'textDocument/formatting' then 1275 callback(nil, {}) 1276 elseif method == 'initialize' then 1277 callback(nil, { 1278 capabilities = { 1279 textDocument = { 1280 formatting = true, 1281 }, 1282 }, 1283 }) 1284 elseif method == 'shutdown' then 1285 callback(nil, nil) 1286 end 1287 request_id = request_id + 1 1288 if notify_reply_callback then 1289 notify_reply_callback(request_id) 1290 end 1291 return true, request_id 1292 end 1293 1294 function srv.notify(method) 1295 if method == 'exit' then 1296 dispatchers.on_exit(0, 15) 1297 end 1298 end 1299 function srv.is_closing() 1300 return closing 1301 end 1302 function srv.terminate() 1303 closing = true 1304 end 1305 1306 return srv 1307 end 1308 1309 local client_id = assert(vim.lsp.start({ cmd = server })) 1310 local client = assert(vim.lsp.get_client_by_id(client_id)) 1311 1312 local ok, request_id = client:request('textDocument/formatting', {}) 1313 assert(ok) 1314 1315 local has_pending = client.requests[request_id] ~= nil 1316 vim.lsp.get_client_by_id(client_id):stop() 1317 1318 return has_pending 1319 end) 1320 1321 eq(false, pending_request, 'expected no pending requests') 1322 end) 1323 1324 it('should trigger LspRequest autocmd when requests table changes', function() 1325 local expected_handlers = { 1326 { NIL, {}, { method = 'finish', client_id = 1 } }, 1327 { 1328 NIL, 1329 {}, 1330 { method = 'slow_request', bufnr = 1, client_id = 1, request_id = 2, version = 0 }, 1331 }, 1332 } 1333 local client --- @type vim.lsp.Client 1334 test_rpc_server { 1335 test_name = 'check_tracked_requests_cleared', 1336 on_init = function(_client) 1337 command('let g:requests = 0') 1338 command('autocmd LspRequest * let g:requests+=1') 1339 client = _client 1340 client:request('slow_request') 1341 eq(1, eval('g:requests')) 1342 client:cancel_request(2) 1343 eq(2, eval('g:requests')) 1344 client:notify('release') 1345 end, 1346 on_exit = function(code, signal) 1347 eq(0, code, 'exit code') 1348 eq(0, signal, 'exit signal') 1349 eq(0, #expected_handlers, 'did not call expected handler') 1350 eq(3, eval('g:requests')) 1351 end, 1352 on_handler = function(err, _, ctx) 1353 eq(table.remove(expected_handlers), { err, {}, ctx }, 'expected handler') 1354 if ctx.method == 'slow_request' then 1355 client:notify('finish') 1356 end 1357 if ctx.method == 'finish' then 1358 client:stop() 1359 end 1360 end, 1361 } 1362 end) 1363 1364 it('should not send didOpen if the buffer closes before init', function() 1365 local expected_handlers = { 1366 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 1367 { NIL, {}, { method = 'finish', client_id = 1 } }, 1368 } 1369 local client --- @type vim.lsp.Client 1370 test_rpc_server { 1371 test_name = 'basic_finish', 1372 on_setup = function() 1373 exec_lua(function() 1374 _G.BUFFER = vim.api.nvim_create_buf(false, true) 1375 vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, { 1376 'testing', 1377 '123', 1378 }) 1379 assert(_G.TEST_RPC_CLIENT_ID == 1) 1380 assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)) 1381 assert(vim.lsp.buf_is_attached(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)) 1382 vim.cmd(_G.BUFFER .. 'bwipeout') 1383 end) 1384 end, 1385 on_init = function(_client) 1386 client = _client 1387 local full_kind = exec_lua(function() 1388 return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full 1389 end) 1390 eq(full_kind, client.server_capabilities().textDocumentSync.change) 1391 eq(true, client.server_capabilities().textDocumentSync.openClose) 1392 client:notify('finish') 1393 end, 1394 on_exit = function(code, signal) 1395 eq(0, code, 'exit code') 1396 eq(0, signal, 'exit signal') 1397 end, 1398 on_handler = function(err, result, ctx) 1399 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 1400 if ctx.method == 'finish' then 1401 client:stop() 1402 end 1403 end, 1404 } 1405 end) 1406 1407 it('should check the body sent attaching before init', function() 1408 local expected_handlers = { 1409 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 1410 { NIL, {}, { method = 'finish', client_id = 1 } }, 1411 { NIL, {}, { method = 'start', client_id = 1 } }, 1412 } 1413 local client --- @type vim.lsp.Client 1414 test_rpc_server { 1415 test_name = 'basic_check_buffer_open', 1416 on_setup = function() 1417 exec_lua(function() 1418 _G.BUFFER = vim.api.nvim_create_buf(false, true) 1419 vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, { 1420 'testing', 1421 '123', 1422 }) 1423 end) 1424 exec_lua(function() 1425 assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)) 1426 end) 1427 end, 1428 on_init = function(_client) 1429 client = _client 1430 local full_kind = exec_lua(function() 1431 return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full 1432 end) 1433 eq(full_kind, client.server_capabilities().textDocumentSync.change) 1434 eq(true, client.server_capabilities().textDocumentSync.openClose) 1435 exec_lua(function() 1436 assert( 1437 vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID), 1438 'Already attached, returns true' 1439 ) 1440 end) 1441 end, 1442 on_exit = function(code, signal) 1443 eq(0, code, 'exit code') 1444 eq(0, signal, 'exit signal') 1445 end, 1446 on_handler = function(err, result, ctx) 1447 if ctx.method == 'start' then 1448 client:notify('finish') 1449 end 1450 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 1451 if ctx.method == 'finish' then 1452 client:stop() 1453 end 1454 end, 1455 } 1456 end) 1457 1458 it('should check the body sent attaching after init', function() 1459 local expected_handlers = { 1460 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 1461 { NIL, {}, { method = 'finish', client_id = 1 } }, 1462 { NIL, {}, { method = 'start', client_id = 1 } }, 1463 } 1464 local client --- @type vim.lsp.Client 1465 test_rpc_server { 1466 test_name = 'basic_check_buffer_open', 1467 on_setup = function() 1468 exec_lua(function() 1469 _G.BUFFER = vim.api.nvim_create_buf(false, true) 1470 vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, { 1471 'testing', 1472 '123', 1473 }) 1474 end) 1475 end, 1476 on_init = function(_client) 1477 client = _client 1478 local full_kind = exec_lua(function() 1479 return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full 1480 end) 1481 eq(full_kind, client.server_capabilities().textDocumentSync.change) 1482 eq(true, client.server_capabilities().textDocumentSync.openClose) 1483 exec_lua(function() 1484 assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)) 1485 end) 1486 end, 1487 on_exit = function(code, signal) 1488 eq(0, code, 'exit code') 1489 eq(0, signal, 'exit signal') 1490 end, 1491 on_handler = function(err, result, ctx) 1492 if ctx.method == 'start' then 1493 client:notify('finish') 1494 end 1495 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 1496 if ctx.method == 'finish' then 1497 client:stop() 1498 end 1499 end, 1500 } 1501 end) 1502 1503 it('should check the body and didChange full', function() 1504 local expected_handlers = { 1505 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 1506 { NIL, {}, { method = 'finish', client_id = 1 } }, 1507 { NIL, {}, { method = 'start', client_id = 1 } }, 1508 } 1509 local client --- @type vim.lsp.Client 1510 test_rpc_server { 1511 test_name = 'basic_check_buffer_open_and_change', 1512 on_setup = function() 1513 exec_lua(function() 1514 _G.BUFFER = vim.api.nvim_create_buf(false, true) 1515 vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, { 1516 'testing', 1517 '123', 1518 }) 1519 end) 1520 end, 1521 on_init = function(_client) 1522 client = _client 1523 local full_kind = exec_lua(function() 1524 return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full 1525 end) 1526 eq(full_kind, client.server_capabilities().textDocumentSync.change) 1527 eq(true, client.server_capabilities().textDocumentSync.openClose) 1528 exec_lua(function() 1529 assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)) 1530 end) 1531 end, 1532 on_exit = function(code, signal) 1533 eq(0, code, 'exit code') 1534 eq(0, signal, 'exit signal') 1535 end, 1536 on_handler = function(err, result, ctx) 1537 if ctx.method == 'start' then 1538 exec_lua(function() 1539 vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, { 1540 'boop', 1541 }) 1542 end) 1543 client:notify('finish') 1544 end 1545 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 1546 if ctx.method == 'finish' then 1547 client:stop() 1548 end 1549 end, 1550 } 1551 end) 1552 1553 it('should check the body and didChange full with noeol', function() 1554 local expected_handlers = { 1555 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 1556 { NIL, {}, { method = 'finish', client_id = 1 } }, 1557 { NIL, {}, { method = 'start', client_id = 1 } }, 1558 } 1559 local client --- @type vim.lsp.Client 1560 test_rpc_server { 1561 test_name = 'basic_check_buffer_open_and_change_noeol', 1562 on_setup = function() 1563 exec_lua(function() 1564 _G.BUFFER = vim.api.nvim_create_buf(false, true) 1565 vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, { 1566 'testing', 1567 '123', 1568 }) 1569 vim.bo[_G.BUFFER].eol = false 1570 end) 1571 end, 1572 on_init = function(_client) 1573 client = _client 1574 local full_kind = exec_lua(function() 1575 return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full 1576 end) 1577 eq(full_kind, client.server_capabilities().textDocumentSync.change) 1578 eq(true, client.server_capabilities().textDocumentSync.openClose) 1579 exec_lua(function() 1580 assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)) 1581 end) 1582 end, 1583 on_exit = function(code, signal) 1584 eq(0, code, 'exit code') 1585 eq(0, signal, 'exit signal') 1586 end, 1587 on_handler = function(err, result, ctx) 1588 if ctx.method == 'start' then 1589 exec_lua(function() 1590 vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, { 1591 'boop', 1592 }) 1593 end) 1594 client:notify('finish') 1595 end 1596 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 1597 if ctx.method == 'finish' then 1598 client:stop() 1599 end 1600 end, 1601 } 1602 end) 1603 1604 it('should send correct range for inlay hints with noeol', function() 1605 local expected_handlers = { 1606 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 1607 { NIL, {}, { method = 'finish', client_id = 1 } }, 1608 { 1609 NIL, 1610 {}, 1611 { 1612 method = 'textDocument/inlayHint', 1613 params = { 1614 textDocument = { 1615 uri = 'file://', 1616 }, 1617 range = { 1618 start = { line = 0, character = 0 }, 1619 ['end'] = { line = 1, character = 3 }, 1620 }, 1621 }, 1622 bufnr = 2, 1623 client_id = 1, 1624 request_id = 2, 1625 version = 0, 1626 }, 1627 }, 1628 { NIL, {}, { method = 'start', client_id = 1 } }, 1629 } 1630 local client --- @type vim.lsp.Client 1631 test_rpc_server { 1632 test_name = 'inlay_hint', 1633 on_setup = function() 1634 exec_lua(function() 1635 _G.BUFFER = vim.api.nvim_create_buf(false, true) 1636 vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, { 1637 'testing', 1638 '123', 1639 }) 1640 vim.bo[_G.BUFFER].eol = false 1641 end) 1642 end, 1643 on_init = function(_client) 1644 client = _client 1645 eq(true, client:supports_method('textDocument/inlayHint')) 1646 exec_lua(function() 1647 assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)) 1648 end) 1649 end, 1650 on_exit = function(code, signal) 1651 eq(0, code, 'exit code') 1652 eq(0, signal, 'exit signal') 1653 end, 1654 on_handler = function(err, result, ctx) 1655 if ctx.method == 'start' then 1656 exec_lua(function() 1657 vim.lsp.inlay_hint.enable(true, { bufnr = _G.BUFFER }) 1658 end) 1659 end 1660 if ctx.method == 'textDocument/inlayHint' then 1661 client:notify('finish') 1662 end 1663 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 1664 if ctx.method == 'finish' then 1665 client:stop() 1666 end 1667 end, 1668 } 1669 end) 1670 1671 it('should check the body and didChange incremental', function() 1672 local expected_handlers = { 1673 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 1674 { NIL, {}, { method = 'finish', client_id = 1 } }, 1675 { NIL, {}, { method = 'start', client_id = 1 } }, 1676 } 1677 local client --- @type vim.lsp.Client 1678 test_rpc_server { 1679 test_name = 'basic_check_buffer_open_and_change_incremental', 1680 options = { 1681 allow_incremental_sync = true, 1682 }, 1683 on_setup = function() 1684 exec_lua(function() 1685 _G.BUFFER = vim.api.nvim_create_buf(false, true) 1686 vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, { 1687 'testing', 1688 '123', 1689 }) 1690 end) 1691 end, 1692 on_init = function(_client) 1693 client = _client 1694 local sync_kind = exec_lua(function() 1695 return require 'vim.lsp.protocol'.TextDocumentSyncKind.Incremental 1696 end) 1697 eq(sync_kind, client.server_capabilities().textDocumentSync.change) 1698 eq(true, client.server_capabilities().textDocumentSync.openClose) 1699 exec_lua(function() 1700 assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)) 1701 end) 1702 end, 1703 on_exit = function(code, signal) 1704 eq(0, code, 'exit code') 1705 eq(0, signal, 'exit signal') 1706 end, 1707 on_handler = function(err, result, ctx) 1708 if ctx.method == 'start' then 1709 exec_lua(function() 1710 vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, { 1711 '123boop', 1712 }) 1713 end) 1714 client:notify('finish') 1715 end 1716 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 1717 if ctx.method == 'finish' then 1718 client:stop() 1719 end 1720 end, 1721 } 1722 end) 1723 1724 it('should check the body and didChange incremental with debounce', function() 1725 local expected_handlers = { 1726 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 1727 { NIL, {}, { method = 'finish', client_id = 1 } }, 1728 { NIL, {}, { method = 'start', client_id = 1 } }, 1729 } 1730 local client --- @type vim.lsp.Client 1731 test_rpc_server { 1732 test_name = 'basic_check_buffer_open_and_change_incremental', 1733 options = { 1734 allow_incremental_sync = true, 1735 debounce_text_changes = 5, 1736 }, 1737 on_setup = function() 1738 exec_lua(function() 1739 _G.BUFFER = vim.api.nvim_create_buf(false, true) 1740 vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, { 1741 'testing', 1742 '123', 1743 }) 1744 end) 1745 end, 1746 on_init = function(_client) 1747 client = _client 1748 local sync_kind = exec_lua(function() 1749 return require 'vim.lsp.protocol'.TextDocumentSyncKind.Incremental 1750 end) 1751 eq(sync_kind, client.server_capabilities().textDocumentSync.change) 1752 eq(true, client.server_capabilities().textDocumentSync.openClose) 1753 exec_lua(function() 1754 assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)) 1755 end) 1756 end, 1757 on_exit = function(code, signal) 1758 eq(0, code, 'exit code') 1759 eq(0, signal, 'exit signal') 1760 end, 1761 on_handler = function(err, result, ctx) 1762 if ctx.method == 'start' then 1763 exec_lua(function() 1764 vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, { 1765 '123boop', 1766 }) 1767 end) 1768 client:notify('finish') 1769 end 1770 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 1771 if ctx.method == 'finish' then 1772 client:stop() 1773 end 1774 end, 1775 } 1776 end) 1777 1778 -- TODO(askhan) we don't support full for now, so we can disable these tests. 1779 pending('should check the body and didChange incremental normal mode editing', function() 1780 local expected_handlers = { 1781 { NIL, {}, { method = 'shutdown', bufnr = 1, client_id = 1 } }, 1782 { NIL, {}, { method = 'finish', client_id = 1 } }, 1783 { NIL, {}, { method = 'start', client_id = 1 } }, 1784 } 1785 local client --- @type vim.lsp.Client 1786 test_rpc_server { 1787 test_name = 'basic_check_buffer_open_and_change_incremental_editing', 1788 on_setup = function() 1789 exec_lua(function() 1790 _G.BUFFER = vim.api.nvim_create_buf(false, true) 1791 vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, { 1792 'testing', 1793 '123', 1794 }) 1795 end) 1796 end, 1797 on_init = function(_client) 1798 client = _client 1799 local sync_kind = exec_lua(function() 1800 return require 'vim.lsp.protocol'.TextDocumentSyncKind.Incremental 1801 end) 1802 eq(sync_kind, client.server_capabilities().textDocumentSync.change) 1803 eq(true, client.server_capabilities().textDocumentSync.openClose) 1804 exec_lua(function() 1805 assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)) 1806 end) 1807 end, 1808 on_exit = function(code, signal) 1809 eq(0, code, 'exit code') 1810 eq(0, signal, 'exit signal') 1811 end, 1812 on_handler = function(err, result, ctx) 1813 if ctx.method == 'start' then 1814 n.command('normal! 1Go') 1815 client:notify('finish') 1816 end 1817 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 1818 if ctx.method == 'finish' then 1819 client:stop() 1820 end 1821 end, 1822 } 1823 end) 1824 1825 it('should check the body and didChange full with 2 changes', function() 1826 local expected_handlers = { 1827 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 1828 { NIL, {}, { method = 'finish', client_id = 1 } }, 1829 { NIL, {}, { method = 'start', client_id = 1 } }, 1830 } 1831 local client --- @type vim.lsp.Client 1832 test_rpc_server { 1833 test_name = 'basic_check_buffer_open_and_change_multi', 1834 on_setup = function() 1835 exec_lua(function() 1836 _G.BUFFER = vim.api.nvim_create_buf(false, true) 1837 vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, { 1838 'testing', 1839 '123', 1840 }) 1841 end) 1842 end, 1843 on_init = function(_client) 1844 client = _client 1845 local sync_kind = exec_lua(function() 1846 return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full 1847 end) 1848 eq(sync_kind, client.server_capabilities().textDocumentSync.change) 1849 eq(true, client.server_capabilities().textDocumentSync.openClose) 1850 exec_lua(function() 1851 assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)) 1852 end) 1853 end, 1854 on_exit = function(code, signal) 1855 eq(0, code, 'exit code') 1856 eq(0, signal, 'exit signal') 1857 end, 1858 on_handler = function(err, result, ctx) 1859 if ctx.method == 'start' then 1860 exec_lua(function() 1861 vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, { 1862 '321', 1863 }) 1864 vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, { 1865 'boop', 1866 }) 1867 end) 1868 client:notify('finish') 1869 end 1870 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 1871 if ctx.method == 'finish' then 1872 client:stop() 1873 end 1874 end, 1875 } 1876 end) 1877 1878 it('should check the body and didChange full lifecycle', function() 1879 local expected_handlers = { 1880 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 1881 { NIL, {}, { method = 'finish', client_id = 1 } }, 1882 { NIL, {}, { method = 'start', client_id = 1 } }, 1883 } 1884 local client --- @type vim.lsp.Client 1885 test_rpc_server { 1886 test_name = 'basic_check_buffer_open_and_change_multi_and_close', 1887 on_setup = function() 1888 exec_lua(function() 1889 _G.BUFFER = vim.api.nvim_create_buf(false, true) 1890 vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, { 1891 'testing', 1892 '123', 1893 }) 1894 end) 1895 end, 1896 on_init = function(_client) 1897 client = _client 1898 local sync_kind = exec_lua(function() 1899 return require 'vim.lsp.protocol'.TextDocumentSyncKind.Full 1900 end) 1901 eq(sync_kind, client.server_capabilities().textDocumentSync.change) 1902 eq(true, client.server_capabilities().textDocumentSync.openClose) 1903 exec_lua(function() 1904 assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)) 1905 end) 1906 end, 1907 on_exit = function(code, signal) 1908 eq(0, code, 'exit code') 1909 eq(0, signal, 'exit signal') 1910 end, 1911 on_handler = function(err, result, ctx) 1912 if ctx.method == 'start' then 1913 exec_lua(function() 1914 vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, { 1915 '321', 1916 }) 1917 vim.api.nvim_buf_set_lines(_G.BUFFER, 1, 2, false, { 1918 'boop', 1919 }) 1920 vim.api.nvim_command(_G.BUFFER .. 'bwipeout') 1921 end) 1922 client:notify('finish') 1923 end 1924 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 1925 if ctx.method == 'finish' then 1926 client:stop() 1927 end 1928 end, 1929 } 1930 end) 1931 1932 it('vim.lsp.start when existing client has no workspace_folders', function() 1933 exec_lua(create_server_definition) 1934 eq( 1935 { 2, 'foo', 'foo' }, 1936 exec_lua(function() 1937 local server = _G._create_server() 1938 vim.lsp.start { cmd = server.cmd, name = 'foo' } 1939 vim.lsp.start { cmd = server.cmd, name = 'foo', root_dir = 'bar' } 1940 local foos = vim.lsp.get_clients() 1941 return { #foos, foos[1].name, foos[2].name } 1942 end) 1943 ) 1944 end) 1945 end) 1946 1947 describe('parsing tests', function() 1948 local body = '{"jsonrpc":"2.0","id": 1,"method":"demo"}' 1949 1950 before_each(function() 1951 exec_lua(create_tcp_echo_server) 1952 end) 1953 1954 it('should catch error while parsing invalid header', function() 1955 -- No whitespace is allowed between the header field-name and colon. 1956 -- See https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4 1957 local field = 'Content-Length : 10 \r\n' 1958 exec_lua(function() 1959 _G._send_msg_to_server(field .. '\r\n') 1960 end) 1961 verify_single_notification(function(method, args) ---@param args [string, number] 1962 eq('error', method) 1963 eq(1, args[2]) 1964 matches(vim.pesc('Content-Length not found in header: ' .. field) .. '$', args[1]) 1965 end) 1966 end) 1967 1968 it('value of Content-Length shoud be number', function() 1969 local value = '123 foo' 1970 exec_lua(function() 1971 _G._send_msg_to_server('Content-Length: ' .. value .. '\r\n\r\n') 1972 end) 1973 verify_single_notification(function(method, args) ---@param args [string, number] 1974 eq('error', method) 1975 eq(1, args[2]) 1976 matches('value of Content%-Length is not number: ' .. value .. '$', args[1]) 1977 end) 1978 end) 1979 1980 it('field name is case-insensitive', function() 1981 exec_lua(function() 1982 _G._send_msg_to_server('CONTENT-Length: ' .. #body .. ' \r\n\r\n' .. body) 1983 end) 1984 verify_single_notification(function(method, args) ---@param args [string] 1985 eq('body', method) 1986 eq(body, args[1]) 1987 end) 1988 end) 1989 1990 it("ignore some lines ending with LF that don't contain content-length", function() 1991 exec_lua(function() 1992 _G._send_msg_to_server( 1993 'foo \n bar\nWARN: no common words.\nContent-Length: ' .. #body .. ' \r\n\r\n' .. body 1994 ) 1995 end) 1996 verify_single_notification(function(method, args) ---@param args [string] 1997 eq('body', method) 1998 eq(body, args[1]) 1999 end) 2000 end) 2001 2002 it('should not trim vim.NIL from the end of a list', function() 2003 local expected_handlers = { 2004 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 2005 { NIL, {}, { method = 'finish', client_id = 1 } }, 2006 { 2007 NIL, 2008 { 2009 arguments = { 'EXTRACT_METHOD', { metadata = { field = vim.NIL } }, 3, 0, 6123, NIL }, 2010 command = 'refactor.perform', 2011 title = 'EXTRACT_METHOD', 2012 }, 2013 { method = 'workspace/executeCommand', client_id = 1 }, 2014 }, 2015 { NIL, {}, { method = 'start', client_id = 1 } }, 2016 } 2017 local client --- @type vim.lsp.Client 2018 test_rpc_server { 2019 test_name = 'decode_nil', 2020 on_setup = function() 2021 exec_lua(function() 2022 _G.BUFFER = vim.api.nvim_create_buf(false, true) 2023 vim.api.nvim_buf_set_lines(_G.BUFFER, 0, -1, false, { 2024 'testing', 2025 '123', 2026 }) 2027 end) 2028 end, 2029 on_init = function(_client) 2030 client = _client 2031 exec_lua(function() 2032 assert(vim.lsp.buf_attach_client(_G.BUFFER, _G.TEST_RPC_CLIENT_ID)) 2033 end) 2034 end, 2035 on_exit = function(code, signal) 2036 eq(0, code, 'exit code') 2037 eq(0, signal, 'exit signal') 2038 end, 2039 on_handler = function(err, result, ctx) 2040 eq(table.remove(expected_handlers), { err, result, ctx }, 'expected handler') 2041 if ctx.method == 'finish' then 2042 client:stop() 2043 end 2044 end, 2045 } 2046 end) 2047 end) 2048 2049 describe('apply vscode text_edits', function() 2050 it('single replace', function() 2051 insert('012345678901234567890123456789') 2052 apply_text_edits({ 2053 { 0, 3, 0, 6, { 'Hello' } }, 2054 }) 2055 eq({ '012Hello678901234567890123456789' }, buf_lines(1)) 2056 end) 2057 2058 it('two replaces', function() 2059 insert('012345678901234567890123456789') 2060 apply_text_edits({ 2061 { 0, 3, 0, 6, { 'Hello' } }, 2062 { 0, 6, 0, 9, { 'World' } }, 2063 }) 2064 eq({ '012HelloWorld901234567890123456789' }, buf_lines(1)) 2065 end) 2066 2067 it('same start pos insert are kept in order', function() 2068 insert('012345678901234567890123456789') 2069 apply_text_edits({ 2070 { 0, 3, 0, 3, { 'World' } }, 2071 { 0, 3, 0, 3, { 'Hello' } }, 2072 }) 2073 eq({ '012WorldHello345678901234567890123456789' }, buf_lines(1)) 2074 end) 2075 2076 it('same start pos insert and replace are kept in order', function() 2077 insert('012345678901234567890123456789') 2078 apply_text_edits({ 2079 { 0, 3, 0, 3, { 'World' } }, 2080 { 0, 3, 0, 3, { 'Hello' } }, 2081 { 0, 3, 0, 8, { 'No' } }, 2082 }) 2083 eq({ '012WorldHelloNo8901234567890123456789' }, buf_lines(1)) 2084 end) 2085 2086 it('multiline', function() 2087 exec_lua(function() 2088 vim.api.nvim_buf_set_lines(1, 0, 0, true, { ' {', ' "foo": "bar"', ' }' }) 2089 end) 2090 eq({ ' {', ' "foo": "bar"', ' }', '' }, buf_lines(1)) 2091 apply_text_edits({ 2092 { 0, 0, 3, 0, { '' } }, 2093 { 3, 0, 3, 0, { '{\n' } }, 2094 { 3, 0, 3, 0, { ' "foo": "bar"\n' } }, 2095 { 3, 0, 3, 0, { '}\n' } }, 2096 }) 2097 eq({ '{', ' "foo": "bar"', '}', '' }, buf_lines(1)) 2098 end) 2099 end) 2100 2101 describe('apply_text_edits', function() 2102 local buffer_text = { 2103 'First line of text', 2104 'Second line of text', 2105 'Third line of text', 2106 'Fourth line of text', 2107 'å å ɧ 汉语 ↥ 🤦 🦄', 2108 } 2109 2110 before_each(function() 2111 insert(dedent(table.concat(buffer_text, '\n'))) 2112 end) 2113 2114 it('applies simple edits', function() 2115 apply_text_edits({ 2116 { 0, 0, 0, 0, { '123' } }, 2117 { 1, 0, 1, 1, { '2' } }, 2118 { 2, 0, 2, 2, { '3' } }, 2119 { 3, 2, 3, 4, { '' } }, 2120 }) 2121 eq({ 2122 '123First line of text', 2123 '2econd line of text', 2124 '3ird line of text', 2125 'Foth line of text', 2126 'å å ɧ 汉语 ↥ 🤦 🦄', 2127 }, buf_lines(1)) 2128 end) 2129 2130 it('applies complex edits', function() 2131 apply_text_edits({ 2132 { 0, 0, 0, 0, { '', '12' } }, 2133 { 0, 0, 0, 0, { '3', 'foo' } }, 2134 { 0, 1, 0, 1, { 'bar', '123' } }, 2135 { 0, #'First ', 0, #'First line of text', { 'guy' } }, 2136 { 1, 0, 1, #'Second', { 'baz' } }, 2137 { 2, #'Th', 2, #'Third', { 'e next' } }, 2138 { 3, #'', 3, #'Fourth', { 'another line of text', 'before this' } }, 2139 { 3, #'Fourth', 3, #'Fourth line of text', { '!' } }, 2140 }) 2141 eq({ 2142 '', 2143 '123', 2144 'fooFbar', 2145 '123irst guy', 2146 'baz line of text', 2147 'The next line of text', 2148 'another line of text', 2149 'before this!', 2150 'å å ɧ 汉语 ↥ 🤦 🦄', 2151 }, buf_lines(1)) 2152 end) 2153 2154 it('applies complex edits (reversed range)', function() 2155 apply_text_edits({ 2156 { 0, 0, 0, 0, { '', '12' } }, 2157 { 0, 0, 0, 0, { '3', 'foo' } }, 2158 { 0, 1, 0, 1, { 'bar', '123' } }, 2159 { 0, #'First line of text', 0, #'First ', { 'guy' } }, 2160 { 1, #'Second', 1, 0, { 'baz' } }, 2161 { 2, #'Third', 2, #'Th', { 'e next' } }, 2162 { 3, #'Fourth', 3, #'', { 'another line of text', 'before this' } }, 2163 { 3, #'Fourth line of text', 3, #'Fourth', { '!' } }, 2164 }) 2165 eq({ 2166 '', 2167 '123', 2168 'fooFbar', 2169 '123irst guy', 2170 'baz line of text', 2171 'The next line of text', 2172 'another line of text', 2173 'before this!', 2174 'å å ɧ 汉语 ↥ 🤦 🦄', 2175 }, buf_lines(1)) 2176 end) 2177 2178 it('applies non-ASCII characters edits', function() 2179 apply_text_edits({ 2180 { 4, 3, 4, 4, { 'ä' } }, 2181 }) 2182 eq({ 2183 'First line of text', 2184 'Second line of text', 2185 'Third line of text', 2186 'Fourth line of text', 2187 'å ä ɧ 汉语 ↥ 🤦 🦄', 2188 }, buf_lines(1)) 2189 end) 2190 2191 it('applies text edits at the end of the document', function() 2192 apply_text_edits({ 2193 { 5, 0, 5, 0, 'foobar' }, 2194 }) 2195 eq({ 2196 'First line of text', 2197 'Second line of text', 2198 'Third line of text', 2199 'Fourth line of text', 2200 'å å ɧ 汉语 ↥ 🤦 🦄', 2201 'foobar', 2202 }, buf_lines(1)) 2203 end) 2204 2205 it('applies multiple text edits at the end of the document', function() 2206 apply_text_edits({ 2207 { 4, 0, 5, 0, '' }, 2208 { 5, 0, 5, 0, 'foobar' }, 2209 }) 2210 eq({ 2211 'First line of text', 2212 'Second line of text', 2213 'Third line of text', 2214 'Fourth line of text', 2215 'foobar', 2216 }, buf_lines(1)) 2217 end) 2218 2219 it('it restores marks', function() 2220 eq(true, api.nvim_buf_set_mark(1, 'a', 2, 1, {})) 2221 apply_text_edits({ 2222 { 1, 0, 2, 5, 'foobar' }, 2223 { 4, 0, 5, 0, 'barfoo' }, 2224 }) 2225 eq({ 2226 'First line of text', 2227 'foobar line of text', 2228 'Fourth line of text', 2229 'barfoo', 2230 }, buf_lines(1)) 2231 eq({ 2, 1 }, api.nvim_buf_get_mark(1, 'a')) 2232 end) 2233 2234 it('it restores marks to last valid col', function() 2235 eq(true, api.nvim_buf_set_mark(1, 'a', 2, 10, {})) 2236 apply_text_edits({ 2237 { 1, 0, 2, 15, 'foobar' }, 2238 { 4, 0, 5, 0, 'barfoo' }, 2239 }) 2240 eq({ 2241 'First line of text', 2242 'foobarext', 2243 'Fourth line of text', 2244 'barfoo', 2245 }, buf_lines(1)) 2246 eq({ 2, 9 }, api.nvim_buf_get_mark(1, 'a')) 2247 end) 2248 2249 it('it restores marks to last valid line', function() 2250 eq(true, api.nvim_buf_set_mark(1, 'a', 4, 1, {})) 2251 apply_text_edits({ 2252 { 1, 0, 4, 5, 'foobar' }, 2253 { 4, 0, 5, 0, 'barfoo' }, 2254 }) 2255 eq({ 2256 'First line of text', 2257 'foobaro', 2258 }, buf_lines(1)) 2259 eq({ 2, 1 }, api.nvim_buf_get_mark(1, 'a')) 2260 end) 2261 2262 it('applies edit based on confirmation response', function() 2263 --- @type lsp.AnnotatedTextEdit 2264 local edit = make_edit(0, 0, 5, 0, 'foo') 2265 edit.annotationId = 'annotation-id' 2266 2267 local function test(response) 2268 exec_lua(function() 2269 ---@diagnostic disable-next-line: duplicate-set-field 2270 vim.fn.confirm = function() 2271 return response 2272 end 2273 2274 vim.lsp.util.apply_text_edits( 2275 { edit }, 2276 1, 2277 'utf-16', 2278 { ['annotation-id'] = { label = 'Insert "foo"', needsConfirmation = true } } 2279 ) 2280 end, { response }) 2281 end 2282 2283 test(2) -- 2 = No 2284 eq(buffer_text, buf_lines(1)) 2285 2286 test(1) -- 1 = Yes 2287 eq({ 'foo' }, buf_lines(1)) 2288 end) 2289 2290 describe('cursor position', function() 2291 it("don't fix the cursor if the range contains the cursor", function() 2292 api.nvim_win_set_cursor(0, { 2, 6 }) 2293 apply_text_edits({ 2294 { 1, 0, 1, 19, 'Second line of text' }, 2295 }) 2296 eq({ 2297 'First line of text', 2298 'Second line of text', 2299 'Third line of text', 2300 'Fourth line of text', 2301 'å å ɧ 汉语 ↥ 🤦 🦄', 2302 }, buf_lines(1)) 2303 eq({ 2, 6 }, api.nvim_win_get_cursor(0)) 2304 end) 2305 2306 it('fix the cursor to the valid col if the content was removed', function() 2307 api.nvim_win_set_cursor(0, { 2, 6 }) 2308 apply_text_edits({ 2309 { 1, 0, 1, 6, '' }, 2310 { 1, 6, 1, 19, '' }, 2311 }) 2312 eq({ 2313 'First line of text', 2314 '', 2315 'Third line of text', 2316 'Fourth line of text', 2317 'å å ɧ 汉语 ↥ 🤦 🦄', 2318 }, buf_lines(1)) 2319 eq({ 2, 0 }, api.nvim_win_get_cursor(0)) 2320 end) 2321 2322 it('fix the cursor to the valid row if the content was removed', function() 2323 api.nvim_win_set_cursor(0, { 2, 6 }) 2324 apply_text_edits({ 2325 { 1, 0, 1, 6, '' }, 2326 { 0, 18, 5, 0, '' }, 2327 }) 2328 eq({ 2329 'First line of text', 2330 }, buf_lines(1)) 2331 eq({ 1, 17 }, api.nvim_win_get_cursor(0)) 2332 end) 2333 2334 it('fix the cursor row', function() 2335 api.nvim_win_set_cursor(0, { 3, 0 }) 2336 apply_text_edits({ 2337 { 1, 0, 2, 0, '' }, 2338 }) 2339 eq({ 2340 'First line of text', 2341 'Third line of text', 2342 'Fourth line of text', 2343 'å å ɧ 汉语 ↥ 🤦 🦄', 2344 }, buf_lines(1)) 2345 eq({ 2, 0 }, api.nvim_win_get_cursor(0)) 2346 end) 2347 2348 it('fix the cursor col', function() 2349 -- append empty last line. See #22636 2350 api.nvim_buf_set_lines(1, -1, -1, true, { '' }) 2351 2352 api.nvim_win_set_cursor(0, { 2, 11 }) 2353 apply_text_edits({ 2354 { 1, 7, 1, 11, '' }, 2355 }) 2356 eq({ 2357 'First line of text', 2358 'Second of text', 2359 'Third line of text', 2360 'Fourth line of text', 2361 'å å ɧ 汉语 ↥ 🤦 🦄', 2362 '', 2363 }, buf_lines(1)) 2364 eq({ 2, 7 }, api.nvim_win_get_cursor(0)) 2365 end) 2366 2367 it('fix the cursor row and col', function() 2368 api.nvim_win_set_cursor(0, { 2, 12 }) 2369 apply_text_edits({ 2370 { 0, 11, 1, 12, '' }, 2371 }) 2372 eq({ 2373 'First line of text', 2374 'Third line of text', 2375 'Fourth line of text', 2376 'å å ɧ 汉语 ↥ 🤦 🦄', 2377 }, buf_lines(1)) 2378 eq({ 1, 11 }, api.nvim_win_get_cursor(0)) 2379 end) 2380 end) 2381 2382 describe('with LSP end line after what Vim considers to be the end line', function() 2383 it('applies edits when the last linebreak is considered a new line', function() 2384 apply_text_edits({ 2385 { 0, 0, 5, 0, { 'All replaced' } }, 2386 }) 2387 eq({ 'All replaced' }, buf_lines(1)) 2388 end) 2389 2390 it("applies edits when the end line is 2 larger than vim's", function() 2391 apply_text_edits({ 2392 { 0, 0, 6, 0, { 'All replaced' } }, 2393 }) 2394 eq({ 'All replaced' }, buf_lines(1)) 2395 end) 2396 2397 it('applies edits with a column offset', function() 2398 apply_text_edits({ 2399 { 0, 0, 5, 2, { 'All replaced' } }, 2400 }) 2401 eq({ 'All replaced' }, buf_lines(1)) 2402 end) 2403 end) 2404 end) 2405 2406 describe('apply_text_edits regression tests for #20116', function() 2407 before_each(function() 2408 insert(dedent([[ 2409 Test line one 2410 Test line two 21 char]])) 2411 end) 2412 2413 describe('with LSP end column out of bounds and start column at 0', function() 2414 it('applies edits at the end of the buffer', function() 2415 apply_text_edits({ 2416 { 0, 0, 1, 22, { '#include "whatever.h"\r\n#include <algorithm>\r' } }, 2417 }, 'utf-8') 2418 eq({ '#include "whatever.h"', '#include <algorithm>' }, buf_lines(1)) 2419 end) 2420 2421 it('applies edits in the middle of the buffer', function() 2422 apply_text_edits({ 2423 { 0, 0, 0, 22, { '#include "whatever.h"\r\n#include <algorithm>\r' } }, 2424 }, 'utf-8') 2425 eq( 2426 { '#include "whatever.h"', '#include <algorithm>', 'Test line two 21 char' }, 2427 buf_lines(1) 2428 ) 2429 end) 2430 end) 2431 2432 describe('with LSP end column out of bounds and start column NOT at 0', function() 2433 it('applies edits at the end of the buffer', function() 2434 apply_text_edits({ 2435 { 0, 2, 1, 22, { '#include "whatever.h"\r\n#include <algorithm>\r' } }, 2436 }, 'utf-8') 2437 eq({ 'Te#include "whatever.h"', '#include <algorithm>' }, buf_lines(1)) 2438 end) 2439 2440 it('applies edits in the middle of the buffer', function() 2441 apply_text_edits({ 2442 { 0, 2, 0, 22, { '#include "whatever.h"\r\n#include <algorithm>\r' } }, 2443 }, 'utf-8') 2444 eq( 2445 { 'Te#include "whatever.h"', '#include <algorithm>', 'Test line two 21 char' }, 2446 buf_lines(1) 2447 ) 2448 end) 2449 end) 2450 end) 2451 2452 describe('apply_text_document_edit', function() 2453 local target_bufnr --- @type integer 2454 2455 local text_document_edit = function(editVersion) 2456 return { 2457 edits = { 2458 make_edit(0, 0, 0, 3, 'First ↥ 🤦 🦄'), 2459 }, 2460 textDocument = { 2461 uri = 'file:///fake/uri', 2462 version = editVersion, 2463 }, 2464 } 2465 end 2466 2467 before_each(function() 2468 target_bufnr = exec_lua(function() 2469 local bufnr = vim.uri_to_bufnr('file:///fake/uri') 2470 local lines = { '1st line of text', '2nd line of 语text' } 2471 vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) 2472 return bufnr 2473 end) 2474 end) 2475 2476 it('correctly goes ahead with the edit if all is normal', function() 2477 exec_lua(function(text_edit) 2478 vim.lsp.util.apply_text_document_edit(text_edit, nil, 'utf-16') 2479 end, text_document_edit(5)) 2480 eq({ 2481 'First ↥ 🤦 🦄 line of text', 2482 '2nd line of 语text', 2483 }, buf_lines(target_bufnr)) 2484 end) 2485 2486 it('always accepts edit with version = 0', function() 2487 exec_lua(function(text_edit) 2488 vim.lsp.util.buf_versions[target_bufnr] = 10 2489 vim.lsp.util.apply_text_document_edit(text_edit, nil, 'utf-16') 2490 end, text_document_edit(0)) 2491 eq({ 2492 'First ↥ 🤦 🦄 line of text', 2493 '2nd line of 语text', 2494 }, buf_lines(target_bufnr)) 2495 end) 2496 2497 it('skips the edit if the version of the edit is behind the local buffer ', function() 2498 local apply_edit_mocking_current_version = function(edit, versionedBuf) 2499 exec_lua(function() 2500 vim.lsp.util.buf_versions[versionedBuf.bufnr] = versionedBuf.currentVersion 2501 vim.lsp.util.apply_text_document_edit(edit, nil, 'utf-16') 2502 end) 2503 end 2504 2505 local baseText = { 2506 '1st line of text', 2507 '2nd line of 语text', 2508 } 2509 2510 eq(baseText, buf_lines(target_bufnr)) 2511 2512 -- Apply an edit for an old version, should skip 2513 apply_edit_mocking_current_version( 2514 text_document_edit(2), 2515 { currentVersion = 7, bufnr = target_bufnr } 2516 ) 2517 eq(baseText, buf_lines(target_bufnr)) -- no change 2518 2519 -- Sanity check that next version to current does apply change 2520 apply_edit_mocking_current_version( 2521 text_document_edit(8), 2522 { currentVersion = 7, bufnr = target_bufnr } 2523 ) 2524 eq({ 2525 'First ↥ 🤦 🦄 line of text', 2526 '2nd line of 语text', 2527 }, buf_lines(target_bufnr)) 2528 end) 2529 end) 2530 2531 describe('workspace_apply_edit', function() 2532 it('workspace/applyEdit returns ApplyWorkspaceEditResponse', function() 2533 local expected_handlers = { 2534 { NIL, {}, { method = 'test', client_id = 1 } }, 2535 } 2536 test_rpc_server { 2537 test_name = 'basic_init', 2538 on_init = function(client, _) 2539 client:stop() 2540 end, 2541 -- If the program timed out, then code will be nil. 2542 on_exit = function(code, signal) 2543 eq(0, code, 'exit code') 2544 eq(0, signal, 'exit signal') 2545 end, 2546 -- Note that NIL must be used here. 2547 -- on_handler(err, method, result, client_id) 2548 on_handler = function(...) 2549 local expected = { 2550 applied = true, 2551 failureReason = nil, 2552 } 2553 eq( 2554 expected, 2555 exec_lua(function() 2556 local apply_edit = { 2557 label = nil, 2558 edit = {}, 2559 } 2560 return vim.lsp.handlers['workspace/applyEdit']( 2561 nil, 2562 apply_edit, 2563 { client_id = _G.TEST_RPC_CLIENT_ID } 2564 ) 2565 end) 2566 ) 2567 eq(table.remove(expected_handlers), { ... }) 2568 end, 2569 } 2570 end) 2571 end) 2572 2573 describe('apply_workspace_edit', function() 2574 local replace_line_edit = function(row, new_line, editVersion) 2575 return { 2576 edits = { 2577 -- NOTE: This is a hack if you have a line longer than 1000 it won't replace it 2578 make_edit(row, 0, row, 1000, new_line), 2579 }, 2580 textDocument = { 2581 uri = 'file:///fake/uri', 2582 version = editVersion, 2583 }, 2584 } 2585 end 2586 2587 -- Some servers send all the edits separately, but with the same version. 2588 -- We should not stop applying the edits 2589 local make_workspace_edit = function(changes) 2590 return { 2591 documentChanges = changes, 2592 } 2593 end 2594 2595 local target_bufnr --- @type integer 2596 local changedtick --- @type integer 2597 2598 before_each(function() 2599 exec_lua(function() 2600 target_bufnr = vim.uri_to_bufnr('file:///fake/uri') 2601 local lines = { 2602 'Original Line #1', 2603 'Original Line #2', 2604 } 2605 2606 vim.api.nvim_buf_set_lines(target_bufnr, 0, -1, false, lines) 2607 2608 local function update_changed_tick() 2609 vim.lsp.util.buf_versions[target_bufnr] = vim.b[target_bufnr].changedtick 2610 end 2611 2612 update_changed_tick() 2613 vim.api.nvim_buf_attach(target_bufnr, false, { 2614 on_changedtick = update_changed_tick, 2615 }) 2616 2617 changedtick = vim.b[target_bufnr].changedtick 2618 end) 2619 end) 2620 2621 it('apply_workspace_edit applies a single edit', function() 2622 local new_lines = { 2623 'First Line', 2624 } 2625 2626 local edits = {} 2627 for row, line in ipairs(new_lines) do 2628 table.insert(edits, replace_line_edit(row - 1, line, changedtick)) 2629 end 2630 2631 eq( 2632 { 2633 'First Line', 2634 'Original Line #2', 2635 }, 2636 exec_lua(function(workspace_edits) 2637 vim.lsp.util.apply_workspace_edit(workspace_edits, 'utf-16') 2638 2639 return vim.api.nvim_buf_get_lines(target_bufnr, 0, -1, false) 2640 end, make_workspace_edit(edits)) 2641 ) 2642 end) 2643 2644 it('apply_workspace_edit applies multiple edits', function() 2645 local new_lines = { 2646 'First Line', 2647 'Second Line', 2648 } 2649 2650 local edits = {} 2651 for row, line in ipairs(new_lines) do 2652 table.insert(edits, replace_line_edit(row - 1, line, changedtick)) 2653 end 2654 2655 eq( 2656 new_lines, 2657 exec_lua(function(workspace_edits) 2658 vim.lsp.util.apply_workspace_edit(workspace_edits, 'utf-16') 2659 return vim.api.nvim_buf_get_lines(target_bufnr, 0, -1, false) 2660 end, make_workspace_edit(edits)) 2661 ) 2662 end) 2663 2664 it('supports file creation with CreateFile payload', function() 2665 local tmpfile = tmpname(false) 2666 local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile) 2667 local edit = { 2668 documentChanges = { 2669 { 2670 kind = 'create', 2671 uri = uri, 2672 }, 2673 }, 2674 } 2675 exec_lua(function() 2676 vim.lsp.util.apply_workspace_edit(edit, 'utf-16') 2677 end) 2678 eq(true, vim.uv.fs_stat(tmpfile) ~= nil) 2679 end) 2680 2681 it( 2682 'supports file creation in folder that needs to be created with CreateFile payload', 2683 function() 2684 local tmpfile = tmpname(false) .. '/dummy/x/' 2685 local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile) 2686 local edit = { 2687 documentChanges = { 2688 { 2689 kind = 'create', 2690 uri = uri, 2691 }, 2692 }, 2693 } 2694 exec_lua(function() 2695 vim.lsp.util.apply_workspace_edit(edit, 'utf-16') 2696 end) 2697 eq(true, vim.uv.fs_stat(tmpfile) ~= nil) 2698 end 2699 ) 2700 2701 it('createFile does not touch file if it exists and ignoreIfExists is set', function() 2702 local tmpfile = tmpname() 2703 write_file(tmpfile, 'Dummy content') 2704 local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile) 2705 local edit = { 2706 documentChanges = { 2707 { 2708 kind = 'create', 2709 uri = uri, 2710 options = { 2711 ignoreIfExists = true, 2712 }, 2713 }, 2714 }, 2715 } 2716 exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16') 2717 eq(true, vim.uv.fs_stat(tmpfile) ~= nil) 2718 eq('Dummy content', read_file(tmpfile)) 2719 end) 2720 2721 it('createFile overrides file if overwrite is set', function() 2722 local tmpfile = tmpname() 2723 write_file(tmpfile, 'Dummy content') 2724 local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile) 2725 local edit = { 2726 documentChanges = { 2727 { 2728 kind = 'create', 2729 uri = uri, 2730 options = { 2731 overwrite = true, 2732 ignoreIfExists = true, -- overwrite must win over ignoreIfExists 2733 }, 2734 }, 2735 }, 2736 } 2737 exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16') 2738 eq(true, vim.uv.fs_stat(tmpfile) ~= nil) 2739 eq('', read_file(tmpfile)) 2740 end) 2741 2742 it('DeleteFile delete file and buffer', function() 2743 local tmpfile = tmpname() 2744 write_file(tmpfile, 'Be gone') 2745 local uri = exec_lua(function() 2746 local bufnr = vim.fn.bufadd(tmpfile) 2747 vim.fn.bufload(bufnr) 2748 return vim.uri_from_fname(tmpfile) 2749 end) 2750 local edit = { 2751 documentChanges = { 2752 { 2753 kind = 'delete', 2754 uri = uri, 2755 }, 2756 }, 2757 } 2758 eq(true, pcall(exec_lua, 'vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16')) 2759 eq(false, vim.uv.fs_stat(tmpfile) ~= nil) 2760 eq(false, api.nvim_buf_is_loaded(fn.bufadd(tmpfile))) 2761 end) 2762 2763 it('DeleteFile fails if file does not exist and ignoreIfNotExists is false', function() 2764 local tmpfile = tmpname(false) 2765 local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile) 2766 local edit = { 2767 documentChanges = { 2768 { 2769 kind = 'delete', 2770 uri = uri, 2771 options = { 2772 ignoreIfNotExists = false, 2773 }, 2774 }, 2775 }, 2776 } 2777 eq(false, pcall(exec_lua, 'vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16')) 2778 eq(false, vim.uv.fs_stat(tmpfile) ~= nil) 2779 end) 2780 end) 2781 2782 describe('lsp.util.rename', function() 2783 local pathsep = n.get_pathsep() 2784 2785 it('can rename an existing file', function() 2786 local old = tmpname() 2787 write_file(old, 'Test content') 2788 local new = tmpname(false) 2789 local lines = exec_lua(function() 2790 local old_bufnr = vim.fn.bufadd(old) 2791 vim.fn.bufload(old_bufnr) 2792 vim.lsp.util.rename(old, new) 2793 -- the existing buffer is renamed in-place and its contents is kept 2794 local new_bufnr = vim.fn.bufadd(new) 2795 vim.fn.bufload(new_bufnr) 2796 return (old_bufnr == new_bufnr) and vim.api.nvim_buf_get_lines(new_bufnr, 0, -1, true) 2797 end) 2798 eq({ 'Test content' }, lines) 2799 local exists = vim.uv.fs_stat(old) ~= nil 2800 eq(false, exists) 2801 exists = vim.uv.fs_stat(new) ~= nil 2802 eq(true, exists) 2803 os.remove(new) 2804 end) 2805 2806 it('can rename a directory', function() 2807 -- only reserve the name, file must not exist for the test scenario 2808 local old_dir = tmpname(false) 2809 local new_dir = tmpname(false) 2810 2811 n.mkdir_p(old_dir) 2812 2813 local file = 'file.txt' 2814 write_file(old_dir .. pathsep .. file, 'Test content') 2815 2816 local lines = exec_lua(function() 2817 local old_bufnr = vim.fn.bufadd(old_dir .. pathsep .. file) 2818 vim.fn.bufload(old_bufnr) 2819 vim.lsp.util.rename(old_dir, new_dir) 2820 -- the existing buffer is renamed in-place and its contents is kept 2821 local new_bufnr = vim.fn.bufadd(new_dir .. pathsep .. file) 2822 vim.fn.bufload(new_bufnr) 2823 return (old_bufnr == new_bufnr) and vim.api.nvim_buf_get_lines(new_bufnr, 0, -1, true) 2824 end) 2825 eq({ 'Test content' }, lines) 2826 eq(false, vim.uv.fs_stat(old_dir) ~= nil) 2827 eq(true, vim.uv.fs_stat(new_dir) ~= nil) 2828 eq(true, vim.uv.fs_stat(new_dir .. pathsep .. file) ~= nil) 2829 2830 os.remove(new_dir) 2831 end) 2832 2833 it('does not touch buffers that do not match path prefix', function() 2834 local old = tmpname(false) 2835 local new = tmpname(false) 2836 n.mkdir_p(old) 2837 2838 eq( 2839 true, 2840 exec_lua(function() 2841 local old_prefixed = 'explorer://' .. old 2842 local old_suffixed = old .. '.bak' 2843 local new_prefixed = 'explorer://' .. new 2844 local new_suffixed = new .. '.bak' 2845 2846 local old_prefixed_buf = vim.fn.bufadd(old_prefixed) 2847 local old_suffixed_buf = vim.fn.bufadd(old_suffixed) 2848 local new_prefixed_buf = vim.fn.bufadd(new_prefixed) 2849 local new_suffixed_buf = vim.fn.bufadd(new_suffixed) 2850 2851 vim.lsp.util.rename(old, new) 2852 2853 return vim.api.nvim_buf_is_valid(old_prefixed_buf) 2854 and vim.api.nvim_buf_is_valid(old_suffixed_buf) 2855 and vim.api.nvim_buf_is_valid(new_prefixed_buf) 2856 and vim.api.nvim_buf_is_valid(new_suffixed_buf) 2857 and vim.api.nvim_buf_get_name(old_prefixed_buf) == old_prefixed 2858 and vim.api.nvim_buf_get_name(old_suffixed_buf) == old_suffixed 2859 and vim.api.nvim_buf_get_name(new_prefixed_buf) == new_prefixed 2860 and vim.api.nvim_buf_get_name(new_suffixed_buf) == new_suffixed 2861 end) 2862 ) 2863 2864 os.remove(new) 2865 end) 2866 2867 it( 2868 'does not rename file if target exists and ignoreIfExists is set or overwrite is false', 2869 function() 2870 local old = tmpname() 2871 write_file(old, 'Old File') 2872 local new = tmpname() 2873 write_file(new, 'New file') 2874 2875 exec_lua(function() 2876 vim.lsp.util.rename(old, new, { ignoreIfExists = true }) 2877 end) 2878 2879 eq(true, vim.uv.fs_stat(old) ~= nil) 2880 eq('New file', read_file(new)) 2881 2882 exec_lua(function() 2883 vim.lsp.util.rename(old, new, { overwrite = false }) 2884 end) 2885 2886 eq(true, vim.uv.fs_stat(old) ~= nil) 2887 eq('New file', read_file(new)) 2888 end 2889 ) 2890 2891 it('maintains undo information for loaded buffer', function() 2892 local old = tmpname() 2893 write_file(old, 'line') 2894 local new = tmpname(false) 2895 2896 local undo_kept = exec_lua(function() 2897 vim.opt.undofile = true 2898 vim.cmd.edit(old) 2899 vim.cmd.normal('dd') 2900 vim.cmd.write() 2901 local undotree = vim.fn.undotree() 2902 vim.lsp.util.rename(old, new) 2903 -- Renaming uses :saveas, which updates the "last write" information. 2904 -- Other than that, the undotree should remain the same. 2905 undotree.save_cur = undotree.save_cur + 1 2906 undotree.save_last = undotree.save_last + 1 2907 undotree.entries[1].save = undotree.entries[1].save + 1 2908 return vim.deep_equal(undotree, vim.fn.undotree()) 2909 end) 2910 eq(false, vim.uv.fs_stat(old) ~= nil) 2911 eq(true, vim.uv.fs_stat(new) ~= nil) 2912 eq(true, undo_kept) 2913 end) 2914 2915 it('maintains undo information for unloaded buffer', function() 2916 local old = tmpname() 2917 write_file(old, 'line') 2918 local new = tmpname(false) 2919 2920 local undo_kept = exec_lua(function() 2921 vim.opt.undofile = true 2922 vim.cmd.split(old) 2923 vim.cmd.normal('dd') 2924 vim.cmd.write() 2925 local undotree = vim.fn.undotree() 2926 vim.cmd.bdelete() 2927 vim.lsp.util.rename(old, new) 2928 vim.cmd.edit(new) 2929 return vim.deep_equal(undotree, vim.fn.undotree()) 2930 end) 2931 eq(false, vim.uv.fs_stat(old) ~= nil) 2932 eq(true, vim.uv.fs_stat(new) ~= nil) 2933 eq(true, undo_kept) 2934 end) 2935 2936 it('does not rename file when it conflicts with a buffer without file', function() 2937 local old = tmpname() 2938 write_file(old, 'Old File') 2939 local new = tmpname(false) 2940 2941 local lines = exec_lua(function() 2942 local old_buf = vim.fn.bufadd(old) 2943 vim.fn.bufload(old_buf) 2944 local conflict_buf = vim.api.nvim_create_buf(true, false) 2945 vim.api.nvim_buf_set_name(conflict_buf, new) 2946 vim.api.nvim_buf_set_lines(conflict_buf, 0, -1, true, { 'conflict' }) 2947 vim.api.nvim_win_set_buf(0, conflict_buf) 2948 vim.lsp.util.rename(old, new) 2949 return vim.api.nvim_buf_get_lines(conflict_buf, 0, -1, true) 2950 end) 2951 eq({ 'conflict' }, lines) 2952 eq('Old File', read_file(old)) 2953 end) 2954 2955 it('does override target if overwrite is true', function() 2956 local old = tmpname() 2957 write_file(old, 'Old file') 2958 local new = tmpname() 2959 write_file(new, 'New file') 2960 exec_lua(function() 2961 vim.lsp.util.rename(old, new, { overwrite = true }) 2962 end) 2963 2964 eq(false, vim.uv.fs_stat(old) ~= nil) 2965 eq(true, vim.uv.fs_stat(new) ~= nil) 2966 eq('Old file', read_file(new)) 2967 end) 2968 end) 2969 2970 describe('lsp.util.locations_to_items', function() 2971 it('convert Location[] to items', function() 2972 local expected_template = { 2973 { 2974 filename = '/fake/uri', 2975 lnum = 1, 2976 end_lnum = 2, 2977 col = 3, 2978 end_col = 4, 2979 text = 'testing', 2980 user_data = {}, 2981 }, 2982 } 2983 local test_params = { 2984 { 2985 { 2986 uri = 'file:///fake/uri', 2987 range = { 2988 start = { line = 0, character = 2 }, 2989 ['end'] = { line = 1, character = 3 }, 2990 }, 2991 }, 2992 }, 2993 { 2994 { 2995 uri = 'file:///fake/uri', 2996 range = { 2997 start = { line = 0, character = 2 }, 2998 -- LSP spec: if character > line length, default to the line length. 2999 ['end'] = { line = 1, character = 10000 }, 3000 }, 3001 }, 3002 }, 3003 } 3004 for _, params in ipairs(test_params) do 3005 local actual = exec_lua(function(params0) 3006 local bufnr = vim.uri_to_bufnr('file:///fake/uri') 3007 local lines = { 'testing', '123' } 3008 vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) 3009 return vim.lsp.util.locations_to_items(params0, 'utf-16') 3010 end, params) 3011 local expected = vim.deepcopy(expected_template) 3012 expected[1].user_data = params[1] 3013 eq(expected, actual) 3014 end 3015 end) 3016 3017 it('convert LocationLink[] to items', function() 3018 local expected = { 3019 { 3020 filename = '/fake/uri', 3021 lnum = 1, 3022 end_lnum = 1, 3023 col = 3, 3024 end_col = 4, 3025 text = 'testing', 3026 user_data = { 3027 targetUri = 'file:///fake/uri', 3028 targetRange = { 3029 start = { line = 0, character = 2 }, 3030 ['end'] = { line = 0, character = 3 }, 3031 }, 3032 targetSelectionRange = { 3033 start = { line = 0, character = 2 }, 3034 ['end'] = { line = 0, character = 3 }, 3035 }, 3036 }, 3037 }, 3038 } 3039 local actual = exec_lua(function() 3040 local bufnr = vim.uri_to_bufnr('file:///fake/uri') 3041 local lines = { 'testing', '123' } 3042 vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) 3043 local locations = { 3044 { 3045 targetUri = vim.uri_from_bufnr(bufnr), 3046 targetRange = { 3047 start = { line = 0, character = 2 }, 3048 ['end'] = { line = 0, character = 3 }, 3049 }, 3050 targetSelectionRange = { 3051 start = { line = 0, character = 2 }, 3052 ['end'] = { line = 0, character = 3 }, 3053 }, 3054 }, 3055 } 3056 return vim.lsp.util.locations_to_items(locations, 'utf-16') 3057 end) 3058 eq(expected, actual) 3059 end) 3060 end) 3061 3062 describe('lsp.util.symbols_to_items', function() 3063 describe('convert DocumentSymbol[] to items', function() 3064 it('documentSymbol has children', function() 3065 local expected = { 3066 { 3067 col = 1, 3068 end_col = 1, 3069 end_lnum = 2, 3070 filename = '', 3071 kind = 'File', 3072 lnum = 2, 3073 text = '[File] TestA', 3074 }, 3075 { 3076 col = 1, 3077 end_col = 1, 3078 end_lnum = 4, 3079 filename = '', 3080 kind = 'Module', 3081 lnum = 4, 3082 text = '[Module] TestB', 3083 }, 3084 { 3085 col = 1, 3086 end_col = 1, 3087 end_lnum = 6, 3088 filename = '', 3089 kind = 'Namespace', 3090 lnum = 6, 3091 text = '[Namespace] TestC', 3092 }, 3093 } 3094 eq( 3095 expected, 3096 exec_lua(function() 3097 local doc_syms = { 3098 { 3099 deprecated = false, 3100 detail = 'A', 3101 kind = 1, 3102 name = 'TestA', 3103 range = { 3104 start = { 3105 character = 0, 3106 line = 1, 3107 }, 3108 ['end'] = { 3109 character = 0, 3110 line = 2, 3111 }, 3112 }, 3113 selectionRange = { 3114 start = { 3115 character = 0, 3116 line = 1, 3117 }, 3118 ['end'] = { 3119 character = 4, 3120 line = 1, 3121 }, 3122 }, 3123 children = { 3124 { 3125 children = {}, 3126 deprecated = false, 3127 detail = 'B', 3128 kind = 2, 3129 name = 'TestB', 3130 range = { 3131 start = { 3132 character = 0, 3133 line = 3, 3134 }, 3135 ['end'] = { 3136 character = 0, 3137 line = 4, 3138 }, 3139 }, 3140 selectionRange = { 3141 start = { 3142 character = 0, 3143 line = 3, 3144 }, 3145 ['end'] = { 3146 character = 4, 3147 line = 3, 3148 }, 3149 }, 3150 }, 3151 }, 3152 }, 3153 { 3154 deprecated = false, 3155 detail = 'C', 3156 kind = 3, 3157 name = 'TestC', 3158 range = { 3159 start = { 3160 character = 0, 3161 line = 5, 3162 }, 3163 ['end'] = { 3164 character = 0, 3165 line = 6, 3166 }, 3167 }, 3168 selectionRange = { 3169 start = { 3170 character = 0, 3171 line = 5, 3172 }, 3173 ['end'] = { 3174 character = 4, 3175 line = 5, 3176 }, 3177 }, 3178 }, 3179 } 3180 return vim.lsp.util.symbols_to_items(doc_syms, nil, 'utf-16') 3181 end) 3182 ) 3183 end) 3184 3185 it('documentSymbol has no children', function() 3186 local expected = { 3187 { 3188 col = 1, 3189 end_col = 1, 3190 end_lnum = 2, 3191 filename = '', 3192 kind = 'File', 3193 lnum = 2, 3194 text = '[File] TestA', 3195 }, 3196 { 3197 col = 1, 3198 end_col = 1, 3199 end_lnum = 6, 3200 filename = '', 3201 kind = 'Namespace', 3202 lnum = 6, 3203 text = '[Namespace] TestC', 3204 }, 3205 } 3206 eq( 3207 expected, 3208 exec_lua(function() 3209 local doc_syms = { 3210 { 3211 deprecated = false, 3212 detail = 'A', 3213 kind = 1, 3214 name = 'TestA', 3215 range = { 3216 start = { 3217 character = 0, 3218 line = 1, 3219 }, 3220 ['end'] = { 3221 character = 0, 3222 line = 2, 3223 }, 3224 }, 3225 selectionRange = { 3226 start = { 3227 character = 0, 3228 line = 1, 3229 }, 3230 ['end'] = { 3231 character = 4, 3232 line = 1, 3233 }, 3234 }, 3235 }, 3236 { 3237 deprecated = false, 3238 detail = 'C', 3239 kind = 3, 3240 name = 'TestC', 3241 range = { 3242 start = { 3243 character = 0, 3244 line = 5, 3245 }, 3246 ['end'] = { 3247 character = 0, 3248 line = 6, 3249 }, 3250 }, 3251 selectionRange = { 3252 start = { 3253 character = 0, 3254 line = 5, 3255 }, 3256 ['end'] = { 3257 character = 4, 3258 line = 5, 3259 }, 3260 }, 3261 }, 3262 } 3263 return vim.lsp.util.symbols_to_items(doc_syms, nil, 'utf-16') 3264 end) 3265 ) 3266 end) 3267 3268 it('handles deprecated items', function() 3269 local expected = { 3270 { 3271 col = 1, 3272 end_col = 1, 3273 end_lnum = 2, 3274 filename = '', 3275 kind = 'File', 3276 lnum = 2, 3277 text = '[File] TestA (deprecated)', 3278 }, 3279 { 3280 col = 1, 3281 end_col = 1, 3282 end_lnum = 6, 3283 filename = '', 3284 kind = 'Namespace', 3285 lnum = 6, 3286 text = '[Namespace] TestC (deprecated)', 3287 }, 3288 } 3289 eq( 3290 expected, 3291 exec_lua(function() 3292 local doc_syms = { 3293 { 3294 deprecated = true, 3295 detail = 'A', 3296 kind = 1, 3297 name = 'TestA', 3298 range = { 3299 start = { 3300 character = 0, 3301 line = 1, 3302 }, 3303 ['end'] = { 3304 character = 0, 3305 line = 2, 3306 }, 3307 }, 3308 selectionRange = { 3309 start = { 3310 character = 0, 3311 line = 1, 3312 }, 3313 ['end'] = { 3314 character = 4, 3315 line = 1, 3316 }, 3317 }, 3318 }, 3319 { 3320 detail = 'C', 3321 kind = 3, 3322 name = 'TestC', 3323 range = { 3324 start = { 3325 character = 0, 3326 line = 5, 3327 }, 3328 ['end'] = { 3329 character = 0, 3330 line = 6, 3331 }, 3332 }, 3333 selectionRange = { 3334 start = { 3335 character = 0, 3336 line = 5, 3337 }, 3338 ['end'] = { 3339 character = 4, 3340 line = 5, 3341 }, 3342 }, 3343 tags = { 1 }, -- deprecated 3344 }, 3345 } 3346 return vim.lsp.util.symbols_to_items(doc_syms, nil, 'utf-16') 3347 end) 3348 ) 3349 end) 3350 end) 3351 3352 it('convert SymbolInformation[] to items', function() 3353 local expected = { 3354 { 3355 col = 1, 3356 end_col = 1, 3357 end_lnum = 3, 3358 filename = '/test_a', 3359 kind = 'File', 3360 lnum = 2, 3361 text = '[File] TestA in TestAContainer', 3362 }, 3363 { 3364 col = 1, 3365 end_col = 1, 3366 end_lnum = 5, 3367 filename = '/test_b', 3368 kind = 'Module', 3369 lnum = 4, 3370 text = '[Module] TestB in TestBContainer (deprecated)', 3371 }, 3372 } 3373 eq( 3374 expected, 3375 exec_lua(function() 3376 local sym_info = { 3377 { 3378 deprecated = false, 3379 kind = 1, 3380 name = 'TestA', 3381 location = { 3382 range = { 3383 start = { 3384 character = 0, 3385 line = 1, 3386 }, 3387 ['end'] = { 3388 character = 0, 3389 line = 2, 3390 }, 3391 }, 3392 uri = 'file:///test_a', 3393 }, 3394 containerName = 'TestAContainer', 3395 }, 3396 { 3397 deprecated = true, 3398 kind = 2, 3399 name = 'TestB', 3400 location = { 3401 range = { 3402 start = { 3403 character = 0, 3404 line = 3, 3405 }, 3406 ['end'] = { 3407 character = 0, 3408 line = 4, 3409 }, 3410 }, 3411 uri = 'file:///test_b', 3412 }, 3413 containerName = 'TestBContainer', 3414 }, 3415 } 3416 return vim.lsp.util.symbols_to_items(sym_info, nil, 'utf-16') 3417 end) 3418 ) 3419 end) 3420 end) 3421 3422 describe('lsp.util.jump_to_location', function() 3423 local target_bufnr --- @type integer 3424 3425 before_each(function() 3426 target_bufnr = exec_lua(function() 3427 local bufnr = vim.uri_to_bufnr('file:///fake/uri') 3428 local lines = { '1st line of text', 'å å ɧ 汉语 ↥ 🤦 🦄' } 3429 vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) 3430 return bufnr 3431 end) 3432 end) 3433 3434 local location = function(start_line, start_char, end_line, end_char) 3435 return { 3436 uri = 'file:///fake/uri', 3437 range = { 3438 start = { line = start_line, character = start_char }, 3439 ['end'] = { line = end_line, character = end_char }, 3440 }, 3441 } 3442 end 3443 3444 local jump = function(msg) 3445 eq(true, exec_lua('return vim.lsp.util.jump_to_location(...)', msg, 'utf-16')) 3446 eq(target_bufnr, fn.bufnr('%')) 3447 return { 3448 line = fn.line('.'), 3449 col = fn.col('.'), 3450 } 3451 end 3452 3453 it('jumps to a Location', function() 3454 local pos = jump(location(0, 9, 0, 9)) 3455 eq(1, pos.line) 3456 eq(10, pos.col) 3457 end) 3458 3459 it('jumps to a LocationLink', function() 3460 local pos = jump({ 3461 targetUri = 'file:///fake/uri', 3462 targetSelectionRange = { 3463 start = { line = 0, character = 4 }, 3464 ['end'] = { line = 0, character = 4 }, 3465 }, 3466 targetRange = { 3467 start = { line = 1, character = 5 }, 3468 ['end'] = { line = 1, character = 5 }, 3469 }, 3470 }) 3471 eq(1, pos.line) 3472 eq(5, pos.col) 3473 end) 3474 3475 it('jumps to the correct multibyte column', function() 3476 local pos = jump(location(1, 2, 1, 2)) 3477 eq(2, pos.line) 3478 eq(4, pos.col) 3479 eq('å', fn.expand('<cword>')) 3480 end) 3481 3482 it('adds current position to jumplist before jumping', function() 3483 api.nvim_win_set_buf(0, target_bufnr) 3484 local mark = api.nvim_buf_get_mark(target_bufnr, "'") 3485 eq({ 1, 0 }, mark) 3486 3487 api.nvim_win_set_cursor(0, { 2, 3 }) 3488 jump(location(0, 9, 0, 9)) 3489 3490 mark = api.nvim_buf_get_mark(target_bufnr, "'") 3491 eq({ 2, 3 }, mark) 3492 end) 3493 end) 3494 3495 describe('lsp.util.show_document', function() 3496 local target_bufnr --- @type integer 3497 local target_bufnr2 --- @type integer 3498 3499 before_each(function() 3500 target_bufnr = exec_lua(function() 3501 local bufnr = vim.uri_to_bufnr('file:///fake/uri') 3502 local lines = { '1st line of text', 'å å ɧ 汉语 ↥ 🤦 🦄' } 3503 vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) 3504 return bufnr 3505 end) 3506 3507 target_bufnr2 = exec_lua(function() 3508 local bufnr = vim.uri_to_bufnr('file:///fake/uri2') 3509 local lines = { '1st line of text', 'å å ɧ 汉语 ↥ 🤦 🦄' } 3510 vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) 3511 return bufnr 3512 end) 3513 end) 3514 3515 local location = function(start_line, start_char, end_line, end_char, second_uri) 3516 return { 3517 uri = second_uri and 'file:///fake/uri2' or 'file:///fake/uri', 3518 range = { 3519 start = { line = start_line, character = start_char }, 3520 ['end'] = { line = end_line, character = end_char }, 3521 }, 3522 } 3523 end 3524 3525 local show_document = function(msg, focus, reuse_win) 3526 eq( 3527 true, 3528 exec_lua( 3529 'return vim.lsp.util.show_document(...)', 3530 msg, 3531 'utf-16', 3532 { reuse_win = reuse_win, focus = focus } 3533 ) 3534 ) 3535 if focus == true or focus == nil then 3536 eq(target_bufnr, fn.bufnr('%')) 3537 end 3538 return { 3539 line = fn.line('.'), 3540 col = fn.col('.'), 3541 } 3542 end 3543 3544 it('jumps to a Location if focus is true', function() 3545 local pos = show_document(location(0, 9, 0, 9), true, true) 3546 eq(1, pos.line) 3547 eq(10, pos.col) 3548 end) 3549 3550 it('jumps to a Location if focus is true via handler', function() 3551 exec_lua(create_server_definition) 3552 local result = exec_lua(function() 3553 local server = _G._create_server() 3554 local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = server.cmd })) 3555 local result = { 3556 uri = 'file:///fake/uri', 3557 selection = { 3558 start = { line = 0, character = 9 }, 3559 ['end'] = { line = 0, character = 9 }, 3560 }, 3561 takeFocus = true, 3562 } 3563 local ctx = { 3564 client_id = client_id, 3565 method = 'window/showDocument', 3566 } 3567 vim.lsp.handlers['window/showDocument'](nil, result, ctx) 3568 vim.lsp.get_client_by_id(client_id):stop() 3569 return { 3570 cursor = vim.api.nvim_win_get_cursor(0), 3571 } 3572 end) 3573 eq(1, result.cursor[1]) 3574 eq(9, result.cursor[2]) 3575 end) 3576 3577 it('jumps to a Location if focus not set', function() 3578 local pos = show_document(location(0, 9, 0, 9), nil, true) 3579 eq(1, pos.line) 3580 eq(10, pos.col) 3581 end) 3582 3583 it('does not add current position to jumplist if not focus', function() 3584 api.nvim_win_set_buf(0, target_bufnr) 3585 local mark = api.nvim_buf_get_mark(target_bufnr, "'") 3586 eq({ 1, 0 }, mark) 3587 3588 api.nvim_win_set_cursor(0, { 2, 3 }) 3589 show_document(location(0, 9, 0, 9), false, true) 3590 show_document(location(0, 9, 0, 9, true), false, true) 3591 3592 mark = api.nvim_buf_get_mark(target_bufnr, "'") 3593 eq({ 1, 0 }, mark) 3594 end) 3595 3596 it('does not change cursor position if not focus and not reuse_win', function() 3597 api.nvim_win_set_buf(0, target_bufnr) 3598 local cursor = api.nvim_win_get_cursor(0) 3599 3600 show_document(location(0, 9, 0, 9), false, false) 3601 eq(cursor, api.nvim_win_get_cursor(0)) 3602 end) 3603 3604 it('does not change window if not focus', function() 3605 api.nvim_win_set_buf(0, target_bufnr) 3606 local win = api.nvim_get_current_win() 3607 3608 -- same document/bufnr 3609 show_document(location(0, 9, 0, 9), false, true) 3610 eq(win, api.nvim_get_current_win()) 3611 3612 -- different document/bufnr, new window/split 3613 show_document(location(0, 9, 0, 9, true), false, true) 3614 eq(2, #api.nvim_list_wins()) 3615 eq(win, api.nvim_get_current_win()) 3616 end) 3617 3618 it("respects 'reuse_win' parameter", function() 3619 api.nvim_win_set_buf(0, target_bufnr) 3620 3621 -- does not create a new window if the buffer is already open 3622 show_document(location(0, 9, 0, 9), false, true) 3623 eq(1, #api.nvim_list_wins()) 3624 3625 -- creates a new window even if the buffer is already open 3626 show_document(location(0, 9, 0, 9), false, false) 3627 eq(2, #api.nvim_list_wins()) 3628 end) 3629 3630 it('correctly sets the cursor of the split if range is given without focus', function() 3631 api.nvim_win_set_buf(0, target_bufnr) 3632 3633 show_document(location(0, 9, 0, 9, true), false, true) 3634 3635 local wins = api.nvim_list_wins() 3636 eq(2, #wins) 3637 table.sort(wins) 3638 3639 eq({ 1, 0 }, api.nvim_win_get_cursor(wins[1])) 3640 eq({ 1, 9 }, api.nvim_win_get_cursor(wins[2])) 3641 end) 3642 3643 it('does not change cursor of the split if not range and not focus', function() 3644 api.nvim_win_set_buf(0, target_bufnr) 3645 api.nvim_win_set_cursor(0, { 2, 3 }) 3646 3647 exec_lua(function() 3648 vim.cmd.new() 3649 end) 3650 api.nvim_win_set_buf(0, target_bufnr2) 3651 api.nvim_win_set_cursor(0, { 2, 3 }) 3652 3653 show_document({ uri = 'file:///fake/uri2' }, false, true) 3654 3655 local wins = api.nvim_list_wins() 3656 eq(2, #wins) 3657 eq({ 2, 3 }, api.nvim_win_get_cursor(wins[1])) 3658 eq({ 2, 3 }, api.nvim_win_get_cursor(wins[2])) 3659 end) 3660 3661 it('respects existing buffers', function() 3662 api.nvim_win_set_buf(0, target_bufnr) 3663 local win = api.nvim_get_current_win() 3664 3665 exec_lua(function() 3666 vim.cmd.new() 3667 end) 3668 api.nvim_win_set_buf(0, target_bufnr2) 3669 api.nvim_win_set_cursor(0, { 2, 3 }) 3670 local split = api.nvim_get_current_win() 3671 3672 -- reuse win for open document/bufnr if called from split 3673 show_document(location(0, 9, 0, 9, true), false, true) 3674 eq({ 1, 9 }, api.nvim_win_get_cursor(split)) 3675 eq(2, #api.nvim_list_wins()) 3676 3677 api.nvim_set_current_win(win) 3678 3679 -- reuse win for open document/bufnr if called outside the split 3680 show_document(location(0, 9, 0, 9, true), false, true) 3681 eq({ 1, 9 }, api.nvim_win_get_cursor(split)) 3682 eq(2, #api.nvim_list_wins()) 3683 end) 3684 end) 3685 3686 describe('lsp.util._make_floating_popup_size', function() 3687 before_each(function() 3688 exec_lua(function() 3689 _G.contents = { 'text tαxt txtα tex', 'text tααt tααt text', 'text tαxt tαxt' } 3690 end) 3691 end) 3692 3693 it('calculates size correctly', function() 3694 eq( 3695 { 19, 3 }, 3696 exec_lua(function() 3697 return { vim.lsp.util._make_floating_popup_size(_G.contents) } 3698 end) 3699 ) 3700 end) 3701 3702 it('calculates size correctly with wrapping', function() 3703 eq( 3704 { 15, 5 }, 3705 exec_lua(function() 3706 return { 3707 vim.lsp.util._make_floating_popup_size(_G.contents, { width = 15, wrap_at = 14 }), 3708 } 3709 end) 3710 ) 3711 end) 3712 3713 it('handles NUL bytes in text', function() 3714 exec_lua(function() 3715 _G.contents = { 3716 '\000\001\002\003\004\005\006\007\008\009', 3717 '\010\011\012\013\014\015\016\017\018\019', 3718 '\020\021\022\023\024\025\026\027\028\029', 3719 } 3720 end) 3721 command('set list listchars=') 3722 eq( 3723 { 20, 3 }, 3724 exec_lua(function() 3725 return { vim.lsp.util._make_floating_popup_size(_G.contents) } 3726 end) 3727 ) 3728 command('set display+=uhex') 3729 eq( 3730 { 40, 3 }, 3731 exec_lua(function() 3732 return { vim.lsp.util._make_floating_popup_size(_G.contents) } 3733 end) 3734 ) 3735 end) 3736 it('handles empty line', function() 3737 exec_lua(function() 3738 _G.contents = { 3739 '', 3740 } 3741 end) 3742 eq( 3743 { 20, 1 }, 3744 exec_lua(function() 3745 return { vim.lsp.util._make_floating_popup_size(_G.contents, { width = 20 }) } 3746 end) 3747 ) 3748 end) 3749 3750 it('considers string title when computing width', function() 3751 eq( 3752 { 17, 2 }, 3753 exec_lua(function() 3754 return { 3755 vim.lsp.util._make_floating_popup_size( 3756 { 'foo', 'bar' }, 3757 { title = 'A very long title' } 3758 ), 3759 } 3760 end) 3761 ) 3762 end) 3763 3764 it('considers [string,string][] title when computing width', function() 3765 eq( 3766 { 17, 2 }, 3767 exec_lua(function() 3768 return { 3769 vim.lsp.util._make_floating_popup_size( 3770 { 'foo', 'bar' }, 3771 { title = { { 'A very ', 'Normal' }, { 'long title', 'Normal' } } } 3772 ), 3773 } 3774 end) 3775 ) 3776 end) 3777 end) 3778 3779 describe('lsp.util.trim.trim_empty_lines', function() 3780 it('properly trims empty lines', function() 3781 eq( 3782 { { 'foo', 'bar' } }, 3783 exec_lua(function() 3784 --- @diagnostic disable-next-line:deprecated 3785 return vim.lsp.util.trim_empty_lines({ { 'foo', 'bar' }, nil }) 3786 end) 3787 ) 3788 end) 3789 end) 3790 3791 describe('lsp.util.convert_signature_help_to_markdown_lines', function() 3792 it('can handle negative activeSignature', function() 3793 local result = exec_lua(function() 3794 local signature_help = { 3795 activeParameter = 0, 3796 activeSignature = -1, 3797 signatures = { 3798 { 3799 documentation = 'some doc', 3800 label = 'TestEntity.TestEntity()', 3801 parameters = {}, 3802 }, 3803 }, 3804 } 3805 return vim.lsp.util.convert_signature_help_to_markdown_lines(signature_help, 'cs', { ',' }) 3806 end) 3807 local expected = { '```cs', 'TestEntity.TestEntity()', '```', '---', 'some doc' } 3808 eq(expected, result) 3809 end) 3810 3811 it('highlights active parameters in multiline signature labels', function() 3812 local _, hl = exec_lua(function() 3813 local signature_help = { 3814 activeSignature = 0, 3815 signatures = { 3816 { 3817 activeParameter = 1, 3818 label = 'fn bar(\n _: void,\n _: void,\n) void', 3819 parameters = { 3820 { label = '_: void' }, 3821 { label = '_: void' }, 3822 }, 3823 }, 3824 }, 3825 } 3826 return vim.lsp.util.convert_signature_help_to_markdown_lines(signature_help, 'zig', { '(' }) 3827 end) 3828 -- Note that although the highlight positions below are 0-indexed, the 2nd parameter 3829 -- corresponds to the 3rd line because the first line is the ``` from the 3830 -- Markdown block. 3831 local expected = { 3, 4, 3, 11 } 3832 eq(expected, hl) 3833 end) 3834 end) 3835 3836 describe('lsp.util.get_effective_tabstop', function() 3837 local function test_tabstop(tabsize, shiftwidth) 3838 exec_lua(string.format( 3839 [[ 3840 vim.bo.shiftwidth = %d 3841 vim.bo.tabstop = 2 3842 ]], 3843 shiftwidth 3844 )) 3845 eq( 3846 tabsize, 3847 exec_lua(function() 3848 return vim.lsp.util.get_effective_tabstop() 3849 end) 3850 ) 3851 end 3852 3853 it('with shiftwidth = 1', function() 3854 test_tabstop(1, 1) 3855 end) 3856 3857 it('with shiftwidth = 0', function() 3858 test_tabstop(2, 0) 3859 end) 3860 end) 3861 3862 describe('vim.lsp.buf.outgoing_calls', function() 3863 it('does nothing for an empty response', function() 3864 local qflist_count = exec_lua(function() 3865 require 'vim.lsp.handlers'['callHierarchy/outgoingCalls'](nil, nil, {}, nil) 3866 return #vim.fn.getqflist() 3867 end) 3868 eq(0, qflist_count) 3869 end) 3870 3871 it('opens the quickfix list with the right caller', function() 3872 local qflist = exec_lua(function() 3873 local rust_analyzer_response = { 3874 { 3875 fromRanges = { 3876 { 3877 ['end'] = { 3878 character = 7, 3879 line = 3, 3880 }, 3881 start = { 3882 character = 4, 3883 line = 3, 3884 }, 3885 }, 3886 }, 3887 to = { 3888 detail = 'fn foo()', 3889 kind = 12, 3890 name = 'foo', 3891 range = { 3892 ['end'] = { 3893 character = 11, 3894 line = 0, 3895 }, 3896 start = { 3897 character = 0, 3898 line = 0, 3899 }, 3900 }, 3901 selectionRange = { 3902 ['end'] = { 3903 character = 6, 3904 line = 0, 3905 }, 3906 start = { 3907 character = 3, 3908 line = 0, 3909 }, 3910 }, 3911 uri = 'file:///src/main.rs', 3912 }, 3913 }, 3914 } 3915 local handler = require 'vim.lsp.handlers'['callHierarchy/outgoingCalls'] 3916 handler(nil, rust_analyzer_response, {}) 3917 return vim.fn.getqflist() 3918 end) 3919 3920 local expected = { 3921 { 3922 bufnr = 2, 3923 col = 5, 3924 end_col = 0, 3925 lnum = 4, 3926 end_lnum = 0, 3927 module = '', 3928 nr = 0, 3929 pattern = '', 3930 text = 'foo', 3931 type = '', 3932 valid = 1, 3933 vcol = 0, 3934 }, 3935 } 3936 3937 eq(expected, qflist) 3938 end) 3939 end) 3940 3941 describe('vim.lsp.buf.incoming_calls', function() 3942 it('does nothing for an empty response', function() 3943 local qflist_count = exec_lua(function() 3944 require 'vim.lsp.handlers'['callHierarchy/incomingCalls'](nil, nil, {}) 3945 return #vim.fn.getqflist() 3946 end) 3947 eq(0, qflist_count) 3948 end) 3949 3950 it('opens the quickfix list with the right callee', function() 3951 local qflist = exec_lua(function() 3952 local rust_analyzer_response = { 3953 { 3954 from = { 3955 detail = 'fn main()', 3956 kind = 12, 3957 name = 'main', 3958 range = { 3959 ['end'] = { 3960 character = 1, 3961 line = 4, 3962 }, 3963 start = { 3964 character = 0, 3965 line = 2, 3966 }, 3967 }, 3968 selectionRange = { 3969 ['end'] = { 3970 character = 7, 3971 line = 2, 3972 }, 3973 start = { 3974 character = 3, 3975 line = 2, 3976 }, 3977 }, 3978 uri = 'file:///src/main.rs', 3979 }, 3980 fromRanges = { 3981 { 3982 ['end'] = { 3983 character = 7, 3984 line = 3, 3985 }, 3986 start = { 3987 character = 4, 3988 line = 3, 3989 }, 3990 }, 3991 }, 3992 }, 3993 } 3994 3995 local handler = require 'vim.lsp.handlers'['callHierarchy/incomingCalls'] 3996 handler(nil, rust_analyzer_response, {}) 3997 return vim.fn.getqflist() 3998 end) 3999 4000 local expected = { 4001 { 4002 bufnr = 2, 4003 col = 5, 4004 end_col = 0, 4005 lnum = 4, 4006 end_lnum = 0, 4007 module = '', 4008 nr = 0, 4009 pattern = '', 4010 text = 'main', 4011 type = '', 4012 valid = 1, 4013 vcol = 0, 4014 }, 4015 } 4016 4017 eq(expected, qflist) 4018 end) 4019 end) 4020 4021 describe('vim.lsp.buf.typehierarchy subtypes', function() 4022 it('does nothing for an empty response', function() 4023 local qflist_count = exec_lua(function() 4024 require 'vim.lsp.handlers'['typeHierarchy/subtypes'](nil, nil, {}) 4025 return #vim.fn.getqflist() 4026 end) 4027 eq(0, qflist_count) 4028 end) 4029 4030 it('opens the quickfix list with the right subtypes', function() 4031 exec_lua(create_server_definition) 4032 local qflist = exec_lua(function() 4033 local clangd_response = { 4034 { 4035 data = { 4036 parents = { 4037 { 4038 parents = { 4039 { 4040 parents = { 4041 { 4042 parents = {}, 4043 symbolID = '62B3D268A01B9978', 4044 }, 4045 }, 4046 symbolID = 'DC9B0AD433B43BEC', 4047 }, 4048 }, 4049 symbolID = '06B5F6A19BA9F6A8', 4050 }, 4051 }, 4052 symbolID = 'EDC336589C09ABB2', 4053 }, 4054 kind = 5, 4055 name = 'D2', 4056 range = { 4057 ['end'] = { 4058 character = 8, 4059 line = 3, 4060 }, 4061 start = { 4062 character = 6, 4063 line = 3, 4064 }, 4065 }, 4066 selectionRange = { 4067 ['end'] = { 4068 character = 8, 4069 line = 3, 4070 }, 4071 start = { 4072 character = 6, 4073 line = 3, 4074 }, 4075 }, 4076 uri = 'file:///home/jiangyinzuo/hello.cpp', 4077 }, 4078 { 4079 data = { 4080 parents = { 4081 { 4082 parents = { 4083 { 4084 parents = { 4085 { 4086 parents = {}, 4087 symbolID = '62B3D268A01B9978', 4088 }, 4089 }, 4090 symbolID = 'DC9B0AD433B43BEC', 4091 }, 4092 }, 4093 symbolID = '06B5F6A19BA9F6A8', 4094 }, 4095 }, 4096 symbolID = 'AFFCAED15557EF08', 4097 }, 4098 kind = 5, 4099 name = 'D1', 4100 range = { 4101 ['end'] = { 4102 character = 8, 4103 line = 2, 4104 }, 4105 start = { 4106 character = 6, 4107 line = 2, 4108 }, 4109 }, 4110 selectionRange = { 4111 ['end'] = { 4112 character = 8, 4113 line = 2, 4114 }, 4115 start = { 4116 character = 6, 4117 line = 2, 4118 }, 4119 }, 4120 uri = 'file:///home/jiangyinzuo/hello.cpp', 4121 }, 4122 } 4123 4124 local server = _G._create_server({ 4125 capabilities = { 4126 positionEncoding = 'utf-8', 4127 }, 4128 }) 4129 local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) 4130 local handler = require 'vim.lsp.handlers'['typeHierarchy/subtypes'] 4131 local bufnr = vim.api.nvim_get_current_buf() 4132 vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 4133 'class B : public A{};', 4134 'class C : public B{};', 4135 'class D1 : public C{};', 4136 'class D2 : public C{};', 4137 'class E : public D1, D2 {};', 4138 }) 4139 handler(nil, clangd_response, { client_id = client_id, bufnr = bufnr }) 4140 return vim.fn.getqflist() 4141 end) 4142 4143 local expected = { 4144 { 4145 bufnr = 2, 4146 col = 7, 4147 end_col = 0, 4148 end_lnum = 0, 4149 lnum = 4, 4150 module = '', 4151 nr = 0, 4152 pattern = '', 4153 text = 'D2', 4154 type = '', 4155 valid = 1, 4156 vcol = 0, 4157 }, 4158 { 4159 bufnr = 2, 4160 col = 7, 4161 end_col = 0, 4162 end_lnum = 0, 4163 lnum = 3, 4164 module = '', 4165 nr = 0, 4166 pattern = '', 4167 text = 'D1', 4168 type = '', 4169 valid = 1, 4170 vcol = 0, 4171 }, 4172 } 4173 4174 eq(expected, qflist) 4175 end) 4176 4177 it('opens the quickfix list with the right subtypes and details', function() 4178 exec_lua(create_server_definition) 4179 local qflist = exec_lua(function() 4180 local jdtls_response = { 4181 { 4182 data = { element = '=hello-java_ed323c3c/_<{Main.java[Main[A' }, 4183 detail = '', 4184 kind = 5, 4185 name = 'A', 4186 range = { 4187 ['end'] = { character = 26, line = 3 }, 4188 start = { character = 1, line = 3 }, 4189 }, 4190 selectionRange = { 4191 ['end'] = { character = 8, line = 3 }, 4192 start = { character = 7, line = 3 }, 4193 }, 4194 tags = {}, 4195 uri = 'file:///home/jiangyinzuo/hello-java/Main.java', 4196 }, 4197 { 4198 data = { element = '=hello-java_ed323c3c/_<mylist{MyList.java[MyList[Inner' }, 4199 detail = 'mylist', 4200 kind = 5, 4201 name = 'MyList$Inner', 4202 range = { 4203 ['end'] = { character = 37, line = 3 }, 4204 start = { character = 1, line = 3 }, 4205 }, 4206 selectionRange = { 4207 ['end'] = { character = 19, line = 3 }, 4208 start = { character = 14, line = 3 }, 4209 }, 4210 tags = {}, 4211 uri = 'file:///home/jiangyinzuo/hello-java/mylist/MyList.java', 4212 }, 4213 } 4214 4215 local server = _G._create_server({ 4216 capabilities = { 4217 positionEncoding = 'utf-8', 4218 }, 4219 }) 4220 local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) 4221 local handler = require 'vim.lsp.handlers'['typeHierarchy/subtypes'] 4222 local bufnr = vim.api.nvim_get_current_buf() 4223 vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 4224 'package mylist;', 4225 '', 4226 'public class MyList {', 4227 ' static class Inner extends MyList{}', 4228 '~}', 4229 }) 4230 handler(nil, jdtls_response, { client_id = client_id, bufnr = bufnr }) 4231 return vim.fn.getqflist() 4232 end) 4233 4234 local expected = { 4235 { 4236 bufnr = 2, 4237 col = 2, 4238 end_col = 0, 4239 end_lnum = 0, 4240 lnum = 4, 4241 module = '', 4242 nr = 0, 4243 pattern = '', 4244 text = 'A', 4245 type = '', 4246 valid = 1, 4247 vcol = 0, 4248 }, 4249 { 4250 bufnr = 3, 4251 col = 2, 4252 end_col = 0, 4253 end_lnum = 0, 4254 lnum = 4, 4255 module = '', 4256 nr = 0, 4257 pattern = '', 4258 text = 'MyList$Inner mylist', 4259 type = '', 4260 valid = 1, 4261 vcol = 0, 4262 }, 4263 } 4264 eq(expected, qflist) 4265 end) 4266 end) 4267 4268 describe('vim.lsp.buf.typehierarchy supertypes', function() 4269 it('does nothing for an empty response', function() 4270 local qflist_count = exec_lua(function() 4271 require 'vim.lsp.handlers'['typeHierarchy/supertypes'](nil, nil, {}) 4272 return #vim.fn.getqflist() 4273 end) 4274 eq(0, qflist_count) 4275 end) 4276 4277 it('opens the quickfix list with the right supertypes', function() 4278 exec_lua(create_server_definition) 4279 local qflist = exec_lua(function() 4280 local clangd_response = { 4281 { 4282 data = { 4283 parents = { 4284 { 4285 parents = { 4286 { 4287 parents = { 4288 { 4289 parents = {}, 4290 symbolID = '62B3D268A01B9978', 4291 }, 4292 }, 4293 symbolID = 'DC9B0AD433B43BEC', 4294 }, 4295 }, 4296 symbolID = '06B5F6A19BA9F6A8', 4297 }, 4298 }, 4299 symbolID = 'EDC336589C09ABB2', 4300 }, 4301 kind = 5, 4302 name = 'D2', 4303 range = { 4304 ['end'] = { 4305 character = 8, 4306 line = 3, 4307 }, 4308 start = { 4309 character = 6, 4310 line = 3, 4311 }, 4312 }, 4313 selectionRange = { 4314 ['end'] = { 4315 character = 8, 4316 line = 3, 4317 }, 4318 start = { 4319 character = 6, 4320 line = 3, 4321 }, 4322 }, 4323 uri = 'file:///home/jiangyinzuo/hello.cpp', 4324 }, 4325 { 4326 data = { 4327 parents = { 4328 { 4329 parents = { 4330 { 4331 parents = { 4332 { 4333 parents = {}, 4334 symbolID = '62B3D268A01B9978', 4335 }, 4336 }, 4337 symbolID = 'DC9B0AD433B43BEC', 4338 }, 4339 }, 4340 symbolID = '06B5F6A19BA9F6A8', 4341 }, 4342 }, 4343 symbolID = 'AFFCAED15557EF08', 4344 }, 4345 kind = 5, 4346 name = 'D1', 4347 range = { 4348 ['end'] = { 4349 character = 8, 4350 line = 2, 4351 }, 4352 start = { 4353 character = 6, 4354 line = 2, 4355 }, 4356 }, 4357 selectionRange = { 4358 ['end'] = { 4359 character = 8, 4360 line = 2, 4361 }, 4362 start = { 4363 character = 6, 4364 line = 2, 4365 }, 4366 }, 4367 uri = 'file:///home/jiangyinzuo/hello.cpp', 4368 }, 4369 } 4370 4371 local server = _G._create_server({ 4372 capabilities = { 4373 positionEncoding = 'utf-8', 4374 }, 4375 }) 4376 local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) 4377 local handler = require 'vim.lsp.handlers'['typeHierarchy/supertypes'] 4378 local bufnr = vim.api.nvim_get_current_buf() 4379 vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 4380 'class B : public A{};', 4381 'class C : public B{};', 4382 'class D1 : public C{};', 4383 'class D2 : public C{};', 4384 'class E : public D1, D2 {};', 4385 }) 4386 4387 handler(nil, clangd_response, { client_id = client_id, bufnr = bufnr }) 4388 return vim.fn.getqflist() 4389 end) 4390 4391 local expected = { 4392 { 4393 bufnr = 2, 4394 col = 7, 4395 end_col = 0, 4396 end_lnum = 0, 4397 lnum = 4, 4398 module = '', 4399 nr = 0, 4400 pattern = '', 4401 text = 'D2', 4402 type = '', 4403 valid = 1, 4404 vcol = 0, 4405 }, 4406 { 4407 bufnr = 2, 4408 col = 7, 4409 end_col = 0, 4410 end_lnum = 0, 4411 lnum = 3, 4412 module = '', 4413 nr = 0, 4414 pattern = '', 4415 text = 'D1', 4416 type = '', 4417 valid = 1, 4418 vcol = 0, 4419 }, 4420 } 4421 4422 eq(expected, qflist) 4423 end) 4424 4425 it('opens the quickfix list with the right supertypes and details', function() 4426 exec_lua(create_server_definition) 4427 local qflist = exec_lua(function() 4428 local jdtls_response = { 4429 { 4430 data = { element = '=hello-java_ed323c3c/_<{Main.java[Main[A' }, 4431 detail = '', 4432 kind = 5, 4433 name = 'A', 4434 range = { 4435 ['end'] = { character = 26, line = 3 }, 4436 start = { character = 1, line = 3 }, 4437 }, 4438 selectionRange = { 4439 ['end'] = { character = 8, line = 3 }, 4440 start = { character = 7, line = 3 }, 4441 }, 4442 tags = {}, 4443 uri = 'file:///home/jiangyinzuo/hello-java/Main.java', 4444 }, 4445 { 4446 data = { element = '=hello-java_ed323c3c/_<mylist{MyList.java[MyList[Inner' }, 4447 detail = 'mylist', 4448 kind = 5, 4449 name = 'MyList$Inner', 4450 range = { 4451 ['end'] = { character = 37, line = 3 }, 4452 start = { character = 1, line = 3 }, 4453 }, 4454 selectionRange = { 4455 ['end'] = { character = 19, line = 3 }, 4456 start = { character = 14, line = 3 }, 4457 }, 4458 tags = {}, 4459 uri = 'file:///home/jiangyinzuo/hello-java/mylist/MyList.java', 4460 }, 4461 } 4462 4463 local server = _G._create_server({ 4464 capabilities = { 4465 positionEncoding = 'utf-8', 4466 }, 4467 }) 4468 local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) 4469 local handler = require 'vim.lsp.handlers'['typeHierarchy/supertypes'] 4470 local bufnr = vim.api.nvim_get_current_buf() 4471 vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 4472 'package mylist;', 4473 '', 4474 'public class MyList {', 4475 ' static class Inner extends MyList{}', 4476 '~}', 4477 }) 4478 handler(nil, jdtls_response, { client_id = client_id, bufnr = bufnr }) 4479 return vim.fn.getqflist() 4480 end) 4481 4482 local expected = { 4483 { 4484 bufnr = 2, 4485 col = 2, 4486 end_col = 0, 4487 end_lnum = 0, 4488 lnum = 4, 4489 module = '', 4490 nr = 0, 4491 pattern = '', 4492 text = 'A', 4493 type = '', 4494 valid = 1, 4495 vcol = 0, 4496 }, 4497 { 4498 bufnr = 3, 4499 col = 2, 4500 end_col = 0, 4501 end_lnum = 0, 4502 lnum = 4, 4503 module = '', 4504 nr = 0, 4505 pattern = '', 4506 text = 'MyList$Inner mylist', 4507 type = '', 4508 valid = 1, 4509 vcol = 0, 4510 }, 4511 } 4512 eq(expected, qflist) 4513 end) 4514 end) 4515 4516 describe('vim.lsp.buf.rename', function() 4517 for _, test in ipairs({ 4518 { 4519 it = 'does not attempt to rename on nil response', 4520 name = 'prepare_rename_nil', 4521 expected_handlers = { 4522 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 4523 { NIL, {}, { method = 'start', client_id = 1 } }, 4524 }, 4525 }, 4526 { 4527 it = 'handles prepareRename placeholder response', 4528 name = 'prepare_rename_placeholder', 4529 expected_handlers = { 4530 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 4531 { {}, NIL, { method = 'textDocument/rename', client_id = 1, request_id = 3, bufnr = 1 } }, 4532 { NIL, {}, { method = 'start', client_id = 1 } }, 4533 }, 4534 expected_text = 'placeholder', -- see fake lsp response 4535 }, 4536 { 4537 it = 'handles range response', 4538 name = 'prepare_rename_range', 4539 expected_handlers = { 4540 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 4541 { {}, NIL, { method = 'textDocument/rename', client_id = 1, request_id = 3, bufnr = 1 } }, 4542 { NIL, {}, { method = 'start', client_id = 1 } }, 4543 }, 4544 expected_text = 'line', -- see test case and fake lsp response 4545 }, 4546 { 4547 it = 'handles error', 4548 name = 'prepare_rename_error', 4549 expected_handlers = { 4550 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 4551 { NIL, {}, { method = 'start', client_id = 1 } }, 4552 }, 4553 }, 4554 }) do 4555 it(test.it, function() 4556 local client --- @type vim.lsp.Client 4557 test_rpc_server { 4558 test_name = test.name, 4559 on_init = function(_client) 4560 client = _client 4561 eq(true, client.server_capabilities().renameProvider.prepareProvider) 4562 end, 4563 on_setup = function() 4564 exec_lua(function() 4565 local bufnr = vim.api.nvim_get_current_buf() 4566 vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID) 4567 vim.lsp._stubs = {} 4568 --- @diagnostic disable-next-line:duplicate-set-field 4569 vim.fn.input = function(opts, _) 4570 vim.lsp._stubs.input_prompt = opts.prompt 4571 vim.lsp._stubs.input_text = opts.default 4572 return 'renameto' -- expect this value in fake lsp 4573 end 4574 vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '', 'this is line two' }) 4575 vim.fn.cursor(2, 13) -- the space between "line" and "two" 4576 end) 4577 end, 4578 on_exit = function(code, signal) 4579 eq(0, code, 'exit code') 4580 eq(0, signal, 'exit signal') 4581 end, 4582 on_handler = function(err, result, ctx) 4583 -- Don't compare & assert params and version, they're not relevant for the testcase 4584 -- This allows us to be lazy and avoid declaring them 4585 ctx.params = nil 4586 ctx.version = nil 4587 4588 eq(table.remove(test.expected_handlers), { err, result, ctx }, 'expected handler') 4589 if ctx.method == 'start' then 4590 exec_lua(function() 4591 vim.lsp.buf.rename() 4592 end) 4593 end 4594 if ctx.method == 'shutdown' then 4595 if test.expected_text then 4596 eq( 4597 'New Name: ', 4598 exec_lua(function() 4599 return vim.lsp._stubs.input_prompt 4600 end) 4601 ) 4602 eq( 4603 test.expected_text, 4604 exec_lua(function() 4605 return vim.lsp._stubs.input_text 4606 end) 4607 ) 4608 end 4609 client:stop() 4610 end 4611 end, 4612 } 4613 end) 4614 end 4615 end) 4616 4617 describe('vim.lsp.buf.code_action', function() 4618 it('calls client side command if available', function() 4619 local client --- @type vim.lsp.Client 4620 local expected_handlers = { 4621 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 4622 { NIL, {}, { method = 'start', client_id = 1 } }, 4623 } 4624 test_rpc_server { 4625 test_name = 'code_action_with_resolve', 4626 on_init = function(client_) 4627 client = client_ 4628 end, 4629 on_setup = function() end, 4630 on_exit = function(code, signal) 4631 eq(0, code, 'exit code') 4632 eq(0, signal, 'exit signal') 4633 end, 4634 on_handler = function(err, result, ctx) 4635 eq(table.remove(expected_handlers), { err, result, ctx }) 4636 if ctx.method == 'start' then 4637 exec_lua(function() 4638 vim.lsp.commands['dummy1'] = function(_) 4639 vim.lsp.commands['dummy2'] = function() end 4640 end 4641 local bufnr = vim.api.nvim_get_current_buf() 4642 vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID) 4643 --- @diagnostic disable-next-line:duplicate-set-field 4644 vim.fn.inputlist = function() 4645 return 1 4646 end 4647 vim.lsp.buf.code_action() 4648 end) 4649 elseif ctx.method == 'shutdown' then 4650 eq( 4651 'function', 4652 exec_lua(function() 4653 return type(vim.lsp.commands['dummy2']) 4654 end) 4655 ) 4656 client:stop() 4657 end 4658 end, 4659 } 4660 end) 4661 4662 it('calls workspace/executeCommand if no client side command', function() 4663 local client --- @type vim.lsp.Client 4664 local expected_handlers = { 4665 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 4666 { 4667 NIL, 4668 { command = 'dummy1', title = 'Command 1' }, 4669 { bufnr = 1, method = 'workspace/executeCommand', request_id = 3, client_id = 1 }, 4670 }, 4671 { NIL, {}, { method = 'start', client_id = 1 } }, 4672 } 4673 test_rpc_server({ 4674 test_name = 'code_action_server_side_command', 4675 on_init = function(client_) 4676 client = client_ 4677 end, 4678 on_setup = function() end, 4679 on_exit = function(code, signal) 4680 eq(0, code, 'exit code', fake_lsp_logfile) 4681 eq(0, signal, 'exit signal', fake_lsp_logfile) 4682 end, 4683 on_handler = function(err, result, ctx) 4684 ctx.params = nil -- don't compare in assert 4685 ctx.version = nil 4686 eq(table.remove(expected_handlers), { err, result, ctx }) 4687 if ctx.method == 'start' then 4688 exec_lua(function() 4689 local bufnr = vim.api.nvim_get_current_buf() 4690 vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID) 4691 vim.fn.inputlist = function() 4692 return 1 4693 end 4694 vim.lsp.buf.code_action() 4695 end) 4696 elseif ctx.method == 'shutdown' then 4697 client:stop() 4698 end 4699 end, 4700 }) 4701 end) 4702 4703 it('filters and automatically applies action if requested', function() 4704 local client --- @type vim.lsp.Client 4705 local expected_handlers = { 4706 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 4707 { NIL, {}, { method = 'start', client_id = 1 } }, 4708 } 4709 test_rpc_server { 4710 test_name = 'code_action_filter', 4711 on_init = function(client_) 4712 client = client_ 4713 end, 4714 on_setup = function() end, 4715 on_exit = function(code, signal) 4716 eq(0, code, 'exit code') 4717 eq(0, signal, 'exit signal') 4718 end, 4719 on_handler = function(err, result, ctx) 4720 eq(table.remove(expected_handlers), { err, result, ctx }) 4721 if ctx.method == 'start' then 4722 exec_lua(function() 4723 vim.lsp.commands['preferred_command'] = function(_) 4724 vim.lsp.commands['executed_preferred'] = function() end 4725 end 4726 vim.lsp.commands['type_annotate_command'] = function(_) 4727 vim.lsp.commands['executed_type_annotate'] = function() end 4728 end 4729 local bufnr = vim.api.nvim_get_current_buf() 4730 vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID) 4731 vim.lsp.buf.code_action({ 4732 filter = function(a) 4733 return a.isPreferred 4734 end, 4735 apply = true, 4736 }) 4737 vim.lsp.buf.code_action({ 4738 -- expect to be returned actions 'type-annotate' and 'type-annotate.foo' 4739 context = { only = { 'type-annotate' } }, 4740 apply = true, 4741 filter = function(a) 4742 if a.kind == 'type-annotate.foo' then 4743 vim.lsp.commands['filtered_type_annotate_foo'] = function() end 4744 return false 4745 elseif a.kind == 'type-annotate' then 4746 return true 4747 else 4748 assert(nil, 'unreachable') 4749 end 4750 end, 4751 }) 4752 end) 4753 elseif ctx.method == 'shutdown' then 4754 eq( 4755 'function', 4756 exec_lua(function() 4757 return type(vim.lsp.commands['executed_preferred']) 4758 end) 4759 ) 4760 eq( 4761 'function', 4762 exec_lua(function() 4763 return type(vim.lsp.commands['filtered_type_annotate_foo']) 4764 end) 4765 ) 4766 eq( 4767 'function', 4768 exec_lua(function() 4769 return type(vim.lsp.commands['executed_type_annotate']) 4770 end) 4771 ) 4772 client:stop() 4773 end 4774 end, 4775 } 4776 end) 4777 4778 it('fallback to command execution on resolve error', function() 4779 exec_lua(create_server_definition) 4780 local result = exec_lua(function() 4781 local server = _G._create_server({ 4782 capabilities = { 4783 executeCommandProvider = { 4784 commands = { 'command:1' }, 4785 }, 4786 codeActionProvider = { 4787 resolveProvider = true, 4788 }, 4789 }, 4790 handlers = { 4791 ['textDocument/codeAction'] = function(_, _, callback) 4792 callback(nil, { 4793 { 4794 title = 'Code Action 1', 4795 command = { 4796 title = 'Command 1', 4797 command = 'command:1', 4798 }, 4799 }, 4800 }) 4801 end, 4802 ['codeAction/resolve'] = function(_, _, callback) 4803 callback('resolve failed', nil) 4804 end, 4805 }, 4806 }) 4807 4808 local client_id = assert(vim.lsp.start({ 4809 name = 'dummy', 4810 cmd = server.cmd, 4811 })) 4812 4813 vim.lsp.buf.code_action({ apply = true }) 4814 vim.lsp.get_client_by_id(client_id):stop() 4815 return server.messages 4816 end) 4817 eq('codeAction/resolve', result[4].method) 4818 eq('workspace/executeCommand', result[5].method) 4819 eq('command:1', result[5].params.command) 4820 end) 4821 4822 it('resolves command property', function() 4823 exec_lua(create_server_definition) 4824 local result = exec_lua(function() 4825 local server = _G._create_server({ 4826 capabilities = { 4827 executeCommandProvider = { 4828 commands = { 'command:1' }, 4829 }, 4830 codeActionProvider = { 4831 resolveProvider = true, 4832 }, 4833 }, 4834 handlers = { 4835 ['textDocument/codeAction'] = function(_, _, callback) 4836 callback(nil, { 4837 { title = 'Code Action 1' }, 4838 }) 4839 end, 4840 ['codeAction/resolve'] = function(_, _, callback) 4841 callback(nil, { 4842 title = 'Code Action 1', 4843 command = { 4844 title = 'Command 1', 4845 command = 'command:1', 4846 }, 4847 }) 4848 end, 4849 }, 4850 }) 4851 4852 local client_id = assert(vim.lsp.start({ 4853 name = 'dummy', 4854 cmd = server.cmd, 4855 })) 4856 4857 vim.lsp.buf.code_action({ apply = true }) 4858 vim.lsp.get_client_by_id(client_id):stop() 4859 return server.messages 4860 end) 4861 eq('codeAction/resolve', result[4].method) 4862 eq('workspace/executeCommand', result[5].method) 4863 eq('command:1', result[5].params.command) 4864 end) 4865 4866 it('supports disabled actions', function() 4867 exec_lua(create_server_definition) 4868 local result = exec_lua(function() 4869 local server = _G._create_server({ 4870 capabilities = { 4871 executeCommandProvider = { 4872 commands = { 'command:1' }, 4873 }, 4874 codeActionProvider = { 4875 resolveProvider = true, 4876 }, 4877 }, 4878 handlers = { 4879 ['textDocument/codeAction'] = function(_, _, callback) 4880 callback(nil, { 4881 { 4882 title = 'Code Action 1', 4883 disabled = { 4884 reason = 'This action is disabled', 4885 }, 4886 }, 4887 }) 4888 end, 4889 ['codeAction/resolve'] = function(_, _, callback) 4890 callback(nil, { 4891 title = 'Code Action 1', 4892 command = { 4893 title = 'Command 1', 4894 command = 'command:1', 4895 }, 4896 }) 4897 end, 4898 }, 4899 }) 4900 4901 local client_id = assert(vim.lsp.start({ 4902 name = 'dummy', 4903 cmd = server.cmd, 4904 })) 4905 4906 --- @diagnostic disable-next-line:duplicate-set-field 4907 vim.notify = function(message, code) 4908 server.messages[#server.messages + 1] = { 4909 params = { 4910 message = message, 4911 code = code, 4912 }, 4913 } 4914 end 4915 4916 vim.lsp.buf.code_action({ apply = true }) 4917 vim.lsp.get_client_by_id(client_id):stop() 4918 return server.messages 4919 end) 4920 eq( 4921 exec_lua(function() 4922 return { message = 'This action is disabled', code = vim.log.levels.ERROR } 4923 end), 4924 result[4].params 4925 ) 4926 -- No command is resolved/applied after selecting a disabled code action 4927 eq('shutdown', result[5].method) 4928 end) 4929 end) 4930 4931 describe('vim.lsp.commands', function() 4932 it('accepts only string keys', function() 4933 matches( 4934 '.*The key for commands in `vim.lsp.commands` must be a string', 4935 pcall_err(exec_lua, 'vim.lsp.commands[1] = function() end') 4936 ) 4937 end) 4938 4939 it('accepts only function values', function() 4940 matches( 4941 '.*Command added to `vim.lsp.commands` must be a function', 4942 pcall_err(exec_lua, 'vim.lsp.commands.dummy = 10') 4943 ) 4944 end) 4945 end) 4946 4947 describe('vim.lsp.buf.format', function() 4948 it('aborts with notify if no client matches filter', function() 4949 local client --- @type vim.lsp.Client 4950 test_rpc_server { 4951 test_name = 'basic_init', 4952 on_init = function(c) 4953 client = c 4954 end, 4955 on_handler = function() 4956 local notify_msg = exec_lua(function() 4957 local bufnr = vim.api.nvim_get_current_buf() 4958 vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID) 4959 local notify_msg --- @type string? 4960 local notify = vim.notify 4961 vim.notify = function(msg, _) 4962 notify_msg = msg 4963 end 4964 vim.lsp.buf.format({ name = 'does-not-exist' }) 4965 vim.notify = notify 4966 return notify_msg 4967 end) 4968 eq('[LSP] Format request failed, no matching language servers.', notify_msg) 4969 client:stop() 4970 end, 4971 } 4972 end) 4973 4974 it('sends textDocument/formatting request to format buffer', function() 4975 local expected_handlers = { 4976 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 4977 { NIL, {}, { method = 'start', client_id = 1 } }, 4978 } 4979 local client --- @type vim.lsp.Client 4980 test_rpc_server { 4981 test_name = 'basic_formatting', 4982 on_init = function(c) 4983 client = c 4984 end, 4985 on_handler = function(_, _, ctx) 4986 table.remove(expected_handlers) 4987 if ctx.method == 'start' then 4988 local notify_msg = exec_lua(function() 4989 local bufnr = vim.api.nvim_get_current_buf() 4990 vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID) 4991 local notify_msg --- @type string? 4992 local notify = vim.notify 4993 vim.notify = function(msg, _) 4994 notify_msg = msg 4995 end 4996 vim.lsp.buf.format({ bufnr = bufnr }) 4997 vim.notify = notify 4998 return notify_msg 4999 end) 5000 eq(nil, notify_msg) 5001 elseif ctx.method == 'shutdown' then 5002 client:stop() 5003 end 5004 end, 5005 } 5006 end) 5007 5008 it('sends textDocument/rangeFormatting request to format a range', function() 5009 local expected_handlers = { 5010 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 5011 { NIL, {}, { method = 'start', client_id = 1 } }, 5012 } 5013 local client --- @type vim.lsp.Client 5014 test_rpc_server { 5015 test_name = 'range_formatting', 5016 on_init = function(c) 5017 client = c 5018 end, 5019 on_handler = function(_, _, ctx) 5020 table.remove(expected_handlers) 5021 if ctx.method == 'start' then 5022 local notify_msg = exec_lua(function() 5023 local bufnr = vim.api.nvim_get_current_buf() 5024 vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { 'foo', 'bar' }) 5025 vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID) 5026 local notify_msg --- @type string? 5027 local notify = vim.notify 5028 vim.notify = function(msg, _) 5029 notify_msg = msg 5030 end 5031 vim.lsp.buf.format({ 5032 bufnr = bufnr, 5033 range = { 5034 start = { 1, 1 }, 5035 ['end'] = { 1, 1 }, 5036 }, 5037 }) 5038 vim.notify = notify 5039 return notify_msg 5040 end) 5041 eq(nil, notify_msg) 5042 elseif ctx.method == 'shutdown' then 5043 client:stop() 5044 end 5045 end, 5046 } 5047 end) 5048 5049 it('sends textDocument/rangesFormatting request to format multiple ranges', function() 5050 local expected_handlers = { 5051 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 5052 { NIL, {}, { method = 'start', client_id = 1 } }, 5053 } 5054 local client --- @type vim.lsp.Client 5055 test_rpc_server { 5056 test_name = 'ranges_formatting', 5057 on_init = function(c) 5058 client = c 5059 end, 5060 on_handler = function(_, _, ctx) 5061 table.remove(expected_handlers) 5062 if ctx.method == 'start' then 5063 local notify_msg = exec_lua(function() 5064 local bufnr = vim.api.nvim_get_current_buf() 5065 vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { 'foo', 'bar', 'baz' }) 5066 vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID) 5067 local notify_msg --- @type string? 5068 local notify = vim.notify 5069 vim.notify = function(msg, _) 5070 notify_msg = msg 5071 end 5072 vim.lsp.buf.format({ 5073 bufnr = bufnr, 5074 range = { 5075 { 5076 start = { 1, 1 }, 5077 ['end'] = { 1, 1 }, 5078 }, 5079 { 5080 start = { 2, 2 }, 5081 ['end'] = { 2, 2 }, 5082 }, 5083 }, 5084 }) 5085 vim.notify = notify 5086 return notify_msg 5087 end) 5088 eq(nil, notify_msg) 5089 elseif ctx.method == 'shutdown' then 5090 client:stop() 5091 end 5092 end, 5093 } 5094 end) 5095 5096 it('can format async', function() 5097 local expected_handlers = { 5098 { NIL, {}, { method = 'shutdown', client_id = 1 } }, 5099 { NIL, {}, { method = 'start', client_id = 1 } }, 5100 } 5101 local client --- @type vim.lsp.Client 5102 test_rpc_server { 5103 test_name = 'basic_formatting', 5104 on_init = function(c) 5105 client = c 5106 end, 5107 on_handler = function(_, _, ctx) 5108 table.remove(expected_handlers) 5109 if ctx.method == 'start' then 5110 local result = exec_lua(function() 5111 local bufnr = vim.api.nvim_get_current_buf() 5112 vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID) 5113 5114 local notify_msg --- @type string? 5115 local notify = vim.notify 5116 vim.notify = function(msg, _) 5117 notify_msg = msg 5118 end 5119 5120 _G.handler_called = false 5121 vim.lsp.buf.format({ bufnr = bufnr, async = true }) 5122 vim.wait(1000, function() 5123 return _G.handler_called 5124 end) 5125 5126 vim.notify = notify 5127 return { notify_msg = notify_msg, handler_called = _G.handler_called } 5128 end) 5129 eq({ handler_called = true }, result) 5130 elseif ctx.method == 'textDocument/formatting' then 5131 exec_lua('_G.handler_called = true') 5132 elseif ctx.method == 'shutdown' then 5133 client:stop() 5134 end 5135 end, 5136 } 5137 end) 5138 5139 it('format formats range in visual mode', function() 5140 exec_lua(create_server_definition) 5141 local result = exec_lua(function() 5142 local server = _G._create_server({ 5143 capabilities = { 5144 documentFormattingProvider = true, 5145 documentRangeFormattingProvider = true, 5146 }, 5147 }) 5148 local bufnr = vim.api.nvim_get_current_buf() 5149 local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = server.cmd })) 5150 vim.api.nvim_win_set_buf(0, bufnr) 5151 vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { 'foo', 'bar' }) 5152 vim.api.nvim_win_set_cursor(0, { 1, 0 }) 5153 vim.cmd.normal('v') 5154 vim.api.nvim_win_set_cursor(0, { 2, 3 }) 5155 vim.lsp.buf.format({ bufnr = bufnr, false }) 5156 vim.lsp.get_client_by_id(client_id):stop() 5157 return server.messages 5158 end) 5159 eq('textDocument/rangeFormatting', result[3].method) 5160 local expected_range = { 5161 start = { line = 0, character = 0 }, 5162 ['end'] = { line = 1, character = 4 }, 5163 } 5164 eq(expected_range, result[3].params.range) 5165 end) 5166 5167 it('format formats range in visual line mode', function() 5168 exec_lua(create_server_definition) 5169 local result = exec_lua(function() 5170 local server = _G._create_server({ 5171 capabilities = { 5172 documentFormattingProvider = true, 5173 documentRangeFormattingProvider = true, 5174 }, 5175 }) 5176 local bufnr = vim.api.nvim_get_current_buf() 5177 local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = server.cmd })) 5178 vim.api.nvim_win_set_buf(0, bufnr) 5179 vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { 'foo', 'bar baz' }) 5180 vim.api.nvim_win_set_cursor(0, { 1, 2 }) 5181 vim.cmd.normal('V') 5182 vim.api.nvim_win_set_cursor(0, { 2, 1 }) 5183 vim.lsp.buf.format({ bufnr = bufnr, false }) 5184 5185 -- Format again with visual lines going from bottom to top 5186 -- Must result in same formatting 5187 vim.cmd.normal('<ESC>') 5188 vim.api.nvim_win_set_cursor(0, { 2, 1 }) 5189 vim.cmd.normal('V') 5190 vim.api.nvim_win_set_cursor(0, { 1, 2 }) 5191 vim.lsp.buf.format({ bufnr = bufnr, false }) 5192 5193 vim.lsp.get_client_by_id(client_id):stop() 5194 return server.messages 5195 end) 5196 local expected_methods = { 5197 'initialize', 5198 'initialized', 5199 'textDocument/rangeFormatting', 5200 '$/cancelRequest', 5201 'textDocument/rangeFormatting', 5202 '$/cancelRequest', 5203 'shutdown', 5204 'exit', 5205 } 5206 eq( 5207 expected_methods, 5208 vim.tbl_map(function(x) 5209 return x.method 5210 end, result) 5211 ) 5212 -- uses first column of start line and last column of end line 5213 local expected_range = { 5214 start = { line = 0, character = 0 }, 5215 ['end'] = { line = 1, character = 7 }, 5216 } 5217 eq(expected_range, result[3].params.range) 5218 eq(expected_range, result[5].params.range) 5219 end) 5220 5221 it('aborts with notify if no clients support requested method', function() 5222 exec_lua(create_server_definition) 5223 exec_lua(function() 5224 vim.notify = function(msg, _) 5225 _G.notify_msg = msg 5226 end 5227 end) 5228 local fail_msg = '[LSP] Format request failed, no matching language servers.' 5229 --- @param name string 5230 --- @param formatting boolean 5231 --- @param range_formatting boolean 5232 local function check_notify(name, formatting, range_formatting) 5233 local timeout_msg = '[LSP][' .. name .. '] timeout' 5234 exec_lua(function() 5235 local server = _G._create_server({ 5236 capabilities = { 5237 documentFormattingProvider = formatting, 5238 documentRangeFormattingProvider = range_formatting, 5239 }, 5240 }) 5241 vim.lsp.start({ name = name, cmd = server.cmd }) 5242 _G.notify_msg = nil 5243 vim.lsp.buf.format({ name = name, timeout_ms = 1 }) 5244 end) 5245 eq( 5246 formatting and timeout_msg or fail_msg, 5247 exec_lua(function() 5248 return _G.notify_msg 5249 end) 5250 ) 5251 exec_lua(function() 5252 _G.notify_msg = nil 5253 vim.lsp.buf.format({ 5254 name = name, 5255 timeout_ms = 1, 5256 range = { 5257 start = { 1, 0 }, 5258 ['end'] = { 5259 1, 5260 0, 5261 }, 5262 }, 5263 }) 5264 end) 5265 eq( 5266 range_formatting and timeout_msg or fail_msg, 5267 exec_lua(function() 5268 return _G.notify_msg 5269 end) 5270 ) 5271 end 5272 check_notify('none', false, false) 5273 check_notify('formatting', true, false) 5274 check_notify('rangeFormatting', false, true) 5275 check_notify('both', true, true) 5276 end) 5277 end) 5278 5279 describe('lsp.buf.definition', function() 5280 it('jumps to single location and can reuse win', function() 5281 exec_lua(create_server_definition) 5282 local result = exec_lua(function() 5283 local bufnr = vim.api.nvim_get_current_buf() 5284 local server = _G._create_server({ 5285 capabilities = { 5286 definitionProvider = true, 5287 }, 5288 handlers = { 5289 ['textDocument/definition'] = function(_, _, callback) 5290 local location = { 5291 range = { 5292 start = { line = 0, character = 0 }, 5293 ['end'] = { line = 0, character = 0 }, 5294 }, 5295 uri = vim.uri_from_bufnr(bufnr), 5296 } 5297 callback(nil, location) 5298 end, 5299 }, 5300 }) 5301 local win = vim.api.nvim_get_current_win() 5302 vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { 'local x = 10', '', 'print(x)' }) 5303 vim.api.nvim_win_set_cursor(win, { 3, 6 }) 5304 local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = server.cmd })) 5305 vim.lsp.buf.definition() 5306 return { 5307 win = win, 5308 bufnr = bufnr, 5309 client_id = client_id, 5310 cursor = vim.api.nvim_win_get_cursor(win), 5311 messages = server.messages, 5312 tagstack = vim.fn.gettagstack(win), 5313 } 5314 end) 5315 eq('textDocument/definition', result.messages[3].method) 5316 eq({ 1, 0 }, result.cursor) 5317 eq(1, #result.tagstack.items) 5318 eq('x', result.tagstack.items[1].tagname) 5319 eq(3, result.tagstack.items[1].from[2]) 5320 eq(7, result.tagstack.items[1].from[3]) 5321 5322 local result_bufnr = api.nvim_get_current_buf() 5323 n.feed(':tabe<CR>') 5324 api.nvim_win_set_buf(0, result_bufnr) 5325 local displayed_result_win = api.nvim_get_current_win() 5326 n.feed(':vnew<CR>') 5327 api.nvim_win_set_buf(0, result.bufnr) 5328 api.nvim_win_set_cursor(0, { 3, 6 }) 5329 n.feed(':=vim.lsp.buf.definition({ reuse_win = true })<CR>') 5330 eq(displayed_result_win, api.nvim_get_current_win()) 5331 exec_lua(function() 5332 vim.lsp.get_client_by_id(result.client_id):stop() 5333 end) 5334 end) 5335 it('merges results from multiple servers', function() 5336 exec_lua(create_server_definition) 5337 local result = exec_lua(function() 5338 local bufnr = vim.api.nvim_get_current_buf() 5339 local function serveropts(character) 5340 return { 5341 capabilities = { 5342 definitionProvider = true, 5343 }, 5344 handlers = { 5345 ['textDocument/definition'] = function(_, _, callback) 5346 local location = { 5347 range = { 5348 start = { line = 0, character = character }, 5349 ['end'] = { line = 0, character = character }, 5350 }, 5351 uri = vim.uri_from_bufnr(bufnr), 5352 } 5353 callback(nil, location) 5354 end, 5355 }, 5356 } 5357 end 5358 local server1 = _G._create_server(serveropts(0)) 5359 local server2 = _G._create_server(serveropts(7)) 5360 local win = vim.api.nvim_get_current_win() 5361 vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { 'local x = 10', '', 'print(x)' }) 5362 vim.api.nvim_win_set_cursor(win, { 3, 6 }) 5363 local client_id1 = assert(vim.lsp.start({ name = 'dummy1', cmd = server1.cmd })) 5364 local client_id2 = assert(vim.lsp.start({ name = 'dummy2', cmd = server2.cmd })) 5365 local response 5366 vim.lsp.buf.definition({ 5367 on_list = function(r) 5368 response = r 5369 end, 5370 }) 5371 vim.lsp.get_client_by_id(client_id1):stop() 5372 vim.lsp.get_client_by_id(client_id2):stop() 5373 return response 5374 end) 5375 eq(2, #result.items) 5376 end) 5377 end) 5378 5379 describe('vim.lsp.tagfunc', function() 5380 before_each(function() 5381 ---@type lsp.Location[] 5382 local mock_locations = { 5383 { 5384 range = { 5385 ['start'] = { line = 5, character = 23 }, 5386 ['end'] = { line = 10, character = 0 }, 5387 }, 5388 uri = 'test://buf', 5389 }, 5390 { 5391 range = { 5392 ['start'] = { line = 42, character = 10 }, 5393 ['end'] = { line = 44, character = 0 }, 5394 }, 5395 uri = 'test://another-file', 5396 }, 5397 } 5398 exec_lua(create_server_definition) 5399 exec_lua(function() 5400 _G.mock_locations = mock_locations 5401 _G.server = _G._create_server({ 5402 ---@type lsp.ServerCapabilities 5403 capabilities = { 5404 definitionProvider = true, 5405 workspaceSymbolProvider = true, 5406 }, 5407 handlers = { 5408 ---@return lsp.Location[] 5409 ['textDocument/definition'] = function(_, _, callback) 5410 callback(nil, { _G.mock_locations[1] }) 5411 end, 5412 ---@return lsp.WorkspaceSymbol[] 5413 ['workspace/symbol'] = function(_, request, callback) 5414 assert(request.query == 'foobar') 5415 callback(nil, { 5416 { 5417 name = 'foobar', 5418 kind = 13, ---@type lsp.SymbolKind 5419 location = _G.mock_locations[1], 5420 }, 5421 { 5422 name = 'vim.foobar', 5423 kind = 12, ---@type lsp.SymbolKind 5424 location = _G.mock_locations[2], 5425 }, 5426 }) 5427 end, 5428 }, 5429 }) 5430 _G.client_id = vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) 5431 end) 5432 end) 5433 5434 after_each(function() 5435 exec_lua(function() 5436 vim.lsp.get_client_by_id(_G.client_id):stop() 5437 end) 5438 end) 5439 5440 it('with flags=c, returns matching tags using textDocument/definition', function() 5441 local result = exec_lua(function() 5442 return vim.lsp.tagfunc('foobar', 'c') 5443 end) 5444 eq({ 5445 { 5446 cmd = '/\\%6l\\%1c/', -- for location (5, 23) 5447 filename = 'test://buf', 5448 name = 'foobar', 5449 }, 5450 }, result) 5451 end) 5452 5453 it('without flags=c, returns all matching tags using workspace/symbol', function() 5454 local result = exec_lua(function() 5455 return vim.lsp.tagfunc('foobar', '') 5456 end) 5457 eq({ 5458 { 5459 cmd = '/\\%6l\\%1c/', -- for location (5, 23) 5460 filename = 'test://buf', 5461 kind = 'Variable', 5462 name = 'foobar', 5463 }, 5464 { 5465 cmd = '/\\%43l\\%1c/', -- for location (42, 10) 5466 filename = 'test://another-file', 5467 kind = 'Function', 5468 name = 'vim.foobar', 5469 }, 5470 }, result) 5471 end) 5472 5473 it('with flags including i, returns NIL', function() 5474 exec_lua(function() 5475 local result = vim.lsp.tagfunc('foobar', 'cir') 5476 assert(result == vim.NIL, 'should not issue LSP requests') 5477 return {} 5478 end) 5479 end) 5480 end) 5481 5482 describe('cmd', function() 5483 it('connects to lsp server via rpc.connect using ip address', function() 5484 exec_lua(create_tcp_echo_server) 5485 exec_lua(function() 5486 local port = _G._create_tcp_server('127.0.0.1') 5487 vim.lsp.start({ name = 'dummy', cmd = vim.lsp.rpc.connect('127.0.0.1', port) }) 5488 end) 5489 verify_single_notification(function(method, args) ---@param args [string] 5490 eq('body', method) 5491 eq('initialize', vim.json.decode(args[1]).method) 5492 end) 5493 end) 5494 5495 it('connects to lsp server via rpc.connect using hostname', function() 5496 skip(is_os('bsd'), 'issue with host resolution in ci') 5497 exec_lua(create_tcp_echo_server) 5498 exec_lua(function() 5499 local port = _G._create_tcp_server('::1') 5500 vim.lsp.start({ name = 'dummy', cmd = vim.lsp.rpc.connect('localhost', port) }) 5501 end) 5502 verify_single_notification(function(method, args) ---@param args [string] 5503 eq('body', method) 5504 eq('initialize', vim.json.decode(args[1]).method) 5505 end) 5506 end) 5507 5508 it('can connect to lsp server via pipe or domain_socket', function() 5509 local tmpfile = is_os('win') and '\\\\.\\\\pipe\\pipe.test' or tmpname(false) 5510 local result = exec_lua(function() 5511 local uv = vim.uv 5512 local server = assert(uv.new_pipe(false)) 5513 server:bind(tmpfile) 5514 local init = nil 5515 5516 server:listen(127, function(err) 5517 assert(not err, err) 5518 local client = assert(vim.uv.new_pipe()) 5519 server:accept(client) 5520 client:read_start(require('vim.lsp.rpc').create_read_loop(function(body) 5521 init = body 5522 client:close() 5523 end)) 5524 end) 5525 vim.lsp.start({ name = 'dummy', cmd = vim.lsp.rpc.connect(tmpfile) }) 5526 vim.wait(1000, function() 5527 return init ~= nil 5528 end) 5529 assert(init, 'server must receive `initialize` request') 5530 server:close() 5531 server:shutdown() 5532 return vim.json.decode(init) 5533 end) 5534 eq('initialize', result.method) 5535 end) 5536 end) 5537 5538 describe('handlers', function() 5539 it('handler can return false as response', function() 5540 local result = exec_lua(function() 5541 local server = assert(vim.uv.new_tcp()) 5542 local messages = {} 5543 local responses = {} 5544 server:bind('127.0.0.1', 0) 5545 server:listen(127, function(err) 5546 assert(not err, err) 5547 local socket = assert(vim.uv.new_tcp()) 5548 server:accept(socket) 5549 socket:read_start(require('vim.lsp.rpc').create_read_loop(function(body) 5550 local payload = vim.json.decode(body) 5551 if payload.method then 5552 table.insert(messages, payload.method) 5553 if payload.method == 'initialize' then 5554 local msg = vim.json.encode({ 5555 id = payload.id, 5556 jsonrpc = '2.0', 5557 result = { 5558 capabilities = {}, 5559 }, 5560 }) 5561 socket:write(table.concat({ 'Content-Length: ', tostring(#msg), '\r\n\r\n', msg })) 5562 elseif payload.method == 'initialized' then 5563 local msg = vim.json.encode({ 5564 id = 10, 5565 jsonrpc = '2.0', 5566 method = 'dummy', 5567 params = {}, 5568 }) 5569 socket:write(table.concat({ 'Content-Length: ', tostring(#msg), '\r\n\r\n', msg })) 5570 end 5571 else 5572 table.insert(responses, payload) 5573 socket:close() 5574 end 5575 end)) 5576 end) 5577 local port = server:getsockname().port 5578 local handler_called = false 5579 vim.lsp.handlers['dummy'] = function(_, _) 5580 handler_called = true 5581 return false 5582 end 5583 local client_id = 5584 assert(vim.lsp.start({ name = 'dummy', cmd = vim.lsp.rpc.connect('127.0.0.1', port) })) 5585 vim.lsp.get_client_by_id(client_id) 5586 vim.wait(1000, function() 5587 return #messages == 2 and handler_called and #responses == 1 5588 end) 5589 server:close() 5590 server:shutdown() 5591 return { 5592 messages = messages, 5593 handler_called = handler_called, 5594 responses = responses, 5595 } 5596 end) 5597 local expected = { 5598 messages = { 'initialize', 'initialized' }, 5599 handler_called = true, 5600 responses = { 5601 { 5602 id = 10, 5603 jsonrpc = '2.0', 5604 result = false, 5605 }, 5606 }, 5607 } 5608 eq(expected, result) 5609 end) 5610 end) 5611 5612 describe('#dynamic vim.lsp._dynamic', function() 5613 it('supports dynamic registration', function() 5614 local root_dir = tmpname(false) 5615 mkdir(root_dir) 5616 local tmpfile = root_dir .. '/dynamic.foo' 5617 local file = io.open(tmpfile, 'w') 5618 if file then 5619 file:close() 5620 end 5621 5622 exec_lua(create_server_definition) 5623 local result = exec_lua(function() 5624 local server = _G._create_server() 5625 local client_id = assert(vim.lsp.start({ 5626 name = 'dynamic-test', 5627 cmd = server.cmd, 5628 root_dir = root_dir, 5629 get_language_id = function() 5630 return 'dummy-lang' 5631 end, 5632 capabilities = { 5633 textDocument = { 5634 formatting = { 5635 dynamicRegistration = true, 5636 }, 5637 rangeFormatting = { 5638 dynamicRegistration = true, 5639 }, 5640 }, 5641 workspace = { 5642 didChangeWatchedFiles = { 5643 dynamicRegistration = true, 5644 }, 5645 didChangeConfiguration = { 5646 dynamicRegistration = true, 5647 }, 5648 }, 5649 }, 5650 })) 5651 5652 vim.lsp.handlers['client/registerCapability'](nil, { 5653 registrations = { 5654 { 5655 id = 'formatting', 5656 method = 'textDocument/formatting', 5657 registerOptions = { 5658 documentSelector = { 5659 { 5660 pattern = root_dir .. '/*.foo', 5661 }, 5662 }, 5663 }, 5664 }, 5665 }, 5666 }, { client_id = client_id }) 5667 5668 vim.lsp.handlers['client/registerCapability'](nil, { 5669 registrations = { 5670 { 5671 id = 'range-formatting', 5672 method = 'textDocument/rangeFormatting', 5673 registerOptions = { 5674 documentSelector = { 5675 { 5676 language = 'dummy-lang', 5677 }, 5678 }, 5679 }, 5680 }, 5681 }, 5682 }, { client_id = client_id }) 5683 5684 vim.lsp.handlers['client/registerCapability'](nil, { 5685 registrations = { 5686 { 5687 id = 'completion', 5688 method = 'textDocument/completion', 5689 }, 5690 }, 5691 }, { client_id = client_id }) 5692 5693 local result = {} 5694 local function check(method, fname, ...) 5695 local bufnr = fname and vim.fn.bufadd(fname) or nil 5696 local client = assert(vim.lsp.get_client_by_id(client_id)) 5697 result[#result + 1] = { 5698 method = method, 5699 fname = fname, 5700 supported = client:supports_method(method, bufnr), 5701 cap = select('#', ...) > 0 and client:_provider_value_get(method, ...) or nil, 5702 } 5703 end 5704 5705 check('textDocument/formatting') 5706 check('textDocument/formatting', tmpfile) 5707 check('textDocument/rangeFormatting') 5708 check('textDocument/rangeFormatting', tmpfile) 5709 check('textDocument/completion') 5710 5711 check('workspace/didChangeWatchedFiles') 5712 check('workspace/didChangeWatchedFiles', tmpfile) 5713 5714 vim.lsp.handlers['client/registerCapability'](nil, { 5715 registrations = { 5716 { 5717 id = 'didChangeWatched', 5718 method = 'workspace/didChangeWatchedFiles', 5719 registerOptions = { 5720 watchers = { 5721 { 5722 globPattern = 'something', 5723 kind = 4, 5724 }, 5725 }, 5726 }, 5727 }, 5728 }, 5729 }, { client_id = client_id }) 5730 5731 check('workspace/didChangeWatchedFiles') 5732 check('workspace/didChangeWatchedFiles', tmpfile) 5733 5734 -- Initial support false 5735 check('workspace/diagnostic') 5736 5737 vim.lsp.handlers['client/registerCapability'](nil, { 5738 registrations = { 5739 { 5740 id = 'diag1', 5741 method = 'textDocument/diagnostic', 5742 registerOptions = { 5743 identifier = 'diag-ident-1', 5744 -- workspaceDiagnostics field omitted 5745 }, 5746 }, 5747 }, 5748 }, { client_id = client_id }) 5749 5750 -- Checks after registering without workspaceDiagnostics support 5751 -- Returns false 5752 check('workspace/diagnostic', nil, 'identifier') 5753 5754 vim.lsp.handlers['client/registerCapability'](nil, { 5755 registrations = { 5756 { 5757 id = 'diag2', 5758 method = 'textDocument/diagnostic', 5759 registerOptions = { 5760 identifier = 'diag-ident-2', 5761 workspaceDiagnostics = true, 5762 }, 5763 }, 5764 }, 5765 }, { client_id = client_id }) 5766 5767 -- Check after second registration with support 5768 -- Returns true 5769 check('workspace/diagnostic', nil, 'identifier') 5770 5771 vim.lsp.handlers['client/unregisterCapability'](nil, { 5772 unregisterations = { 5773 { id = 'diag2', method = 'textDocument/diagnostic' }, 5774 }, 5775 }, { client_id = client_id }) 5776 5777 -- Check after unregistering 5778 -- Returns false 5779 check('workspace/diagnostic', nil, 'identifier') 5780 5781 check('textDocument/codeAction') 5782 check('codeAction/resolve') 5783 5784 vim.lsp.handlers['client/registerCapability'](nil, { 5785 registrations = { 5786 { 5787 id = 'codeAction', 5788 method = 'textDocument/codeAction', 5789 registerOptions = { 5790 resolveProvider = true, 5791 }, 5792 }, 5793 }, 5794 }, { client_id = client_id }) 5795 5796 check('textDocument/codeAction') 5797 check('codeAction/resolve') 5798 5799 check('workspace/didChangeWorkspaceFolders') 5800 vim.lsp.handlers['client/registerCapability'](nil, { 5801 registrations = { 5802 { 5803 id = 'didChangeWorkspaceFolders-id', 5804 method = 'workspace/didChangeWorkspaceFolders', 5805 }, 5806 }, 5807 }, { client_id = client_id }) 5808 check('workspace/didChangeWorkspaceFolders') 5809 5810 check('workspace/didChangeConfiguration') 5811 vim.lsp.handlers['client/registerCapability'](nil, { 5812 registrations = { 5813 { 5814 id = 'didChangeConfiguration-id', 5815 method = 'workspace/didChangeConfiguration', 5816 registerOptions = { 5817 section = 'dummy-section', 5818 }, 5819 }, 5820 }, 5821 }, { client_id = client_id }) 5822 check('workspace/didChangeConfiguration', nil, 'section') 5823 5824 return result 5825 end) 5826 5827 eq(21, #result) 5828 eq({ method = 'textDocument/formatting', supported = false }, result[1]) 5829 eq({ method = 'textDocument/formatting', supported = true, fname = tmpfile }, result[2]) 5830 eq({ method = 'textDocument/rangeFormatting', supported = true }, result[3]) 5831 eq({ method = 'textDocument/rangeFormatting', supported = true, fname = tmpfile }, result[4]) 5832 eq({ method = 'textDocument/completion', supported = false }, result[5]) 5833 eq({ method = 'workspace/didChangeWatchedFiles', supported = false }, result[6]) 5834 eq( 5835 { method = 'workspace/didChangeWatchedFiles', supported = false, fname = tmpfile }, 5836 result[7] 5837 ) 5838 eq({ method = 'workspace/didChangeWatchedFiles', supported = true }, result[8]) 5839 eq( 5840 { method = 'workspace/didChangeWatchedFiles', supported = true, fname = tmpfile }, 5841 result[9] 5842 ) 5843 eq({ method = 'workspace/diagnostic', supported = false }, result[10]) 5844 eq({ method = 'workspace/diagnostic', supported = false, cap = {} }, result[11]) 5845 eq({ 5846 method = 'workspace/diagnostic', 5847 supported = true, 5848 cap = { 'diag-ident-2' }, 5849 }, result[12]) 5850 eq({ method = 'workspace/diagnostic', supported = false, cap = {} }, result[13]) 5851 eq({ method = 'textDocument/codeAction', supported = false }, result[14]) 5852 eq({ method = 'codeAction/resolve', supported = false }, result[15]) 5853 eq({ method = 'textDocument/codeAction', supported = true }, result[16]) 5854 eq({ method = 'codeAction/resolve', supported = true }, result[17]) 5855 eq({ method = 'workspace/didChangeWorkspaceFolders', supported = false }, result[18]) 5856 eq({ method = 'workspace/didChangeWorkspaceFolders', supported = true }, result[19]) 5857 eq({ method = 'workspace/didChangeConfiguration', supported = false }, result[20]) 5858 eq( 5859 { method = 'workspace/didChangeConfiguration', supported = true, cap = { 'dummy-section' } }, 5860 result[21] 5861 ) 5862 end) 5863 5864 it('identifies client dynamic registration capability', function() 5865 exec_lua(create_server_definition) 5866 local result = exec_lua(function() 5867 local server = _G._create_server() 5868 local client_id = assert(vim.lsp.start({ 5869 name = 'dynamic-test', 5870 cmd = server.cmd, 5871 capabilities = { 5872 textDocument = { 5873 formatting = { 5874 dynamicRegistration = true, 5875 }, 5876 synchronization = { 5877 dynamicRegistration = true, 5878 }, 5879 diagnostic = { 5880 dynamicRegistration = true, 5881 }, 5882 }, 5883 }, 5884 })) 5885 5886 local result = {} 5887 local function check(method) 5888 local client = assert(vim.lsp.get_client_by_id(client_id)) 5889 result[#result + 1] = { 5890 method = method, 5891 supports_reg = client:_supports_registration(method), 5892 } 5893 end 5894 5895 check('textDocument/formatting') 5896 check('textDocument/didSave') 5897 check('textDocument/didOpen') 5898 check('textDocument/codeLens') 5899 check('textDocument/diagnostic') 5900 check('workspace/diagnostic') 5901 5902 return result 5903 end) 5904 5905 eq(6, #result) 5906 eq({ method = 'textDocument/formatting', supports_reg = true }, result[1]) 5907 eq({ method = 'textDocument/didSave', supports_reg = true }, result[2]) 5908 eq({ method = 'textDocument/didOpen', supports_reg = true }, result[3]) 5909 eq({ method = 'textDocument/codeLens', supports_reg = false }, result[4]) 5910 eq({ method = 'textDocument/diagnostic', supports_reg = true }, result[5]) 5911 eq({ method = 'workspace/diagnostic', supports_reg = true }, result[6]) 5912 end) 5913 5914 it('supports static registration', function() 5915 exec_lua(create_server_definition) 5916 5917 local client_id = exec_lua(function() 5918 local server = _G._create_server({ 5919 capabilities = { 5920 colorProvider = { id = 'color-registration' }, 5921 diagnosticProvider = { 5922 id = 'diag-registration', 5923 identifier = 'diag-ident-static', 5924 workspaceDiagnostics = true, 5925 }, 5926 }, 5927 }) 5928 5929 return assert(vim.lsp.start({ name = 'dynamic-test', cmd = server.cmd })) 5930 end) 5931 5932 local function sort_method(tbl) 5933 local result_t = vim.deepcopy(tbl) 5934 table.sort(result_t, function(a, b) 5935 return (a.method or '') < (b.method or '') 5936 end) 5937 return result_t 5938 end 5939 5940 eq( 5941 { 5942 { 5943 id = 'color-registration', 5944 method = 'textDocument/colorPresentation', 5945 registerOptions = { id = 'color-registration' }, 5946 }, 5947 { 5948 id = 'color-registration', 5949 method = 'textDocument/documentColor', 5950 registerOptions = { id = 'color-registration' }, 5951 }, 5952 }, 5953 sort_method(exec_lua(function() 5954 local client = assert(vim.lsp.get_client_by_id(client_id)) 5955 return client.dynamic_capabilities:get('colorProvider') 5956 end)) 5957 ) 5958 5959 eq( 5960 { 5961 { 5962 id = 'diag-registration', 5963 method = 'textDocument/diagnostic', 5964 registerOptions = { 5965 id = 'diag-registration', 5966 identifier = 'diag-ident-static', 5967 workspaceDiagnostics = true, 5968 }, 5969 }, 5970 }, 5971 sort_method(exec_lua(function() 5972 local client = assert(vim.lsp.get_client_by_id(client_id)) 5973 return client.dynamic_capabilities:get('diagnosticProvider') 5974 end)) 5975 ) 5976 5977 eq( 5978 { 'diag-ident-static' }, 5979 exec_lua(function() 5980 local client = assert(vim.lsp.get_client_by_id(client_id)) 5981 return client:_provider_value_get('textDocument/diagnostic', 'identifier') 5982 end) 5983 ) 5984 end) 5985 end) 5986 5987 describe('vim.lsp._watchfiles', function() 5988 --- @type integer, integer, integer 5989 local created, changed, deleted 5990 5991 setup(function() 5992 n.clear() 5993 created = exec_lua([[return vim.lsp.protocol.FileChangeType.Created]]) 5994 changed = exec_lua([[return vim.lsp.protocol.FileChangeType.Changed]]) 5995 deleted = exec_lua([[return vim.lsp.protocol.FileChangeType.Deleted]]) 5996 end) 5997 5998 local function test_filechanges(watchfunc) 5999 it( 6000 string.format('sends notifications when files change (watchfunc=%s)', watchfunc), 6001 function() 6002 if watchfunc == 'inotify' then 6003 skip(is_os('win'), 'not supported on windows') 6004 skip(is_os('mac'), 'flaky test on mac') 6005 skip( 6006 not is_ci() and fn.executable('inotifywait') == 0, 6007 'inotify-tools not installed and not on CI' 6008 ) 6009 end 6010 6011 if watchfunc == 'watch' then 6012 skip(is_os('mac'), 'flaky test on mac') 6013 skip( 6014 is_os('bsd'), 6015 'Stopped working on bsd after 3ca967387c49c754561c3b11a574797504d40f38' 6016 ) 6017 else 6018 skip( 6019 is_os('bsd'), 6020 'kqueue only reports events on watched folder itself, not contained files #26110' 6021 ) 6022 end 6023 6024 local root_dir = tmpname(false) 6025 mkdir(root_dir) 6026 6027 exec_lua(create_server_definition) 6028 local result = exec_lua(function() 6029 local server = _G._create_server() 6030 local client_id = assert(vim.lsp.start({ 6031 name = 'watchfiles-test', 6032 cmd = server.cmd, 6033 root_dir = root_dir, 6034 capabilities = { 6035 workspace = { 6036 didChangeWatchedFiles = { 6037 dynamicRegistration = true, 6038 }, 6039 }, 6040 }, 6041 })) 6042 6043 require('vim.lsp._watchfiles')._watchfunc = require('vim._watch')[watchfunc] 6044 6045 local expected_messages = 0 6046 6047 local msg_wait_timeout = watchfunc == 'watch' and 200 or 2500 6048 6049 local function wait_for_message(incr) 6050 expected_messages = expected_messages + (incr or 1) 6051 assert( 6052 vim.wait(msg_wait_timeout, function() 6053 return #server.messages == expected_messages 6054 end), 6055 'Timed out waiting for expected number of messages. Current messages seen so far: ' 6056 .. vim.inspect(server.messages) 6057 ) 6058 end 6059 6060 wait_for_message(2) -- initialize, initialized 6061 6062 vim.lsp.handlers['client/registerCapability'](nil, { 6063 registrations = { 6064 { 6065 id = 'watchfiles-test-0', 6066 method = 'workspace/didChangeWatchedFiles', 6067 registerOptions = { 6068 watchers = { 6069 { 6070 globPattern = '**/watch', 6071 kind = 7, 6072 }, 6073 }, 6074 }, 6075 }, 6076 }, 6077 }, { client_id = client_id }) 6078 6079 if watchfunc ~= 'watch' then 6080 vim.wait(100) 6081 end 6082 6083 local path = root_dir .. '/watch' 6084 local tmp = vim.fn.tempname() 6085 io.open(tmp, 'w'):close() 6086 vim.uv.fs_rename(tmp, path) 6087 6088 wait_for_message() 6089 6090 os.remove(path) 6091 6092 wait_for_message() 6093 6094 vim.lsp.get_client_by_id(client_id):stop() 6095 6096 return server.messages 6097 end) 6098 6099 local uri = vim.uri_from_fname(root_dir .. '/watch') 6100 6101 eq(6, #result) 6102 6103 eq({ 6104 method = 'workspace/didChangeWatchedFiles', 6105 params = { 6106 changes = { 6107 { 6108 type = created, 6109 uri = uri, 6110 }, 6111 }, 6112 }, 6113 }, result[3]) 6114 6115 eq({ 6116 method = 'workspace/didChangeWatchedFiles', 6117 params = { 6118 changes = { 6119 { 6120 type = deleted, 6121 uri = uri, 6122 }, 6123 }, 6124 }, 6125 }, result[4]) 6126 end 6127 ) 6128 end 6129 6130 test_filechanges('watch') 6131 test_filechanges('watchdirs') 6132 test_filechanges('inotify') 6133 6134 it('correctly registers and unregisters', function() 6135 local root_dir = '/some_dir' 6136 exec_lua(create_server_definition) 6137 local result = exec_lua(function() 6138 local server = _G._create_server() 6139 local client_id = assert(vim.lsp.start({ 6140 name = 'watchfiles-test', 6141 cmd = server.cmd, 6142 root_dir = root_dir, 6143 capabilities = { 6144 workspace = { 6145 didChangeWatchedFiles = { 6146 dynamicRegistration = true, 6147 }, 6148 }, 6149 }, 6150 })) 6151 6152 local expected_messages = 2 -- initialize, initialized 6153 local function wait_for_messages() 6154 assert( 6155 vim.wait(200, function() 6156 return #server.messages == expected_messages 6157 end), 6158 'Timed out waiting for expected number of messages. Current messages seen so far: ' 6159 .. vim.inspect(server.messages) 6160 ) 6161 end 6162 6163 wait_for_messages() 6164 6165 local send_event --- @type function 6166 require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback) 6167 local stopped = false 6168 send_event = function(...) 6169 if not stopped then 6170 callback(...) 6171 end 6172 end 6173 return function() 6174 stopped = true 6175 end 6176 end 6177 6178 vim.lsp.handlers['client/registerCapability'](nil, { 6179 registrations = { 6180 { 6181 id = 'watchfiles-test-0', 6182 method = 'workspace/didChangeWatchedFiles', 6183 registerOptions = { 6184 watchers = { 6185 { 6186 globPattern = '**/*.watch0', 6187 }, 6188 }, 6189 }, 6190 }, 6191 }, 6192 }, { client_id = client_id }) 6193 6194 send_event(root_dir .. '/file.watch0', vim._watch.FileChangeType.Created) 6195 send_event(root_dir .. '/file.watch1', vim._watch.FileChangeType.Created) 6196 6197 expected_messages = expected_messages + 1 6198 wait_for_messages() 6199 6200 vim.lsp.handlers['client/registerCapability'](nil, { 6201 registrations = { 6202 { 6203 id = 'watchfiles-test-1', 6204 method = 'workspace/didChangeWatchedFiles', 6205 registerOptions = { 6206 watchers = { 6207 { 6208 globPattern = '**/*.watch1', 6209 }, 6210 }, 6211 }, 6212 }, 6213 }, 6214 }, { client_id = client_id }) 6215 6216 vim.lsp.handlers['client/unregisterCapability'](nil, { 6217 unregisterations = { 6218 { 6219 id = 'watchfiles-test-0', 6220 method = 'workspace/didChangeWatchedFiles', 6221 }, 6222 }, 6223 }, { client_id = client_id }) 6224 6225 send_event(root_dir .. '/file.watch0', vim._watch.FileChangeType.Created) 6226 send_event(root_dir .. '/file.watch1', vim._watch.FileChangeType.Created) 6227 6228 expected_messages = expected_messages + 1 6229 wait_for_messages() 6230 6231 return server.messages 6232 end) 6233 6234 local function watched_uri(fname) 6235 return vim.uri_from_fname(root_dir .. '/' .. fname) 6236 end 6237 6238 eq(4, #result) 6239 eq('workspace/didChangeWatchedFiles', result[3].method) 6240 eq({ 6241 changes = { 6242 { 6243 type = created, 6244 uri = watched_uri('file.watch0'), 6245 }, 6246 }, 6247 }, result[3].params) 6248 eq('workspace/didChangeWatchedFiles', result[4].method) 6249 eq({ 6250 changes = { 6251 { 6252 type = created, 6253 uri = watched_uri('file.watch1'), 6254 }, 6255 }, 6256 }, result[4].params) 6257 end) 6258 6259 it('correctly handles the registered watch kind', function() 6260 local root_dir = 'some_dir' 6261 exec_lua(create_server_definition) 6262 local result = exec_lua(function() 6263 local server = _G._create_server() 6264 local client_id = assert(vim.lsp.start({ 6265 name = 'watchfiles-test', 6266 cmd = server.cmd, 6267 root_dir = root_dir, 6268 capabilities = { 6269 workspace = { 6270 didChangeWatchedFiles = { 6271 dynamicRegistration = true, 6272 }, 6273 }, 6274 }, 6275 })) 6276 6277 local expected_messages = 2 -- initialize, initialized 6278 local function wait_for_messages() 6279 assert( 6280 vim.wait(200, function() 6281 return #server.messages == expected_messages 6282 end), 6283 'Timed out waiting for expected number of messages. Current messages seen so far: ' 6284 .. vim.inspect(server.messages) 6285 ) 6286 end 6287 6288 wait_for_messages() 6289 6290 local watch_callbacks = {} --- @type function[] 6291 local function send_event(...) 6292 for _, cb in ipairs(watch_callbacks) do 6293 cb(...) 6294 end 6295 end 6296 require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback) 6297 table.insert(watch_callbacks, callback) 6298 return function() 6299 -- noop because this test never stops the watch 6300 end 6301 end 6302 6303 local protocol = require('vim.lsp.protocol') 6304 6305 local watchers = {} 6306 local max_kind = protocol.WatchKind.Create 6307 + protocol.WatchKind.Change 6308 + protocol.WatchKind.Delete 6309 for i = 0, max_kind do 6310 table.insert(watchers, { 6311 globPattern = { 6312 baseUri = vim.uri_from_fname('/dir'), 6313 pattern = 'watch' .. tostring(i), 6314 }, 6315 kind = i, 6316 }) 6317 end 6318 vim.lsp.handlers['client/registerCapability'](nil, { 6319 registrations = { 6320 { 6321 id = 'watchfiles-test-kind', 6322 method = 'workspace/didChangeWatchedFiles', 6323 registerOptions = { 6324 watchers = watchers, 6325 }, 6326 }, 6327 }, 6328 }, { client_id = client_id }) 6329 6330 for i = 0, max_kind do 6331 local filename = '/dir/watch' .. tostring(i) 6332 send_event(filename, vim._watch.FileChangeType.Created) 6333 send_event(filename, vim._watch.FileChangeType.Changed) 6334 send_event(filename, vim._watch.FileChangeType.Deleted) 6335 end 6336 6337 expected_messages = expected_messages + 1 6338 wait_for_messages() 6339 6340 return server.messages 6341 end) 6342 6343 local function watched_uri(fname) 6344 return vim.uri_from_fname('/dir/' .. fname) 6345 end 6346 6347 eq(3, #result) 6348 eq('workspace/didChangeWatchedFiles', result[3].method) 6349 eq({ 6350 changes = { 6351 { 6352 type = created, 6353 uri = watched_uri('watch1'), 6354 }, 6355 { 6356 type = changed, 6357 uri = watched_uri('watch2'), 6358 }, 6359 { 6360 type = created, 6361 uri = watched_uri('watch3'), 6362 }, 6363 { 6364 type = changed, 6365 uri = watched_uri('watch3'), 6366 }, 6367 { 6368 type = deleted, 6369 uri = watched_uri('watch4'), 6370 }, 6371 { 6372 type = created, 6373 uri = watched_uri('watch5'), 6374 }, 6375 { 6376 type = deleted, 6377 uri = watched_uri('watch5'), 6378 }, 6379 { 6380 type = changed, 6381 uri = watched_uri('watch6'), 6382 }, 6383 { 6384 type = deleted, 6385 uri = watched_uri('watch6'), 6386 }, 6387 { 6388 type = created, 6389 uri = watched_uri('watch7'), 6390 }, 6391 { 6392 type = changed, 6393 uri = watched_uri('watch7'), 6394 }, 6395 { 6396 type = deleted, 6397 uri = watched_uri('watch7'), 6398 }, 6399 }, 6400 }, result[3].params) 6401 end) 6402 6403 it('prunes duplicate events', function() 6404 local root_dir = 'some_dir' 6405 exec_lua(create_server_definition) 6406 local result = exec_lua(function() 6407 local server = _G._create_server() 6408 local client_id = assert(vim.lsp.start({ 6409 name = 'watchfiles-test', 6410 cmd = server.cmd, 6411 root_dir = root_dir, 6412 capabilities = { 6413 workspace = { 6414 didChangeWatchedFiles = { 6415 dynamicRegistration = true, 6416 }, 6417 }, 6418 }, 6419 })) 6420 6421 local expected_messages = 2 -- initialize, initialized 6422 local function wait_for_messages() 6423 assert( 6424 vim.wait(200, function() 6425 return #server.messages == expected_messages 6426 end), 6427 'Timed out waiting for expected number of messages. Current messages seen so far: ' 6428 .. vim.inspect(server.messages) 6429 ) 6430 end 6431 6432 wait_for_messages() 6433 6434 local send_event --- @type function 6435 require('vim.lsp._watchfiles')._watchfunc = function(_, _, callback) 6436 send_event = callback 6437 return function() 6438 -- noop because this test never stops the watch 6439 end 6440 end 6441 6442 vim.lsp.handlers['client/registerCapability'](nil, { 6443 registrations = { 6444 { 6445 id = 'watchfiles-test-kind', 6446 method = 'workspace/didChangeWatchedFiles', 6447 registerOptions = { 6448 watchers = { 6449 { 6450 globPattern = '**/*', 6451 }, 6452 }, 6453 }, 6454 }, 6455 }, 6456 }, { client_id = client_id }) 6457 6458 send_event('file1', vim._watch.FileChangeType.Created) 6459 send_event('file1', vim._watch.FileChangeType.Created) -- pruned 6460 send_event('file1', vim._watch.FileChangeType.Changed) 6461 send_event('file2', vim._watch.FileChangeType.Created) 6462 send_event('file1', vim._watch.FileChangeType.Changed) -- pruned 6463 6464 expected_messages = expected_messages + 1 6465 wait_for_messages() 6466 6467 return server.messages 6468 end) 6469 6470 eq(3, #result) 6471 eq('workspace/didChangeWatchedFiles', result[3].method) 6472 eq({ 6473 changes = { 6474 { 6475 type = created, 6476 uri = vim.uri_from_fname('file1'), 6477 }, 6478 { 6479 type = changed, 6480 uri = vim.uri_from_fname('file1'), 6481 }, 6482 { 6483 type = created, 6484 uri = vim.uri_from_fname('file2'), 6485 }, 6486 }, 6487 }, result[3].params) 6488 end) 6489 6490 it("ignores registrations by servers when the client doesn't advertise support", function() 6491 exec_lua(create_server_definition) 6492 exec_lua(function() 6493 _G.server = _G._create_server() 6494 require('vim.lsp._watchfiles')._watchfunc = function(_, _, _) 6495 -- Since the registration is ignored, this should not execute and `watching` should stay false 6496 _G.watching = true 6497 return function() end 6498 end 6499 end) 6500 6501 local function check_registered(capabilities) 6502 return exec_lua(function() 6503 _G.watching = false 6504 local client_id = assert(vim.lsp.start({ 6505 name = 'watchfiles-test', 6506 cmd = _G.server.cmd, 6507 root_dir = 'some_dir', 6508 capabilities = capabilities, 6509 }, { 6510 reuse_client = function() 6511 return false 6512 end, 6513 })) 6514 6515 vim.lsp.handlers['client/registerCapability'](nil, { 6516 registrations = { 6517 { 6518 id = 'watchfiles-test-kind', 6519 method = 'workspace/didChangeWatchedFiles', 6520 registerOptions = { 6521 watchers = { 6522 { 6523 globPattern = '**/*', 6524 }, 6525 }, 6526 }, 6527 }, 6528 }, 6529 }, { client_id = client_id }) 6530 6531 -- Ensure no errors occur when unregistering something that was never really registered. 6532 vim.lsp.handlers['client/unregisterCapability'](nil, { 6533 unregisterations = { 6534 { 6535 id = 'watchfiles-test-kind', 6536 method = 'workspace/didChangeWatchedFiles', 6537 }, 6538 }, 6539 }, { client_id = client_id }) 6540 6541 vim.lsp.get_client_by_id(client_id):stop(true) 6542 return _G.watching 6543 end) 6544 end 6545 6546 eq(is_os('mac') or is_os('win'), check_registered(nil)) -- start{_client}() defaults to make_client_capabilities(). 6547 eq( 6548 false, 6549 check_registered({ 6550 workspace = { 6551 didChangeWatchedFiles = { 6552 dynamicRegistration = false, 6553 }, 6554 }, 6555 }) 6556 ) 6557 eq( 6558 true, 6559 check_registered({ 6560 workspace = { 6561 didChangeWatchedFiles = { 6562 dynamicRegistration = true, 6563 }, 6564 }, 6565 }) 6566 ) 6567 end) 6568 end) 6569 6570 describe('vim.lsp.config() and vim.lsp.enable()', function() 6571 it('merges settings from "*"', function() 6572 eq( 6573 { 6574 name = 'foo', 6575 cmd = { 'foo' }, 6576 root_markers = { '.git' }, 6577 }, 6578 exec_lua(function() 6579 vim.lsp.config('*', { root_markers = { '.git' } }) 6580 vim.lsp.config('foo', { cmd = { 'foo' } }) 6581 6582 return vim.lsp.config['foo'] 6583 end) 6584 ) 6585 end) 6586 6587 it('config("bogus") shows a hint', function() 6588 matches( 6589 'hint%: to resolve a config', 6590 pcall_err(exec_lua, function() 6591 vim.print(vim.lsp.config('non-existent-config')) 6592 end) 6593 ) 6594 end) 6595 6596 it('sets up an autocmd', function() 6597 eq( 6598 1, 6599 exec_lua(function() 6600 vim.lsp.config('foo', { 6601 cmd = { 'foo' }, 6602 root_markers = { '.foorc' }, 6603 }) 6604 vim.lsp.enable('foo') 6605 return #vim.api.nvim_get_autocmds({ 6606 group = 'nvim.lsp.enable', 6607 event = 'FileType', 6608 }) 6609 end) 6610 ) 6611 end) 6612 6613 it('handle nil config (some clients may not have a config!)', function() 6614 exec_lua(create_server_definition) 6615 exec_lua(function() 6616 local server = _G._create_server() 6617 vim.bo.filetype = 'lua' 6618 -- Attach a client without defining a config. 6619 local client_id = vim.lsp.start({ 6620 name = 'test_ls', 6621 cmd = function(dispatchers, config) 6622 _G.test_resolved_root = config.root_dir --[[@type string]] 6623 return server.cmd(dispatchers, config) 6624 end, 6625 }, { bufnr = 0 }) 6626 6627 local bufnr = vim.api.nvim_get_current_buf() 6628 local client = vim.lsp.get_client_by_id(client_id) 6629 assert(client.attached_buffers[bufnr]) 6630 6631 -- Exercise the codepath which had a regression: 6632 vim.lsp.enable('test_ls') 6633 vim.api.nvim_exec_autocmds('FileType', { buffer = bufnr }) 6634 6635 -- enable() does _not_ detach the client since it doesn't actually have a config. 6636 -- XXX: otoh, is it confusing to allow `enable("foo")` if there a "foo" _client_ without a "foo" _config_? 6637 assert(client.attached_buffers[bufnr]) 6638 assert(client_id == vim.lsp.get_client_by_id(bufnr).id) 6639 end) 6640 end) 6641 6642 it('attaches to buffers when they are opened', function() 6643 exec_lua(create_server_definition) 6644 6645 local tmp1 = t.tmpname(true) 6646 local tmp2 = t.tmpname(true) 6647 6648 exec_lua(function() 6649 local server = _G._create_server({ 6650 handlers = { 6651 initialize = function(_, _, callback) 6652 callback(nil, { capabilities = {} }) 6653 end, 6654 }, 6655 }) 6656 6657 vim.lsp.config('foo', { 6658 cmd = server.cmd, 6659 filetypes = { 'foo' }, 6660 root_markers = { '.foorc' }, 6661 }) 6662 6663 vim.lsp.config('bar', { 6664 cmd = server.cmd, 6665 filetypes = { 'bar' }, 6666 root_markers = { '.foorc' }, 6667 }) 6668 6669 vim.lsp.enable('foo') 6670 vim.lsp.enable('bar') 6671 6672 vim.cmd.edit(tmp1) 6673 vim.bo.filetype = 'foo' 6674 _G.foo_buf = vim.api.nvim_get_current_buf() 6675 6676 vim.cmd.edit(tmp2) 6677 vim.bo.filetype = 'bar' 6678 _G.bar_buf = vim.api.nvim_get_current_buf() 6679 end) 6680 6681 eq( 6682 { 1, 'foo', 1, 'bar' }, 6683 exec_lua(function() 6684 local foos = vim.lsp.get_clients({ bufnr = assert(_G.foo_buf) }) 6685 local bars = vim.lsp.get_clients({ bufnr = assert(_G.bar_buf) }) 6686 return { #foos, foos[1].name, #bars, bars[1].name } 6687 end) 6688 ) 6689 end) 6690 6691 it('attaches/detaches preexisting buffers', function() 6692 exec_lua(create_server_definition) 6693 6694 local tmp1 = t.tmpname(true) 6695 local tmp2 = t.tmpname(true) 6696 6697 exec_lua(function() 6698 vim.cmd.edit(tmp1) 6699 vim.bo.filetype = 'foo' 6700 _G.foo_buf = vim.api.nvim_get_current_buf() 6701 6702 vim.cmd.edit(tmp2) 6703 vim.bo.filetype = 'bar' 6704 _G.bar_buf = vim.api.nvim_get_current_buf() 6705 6706 local server = _G._create_server({ 6707 handlers = { 6708 initialize = function(_, _, callback) 6709 callback(nil, { capabilities = {} }) 6710 end, 6711 }, 6712 }) 6713 6714 vim.lsp.config('foo', { 6715 cmd = server.cmd, 6716 filetypes = { 'foo' }, 6717 root_markers = { '.foorc' }, 6718 }) 6719 6720 vim.lsp.config('bar', { 6721 cmd = server.cmd, 6722 filetypes = { 'bar' }, 6723 root_markers = { '.foorc' }, 6724 }) 6725 6726 vim.lsp.enable('foo') 6727 vim.lsp.enable('bar') 6728 end) 6729 6730 eq( 6731 { 1, 'foo', 1, 'bar' }, 6732 exec_lua(function() 6733 local foos = vim.lsp.get_clients({ bufnr = assert(_G.foo_buf) }) 6734 local bars = vim.lsp.get_clients({ bufnr = assert(_G.bar_buf) }) 6735 return { #foos, foos[1].name, #bars, bars[1].name } 6736 end) 6737 ) 6738 6739 -- Now disable the 'foo' lsp and confirm that it's detached from the buffer it was previous 6740 -- attached to. 6741 exec_lua([[vim.lsp.enable('foo', false)]]) 6742 eq( 6743 { 0, 'foo', 1, 'bar' }, 6744 exec_lua(function() 6745 local foos = vim.lsp.get_clients({ bufnr = assert(_G.foo_buf) }) 6746 local bars = vim.lsp.get_clients({ bufnr = assert(_G.bar_buf) }) 6747 return { #foos, 'foo', #bars, bars[1].name } 6748 end) 6749 ) 6750 end) 6751 6752 it('in first FileType event (on startup)', function() 6753 local tmp = tmpname() 6754 write_file(tmp, string.dump(create_server_definition)) 6755 n.clear({ 6756 args = { 6757 '--cmd', 6758 string.format([[lua assert(loadfile(%q))()]], tmp), 6759 '--cmd', 6760 [[lua _G.server = _G._create_server({ handlers = {initialize = function(_, _, callback) callback(nil, {capabilities = {}}) end} })]], 6761 '--cmd', 6762 [[lua vim.lsp.config('foo', { cmd = _G.server.cmd, filetypes = { 'foo' }, root_markers = { '.foorc' } })]], 6763 '--cmd', 6764 [[au FileType * ++once lua vim.lsp.enable('foo')]], 6765 '-c', 6766 'set ft=foo', 6767 }, 6768 }) 6769 6770 eq( 6771 { 1, 'foo' }, 6772 exec_lua(function() 6773 local foos = vim.lsp.get_clients({ bufnr = 0 }) 6774 return { #foos, (foos[1] or {}).name } 6775 end) 6776 ) 6777 exec_lua([[vim.lsp.enable('foo', false)]]) 6778 eq( 6779 0, 6780 exec_lua(function() 6781 return #vim.lsp.get_clients({ bufnr = 0 }) 6782 end) 6783 ) 6784 end) 6785 6786 it('does not attach to buffers more than once if no root_dir', function() 6787 exec_lua(create_server_definition) 6788 6789 local tmp1 = t.tmpname(true) 6790 6791 eq( 6792 1, 6793 exec_lua(function() 6794 local server = _G._create_server({ 6795 handlers = { 6796 initialize = function(_, _, callback) 6797 callback(nil, { capabilities = {} }) 6798 end, 6799 }, 6800 }) 6801 6802 vim.lsp.config('foo', { cmd = server.cmd, filetypes = { 'foo' } }) 6803 vim.lsp.enable('foo') 6804 6805 vim.cmd.edit(assert(tmp1)) 6806 vim.bo.filetype = 'foo' 6807 vim.bo.filetype = 'foo' 6808 6809 return #vim.lsp.get_clients({ bufnr = vim.api.nvim_get_current_buf() }) 6810 end) 6811 ) 6812 end) 6813 6814 it('async root_dir, cmd(…,config) gets resolved config', function() 6815 exec_lua(create_server_definition) 6816 6817 local tmp1 = t.tmpname(true) 6818 exec_lua(function() 6819 local server = _G._create_server({ 6820 handlers = { 6821 initialize = function(_, _, callback) 6822 callback(nil, { capabilities = {} }) 6823 end, 6824 }, 6825 }) 6826 6827 vim.lsp.config('foo', { 6828 cmd = function(dispatchers, config) 6829 _G.test_resolved_root = config.root_dir --[[@type string]] 6830 return server.cmd(dispatchers, config) 6831 end, 6832 filetypes = { 'foo' }, 6833 root_dir = function(bufnr, cb) 6834 assert(tmp1 == vim.api.nvim_buf_get_name(bufnr)) 6835 vim.system({ 'sleep', '0' }, {}, function() 6836 cb('some_dir') 6837 end) 6838 end, 6839 }) 6840 vim.lsp.enable('foo') 6841 6842 vim.cmd.edit(assert(tmp1)) 6843 vim.bo.filetype = 'foo' 6844 end) 6845 6846 retry(nil, 1000, function() 6847 eq( 6848 'some_dir', 6849 exec_lua(function() 6850 return vim.lsp.get_clients({ bufnr = vim.api.nvim_get_current_buf() })[1].root_dir 6851 end) 6852 ) 6853 end) 6854 eq( 6855 'some_dir', 6856 exec_lua(function() 6857 return _G.test_resolved_root 6858 end) 6859 ) 6860 end) 6861 6862 it('starts correct LSP and stops incorrect LSP when filetype changes', function() 6863 exec_lua(create_server_definition) 6864 6865 local tmp1 = t.tmpname(true) 6866 6867 exec_lua(function() 6868 local server = _G._create_server({ 6869 handlers = { 6870 initialize = function(_, _, callback) 6871 callback(nil, { capabilities = {} }) 6872 end, 6873 }, 6874 }) 6875 6876 vim.lsp.config('foo', { 6877 cmd = server.cmd, 6878 filetypes = { 'foo' }, 6879 root_markers = { '.foorc' }, 6880 }) 6881 6882 vim.lsp.config('bar', { 6883 cmd = server.cmd, 6884 filetypes = { 'bar' }, 6885 root_markers = { '.foorc' }, 6886 }) 6887 6888 vim.lsp.enable('foo') 6889 vim.lsp.enable('bar') 6890 6891 vim.cmd.edit(tmp1) 6892 end) 6893 6894 local count_clients = function() 6895 return exec_lua(function() 6896 local foos = vim.lsp.get_clients({ name = 'foo', bufnr = 0 }) 6897 local bars = vim.lsp.get_clients({ name = 'bar', bufnr = 0 }) 6898 return { #foos, 'foo', #bars, 'bar' } 6899 end) 6900 end 6901 6902 -- No filetype on the buffer yet, so no LSPs. 6903 eq({ 0, 'foo', 0, 'bar' }, count_clients()) 6904 6905 -- Set the filetype to 'foo', confirm a LSP starts. 6906 exec_lua([[vim.bo.filetype = 'foo']]) 6907 eq({ 1, 'foo', 0, 'bar' }, count_clients()) 6908 6909 -- Set the filetype to 'bar', confirm a new LSP starts, and the old one goes away. 6910 exec_lua([[vim.bo.filetype = 'bar']]) 6911 eq({ 0, 'foo', 1, 'bar' }, count_clients()) 6912 end) 6913 6914 it('validates config on attach', function() 6915 local tmp1 = t.tmpname(true) 6916 exec_lua(function() 6917 vim.fn.writefile({ '' }, fake_lsp_logfile) 6918 vim.lsp.log._set_filename(fake_lsp_logfile) 6919 end) 6920 6921 local function test_cfg(cfg, err) 6922 exec_lua(function() 6923 vim.lsp.config['foo'] = {} 6924 vim.lsp.config('foo', cfg) 6925 vim.lsp.enable('foo') 6926 vim.cmd.edit(assert(tmp1)) 6927 vim.bo.filetype = 'non.applicable.filetype' 6928 end) 6929 6930 -- Assert NO log for non-applicable 'filetype'. #35737 6931 if type(cfg.filetypes) == 'table' then 6932 t.assert_nolog(err, fake_lsp_logfile) 6933 end 6934 6935 exec_lua(function() 6936 vim.bo.filetype = 'foo' 6937 end) 6938 6939 retry(nil, 1000, function() 6940 t.assert_log(err, fake_lsp_logfile) 6941 end) 6942 end 6943 6944 test_cfg({ 6945 filetypes = { 'foo' }, 6946 cmd = { 'lolling' }, 6947 }, 'invalid "foo" config: .* lolling is not executable') 6948 6949 test_cfg({ 6950 cmd = { 'cat' }, 6951 filetypes = true, 6952 }, 'invalid "foo" config: .* filetypes: expected table, got boolean') 6953 end) 6954 6955 it('does not start without workspace if workspace_required=true', function() 6956 exec_lua(create_server_definition) 6957 6958 local tmp1 = t.tmpname(true) 6959 6960 eq( 6961 { workspace_required = false }, 6962 exec_lua(function() 6963 local server = _G._create_server({ 6964 handlers = { 6965 initialize = function(_, _, callback) 6966 callback(nil, { capabilities = {} }) 6967 end, 6968 }, 6969 }) 6970 6971 local ws_required = { cmd = server.cmd, workspace_required = true, filetypes = { 'foo' } } 6972 local ws_not_required = vim.deepcopy(ws_required) 6973 ws_not_required.workspace_required = false 6974 6975 vim.lsp.config('ws_required', ws_required) 6976 vim.lsp.config('ws_not_required', ws_not_required) 6977 vim.lsp.enable('ws_required') 6978 vim.lsp.enable('ws_not_required') 6979 6980 vim.cmd.edit(assert(tmp1)) 6981 vim.bo.filetype = 'foo' 6982 6983 local clients = vim.lsp.get_clients({ bufnr = vim.api.nvim_get_current_buf() }) 6984 assert(1 == #clients) 6985 return { workspace_required = clients[1].config.workspace_required } 6986 end) 6987 ) 6988 end) 6989 6990 it('does not allow wildcards in config name', function() 6991 local err = 6992 '.../lsp.lua:0: name: expected non%-wildcard string, got foo%*%. Info: LSP config name cannot contain wildcard %("%*"%)' 6993 6994 matches( 6995 err, 6996 pcall_err(exec_lua, function() 6997 local _ = vim.lsp.config['foo*'] 6998 end) 6999 ) 7000 matches( 7001 err, 7002 pcall_err(exec_lua, function() 7003 vim.lsp.config['foo*'] = {} 7004 end) 7005 ) 7006 matches( 7007 err, 7008 pcall_err(exec_lua, function() 7009 vim.lsp.config('foo*', {}) 7010 end) 7011 ) 7012 -- Exception for '*' 7013 pcall(exec_lua, function() 7014 vim.lsp.config('*', {}) 7015 end) 7016 end) 7017 7018 it('root_markers priority', function() 7019 --- Setup directories for testing 7020 -- root/ 7021 -- ├── dir_a/ 7022 -- │ ├── dir_b/ 7023 -- │ │ ├── target 7024 -- │ │ └── marker_d 7025 -- │ ├── marker_b 7026 -- │ └── marker_c 7027 -- └── marker_a 7028 7029 ---@param filepath string 7030 local function touch(filepath) 7031 local file = io.open(filepath, 'w') 7032 if file then 7033 file:close() 7034 end 7035 end 7036 7037 local tmp_root = tmpname(false) 7038 local marker_a = tmp_root .. '/marker_a' 7039 local dir_a = tmp_root .. '/dir_a' 7040 local marker_b = dir_a .. '/marker_b' 7041 local marker_c = dir_a .. '/marker_c' 7042 local dir_b = dir_a .. '/dir_b' 7043 local marker_d = dir_b .. '/marker_d' 7044 local target = dir_b .. '/target' 7045 7046 mkdir(tmp_root) 7047 touch(marker_a) 7048 mkdir(dir_a) 7049 touch(marker_b) 7050 touch(marker_c) 7051 mkdir(dir_b) 7052 touch(marker_d) 7053 touch(target) 7054 7055 exec_lua(create_server_definition) 7056 exec_lua(function() 7057 _G._custom_server = _G._create_server() 7058 end) 7059 7060 ---@param root_markers (string|string[])[] 7061 ---@param expected_root_dir string? 7062 local function markers_resolve_to(root_markers, expected_root_dir) 7063 exec_lua(function() 7064 vim.lsp.config['foo'] = {} 7065 vim.lsp.config('foo', { 7066 cmd = _G._custom_server.cmd, 7067 reuse_client = function() 7068 return false 7069 end, 7070 filetypes = { 'foo' }, 7071 root_markers = root_markers, 7072 }) 7073 vim.lsp.enable('foo') 7074 vim.cmd.edit(target) 7075 vim.bo.filetype = 'foo' 7076 end) 7077 retry(nil, 1000, function() 7078 eq( 7079 expected_root_dir, 7080 exec_lua(function() 7081 local clients = vim.lsp.get_clients() 7082 return clients[#clients].root_dir 7083 end) 7084 ) 7085 end) 7086 end 7087 7088 markers_resolve_to({ 'marker_d' }, dir_b) 7089 markers_resolve_to({ 'marker_b' }, dir_a) 7090 markers_resolve_to({ 'marker_c' }, dir_a) 7091 markers_resolve_to({ 'marker_a' }, tmp_root) 7092 markers_resolve_to({ 'foo' }, nil) 7093 markers_resolve_to({ { 'marker_b', 'marker_a' }, 'marker_d' }, dir_a) 7094 markers_resolve_to({ 'marker_a', { 'marker_b', 'marker_d' } }, tmp_root) 7095 markers_resolve_to({ 'foo', { 'bar', 'baz' }, 'marker_d' }, dir_b) 7096 end) 7097 7098 it('vim.lsp.is_enabled()', function() 7099 exec_lua(function() 7100 vim.lsp.config('foo', { 7101 cmd = { 'foo' }, 7102 root_markers = { '.foorc' }, 7103 }) 7104 end) 7105 7106 -- LSP config defaults to disabled. 7107 eq(false, exec_lua([[return vim.lsp.is_enabled('foo')]])) 7108 7109 -- Confirm we can enable it. 7110 exec_lua([[vim.lsp.enable('foo')]]) 7111 eq(true, exec_lua([[return vim.lsp.is_enabled('foo')]])) 7112 7113 -- And finally, disable it again. 7114 exec_lua([[vim.lsp.enable('foo', false)]]) 7115 eq(false, exec_lua([[return vim.lsp.is_enabled('foo')]])) 7116 end) 7117 end) 7118 7119 describe('vim.lsp.buf.workspace_diagnostics()', function() 7120 local fake_uri = 'file:///fake/uri' 7121 7122 --- @param kind lsp.DocumentDiagnosticReportKind 7123 --- @param msg string 7124 --- @param pos integer 7125 --- @return lsp.WorkspaceDocumentDiagnosticReport 7126 local function make_report(kind, msg, pos) 7127 return { 7128 kind = kind, 7129 uri = fake_uri, 7130 items = { 7131 { 7132 range = { 7133 start = { line = pos, character = pos }, 7134 ['end'] = { line = pos, character = pos }, 7135 }, 7136 message = msg, 7137 severity = 1, 7138 }, 7139 }, 7140 } 7141 end 7142 7143 --- @param items lsp.WorkspaceDocumentDiagnosticReport[] 7144 --- @return integer 7145 local function setup_server(items) 7146 exec_lua(create_server_definition) 7147 return exec_lua(function() 7148 _G.server = _G._create_server({ 7149 capabilities = { 7150 diagnosticProvider = { workspaceDiagnostics = true }, 7151 }, 7152 handlers = { 7153 ['workspace/diagnostic'] = function(_, _, callback) 7154 callback(nil, { items = items }) 7155 end, 7156 }, 7157 }) 7158 local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd })) 7159 vim.lsp.buf.workspace_diagnostics() 7160 return client_id 7161 end, { items }) 7162 end 7163 7164 it('updates diagnostics obtained with vim.diagnostic.get()', function() 7165 setup_server({ make_report('full', 'Error here', 1) }) 7166 7167 retry(nil, nil, function() 7168 eq( 7169 1, 7170 exec_lua(function() 7171 return #vim.diagnostic.get() 7172 end) 7173 ) 7174 end) 7175 7176 eq( 7177 'Error here', 7178 exec_lua(function() 7179 return vim.diagnostic.get()[1].message 7180 end) 7181 ) 7182 end) 7183 7184 it('ignores unchanged diagnostic reports', function() 7185 setup_server({ make_report('unchanged', '', 1) }) 7186 7187 eq( 7188 0, 7189 exec_lua(function() 7190 -- Wait for diagnostics to be processed. 7191 vim.uv.sleep(50) 7192 7193 return #vim.diagnostic.get() 7194 end) 7195 ) 7196 end) 7197 7198 it('favors document diagnostics over workspace diagnostics', function() 7199 local client_id = setup_server({ make_report('full', 'Workspace error', 1) }) 7200 local diagnostic_bufnr = exec_lua(function() 7201 return vim.uri_to_bufnr(fake_uri) 7202 end) 7203 7204 exec_lua(function() 7205 vim.lsp.diagnostic.on_diagnostic(nil, { 7206 kind = 'full', 7207 items = { 7208 { 7209 range = { 7210 start = { line = 2, character = 2 }, 7211 ['end'] = { line = 2, character = 2 }, 7212 }, 7213 message = 'Document error', 7214 severity = 1, 7215 }, 7216 }, 7217 }, { 7218 method = 'textDocument/diagnostic', 7219 params = { 7220 textDocument = { uri = fake_uri }, 7221 }, 7222 client_id = client_id, 7223 bufnr = diagnostic_bufnr, 7224 }) 7225 end) 7226 7227 eq( 7228 1, 7229 exec_lua(function() 7230 return #vim.diagnostic.get(diagnostic_bufnr) 7231 end) 7232 ) 7233 7234 eq( 7235 'Document error', 7236 exec_lua(function() 7237 return vim.diagnostic.get(vim.uri_to_bufnr(fake_uri))[1].message 7238 end) 7239 ) 7240 end) 7241 end) 7242 7243 describe('vim.lsp.buf.hover()', function() 7244 it('handles empty contents', function() 7245 exec_lua(create_server_definition) 7246 exec_lua(function() 7247 local server = _G._create_server({ 7248 capabilities = { 7249 hoverProvider = true, 7250 }, 7251 handlers = { 7252 ['textDocument/hover'] = function(_, _, callback) 7253 local res = { 7254 contents = { 7255 kind = 'markdown', 7256 value = '', 7257 }, 7258 } 7259 callback(nil, res) 7260 end, 7261 }, 7262 }) 7263 vim.lsp.start({ name = 'dummy', cmd = server.cmd }) 7264 end) 7265 7266 eq('Empty hover response', n.exec_capture('lua vim.lsp.buf.hover()')) 7267 end) 7268 7269 it('treats markedstring array as not empty', function() 7270 exec_lua(create_server_definition) 7271 exec_lua(function() 7272 local server = _G._create_server({ 7273 capabilities = { 7274 hoverProvider = true, 7275 }, 7276 handlers = { 7277 ['textDocument/hover'] = function(_, _, callback) 7278 local res = { 7279 contents = { 7280 { 7281 language = 'java', 7282 value = 'Example', 7283 }, 7284 'doc comment', 7285 }, 7286 } 7287 callback(nil, res) 7288 end, 7289 }, 7290 }) 7291 vim.lsp.start({ name = 'dummy', cmd = server.cmd }) 7292 end) 7293 7294 eq('', n.exec_capture('lua vim.lsp.buf.hover()')) 7295 end) 7296 end) 7297 end)