neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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)