aboutsummaryrefslogtreecommitdiff
path: root/test/functional/plugin
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2023-01-25 18:31:31 +0000
committerJosh Rahm <joshuarahm@gmail.com>2023-01-25 18:31:31 +0000
commit9243becbedbb6a1592208051f8fa2b090dcc5e7d (patch)
tree607c2a862ec3f4399b8766383f6f8e04c4aa43b4 /test/functional/plugin
parent9e40b6e9e1bc67f2d856adb837ee64dd0e25b717 (diff)
parent3c48d3c83fc21dbc0841f9210f04bdb073d73cd1 (diff)
downloadrneovim-usermarks.tar.gz
rneovim-usermarks.tar.bz2
rneovim-usermarks.zip
Merge remote-tracking branch 'upstream/master' into usermarksusermarks
Diffstat (limited to 'test/functional/plugin')
-rw-r--r--test/functional/plugin/ccomplete_spec.lua61
-rw-r--r--test/functional/plugin/cfilter_spec.lua106
-rw-r--r--test/functional/plugin/editorconfig_spec.lua210
-rw-r--r--test/functional/plugin/health_spec.lua178
-rw-r--r--test/functional/plugin/lsp/codelens_spec.lua35
-rw-r--r--test/functional/plugin/lsp/helpers.lua176
-rw-r--r--test/functional/plugin/lsp/semantic_tokens_spec.lua1219
-rw-r--r--test/functional/plugin/lsp_spec.lua626
-rw-r--r--test/functional/plugin/man_spec.lua38
-rw-r--r--test/functional/plugin/shada_spec.lua73
10 files changed, 2500 insertions, 222 deletions
diff --git a/test/functional/plugin/ccomplete_spec.lua b/test/functional/plugin/ccomplete_spec.lua
new file mode 100644
index 0000000000..903f16fc73
--- /dev/null
+++ b/test/functional/plugin/ccomplete_spec.lua
@@ -0,0 +1,61 @@
+local helpers = require('test.functional.helpers')(after_each)
+local clear = helpers.clear
+local command = helpers.command
+local eq = helpers.eq
+local eval = helpers.eval
+local feed = helpers.feed
+local write_file = helpers.write_file
+
+describe('ccomplete#Complete', function()
+ setup(function()
+ -- Realistic tags generated from neovim source tree using `ctags -R *`
+ write_file(
+ 'Xtags',
+ [[
+augroup_del src/nvim/autocmd.c /^void augroup_del(char *name, bool stupid_legacy_mode)$/;" f typeref:typename:void
+augroup_exists src/nvim/autocmd.c /^bool augroup_exists(const char *name)$/;" f typeref:typename:bool
+augroup_find src/nvim/autocmd.c /^int augroup_find(const char *name)$/;" f typeref:typename:int
+aupat_get_buflocal_nr src/nvim/autocmd.c /^int aupat_get_buflocal_nr(char *pat, int patlen)$/;" f typeref:typename:int
+aupat_is_buflocal src/nvim/autocmd.c /^bool aupat_is_buflocal(char *pat, int patlen)$/;" f typeref:typename:bool
+expand_get_augroup_name src/nvim/autocmd.c /^char *expand_get_augroup_name(expand_T *xp, int idx)$/;" f typeref:typename:char *
+expand_get_event_name src/nvim/autocmd.c /^char *expand_get_event_name(expand_T *xp, int idx)$/;" f typeref:typename:char *
+]]
+ )
+ end)
+
+ before_each(function()
+ clear()
+ command('set tags=Xtags')
+ end)
+
+ teardown(function()
+ os.remove('Xtags')
+ end)
+
+ it('can complete from Xtags', function()
+ local completed = eval('ccomplete#Complete(0, "a")')
+ eq(5, #completed)
+ eq('augroup_del(', completed[1].word)
+ eq('f', completed[1].kind)
+
+ local aupat = eval('ccomplete#Complete(0, "aupat")')
+ eq(2, #aupat)
+ eq('aupat_get_buflocal_nr(', aupat[1].word)
+ eq('f', aupat[1].kind)
+ end)
+
+ it('does not error when returning no matches', function()
+ local completed = eval('ccomplete#Complete(0, "doesnotmatch")')
+ eq({}, completed)
+ end)
+
+ it('can find the beginning of a word for C', function()
+ command('set filetype=c')
+ feed('i int something = augroup')
+ local result = eval('ccomplete#Complete(1, "")')
+ eq(#' int something = ', result)
+
+ local completed = eval('ccomplete#Complete(0, "augroup")')
+ eq(3, #completed)
+ end)
+end)
diff --git a/test/functional/plugin/cfilter_spec.lua b/test/functional/plugin/cfilter_spec.lua
new file mode 100644
index 0000000000..8b1e75b495
--- /dev/null
+++ b/test/functional/plugin/cfilter_spec.lua
@@ -0,0 +1,106 @@
+local helpers = require('test.functional.helpers')(after_each)
+local clear = helpers.clear
+local command = helpers.command
+local eq = helpers.eq
+local funcs = helpers.funcs
+
+describe('cfilter.lua', function()
+ before_each(function()
+ clear()
+ command('packadd cfilter')
+ end)
+
+ for _, list in ipairs({
+ {
+ name = 'Cfilter',
+ get = funcs.getqflist,
+ set = funcs.setqflist,
+ },
+ {
+ name = 'Lfilter',
+ get = function()
+ return funcs.getloclist(0)
+ end,
+ set = function(items)
+ return funcs.setloclist(0, items)
+ end,
+ },
+ }) do
+ local filter = function(s, bang)
+ if not bang then
+ bang = ''
+ else
+ bang = '!'
+ end
+
+ command(string.format('%s%s %s', list.name, bang, s))
+ end
+
+ describe((':%s'):format(list.name), function()
+ it('does not error on empty list', function()
+ filter('nothing')
+ eq({}, funcs.getqflist())
+ end)
+
+ it('requires an argument', function()
+ local ok = pcall(filter, '')
+ eq(false, ok)
+ end)
+
+ local test = function(name, s, res, map, bang)
+ it(('%s (%s)'):format(name, s), function()
+ list.set({
+ { filename = 'foo', lnum = 1, text = 'bar' },
+ { filename = 'foo', lnum = 2, text = 'baz' },
+ { filename = 'foo', lnum = 3, text = 'zed' },
+ })
+
+ filter(s, bang)
+
+ local got = list.get()
+ if map then
+ got = map(got)
+ end
+ eq(res, got)
+ end)
+ end
+
+ local toname = function(qflist)
+ return funcs.map(qflist, 'v:val.text')
+ end
+
+ test('filters with no matches', 'does not match', {})
+
+ test('filters with matches', 'ba', { 'bar', 'baz' }, toname)
+ test('filters with matches', 'z', { 'baz', 'zed' }, toname)
+ test('filters with matches', '^z', { 'zed' }, toname)
+ test('filters with not matches', '^z', { 'bar', 'baz' }, toname, true)
+
+ it('also supports using the / register', function()
+ list.set({
+ { filename = 'foo', lnum = 1, text = 'bar' },
+ { filename = 'foo', lnum = 2, text = 'baz' },
+ { filename = 'foo', lnum = 3, text = 'zed' },
+ })
+
+ funcs.setreg('/', 'ba')
+ filter('/')
+
+ eq({ 'bar', 'baz' }, toname(list.get()))
+ end)
+
+ it('also supports using the / register with bang', function()
+ list.set({
+ { filename = 'foo', lnum = 1, text = 'bar' },
+ { filename = 'foo', lnum = 2, text = 'baz' },
+ { filename = 'foo', lnum = 3, text = 'zed' },
+ })
+
+ funcs.setreg('/', 'ba')
+ filter('/', true)
+
+ eq({ 'zed' }, toname(list.get()))
+ end)
+ end)
+ end
+end)
diff --git a/test/functional/plugin/editorconfig_spec.lua b/test/functional/plugin/editorconfig_spec.lua
new file mode 100644
index 0000000000..e6a2550aba
--- /dev/null
+++ b/test/functional/plugin/editorconfig_spec.lua
@@ -0,0 +1,210 @@
+local helpers = require('test.functional.helpers')(after_each)
+local clear = helpers.clear
+local command = helpers.command
+local eq = helpers.eq
+local pathsep = helpers.get_pathsep()
+local curbufmeths = helpers.curbufmeths
+local funcs = helpers.funcs
+local meths = helpers.meths
+
+local testdir = 'Xtest-editorconfig'
+
+local function test_case(name, expected)
+ local filename = testdir .. pathsep .. name
+ command('edit ' .. filename)
+ for opt, val in pairs(expected) do
+ eq(val, curbufmeths.get_option(opt), name)
+ end
+end
+
+setup(function()
+ helpers.mkdir_p(testdir)
+ helpers.write_file(
+ testdir .. pathsep .. '.editorconfig',
+ [[
+ root = true
+
+ [3_space.txt]
+ indent_style = space
+ indent_size = 3
+ tab_width = 3
+
+ [4_space.py]
+ indent_style = space
+ indent_size = 4
+ tab_width = 8
+
+ [space.txt]
+ indent_style = space
+ indent_size = tab
+
+ [tab.txt]
+ indent_style = tab
+
+ [4_tab.txt]
+ indent_style = tab
+ indent_size = 4
+ tab_width = 4
+
+ [4_tab_width_of_8.txt]
+ indent_style = tab
+ indent_size = 4
+ tab_width = 8
+
+ [lf.txt]
+ end_of_line = lf
+
+ [crlf.txt]
+ end_of_line = crlf
+
+ [cr.txt]
+ end_of_line = cr
+
+ [utf-8.txt]
+ charset = utf-8
+
+ [utf-8-bom.txt]
+ charset = utf-8-bom
+
+ [utf-16be.txt]
+ charset = utf-16be
+
+ [utf-16le.txt]
+ charset = utf-16le
+
+ [latin1.txt]
+ charset = latin1
+
+ [with_newline.txt]
+ insert_final_newline = true
+
+ [without_newline.txt]
+ insert_final_newline = false
+
+ [trim.txt]
+ trim_trailing_whitespace = true
+
+ [no_trim.txt]
+ trim_trailing_whitespace = false
+
+ [max_line_length.txt]
+ max_line_length = 42
+ ]]
+ )
+end)
+
+teardown(function()
+ helpers.rmdir(testdir)
+end)
+
+describe('editorconfig', function()
+ before_each(function()
+ -- Remove -u NONE so that plugins (i.e. editorconfig.lua) are loaded
+ clear({ args_rm = { '-u' } })
+ end)
+
+ it('sets indent options', function()
+ test_case('3_space.txt', {
+ expandtab = true,
+ shiftwidth = 3,
+ softtabstop = -1,
+ tabstop = 3,
+ })
+
+ test_case('4_space.py', {
+ expandtab = true,
+ shiftwidth = 4,
+ softtabstop = -1,
+ tabstop = 8,
+ })
+
+ test_case('space.txt', {
+ expandtab = true,
+ shiftwidth = 0,
+ softtabstop = 0,
+ })
+
+ test_case('tab.txt', {
+ expandtab = false,
+ shiftwidth = 0,
+ softtabstop = 0,
+ })
+
+ test_case('4_tab.txt', {
+ expandtab = false,
+ shiftwidth = 4,
+ softtabstop = -1,
+ tabstop = 4,
+ })
+
+ test_case('4_tab_width_of_8.txt', {
+ expandtab = false,
+ shiftwidth = 4,
+ softtabstop = -1,
+ tabstop = 8,
+ })
+ end)
+
+ it('sets end-of-line options', function()
+ test_case('lf.txt', { fileformat = 'unix' })
+ test_case('crlf.txt', { fileformat = 'dos' })
+ test_case('cr.txt', { fileformat = 'mac' })
+ end)
+
+ it('sets encoding options', function()
+ test_case('utf-8.txt', { fileencoding = 'utf-8', bomb = false })
+ test_case('utf-8-bom.txt', { fileencoding = 'utf-8', bomb = true })
+ test_case('utf-16be.txt', { fileencoding = 'utf-16', bomb = false })
+ test_case('utf-16le.txt', { fileencoding = 'utf-16le', bomb = false })
+ test_case('latin1.txt', { fileencoding = 'latin1', bomb = false })
+ end)
+
+ it('sets newline options', function()
+ test_case('with_newline.txt', { fixendofline = true, endofline = true })
+ test_case('without_newline.txt', { fixendofline = false, endofline = false })
+ end)
+
+ it('respects trim_trailing_whitespace', function()
+ local filename = testdir .. pathsep .. 'trim.txt'
+ -- luacheck: push ignore 613
+ local untrimmed = [[
+This line ends in whitespace
+So does this one
+And this one
+But not this one
+]]
+ -- luacheck: pop
+ local trimmed = untrimmed:gsub('%s+\n', '\n')
+
+ helpers.write_file(filename, untrimmed)
+ command('edit ' .. filename)
+ command('write')
+ command('bdelete')
+ eq(trimmed, helpers.read_file(filename))
+
+ filename = testdir .. pathsep .. 'no_trim.txt'
+ helpers.write_file(filename, untrimmed)
+ command('edit ' .. filename)
+ command('write')
+ command('bdelete')
+ eq(untrimmed, helpers.read_file(filename))
+ end)
+
+ it('sets textwidth', function()
+ test_case('max_line_length.txt', { textwidth = 42 })
+ end)
+
+ it('can be disabled globally', function()
+ meths.set_var('editorconfig', false)
+ meths.set_option_value('shiftwidth', 42, {})
+ test_case('3_space.txt', { shiftwidth = 42 })
+ end)
+
+ it('can be disabled per-buffer', function()
+ meths.set_option_value('shiftwidth', 42, {})
+ local bufnr = funcs.bufadd(testdir .. pathsep .. '3_space.txt')
+ meths.buf_set_var(bufnr, 'editorconfig', false)
+ test_case('3_space.txt', { shiftwidth = 42 })
+ test_case('4_space.py', { shiftwidth = 4 })
+ end)
+end)
diff --git a/test/functional/plugin/health_spec.lua b/test/functional/plugin/health_spec.lua
index ba66117fb1..97d32313e5 100644
--- a/test/functional/plugin/health_spec.lua
+++ b/test/functional/plugin/health_spec.lua
@@ -5,7 +5,7 @@ local Screen = require('test.functional.ui.screen')
local clear = helpers.clear
local curbuf_contents = helpers.curbuf_contents
local command = helpers.command
-local eq, neq = helpers.eq, helpers.neq
+local eq, neq, matches = helpers.eq, helpers.neq, helpers.matches
local getcompletion = helpers.funcs.getcompletion
describe(':checkhealth', function()
@@ -29,8 +29,7 @@ describe(':checkhealth', function()
-- Do this after startup, otherwise it just breaks $VIMRUNTIME.
command("let $VIM='zub'")
command("checkhealth nvim")
- eq("ERROR: $VIM is invalid: zub",
- string.match(curbuf_contents(), "ERROR: $VIM .* zub"))
+ matches('ERROR $VIM .* zub', curbuf_contents())
end)
it('completions can be listed via getcompletion()', function()
clear()
@@ -56,21 +55,22 @@ describe('health.vim', function()
command("checkhealth full_render")
helpers.expect([[
+ ==============================================================================
full_render: health#full_render#check
- ========================================================================
- ## report 1
- - OK: life is fine
- - WARNING: no what installed
- - ADVICE:
- - pip what
- - make what
-
- ## report 2
- - INFO: stuff is stable
- - ERROR: why no hardcopy
- - ADVICE:
- - :help |:hardcopy|
- - :help |:TOhtml|
+
+ report 1 ~
+ - OK life is fine
+ - WARNING no what installed
+ - ADVICE:
+ - pip what
+ - make what
+
+ report 2 ~
+ - stuff is stable
+ - ERROR why no hardcopy
+ - ADVICE:
+ - :help |:hardcopy|
+ - :help |:TOhtml|
]])
end)
@@ -78,26 +78,29 @@ describe('health.vim', function()
command("checkhealth success1 success2 test_plug")
helpers.expect([[
+ ==============================================================================
success1: health#success1#check
- ========================================================================
- ## report 1
- - OK: everything is fine
- ## report 2
- - OK: nothing to see here
+ report 1 ~
+ - OK everything is fine
+
+ report 2 ~
+ - OK nothing to see here
+ ==============================================================================
success2: health#success2#check
- ========================================================================
- ## another 1
- - OK: ok
+ another 1 ~
+ - OK ok
+
+ ==============================================================================
test_plug: require("test_plug.health").check()
- ========================================================================
- ## report 1
- - OK: everything is fine
- ## report 2
- - OK: nothing to see here
+ report 1 ~
+ - OK everything is fine
+
+ report 2 ~
+ - OK nothing to see here
]])
end)
@@ -107,13 +110,14 @@ describe('health.vim', function()
-- and the Lua healthcheck is used instead.
helpers.expect([[
+ ==============================================================================
test_plug: require("test_plug.health").check()
- ========================================================================
- ## report 1
- - OK: everything is fine
- ## report 2
- - OK: nothing to see here
+ report 1 ~
+ - OK everything is fine
+
+ report 2 ~
+ - OK nothing to see here
]])
end)
@@ -121,13 +125,14 @@ describe('health.vim', function()
command("checkhealth test_plug.submodule")
helpers.expect([[
+ ==============================================================================
test_plug.submodule: require("test_plug.submodule.health").check()
- ========================================================================
- ## report 1
- - OK: everything is fine
- ## report 2
- - OK: nothing to see here
+ report 1 ~
+ - OK everything is fine
+
+ report 2 ~
+ - OK nothing to see here
]])
end)
@@ -138,30 +143,34 @@ describe('health.vim', function()
local received = table.concat(buf_lines, '\n', 1, #buf_lines - 5)
local expected = helpers.dedent([[
+ ==============================================================================
test_plug: require("test_plug.health").check()
- ========================================================================
- ## report 1
- - OK: everything is fine
- ## report 2
- - OK: nothing to see here
+ report 1 ~
+ - OK everything is fine
+
+ report 2 ~
+ - OK nothing to see here
+ ==============================================================================
test_plug.submodule: require("test_plug.submodule.health").check()
- ========================================================================
- ## report 1
- - OK: everything is fine
- ## report 2
- - OK: nothing to see here
+ report 1 ~
+ - OK everything is fine
+ report 2 ~
+ - OK nothing to see here
+
+ ==============================================================================
test_plug.submodule_empty: require("test_plug.submodule_empty.health").check()
- ========================================================================
- - ERROR: The healthcheck report for "test_plug.submodule_empty" plugin is empty.
+ - ERROR The healthcheck report for "test_plug.submodule_empty" plugin is empty.
+
+ ==============================================================================
test_plug.submodule_failed: require("test_plug.submodule_failed.health").check()
- ========================================================================
- - ERROR: Failed to run healthcheck for "test_plug.submodule_failed" plugin. Exception:
- function health#check, line 20]])
+
+ - ERROR Failed to run healthcheck for "test_plug.submodule_failed" plugin. Exception:
+ function health#check, line 25]])
eq(expected, received)
end)
@@ -169,11 +178,12 @@ describe('health.vim', function()
command("checkhealth broken")
helpers.expect([[
+ ==============================================================================
broken: health#broken#check
- ========================================================================
- - ERROR: Failed to run healthcheck for "broken" plugin. Exception:
- function health#check[20]..health#broken#check, line 1
- caused an error
+
+ - ERROR Failed to run healthcheck for "broken" plugin. Exception:
+ function health#check[25]..health#broken#check, line 1
+ caused an error
]])
end)
@@ -181,9 +191,10 @@ describe('health.vim', function()
command("checkhealth test_plug.submodule_empty")
helpers.expect([[
+ ==============================================================================
test_plug.submodule_empty: require("test_plug.submodule_empty.health").check()
- ========================================================================
- - ERROR: The healthcheck report for "test_plug.submodule_empty" plugin is empty.
+
+ - ERROR The healthcheck report for "test_plug.submodule_empty" plugin is empty.
]])
end)
@@ -198,38 +209,38 @@ describe('health.vim', function()
local expected = global_helpers.dedent([[
+ ==============================================================================
test_plug.submodule_failed: require("test_plug.submodule_failed.health").check()
- ========================================================================
- - ERROR: Failed to run healthcheck for "test_plug.submodule_failed" plugin. Exception:
- function health#check, line 20]])
+
+ - ERROR Failed to run healthcheck for "test_plug.submodule_failed" plugin. Exception:
+ function health#check, line 25]])
eq(expected, received)
end)
it("highlights OK, ERROR", function()
- local screen = Screen.new(72, 10)
+ local screen = Screen.new(50, 12)
screen:attach()
screen:set_default_attr_ids({
Ok = { foreground = Screen.colors.Grey3, background = 6291200 },
Error = { foreground = Screen.colors.Grey100, background = Screen.colors.Red },
- Heading = { bold=true, foreground=Screen.colors.Magenta },
- Heading2 = { foreground = Screen.colors.SlateBlue },
- Bar = { foreground = 0x6a0dad },
- Bullet = { bold=true, foreground=Screen.colors.Brown },
+ Heading = { foreground = tonumber('0x6a0dad') },
+ Bar = { foreground = Screen.colors.LightGrey, background = Screen.colors.DarkGrey },
})
command("checkhealth foo success1")
- command("1tabclose")
- command("set laststatus=0")
+ command("set nowrap laststatus=0")
screen:expect{grid=[[
- ^ |
- {Heading:foo: } |
- {Bar:========================================================================}|
- {Bullet: -} {Error:ERROR}: No healthcheck found for "foo" plugin. |
- |
- {Heading:success1: health#success1#check} |
- {Bar:========================================================================}|
- {Heading2:##}{Heading: report 1} |
- {Bullet: -} {Ok:OK}: everything is fine |
- |
+ ^ |
+ {Bar:──────────────────────────────────────────────────}|
+ {Heading:foo: } |
+ |
+ - {Error:ERROR} No healthcheck found for "foo" plugin. |
+ |
+ {Bar:──────────────────────────────────────────────────}|
+ {Heading:success1: health#success1#check} |
+ |
+ {Heading:report 1} |
+ - {Ok:OK} everything is fine |
+ |
]]}
end)
@@ -238,9 +249,10 @@ describe('health.vim', function()
-- luacheck: ignore 613
helpers.expect([[
+ ==============================================================================
non_existent_healthcheck:
- ========================================================================
- - ERROR: No healthcheck found for "non_existent_healthcheck" plugin.
+
+ - ERROR No healthcheck found for "non_existent_healthcheck" plugin.
]])
end)
diff --git a/test/functional/plugin/lsp/codelens_spec.lua b/test/functional/plugin/lsp/codelens_spec.lua
index ecc2f579b8..3d7a15a191 100644
--- a/test/functional/plugin/lsp/codelens_spec.lua
+++ b/test/functional/plugin/lsp/codelens_spec.lua
@@ -58,6 +58,41 @@ describe('vim.lsp.codelens', function()
]], bufnr)
eq({[1] = {'Lens1', 'LspCodeLens'}}, virtual_text_chunks)
+ end)
+
+ it('can clear all lens', function()
+ local fake_uri = "file:///fake/uri"
+ local bufnr = exec_lua([[
+ fake_uri = ...
+ local bufnr = vim.uri_to_bufnr(fake_uri)
+ local lines = {'So', 'many', 'lines'}
+ vim.fn.bufload(bufnr)
+ vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
+ return bufnr
+ ]], fake_uri)
+
+ exec_lua([[
+ local bufnr = ...
+ local lenses = {
+ {
+ range = {
+ start = { line = 0, character = 0, },
+ ['end'] = { line = 0, character = 0 }
+ },
+ command = { title = 'Lens1', command = 'Dummy' }
+ },
+ }
+ vim.lsp.codelens.on_codelens(nil, lenses, {method='textDocument/codeLens', client_id=1, bufnr=bufnr})
+ ]], bufnr)
+
+ local stored_lenses = exec_lua('return vim.lsp.codelens.get(...)', bufnr)
+ eq(1, #stored_lenses)
+
+ exec_lua([[
+ vim.lsp.codelens.clear()
+ ]])
+ stored_lenses = exec_lua('return vim.lsp.codelens.get(...)', bufnr)
+ eq(0, #stored_lenses)
end)
end)
diff --git a/test/functional/plugin/lsp/helpers.lua b/test/functional/plugin/lsp/helpers.lua
new file mode 100644
index 0000000000..028ccb9e2c
--- /dev/null
+++ b/test/functional/plugin/lsp/helpers.lua
@@ -0,0 +1,176 @@
+local helpers = require('test.functional.helpers')(nil)
+
+local clear = helpers.clear
+local exec_lua = helpers.exec_lua
+local run = helpers.run
+local stop = helpers.stop
+local NIL = helpers.NIL
+
+local M = {}
+
+function M.clear_notrace()
+ -- problem: here be dragons
+ -- solution: don't look too closely for dragons
+ clear {env={
+ NVIM_LUA_NOTRACK="1";
+ VIMRUNTIME=os.getenv"VIMRUNTIME";
+ }}
+end
+
+M.create_server_definition = [[
+ function _create_server(opts)
+ opts = opts or {}
+ local server = {}
+ server.messages = {}
+
+ function server.cmd(dispatchers)
+ local closing = false
+ local handlers = opts.handlers or {}
+ local srv = {}
+
+ function srv.request(method, params, callback)
+ table.insert(server.messages, {
+ method = method,
+ params = params,
+ })
+ local handler = handlers[method]
+ if handler then
+ local response, err = handler(method, params)
+ callback(err, response)
+ elseif method == 'initialize' then
+ callback(nil, {
+ capabilities = opts.capabilities or {}
+ })
+ elseif method == 'shutdown' then
+ callback(nil, nil)
+ end
+ local request_id = #server.messages
+ return true, request_id
+ end
+
+ function srv.notify(method, params)
+ table.insert(server.messages, {
+ method = method,
+ params = params
+ })
+ if method == 'exit' then
+ dispatchers.on_exit(0, 15)
+ end
+ end
+
+ function srv.is_closing()
+ return closing
+ end
+
+ function srv.terminate()
+ closing = true
+ end
+
+ return srv
+ end
+
+ return server
+ end
+]]
+
+-- Fake LSP server.
+M.fake_lsp_code = 'test/functional/fixtures/fake-lsp-server.lua'
+M.fake_lsp_logfile = 'Xtest-fake-lsp.log'
+
+local function fake_lsp_server_setup(test_name, timeout_ms, options, settings)
+ exec_lua([=[
+ lsp = require('vim.lsp')
+ local test_name, fixture_filename, logfile, timeout, options, settings = ...
+ TEST_RPC_CLIENT_ID = lsp.start_client {
+ cmd_env = {
+ NVIM_LOG_FILE = logfile;
+ NVIM_LUA_NOTRACK = "1";
+ };
+ cmd = {
+ vim.v.progpath, '-Es', '-u', 'NONE', '--headless',
+ "-c", string.format("lua TEST_NAME = %q", test_name),
+ "-c", string.format("lua TIMEOUT = %d", timeout),
+ "-c", "luafile "..fixture_filename,
+ };
+ handlers = setmetatable({}, {
+ __index = function(t, method)
+ return function(...)
+ return vim.rpcrequest(1, 'handler', ...)
+ end
+ end;
+ });
+ workspace_folders = {{
+ uri = 'file://' .. vim.loop.cwd(),
+ name = 'test_folder',
+ }};
+ on_init = function(client, result)
+ TEST_RPC_CLIENT = client
+ vim.rpcrequest(1, "init", result)
+ end;
+ flags = {
+ allow_incremental_sync = options.allow_incremental_sync or false;
+ debounce_text_changes = options.debounce_text_changes or 0;
+ };
+ settings = settings;
+ on_exit = function(...)
+ vim.rpcnotify(1, "exit", ...)
+ end;
+ }
+ ]=], test_name, M.fake_lsp_code, M.fake_lsp_logfile, timeout_ms or 1e3, options or {}, settings or {})
+end
+
+function M.test_rpc_server(config)
+ if config.test_name then
+ M.clear_notrace()
+ fake_lsp_server_setup(config.test_name, config.timeout_ms or 1e3, config.options, config.settings)
+ end
+ local client = setmetatable({}, {
+ __index = function(_, name)
+ -- Workaround for not being able to yield() inside __index for Lua 5.1 :(
+ -- Otherwise I would just return the value here.
+ return function(...)
+ return exec_lua([=[
+ local name = ...
+ if type(TEST_RPC_CLIENT[name]) == 'function' then
+ return TEST_RPC_CLIENT[name](select(2, ...))
+ else
+ return TEST_RPC_CLIENT[name]
+ end
+ ]=], name, ...)
+ end
+ end;
+ })
+ local code, signal
+ local function on_request(method, args)
+ if method == "init" then
+ if config.on_init then
+ config.on_init(client, unpack(args))
+ end
+ return NIL
+ end
+ if method == 'handler' then
+ if config.on_handler then
+ config.on_handler(unpack(args))
+ end
+ end
+ return NIL
+ end
+ local function on_notify(method, args)
+ if method == 'exit' then
+ code, signal = unpack(args)
+ return stop()
+ end
+ end
+ -- TODO specify timeout?
+ -- run(on_request, on_notify, config.on_setup, 1000)
+ run(on_request, on_notify, config.on_setup)
+ if config.on_exit then
+ config.on_exit(code, signal)
+ end
+ stop()
+ if config.test_name then
+ exec_lua("vim.api.nvim_exec_autocmds('VimLeavePre', { modeline = false })")
+ end
+end
+
+return M
diff --git a/test/functional/plugin/lsp/semantic_tokens_spec.lua b/test/functional/plugin/lsp/semantic_tokens_spec.lua
new file mode 100644
index 0000000000..9c1ba86fe1
--- /dev/null
+++ b/test/functional/plugin/lsp/semantic_tokens_spec.lua
@@ -0,0 +1,1219 @@
+local helpers = require('test.functional.helpers')(after_each)
+local lsp_helpers = require('test.functional.plugin.lsp.helpers')
+local Screen = require('test.functional.ui.screen')
+
+local command = helpers.command
+local dedent = helpers.dedent
+local eq = helpers.eq
+local exec_lua = helpers.exec_lua
+local feed = helpers.feed
+local feed_command = helpers.feed_command
+local insert = helpers.insert
+local matches = helpers.matches
+
+local clear_notrace = lsp_helpers.clear_notrace
+local create_server_definition = lsp_helpers.create_server_definition
+
+before_each(function()
+ clear_notrace()
+end)
+
+after_each(function()
+ exec_lua("vim.api.nvim_exec_autocmds('VimLeavePre', { modeline = false })")
+end)
+
+describe('semantic token highlighting', function()
+
+ describe('general', function()
+ local text = dedent([[
+ #include <iostream>
+
+ int main()
+ {
+ int x;
+ #ifdef __cplusplus
+ std::cout << x << "\n";
+ #else
+ printf("%d\n", x);
+ #endif
+ }
+ }]])
+
+ local legend = [[{
+ "tokenTypes": [
+ "variable", "variable", "parameter", "function", "method", "function", "property", "variable", "class", "interface", "enum", "enumMember", "type", "type", "unknown", "namespace", "typeParameter", "concept", "type", "macro", "comment"
+ ],
+ "tokenModifiers": [
+ "declaration", "deprecated", "deduced", "readonly", "static", "abstract", "virtual", "dependentName", "defaultLibrary", "usedAsMutableReference", "functionScope", "classScope", "fileScope", "globalScope"
+ ]
+ }]]
+
+ local response = [[{
+ "data": [ 2, 4, 4, 3, 8193, 2, 8, 1, 1, 1025, 1, 7, 11, 19, 8192, 1, 4, 3, 15, 8448, 0, 5, 4, 0, 8448, 0, 8, 1, 1, 1024, 1, 0, 5, 20, 0, 1, 0, 22, 20, 0, 1, 0, 6, 20, 0 ],
+ "resultId": 1
+ }]]
+
+ local edit_response = [[{
+ "edits": [ {"data": [ 2, 8, 1, 3, 8193, 1, 7, 11, 19, 8192, 1, 4, 3, 15, 8448, 0, 5, 4, 0, 8448, 0, 8, 1, 3, 8192 ], "deleteCount": 25, "start": 5 } ],
+ "resultId":"2"
+ }]]
+
+ local screen
+ before_each(function()
+ screen = Screen.new(40, 16)
+ screen:attach()
+ screen:set_default_attr_ids {
+ [1] = { bold = true, foreground = Screen.colors.Blue1 };
+ [2] = { foreground = Screen.colors.DarkCyan };
+ [3] = { foreground = Screen.colors.SlateBlue };
+ [4] = { bold = true, foreground = Screen.colors.SeaGreen };
+ [5] = { foreground = tonumber('0x6a0dad') };
+ [6] = { foreground = Screen.colors.Blue1 };
+ [7] = { bold = true, foreground = Screen.colors.DarkCyan };
+ [8] = { bold = true, foreground = Screen.colors.SlateBlue };
+ }
+ command([[ hi link @namespace Type ]])
+ command([[ hi link @function Special ]])
+ command([[ hi @declaration gui=bold ]])
+
+ exec_lua(create_server_definition)
+ exec_lua([[
+ local legend, response, edit_response = ...
+ server = _create_server({
+ capabilities = {
+ semanticTokensProvider = {
+ full = { delta = true },
+ legend = vim.fn.json_decode(legend),
+ },
+ },
+ handlers = {
+ ['textDocument/semanticTokens/full'] = function()
+ return vim.fn.json_decode(response)
+ end,
+ ['textDocument/semanticTokens/full/delta'] = function()
+ return vim.fn.json_decode(edit_response)
+ end,
+ }
+ })
+ ]], legend, response, edit_response)
+ end)
+
+ it('buffer is highlighted when attached', function()
+ exec_lua([[
+ bufnr = vim.api.nvim_get_current_buf()
+ vim.api.nvim_win_set_buf(0, bufnr)
+ client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
+ ]])
+
+ insert(text)
+
+ screen:expect { grid = [[
+ #include <iostream> |
+ |
+ int {8:main}() |
+ { |
+ int {7:x}; |
+ #ifdef {5:__cplusplus} |
+ {4:std}::{2:cout} << {2:x} << "\n"; |
+ {6:#else} |
+ {6: printf("%d\n", x);} |
+ {6:#endif} |
+ } |
+ ^} |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]] }
+ end)
+
+ it('buffer is unhighlighted when client is detached', function()
+ exec_lua([[
+ bufnr = vim.api.nvim_get_current_buf()
+ vim.api.nvim_win_set_buf(0, bufnr)
+ client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
+ ]])
+
+ insert(text)
+
+ exec_lua([[
+ vim.notify = function() end
+ vim.lsp.buf_detach_client(bufnr, client_id)
+ ]])
+
+ screen:expect { grid = [[
+ #include <iostream> |
+ |
+ int main() |
+ { |
+ int x; |
+ #ifdef __cplusplus |
+ std::cout << x << "\n"; |
+ #else |
+ printf("%d\n", x); |
+ #endif |
+ } |
+ ^} |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]] }
+ end)
+
+ it('buffer is highlighted and unhighlighted when semantic token highlighting is started and stopped'
+ , function()
+ exec_lua([[
+ bufnr = vim.api.nvim_get_current_buf()
+ vim.api.nvim_win_set_buf(0, bufnr)
+ client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
+ ]])
+
+ insert(text)
+
+ exec_lua([[
+ vim.notify = function() end
+ vim.lsp.semantic_tokens.stop(bufnr, client_id)
+ ]])
+
+ screen:expect { grid = [[
+ #include <iostream> |
+ |
+ int main() |
+ { |
+ int x; |
+ #ifdef __cplusplus |
+ std::cout << x << "\n"; |
+ #else |
+ printf("%d\n", x); |
+ #endif |
+ } |
+ ^} |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]] }
+
+ exec_lua([[
+ vim.lsp.semantic_tokens.start(bufnr, client_id)
+ ]])
+
+ screen:expect { grid = [[
+ #include <iostream> |
+ |
+ int {8:main}() |
+ { |
+ int {7:x}; |
+ #ifdef {5:__cplusplus} |
+ {4:std}::{2:cout} << {2:x} << "\n"; |
+ {6:#else} |
+ {6: printf("%d\n", x);} |
+ {6:#endif} |
+ } |
+ ^} |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]] }
+ end)
+
+ it('buffer is re-highlighted when force refreshed', function()
+ exec_lua([[
+ bufnr = vim.api.nvim_get_current_buf()
+ vim.api.nvim_win_set_buf(0, bufnr)
+ client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
+ ]])
+
+ insert(text)
+
+ screen:expect { grid = [[
+ #include <iostream> |
+ |
+ int {8:main}() |
+ { |
+ int {7:x}; |
+ #ifdef {5:__cplusplus} |
+ {4:std}::{2:cout} << {2:x} << "\n"; |
+ {6:#else} |
+ {6: printf("%d\n", x);} |
+ {6:#endif} |
+ } |
+ ^} |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]] }
+
+ exec_lua([[
+ vim.lsp.semantic_tokens.force_refresh(bufnr)
+ ]])
+
+ screen:expect { grid = [[
+ #include <iostream> |
+ |
+ int {8:main}() |
+ { |
+ int {7:x}; |
+ #ifdef {5:__cplusplus} |
+ {4:std}::{2:cout} << {2:x} << "\n"; |
+ {6:#else} |
+ {6: printf("%d\n", x);} |
+ {6:#endif} |
+ } |
+ ^} |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]], unchanged = true }
+
+ local messages = exec_lua('return server.messages')
+ local token_request_count = 0
+ for _, message in ipairs(messages) do
+ assert(message.method ~= 'textDocument/semanticTokens/full/delta', 'delta request received')
+ if message.method == 'textDocument/semanticTokens/full' then
+ token_request_count = token_request_count + 1
+ end
+ end
+ eq(2, token_request_count)
+ end)
+
+ it('destroys the highlighter if the buffer is deleted', function()
+ exec_lua([[
+ bufnr = vim.api.nvim_get_current_buf()
+ vim.api.nvim_win_set_buf(0, bufnr)
+ client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
+ ]])
+
+ insert(text)
+
+ local highlighters = exec_lua([[
+ vim.api.nvim_buf_delete(bufnr, { force = true })
+ local semantic_tokens = vim.lsp.semantic_tokens
+ return semantic_tokens.__STHighlighter.active
+ ]])
+
+ eq({}, highlighters)
+ end)
+
+ it('updates highlights with delta request on buffer change', function()
+ exec_lua([[
+ bufnr = vim.api.nvim_get_current_buf()
+ vim.api.nvim_win_set_buf(0, bufnr)
+ client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
+ ]])
+
+ insert(text)
+ screen:expect { grid = [[
+ #include <iostream> |
+ |
+ int {8:main}() |
+ { |
+ int {7:x}; |
+ #ifdef {5:__cplusplus} |
+ {4:std}::{2:cout} << {2:x} << "\n"; |
+ {6:#else} |
+ {6: printf("%d\n", x);} |
+ {6:#endif} |
+ } |
+ ^} |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]] }
+ feed_command('%s/int x/int x()/')
+ feed_command('noh')
+ screen:expect { grid = [[
+ #include <iostream> |
+ |
+ int {8:main}() |
+ { |
+ ^int {8:x}(); |
+ #ifdef {5:__cplusplus} |
+ {4:std}::{2:cout} << {3:x} << "\n"; |
+ {6:#else} |
+ {6: printf("%d\n", x);} |
+ {6:#endif} |
+ } |
+ } |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ :noh |
+ ]] }
+ end)
+
+ it('prevents starting semantic token highlighting with invalid conditions', function()
+ exec_lua([[
+ bufnr = vim.api.nvim_get_current_buf()
+ vim.api.nvim_win_set_buf(0, bufnr)
+ client_id = vim.lsp.start_client({ name = 'dummy', cmd = server.cmd })
+ notifications = {}
+ vim.notify = function(...) table.insert(notifications, 1, {...}) end
+ ]])
+ eq(false, exec_lua("return vim.lsp.buf_is_attached(bufnr, client_id)"))
+
+ insert(text)
+
+ local notifications = exec_lua([[
+ vim.lsp.semantic_tokens.start(bufnr, client_id)
+ return notifications
+ ]])
+ matches('%[LSP%] Client with id %d not attached to buffer %d', notifications[1][1])
+
+ notifications = exec_lua([[
+ vim.lsp.semantic_tokens.start(bufnr, client_id + 1)
+ return notifications
+ ]])
+ matches('%[LSP%] No client with id %d', notifications[1][1])
+ end)
+
+ it('opt-out: does not activate semantic token highlighting if disabled in client attach',
+ function()
+ exec_lua([[
+ bufnr = vim.api.nvim_get_current_buf()
+ vim.api.nvim_win_set_buf(0, bufnr)
+ client_id = vim.lsp.start({
+ name = 'dummy',
+ cmd = server.cmd,
+ on_attach = vim.schedule_wrap(function(client, bufnr)
+ client.server_capabilities.semanticTokensProvider = nil
+ end),
+ })
+ ]])
+ eq(true, exec_lua("return vim.lsp.buf_is_attached(bufnr, client_id)"))
+
+ insert(text)
+
+ screen:expect { grid = [[
+ #include <iostream> |
+ |
+ int main() |
+ { |
+ int x; |
+ #ifdef __cplusplus |
+ std::cout << x << "\n"; |
+ #else |
+ printf("%d\n", x); |
+ #endif |
+ } |
+ ^} |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]] }
+
+ local notifications = exec_lua([[
+ local notifications = {}
+ vim.notify = function(...) table.insert(notifications, 1, {...}) end
+ vim.lsp.semantic_tokens.start(bufnr, client_id)
+ return notifications
+ ]])
+ eq('[LSP] Server does not support semantic tokens', notifications[1][1])
+
+ screen:expect { grid = [[
+ #include <iostream> |
+ |
+ int main() |
+ { |
+ int x; |
+ #ifdef __cplusplus |
+ std::cout << x << "\n"; |
+ #else |
+ printf("%d\n", x); |
+ #endif |
+ } |
+ ^} |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]], unchanged = true }
+ end)
+
+ it('ignores null responses from the server', function()
+ exec_lua([[
+ local legend, response, edit_response = ...
+ server2 = _create_server({
+ capabilities = {
+ semanticTokensProvider = {
+ full = { delta = false },
+ },
+ },
+ handlers = {
+ ['textDocument/semanticTokens/full'] = function()
+ return nil
+ end,
+ ['textDocument/semanticTokens/full/delta'] = function()
+ return nil
+ end,
+ }
+ })
+ bufnr = vim.api.nvim_get_current_buf()
+ vim.api.nvim_win_set_buf(0, bufnr)
+ client_id = vim.lsp.start({ name = 'dummy', cmd = server2.cmd })
+ ]])
+ eq(true, exec_lua("return vim.lsp.buf_is_attached(bufnr, client_id)"))
+
+ insert(text)
+
+ screen:expect { grid = [[
+ #include <iostream> |
+ |
+ int main() |
+ { |
+ int x; |
+ #ifdef __cplusplus |
+ std::cout << x << "\n"; |
+ #else |
+ printf("%d\n", x); |
+ #endif |
+ } |
+ ^} |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]] }
+ end)
+
+ it('does not send delta requests if not supported by server', function()
+ exec_lua([[
+ local legend, response, edit_response = ...
+ server2 = _create_server({
+ capabilities = {
+ semanticTokensProvider = {
+ full = { delta = false },
+ legend = vim.fn.json_decode(legend),
+ },
+ },
+ handlers = {
+ ['textDocument/semanticTokens/full'] = function()
+ return vim.fn.json_decode(response)
+ end,
+ ['textDocument/semanticTokens/full/delta'] = function()
+ return vim.fn.json_decode(edit_response)
+ end,
+ }
+ })
+ bufnr = vim.api.nvim_get_current_buf()
+ vim.api.nvim_win_set_buf(0, bufnr)
+ client_id = vim.lsp.start({ name = 'dummy', cmd = server2.cmd })
+ ]], legend, response, edit_response)
+
+ insert(text)
+ screen:expect { grid = [[
+ #include <iostream> |
+ |
+ int {8:main}() |
+ { |
+ int {7:x}; |
+ #ifdef {5:__cplusplus} |
+ {4:std}::{2:cout} << {2:x} << "\n"; |
+ {6:#else} |
+ {6: printf("%d\n", x);} |
+ {6:#endif} |
+ } |
+ ^} |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ |
+ ]] }
+ feed_command('%s/int x/int x()/')
+ feed_command('noh')
+
+ -- the highlights don't change because our fake server sent the exact
+ -- same result for the same method (the full request). "x" would have
+ -- changed to highlight index 3 had we sent a delta request
+ screen:expect { grid = [[
+ #include <iostream> |
+ |
+ int {8:main}() |
+ { |
+ ^int {7:x}(); |
+ #ifdef {5:__cplusplus} |
+ {4:std}::{2:cout} << {2:x} << "\n"; |
+ {6:#else} |
+ {6: printf("%d\n", x);} |
+ {6:#endif} |
+ } |
+ } |
+ {1:~ }|
+ {1:~ }|
+ {1:~ }|
+ :noh |
+ ]] }
+ local messages = exec_lua('return server2.messages')
+ local token_request_count = 0
+ for _, message in ipairs(messages) do
+ assert(message.method ~= 'textDocument/semanticTokens/full/delta', 'delta request received')
+ if message.method == 'textDocument/semanticTokens/full' then
+ token_request_count = token_request_count + 1
+ end
+ end
+ eq(2, token_request_count)
+ end)
+ end)
+
+ describe('token array decoding', function()
+ for _, test in ipairs({
+ {
+ it = 'clangd-15 on C',
+ text = [[char* foo = "\n";]],
+ response = [[{"data": [0, 6, 3, 0, 8193], "resultId": "1"}]],
+ legend = [[{
+ "tokenTypes": [
+ "variable", "variable", "parameter", "function", "method", "function", "property", "variable", "class", "interface", "enum", "enumMember", "type", "type", "unknown", "namespace", "typeParameter", "concept", "type", "macro", "comment"
+ ],
+ "tokenModifiers": [
+ "declaration", "deprecated", "deduced", "readonly", "static", "abstract", "virtual", "dependentName", "defaultLibrary", "usedAsMutableReference", "functionScope", "classScope", "fileScope", "globalScope"
+ ]
+ }]],
+ expected = {
+ {
+ line = 0,
+ modifiers = {
+ 'declaration',
+ 'globalScope',
+ },
+ start_col = 6,
+ end_col = 9,
+ type = 'variable',
+ extmark_added = true,
+ },
+ },
+ },
+ {
+ it = 'clangd-15 on C++',
+ text = [[#include <iostream>
+int main()
+{
+ #ifdef __cplusplus
+ const int x = 1;
+ std::cout << x << std::endl;
+ #else
+ comment
+ #endif
+}]],
+ response = [[{"data": [1, 4, 4, 3, 8193, 2, 9, 11, 19, 8192, 1, 12, 1, 1, 1033, 1, 2, 3, 15, 8448, 0, 5, 4, 0, 8448, 0, 8, 1, 1, 1032, 0, 5, 3, 15, 8448, 0, 5, 4, 3, 8448, 1, 0, 7, 20, 0, 1, 0, 11, 20, 0, 1, 0, 8, 20, 0], "resultId": "1"}]],
+ legend = [[{
+ "tokenTypes": [
+ "variable", "variable", "parameter", "function", "method", "function", "property", "variable", "class", "interface", "enum", "enumMember", "type", "type", "unknown", "namespace", "typeParameter", "concept", "type", "macro", "comment"
+ ],
+ "tokenModifiers": [
+ "declaration", "deprecated", "deduced", "readonly", "static", "abstract", "virtual", "dependentName", "defaultLibrary", "usedAsMutableReference", "functionScope", "classScope", "fileScope", "globalScope"
+ ]
+ }]],
+ expected = {
+ { -- main
+ line = 1,
+ modifiers = { 'declaration', 'globalScope' },
+ start_col = 4,
+ end_col = 8,
+ type = 'function',
+ extmark_added = true,
+ },
+ { -- __cplusplus
+ line = 3,
+ modifiers = { 'globalScope' },
+ start_col = 9,
+ end_col = 20,
+ type = 'macro',
+ extmark_added = true,
+ },
+ { -- x
+ line = 4,
+ modifiers = { 'declaration', 'readonly', 'functionScope' },
+ start_col = 12,
+ end_col = 13,
+ type = 'variable',
+ extmark_added = true,
+ },
+ { -- std
+ line = 5,
+ modifiers = { 'defaultLibrary', 'globalScope' },
+ start_col = 2,
+ end_col = 5,
+ type = 'namespace',
+ extmark_added = true,
+ },
+ { -- cout
+ line = 5,
+ modifiers = { 'defaultLibrary', 'globalScope' },
+ start_col = 7,
+ end_col = 11,
+ type = 'variable',
+ extmark_added = true,
+ },
+ { -- x
+ line = 5,
+ modifiers = { 'readonly', 'functionScope' },
+ start_col = 15,
+ end_col = 16,
+ type = 'variable',
+ extmark_added = true,
+ },
+ { -- std
+ line = 5,
+ modifiers = { 'defaultLibrary', 'globalScope' },
+ start_col = 20,
+ end_col = 23,
+ type = 'namespace',
+ extmark_added = true,
+ },
+ { -- endl
+ line = 5,
+ modifiers = { 'defaultLibrary', 'globalScope' },
+ start_col = 25,
+ end_col = 29,
+ type = 'function',
+ extmark_added = true,
+ },
+ { -- #else comment #endif
+ line = 6,
+ modifiers = {},
+ start_col = 0,
+ end_col = 7,
+ type = 'comment',
+ extmark_added = true,
+ },
+ {
+ line = 7,
+ modifiers = {},
+ start_col = 0,
+ end_col = 11,
+ type = 'comment',
+ extmark_added = true,
+ },
+ {
+ line = 8,
+ modifiers = {},
+ start_col = 0,
+ end_col = 8,
+ type = 'comment',
+ extmark_added = true,
+ },
+ },
+ },
+ {
+ it = 'sumneko_lua',
+ text = [[-- comment
+local a = 1
+b = "as"]],
+ response = [[{"data": [0, 0, 10, 17, 0, 1, 6, 1, 8, 1, 1, 0, 1, 8, 8]}]],
+ legend = [[{
+ "tokenTypes": [
+ "namespace", "type", "class", "enum", "interface", "struct", "typeParameter", "parameter", "variable", "property", "enumMember", "event", "function", "method", "macro", "keyword", "modifier", "comment", "string", "number", "regexp", "operator"
+ ],
+ "tokenModifiers": [
+ "declaration", "definition", "readonly", "static", "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary"
+ ]
+ }]],
+ expected = {
+ {
+ line = 0,
+ modifiers = {},
+ start_col = 0,
+ end_col = 10,
+ type = 'comment', -- comment
+ extmark_added = true,
+ },
+ {
+ line = 1,
+ modifiers = { 'declaration' }, -- a
+ start_col = 6,
+ end_col = 7,
+ type = 'variable',
+ extmark_added = true,
+ },
+ {
+ line = 2,
+ modifiers = { 'static' }, -- b (global)
+ start_col = 0,
+ end_col = 1,
+ type = 'variable',
+ extmark_added = true,
+ },
+ },
+ },
+ {
+ it = 'rust-analyzer',
+ text = [[pub fn main() {
+ break rust;
+ /// what?
+}
+]],
+ response = [[{"data": [0, 0, 3, 1, 0, 0, 4, 2, 1, 0, 0, 3, 4, 14, 524290, 0, 4, 1, 45, 0, 0, 1, 1, 45, 0, 0, 2, 1, 26, 0, 1, 4, 5, 1, 8192, 0, 6, 4, 52, 0, 0, 4, 1, 48, 0, 1, 4, 9, 0, 1, 1, 0, 1, 26, 0], "resultId": "1"}]],
+ legend = [[{
+ "tokenTypes": [
+ "comment", "keyword", "string", "number", "regexp", "operator", "namespace", "type", "struct", "class", "interface", "enum", "enumMember", "typeParameter", "function", "method", "property", "macro", "variable",
+ "parameter", "angle", "arithmetic", "attribute", "attributeBracket", "bitwise", "boolean", "brace", "bracket", "builtinAttribute", "builtinType", "character", "colon", "comma", "comparison", "constParameter", "derive",
+ "dot", "escapeSequence", "formatSpecifier", "generic", "label", "lifetime", "logical", "macroBang", "operator", "parenthesis", "punctuation", "selfKeyword", "semicolon", "typeAlias", "toolModule", "union", "unresolvedReference"
+ ],
+ "tokenModifiers": [
+ "documentation", "declaration", "definition", "static", "abstract", "deprecated", "readonly", "defaultLibrary", "async", "attribute", "callable", "constant", "consuming", "controlFlow", "crateRoot", "injected", "intraDocLink",
+ "library", "mutable", "public", "reference", "trait", "unsafe"
+ ]
+ }]],
+ expected = {
+ {
+ line = 0,
+ modifiers = {},
+ start_col = 0,
+ end_col = 3, -- pub
+ type = 'keyword',
+ extmark_added = true,
+ },
+ {
+ line = 0,
+ modifiers = {},
+ start_col = 4,
+ end_col = 6, -- fn
+ type = 'keyword',
+ extmark_added = true,
+ },
+ {
+ line = 0,
+ modifiers = { 'declaration', 'public' },
+ start_col = 7,
+ end_col = 11, -- main
+ type = 'function',
+ extmark_added = true,
+ },
+ {
+ line = 0,
+ modifiers = {},
+ start_col = 11,
+ end_col = 12,
+ type = 'parenthesis',
+ extmark_added = true,
+ },
+ {
+ line = 0,
+ modifiers = {},
+ start_col = 12,
+ end_col = 13,
+ type = 'parenthesis',
+ extmark_added = true,
+ },
+ {
+ line = 0,
+ modifiers = {},
+ start_col = 14,
+ end_col = 15,
+ type = 'brace',
+ extmark_added = true,
+ },
+ {
+ line = 1,
+ modifiers = { 'controlFlow' },
+ start_col = 4,
+ end_col = 9, -- break
+ type = 'keyword',
+ extmark_added = true,
+ },
+ {
+ line = 1,
+ modifiers = {},
+ start_col = 10,
+ end_col = 13, -- rust
+ type = 'unresolvedReference',
+ extmark_added = true,
+ },
+ {
+ line = 1,
+ modifiers = {},
+ start_col = 13,
+ end_col = 13,
+ type = 'semicolon',
+ extmark_added = true,
+ },
+ {
+ line = 2,
+ modifiers = { 'documentation' },
+ start_col = 4,
+ end_col = 11,
+ type = 'comment', -- /// what?
+ extmark_added = true,
+ },
+ {
+ line = 3,
+ modifiers = {},
+ start_col = 0,
+ end_col = 1,
+ type = 'brace',
+ extmark_added = true,
+ },
+ },
+ },
+ }) do
+ it(test.it, function()
+ exec_lua(create_server_definition)
+ exec_lua([[
+ local legend, resp = ...
+ server = _create_server({
+ capabilities = {
+ semanticTokensProvider = {
+ full = { delta = false },
+ legend = vim.fn.json_decode(legend),
+ },
+ },
+ handlers = {
+ ['textDocument/semanticTokens/full'] = function()
+ return vim.fn.json_decode(resp)
+ end,
+ }
+ })
+ bufnr = vim.api.nvim_get_current_buf()
+ vim.api.nvim_win_set_buf(0, bufnr)
+ client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
+ ]], test.legend, test.response)
+
+ insert(test.text)
+
+ local highlights = exec_lua([[
+ local semantic_tokens = vim.lsp.semantic_tokens
+ return semantic_tokens.__STHighlighter.active[bufnr].client_state[client_id].current_result.highlights
+ ]])
+ eq(test.expected, highlights)
+ end)
+ end
+ end)
+
+ describe('token decoding with deltas', function()
+ for _, test in ipairs({
+ {
+ it = 'semantic_tokens_delta: clangd-15 on C',
+ legend = [[{
+ "tokenTypes": [
+ "variable", "variable", "parameter", "function", "method", "function", "property", "variable", "class", "interface", "enum", "enumMember", "type", "type", "unknown", "namespace", "typeParameter", "concept", "type", "macro", "comment"
+ ],
+ "tokenModifiers": [
+ "declaration", "deprecated", "deduced", "readonly", "static", "abstract", "virtual", "dependentName", "defaultLibrary", "usedAsMutableReference", "functionScope", "classScope", "fileScope", "globalScope"
+ ]
+ }]],
+ text1 = [[char* foo = "\n";]],
+ edit = [[ggO<Esc>]],
+ response1 = [[{"data": [0, 6, 3, 0, 8193], "resultId": "1"}]],
+ response2 = [[{"edits": [{ "start": 0, "deleteCount": 1, "data": [1] }], "resultId": "2"}]],
+ expected1 = {
+ {
+ line = 0,
+ modifiers = {
+ 'declaration',
+ 'globalScope',
+ },
+ start_col = 6,
+ end_col = 9,
+ type = 'variable',
+ extmark_added = true,
+ }
+ },
+ expected2 = {
+ {
+ line = 1,
+ modifiers = {
+ 'declaration',
+ 'globalScope',
+ },
+ start_col = 6,
+ end_col = 9,
+ type = 'variable',
+ extmark_added = true,
+ }
+ },
+ },
+ {
+ it = 'response with multiple delta edits',
+ legend = [[{
+ "tokenTypes": [
+ "variable", "variable", "parameter", "function", "method", "function", "property", "variable", "class", "interface", "enum", "enumMember", "type", "type", "unknown", "namespace", "typeParameter", "concept", "type", "macro", "comment"
+ ],
+ "tokenModifiers": [
+ "declaration", "deprecated", "deduced", "readonly", "static", "abstract", "virtual", "dependentName", "defaultLibrary", "usedAsMutableReference", "functionScope", "classScope", "fileScope", "globalScope"
+ ]
+ }]],
+ text1 = dedent([[
+ #include <iostream>
+
+ int main()
+ {
+ int x;
+ #ifdef __cplusplus
+ std::cout << x << "\n";
+ #else
+ printf("%d\n", x);
+ #endif
+ }]]),
+ text2 = [[#include <iostream>
+
+int main()
+{
+ int x();
+ double y;
+#ifdef __cplusplus
+ std::cout << x << "\n";
+#else
+ printf("%d\n", x);
+#endif
+}]],
+ response1 = [[{
+ "data": [ 2, 4, 4, 3, 8193, 2, 8, 1, 1, 1025, 1, 7, 11, 19, 8192, 1, 4, 3, 15, 8448, 0, 5, 4, 0, 8448, 0, 8, 1, 1, 1024, 1, 0, 5, 20, 0, 1, 0, 22, 20, 0, 1, 0, 6, 20, 0 ],
+ "resultId": 1
+ }]],
+ response2 = [[{
+ "edits": [ {"data": [ 2, 8, 1, 3, 8193, 1, 11, 1, 1, 1025 ], "deleteCount": 5, "start": 5}, {"data": [ 0, 8, 1, 3, 8192 ], "deleteCount": 5, "start": 25 } ],
+ "resultId":"2"
+ }]],
+ expected1 = {
+ {
+ line = 2,
+ start_col = 4,
+ end_col = 8,
+ modifiers = { 'declaration', 'globalScope' },
+ type = 'function',
+ extmark_added = true,
+ },
+ {
+ line = 4,
+ start_col = 8,
+ end_col = 9,
+ modifiers = { 'declaration', 'functionScope' },
+ type = 'variable',
+ extmark_added = true,
+ },
+ {
+ line = 5,
+ start_col = 7,
+ end_col = 18,
+ modifiers = { 'globalScope' },
+ type = 'macro',
+ extmark_added = true,
+ },
+ {
+ line = 6,
+ start_col = 4,
+ end_col = 7,
+ modifiers = { 'defaultLibrary', 'globalScope' },
+ type = 'namespace',
+ extmark_added = true,
+ },
+ {
+ line = 6,
+ start_col = 9,
+ end_col = 13,
+ modifiers = { 'defaultLibrary', 'globalScope' },
+ type = 'variable',
+ extmark_added = true,
+ },
+ {
+ line = 6,
+ start_col = 17,
+ end_col = 18,
+ extmark_added = true,
+ modifiers = { 'functionScope' },
+ type = 'variable',
+ },
+ {
+ line = 7,
+ start_col = 0,
+ end_col = 5,
+ extmark_added = true,
+ modifiers = {},
+ type = 'comment',
+ },
+ {
+ line = 8,
+ end_col = 22,
+ modifiers = {},
+ start_col = 0,
+ type = 'comment',
+ extmark_added = true,
+ },
+ {
+ line = 9,
+ start_col = 0,
+ end_col = 6,
+ modifiers = {},
+ type = 'comment',
+ extmark_added = true,
+ }
+ },
+ expected2 = {
+ {
+ line = 2,
+ start_col = 4,
+ end_col = 8,
+ modifiers = { 'declaration', 'globalScope' },
+ type = 'function',
+ extmark_added = true,
+ },
+ {
+ line = 4,
+ start_col = 8,
+ end_col = 9,
+ modifiers = { 'declaration', 'globalScope' },
+ type = 'function',
+ extmark_added = true,
+ },
+ {
+ line = 5,
+ end_col = 12,
+ start_col = 11,
+ modifiers = { 'declaration', 'functionScope' },
+ type = 'variable',
+ extmark_added = true,
+ },
+ {
+ line = 6,
+ start_col = 7,
+ end_col = 18,
+ modifiers = { 'globalScope' },
+ type = 'macro',
+ extmark_added = true,
+ },
+ {
+ line = 7,
+ start_col = 4,
+ end_col = 7,
+ modifiers = { 'defaultLibrary', 'globalScope' },
+ type = 'namespace',
+ extmark_added = true,
+ },
+ {
+ line = 7,
+ start_col = 9,
+ end_col = 13,
+ modifiers = { 'defaultLibrary', 'globalScope' },
+ type = 'variable',
+ extmark_added = true,
+ },
+ {
+ line = 7,
+ start_col = 17,
+ end_col = 18,
+ extmark_added = true,
+ modifiers = { 'globalScope' },
+ type = 'function',
+ },
+ {
+ line = 8,
+ start_col = 0,
+ end_col = 5,
+ extmark_added = true,
+ modifiers = {},
+ type = 'comment',
+ },
+ {
+ line = 9,
+ end_col = 22,
+ modifiers = {},
+ start_col = 0,
+ type = 'comment',
+ extmark_added = true,
+ },
+ {
+ line = 10,
+ start_col = 0,
+ end_col = 6,
+ modifiers = {},
+ type = 'comment',
+ extmark_added = true,
+ }
+ },
+ },
+ {
+ it = 'optional token_edit.data on deletion',
+ legend = [[{
+ "tokenTypes": [
+ "comment", "keyword", "operator", "string", "number", "regexp", "type", "class", "interface", "enum", "enumMember", "typeParameter", "function", "method", "property", "variable", "parameter", "module", "intrinsic", "selfParameter", "clsParameter", "magicFunction", "builtinConstant", "parenthesis", "curlybrace", "bracket", "colon", "semicolon", "arrow"
+ ],
+ "tokenModifiers": [
+ "declaration", "static", "abstract", "async", "documentation", "typeHint", "typeHintComment", "readonly", "decorator", "builtin"
+ ]
+ }]],
+ text1 = [[string = "test"]],
+ text2 = [[]],
+ response1 = [[{"data": [0, 0, 6, 15, 1], "resultId": "1"}]],
+ response2 = [[{"edits": [{ "start": 0, "deleteCount": 5 }], "resultId": "2"}]],
+ expected1 = {
+ {
+ line = 0,
+ modifiers = {
+ 'declaration',
+ },
+ start_col = 0,
+ end_col = 6,
+ type = 'variable',
+ extmark_added = true,
+ }
+ },
+ expected2 = {
+ },
+ },
+ }) do
+ it(test.it, function()
+ exec_lua(create_server_definition)
+ exec_lua([[
+ local legend, resp1, resp2 = ...
+ server = _create_server({
+ capabilities = {
+ semanticTokensProvider = {
+ full = { delta = true },
+ legend = vim.fn.json_decode(legend),
+ },
+ },
+ handlers = {
+ ['textDocument/semanticTokens/full'] = function()
+ return vim.fn.json_decode(resp1)
+ end,
+ ['textDocument/semanticTokens/full/delta'] = function()
+ return vim.fn.json_decode(resp2)
+ end,
+ }
+ })
+ bufnr = vim.api.nvim_get_current_buf()
+ vim.api.nvim_win_set_buf(0, bufnr)
+ client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
+
+ -- speed up vim.api.nvim_buf_set_lines calls by changing debounce to 10 for these tests
+ semantic_tokens = vim.lsp.semantic_tokens
+ vim.schedule(function()
+ semantic_tokens.stop(bufnr, client_id)
+ semantic_tokens.start(bufnr, client_id, { debounce = 10 })
+ end)
+ ]], test.legend, test.response1, test.response2)
+
+ insert(test.text1)
+
+ local highlights = exec_lua([[
+ return semantic_tokens.__STHighlighter.active[bufnr].client_state[client_id].current_result.highlights
+ ]])
+
+ eq(test.expected1, highlights)
+
+ if test.edit then
+ feed(test.edit)
+ else
+ exec_lua([[
+ local text = ...
+ vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, vim.fn.split(text, "\n"))
+ vim.wait(15) -- wait fot debounce
+ ]], test.text2)
+ end
+
+ highlights = exec_lua([[
+ return semantic_tokens.__STHighlighter.active[bufnr].client_state[client_id].current_result.highlights
+ ]])
+
+ eq(test.expected2, highlights)
+ end)
+ end
+ end)
+end)
diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua
index cd7415de90..5229022564 100644
--- a/test/functional/plugin/lsp_spec.lua
+++ b/test/functional/plugin/lsp_spec.lua
@@ -1,8 +1,9 @@
local helpers = require('test.functional.helpers')(after_each)
+local lsp_helpers = require('test.functional.plugin.lsp.helpers')
local assert_log = helpers.assert_log
-local clear = helpers.clear
local buf_lines = helpers.buf_lines
+local clear = helpers.clear
local command = helpers.command
local dedent = helpers.dedent
local exec_lua = helpers.exec_lua
@@ -14,133 +15,28 @@ local pesc = helpers.pesc
local insert = helpers.insert
local funcs = helpers.funcs
local retry = helpers.retry
+local stop = helpers.stop
local NIL = helpers.NIL
local read_file = require('test.helpers').read_file
local write_file = require('test.helpers').write_file
-local isCI = helpers.isCI
+local is_ci = helpers.is_ci
local meths = helpers.meths
+local is_os = helpers.is_os
+local skip = helpers.skip
--- Use these to get access to a coroutine so that I can run async tests and use
--- yield.
-local run, stop = helpers.run, helpers.stop
+local clear_notrace = lsp_helpers.clear_notrace
+local create_server_definition = lsp_helpers.create_server_definition
+local fake_lsp_code = lsp_helpers.fake_lsp_code
+local fake_lsp_logfile = lsp_helpers.fake_lsp_logfile
+local test_rpc_server = lsp_helpers.test_rpc_server
-- TODO(justinmk): hangs on Windows https://github.com/neovim/neovim/pull/11837
-if helpers.pending_win32(pending) then return end
-
--- Fake LSP server.
-local fake_lsp_code = 'test/functional/fixtures/fake-lsp-server.lua'
-local fake_lsp_logfile = 'Xtest-fake-lsp.log'
+if skip(is_os('win')) then return end
teardown(function()
os.remove(fake_lsp_logfile)
end)
-local function clear_notrace()
- -- problem: here be dragons
- -- solution: don't look for dragons to closely
- clear {env={
- NVIM_LUA_NOTRACK="1";
- VIMRUNTIME=os.getenv"VIMRUNTIME";
- }}
-end
-
-
-local function fake_lsp_server_setup(test_name, timeout_ms, options, settings)
- exec_lua([=[
- lsp = require('vim.lsp')
- local test_name, fixture_filename, logfile, timeout, options, settings = ...
- TEST_RPC_CLIENT_ID = lsp.start_client {
- cmd_env = {
- NVIM_LOG_FILE = logfile;
- NVIM_LUA_NOTRACK = "1";
- };
- cmd = {
- vim.v.progpath, '-Es', '-u', 'NONE', '--headless',
- "-c", string.format("lua TEST_NAME = %q", test_name),
- "-c", string.format("lua TIMEOUT = %d", timeout),
- "-c", "luafile "..fixture_filename,
- };
- handlers = setmetatable({}, {
- __index = function(t, method)
- return function(...)
- return vim.rpcrequest(1, 'handler', ...)
- end
- end;
- });
- workspace_folders = {{
- uri = 'file://' .. vim.loop.cwd(),
- name = 'test_folder',
- }};
- on_init = function(client, result)
- TEST_RPC_CLIENT = client
- vim.rpcrequest(1, "init", result)
- end;
- flags = {
- allow_incremental_sync = options.allow_incremental_sync or false;
- debounce_text_changes = options.debounce_text_changes or 0;
- };
- settings = settings;
- on_exit = function(...)
- vim.rpcnotify(1, "exit", ...)
- end;
- }
- ]=], test_name, fake_lsp_code, fake_lsp_logfile, timeout_ms or 1e3, options or {}, settings or {})
-end
-
-local function test_rpc_server(config)
- if config.test_name then
- clear_notrace()
- fake_lsp_server_setup(config.test_name, config.timeout_ms or 1e3, config.options, config.settings)
- end
- local client = setmetatable({}, {
- __index = function(_, name)
- -- Workaround for not being able to yield() inside __index for Lua 5.1 :(
- -- Otherwise I would just return the value here.
- return function(...)
- return exec_lua([=[
- local name = ...
- if type(TEST_RPC_CLIENT[name]) == 'function' then
- return TEST_RPC_CLIENT[name](select(2, ...))
- else
- return TEST_RPC_CLIENT[name]
- end
- ]=], name, ...)
- end
- end;
- })
- local code, signal
- local function on_request(method, args)
- if method == "init" then
- if config.on_init then
- config.on_init(client, unpack(args))
- end
- return NIL
- end
- if method == 'handler' then
- if config.on_handler then
- config.on_handler(unpack(args))
- end
- end
- return NIL
- end
- local function on_notify(method, args)
- if method == 'exit' then
- code, signal = unpack(args)
- return stop()
- end
- end
- -- TODO specify timeout?
- -- run(on_request, on_notify, config.on_setup, 1000)
- run(on_request, on_notify, config.on_setup)
- if config.on_exit then
- config.on_exit(code, signal)
- end
- stop()
- if config.test_name then
- exec_lua("vim.api.nvim_exec_autocmds('VimLeavePre', { modeline = false })")
- end
-end
-
describe('LSP', function()
before_each(function()
clear_notrace()
@@ -236,9 +132,9 @@ describe('LSP', function()
end)
it('should invalid cmd argument', function()
- eq('Error executing lua: .../lsp.lua:0: cmd: expected list, got nvim',
+ eq('.../lsp.lua:0: cmd: expected list, got nvim',
pcall_err(_cmd_parts, 'nvim'))
- eq('Error executing lua: .../lsp.lua:0: cmd argument: expected string, got number',
+ eq('.../lsp.lua:0: cmd argument: expected string, got number',
pcall_err(_cmd_parts, {'nvim', 1}))
end)
end)
@@ -316,7 +212,7 @@ describe('LSP', function()
end)
it('should succeed with manual shutdown', function()
- if isCI() then
+ if is_ci() then
pending('hangs the build on CI #14028, re-enable with freeze timeout #14204')
return
elseif helpers.skip_fragile(pending) then
@@ -418,6 +314,31 @@ describe('LSP', function()
}
end)
+ it('should detach buffer on bufwipe', function()
+ clear()
+ exec_lua(create_server_definition)
+ local result = exec_lua([[
+ local server = _create_server()
+ local bufnr = vim.api.nvim_create_buf(false, true)
+ vim.api.nvim_set_current_buf(bufnr)
+ local client_id = vim.lsp.start({ name = 'detach-dummy', cmd = server.cmd })
+ assert(client_id, "lsp.start must return client_id")
+ local client = vim.lsp.get_client_by_id(client_id)
+ local num_attached_before = vim.tbl_count(client.attached_buffers)
+ vim.api.nvim_buf_delete(bufnr, { force = true })
+ local num_attached_after = vim.tbl_count(client.attached_buffers)
+ return {
+ bufnr = bufnr,
+ client_id = client_id,
+ num_attached_before = num_attached_before,
+ num_attached_after = num_attached_after,
+ }
+ ]])
+ eq(true, result ~= nil, "exec_lua must return result")
+ eq(1, result.num_attached_before)
+ eq(0, result.num_attached_after)
+ end)
+
it('client should return settings via workspace/configuration handler', function()
local expected_handlers = {
{NIL, {}, {method="shutdown", client_id=1}};
@@ -535,6 +456,70 @@ describe('LSP', function()
}
end)
+ it('BufWritePre does not send notifications if server lacks willSave capabilities', function()
+ clear()
+ exec_lua(create_server_definition)
+ local messages = exec_lua([[
+ local server = _create_server({
+ capabilities = {
+ textDocumentSync = {
+ willSave = false,
+ willSaveWaitUntil = false,
+ }
+ },
+ })
+ local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
+ local buf = vim.api.nvim_get_current_buf()
+ vim.api.nvim_exec_autocmds('BufWritePre', { buffer = buf, modeline = false })
+ vim.lsp.stop_client(client_id)
+ return server.messages
+ ]])
+ eq(#messages, 4)
+ eq(messages[1].method, 'initialize')
+ eq(messages[2].method, 'initialized')
+ eq(messages[3].method, 'shutdown')
+ eq(messages[4].method, 'exit')
+ end)
+
+ it('BufWritePre sends willSave / willSaveWaitUntil, applies textEdits', function()
+ clear()
+ exec_lua(create_server_definition)
+ local result = exec_lua([[
+ local server = _create_server({
+ capabilities = {
+ textDocumentSync = {
+ willSave = true,
+ willSaveWaitUntil = true,
+ }
+ },
+ handlers = {
+ ['textDocument/willSaveWaitUntil'] = function()
+ local text_edit = {
+ range = {
+ start = { line = 0, character = 0 },
+ ['end'] = { line = 0, character = 0 },
+ },
+ newText = 'Hello'
+ }
+ return { text_edit, }
+ end
+ },
+ })
+ local buf = vim.api.nvim_get_current_buf()
+ local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
+ vim.api.nvim_exec_autocmds('BufWritePre', { buffer = buf, modeline = false })
+ vim.lsp.stop_client(client_id)
+ return {
+ messages = server.messages,
+ lines = vim.api.nvim_buf_get_lines(buf, 0, -1, true)
+ }
+ ]])
+ local messages = result.messages
+ eq('textDocument/willSave', messages[3].method)
+ eq('textDocument/willSaveWaitUntil', messages[4].method)
+ eq({'Hello'}, result.lines)
+ end)
+
it('saveas sends didOpen if filename changed', function()
local expected_handlers = {
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
@@ -1682,6 +1667,46 @@ describe('LSP', function()
end)
end)
+ describe('apply_text_edits regression tests for #20116', function()
+ before_each(function()
+ insert(dedent([[
+ Test line one
+ Test line two 21 char]]))
+ end)
+ describe('with LSP end column out of bounds and start column at 0', function()
+ it('applies edits at the end of the buffer', function()
+ local edits = {
+ make_edit(0, 0, 1, 22, {'#include "whatever.h"\r\n#include <algorithm>\r'});
+ }
+ exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-8")
+ eq({'#include "whatever.h"', '#include <algorithm>'}, buf_lines(1))
+ end)
+ it('applies edits in the middle of the buffer', function()
+ local edits = {
+ make_edit(0, 0, 0, 22, {'#include "whatever.h"\r\n#include <algorithm>\r'});
+ }
+ exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-8")
+ eq({'#include "whatever.h"', '#include <algorithm>', 'Test line two 21 char'}, buf_lines(1))
+ end)
+ end)
+ describe('with LSP end column out of bounds and start column NOT at 0', function()
+ it('applies edits at the end of the buffer', function()
+ local edits = {
+ make_edit(0, 2, 1, 22, {'#include "whatever.h"\r\n#include <algorithm>\r'});
+ }
+ exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-8")
+ eq({'Te#include "whatever.h"', '#include <algorithm>'}, buf_lines(1))
+ end)
+ it('applies edits in the middle of the buffer', function()
+ local edits = {
+ make_edit(0, 2, 0, 22, {'#include "whatever.h"\r\n#include <algorithm>\r'});
+ }
+ exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1, "utf-8")
+ eq({'Te#include "whatever.h"', '#include <algorithm>', 'Test line two 21 char'}, buf_lines(1))
+ end)
+ end)
+ end)
+
describe('apply_text_document_edit', function()
local target_bufnr
local text_document_edit = function(editVersion)
@@ -1900,6 +1925,22 @@ describe('LSP', function()
exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16')
eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile))
end)
+ it('Supports file creation in folder that needs to be created with CreateFile payload', function()
+ local tmpfile = helpers.tmpname()
+ os.remove(tmpfile) -- Should not exist, only interested in a tmpname
+ tmpfile = tmpfile .. '/dummy/x/'
+ local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
+ local edit = {
+ documentChanges = {
+ {
+ kind = 'create',
+ uri = uri,
+ },
+ }
+ }
+ exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit, 'utf-16')
+ eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile))
+ end)
it('createFile does not touch file if it exists and ignoreIfExists is set', function()
local tmpfile = helpers.tmpname()
write_file(tmpfile, 'Dummy content')
@@ -2377,7 +2418,7 @@ describe('LSP', function()
},
uri = "file:///test_a"
},
- contanerName = "TestAContainer"
+ containerName = "TestAContainer"
},
{
deprecated = false,
@@ -2396,7 +2437,7 @@ describe('LSP', function()
},
uri = "file:///test_b"
},
- contanerName = "TestBContainer"
+ containerName = "TestBContainer"
}
}
return vim.lsp.util.symbols_to_items(sym_info, nil)
@@ -2493,7 +2534,7 @@ describe('LSP', function()
local mark = funcs.nvim_buf_get_mark(target_bufnr, "'")
eq({ 1, 0 }, mark)
- funcs.nvim_win_set_cursor(0, {2, 3})
+ funcs.nvim_win_set_cursor(0, { 2, 3 })
jump(location(0, 9, 0, 9))
mark = funcs.nvim_buf_get_mark(target_bufnr, "'")
@@ -2501,6 +2542,193 @@ describe('LSP', function()
end)
end)
+ describe('lsp.util.show_document', function()
+ local target_bufnr
+ local target_bufnr2
+
+ before_each(function()
+ target_bufnr = exec_lua([[
+ local bufnr = vim.uri_to_bufnr("file:///fake/uri")
+ local lines = {"1st line of text", "å å ɧ 汉语 ↥ 🤦 🦄"}
+ vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines)
+ return bufnr
+ ]])
+
+ target_bufnr2 = exec_lua([[
+ local bufnr = vim.uri_to_bufnr("file:///fake/uri2")
+ local lines = {"1st line of text", "å å ɧ 汉语 ↥ 🤦 🦄"}
+ vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines)
+ return bufnr
+ ]])
+ end)
+
+ local location = function(start_line, start_char, end_line, end_char, second_uri)
+ return {
+ uri = second_uri and 'file:///fake/uri2' or 'file:///fake/uri',
+ range = {
+ start = { line = start_line, character = start_char },
+ ['end'] = { line = end_line, character = end_char },
+ },
+ }
+ end
+
+ local show_document = function(msg, focus, reuse_win)
+ eq(
+ true,
+ exec_lua(
+ 'return vim.lsp.util.show_document(...)',
+ msg,
+ 'utf-16',
+ { reuse_win = reuse_win, focus = focus }
+ )
+ )
+ if focus == true or focus == nil then
+ eq(target_bufnr, exec_lua([[return vim.fn.bufnr('%')]]))
+ end
+ return {
+ line = exec_lua([[return vim.fn.line('.')]]),
+ col = exec_lua([[return vim.fn.col('.')]]),
+ }
+ end
+
+ it('jumps to a Location if focus is true', function()
+ local pos = show_document(location(0, 9, 0, 9), true, true)
+ eq(1, pos.line)
+ eq(10, pos.col)
+ end)
+
+ it('jumps to a Location if focus is true via handler', function()
+ exec_lua(create_server_definition)
+ local result = exec_lua([[
+ local server = _create_server()
+ local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
+ local result = {
+ uri = 'file:///fake/uri',
+ selection = {
+ start = { line = 0, character = 9 },
+ ['end'] = { line = 0, character = 9 }
+ },
+ takeFocus = true,
+ }
+ local ctx = {
+ client_id = client_id,
+ method = 'window/showDocument',
+ }
+ vim.lsp.handlers['window/showDocument'](nil, result, ctx)
+ vim.lsp.stop_client(client_id)
+ return {
+ cursor = vim.api.nvim_win_get_cursor(0)
+ }
+ ]])
+ eq(1, result.cursor[1])
+ eq(9, result.cursor[2])
+ end)
+
+ it('jumps to a Location if focus not set', function()
+ local pos = show_document(location(0, 9, 0, 9), nil, true)
+ eq(1, pos.line)
+ eq(10, pos.col)
+ end)
+
+ it('does not add current position to jumplist if not focus', function()
+ funcs.nvim_win_set_buf(0, target_bufnr)
+ local mark = funcs.nvim_buf_get_mark(target_bufnr, "'")
+ eq({ 1, 0 }, mark)
+
+ funcs.nvim_win_set_cursor(0, { 2, 3 })
+ show_document(location(0, 9, 0, 9), false, true)
+ show_document(location(0, 9, 0, 9, true), false, true)
+
+ mark = funcs.nvim_buf_get_mark(target_bufnr, "'")
+ eq({ 1, 0 }, mark)
+ end)
+
+ it('does not change cursor position if not focus and not reuse_win', function()
+ funcs.nvim_win_set_buf(0, target_bufnr)
+ local cursor = funcs.nvim_win_get_cursor(0)
+
+ show_document(location(0, 9, 0, 9), false, false)
+ eq(cursor, funcs.nvim_win_get_cursor(0))
+ end)
+
+ it('does not change window if not focus', function()
+ funcs.nvim_win_set_buf(0, target_bufnr)
+ local win = funcs.nvim_get_current_win()
+
+ -- same document/bufnr
+ show_document(location(0, 9, 0, 9), false, true)
+ eq(win, funcs.nvim_get_current_win())
+
+ -- different document/bufnr, new window/split
+ show_document(location(0, 9, 0, 9, true), false, true)
+ eq(2, #funcs.nvim_list_wins())
+ eq(win, funcs.nvim_get_current_win())
+ end)
+
+ it("respects 'reuse_win' parameter", function()
+ funcs.nvim_win_set_buf(0, target_bufnr)
+
+ -- does not create a new window if the buffer is already open
+ show_document(location(0, 9, 0, 9), false, true)
+ eq(1, #funcs.nvim_list_wins())
+
+ -- creates a new window even if the buffer is already open
+ show_document(location(0, 9, 0, 9), false, false)
+ eq(2, #funcs.nvim_list_wins())
+ end)
+
+ it('correctly sets the cursor of the split if range is given without focus', function()
+ funcs.nvim_win_set_buf(0, target_bufnr)
+
+ show_document(location(0, 9, 0, 9, true), false, true)
+
+ local wins = funcs.nvim_list_wins()
+ eq(2, #wins)
+ table.sort(wins)
+
+ eq({ 1, 0 }, funcs.nvim_win_get_cursor(wins[1]))
+ eq({ 1, 9 }, funcs.nvim_win_get_cursor(wins[2]))
+ end)
+
+ it('does not change cursor of the split if not range and not focus', function()
+ funcs.nvim_win_set_buf(0, target_bufnr)
+ funcs.nvim_win_set_cursor(0, { 2, 3 })
+
+ exec_lua([[vim.cmd.new()]])
+ funcs.nvim_win_set_buf(0, target_bufnr2)
+ funcs.nvim_win_set_cursor(0, { 2, 3 })
+
+ show_document({ uri = 'file:///fake/uri2' }, false, true)
+
+ local wins = funcs.nvim_list_wins()
+ eq(2, #wins)
+ eq({ 2, 3 }, funcs.nvim_win_get_cursor(wins[1]))
+ eq({ 2, 3 }, funcs.nvim_win_get_cursor(wins[2]))
+ end)
+
+ it('respects existing buffers', function()
+ funcs.nvim_win_set_buf(0, target_bufnr)
+ local win = funcs.nvim_get_current_win()
+
+ exec_lua([[vim.cmd.new()]])
+ funcs.nvim_win_set_buf(0, target_bufnr2)
+ funcs.nvim_win_set_cursor(0, { 2, 3 })
+ local split = funcs.nvim_get_current_win()
+
+ -- reuse win for open document/bufnr if called from split
+ show_document(location(0, 9, 0, 9, true), false, true)
+ eq({ 1, 9 }, funcs.nvim_win_get_cursor(split))
+ eq(2, #funcs.nvim_list_wins())
+
+ funcs.nvim_set_current_win(win)
+
+ -- reuse win for open document/bufnr if called outside the split
+ show_document(location(0, 9, 0, 9, true), false, true)
+ eq({ 1, 9 }, funcs.nvim_win_get_cursor(split))
+ eq(2, #funcs.nvim_list_wins())
+ end)
+ end)
+
describe('lsp.util._make_floating_popup_size', function()
before_each(function()
exec_lua [[ contents =
@@ -3180,5 +3408,159 @@ describe('LSP', function()
end,
}
end)
+ it('format formats range in visual mode', function()
+ exec_lua(create_server_definition)
+ local result = exec_lua([[
+ local server = _create_server({ capabilities = {
+ documentFormattingProvider = true,
+ documentRangeFormattingProvider = true,
+ }})
+ local bufnr = vim.api.nvim_get_current_buf()
+ local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
+ vim.api.nvim_win_set_buf(0, bufnr)
+ vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, {'foo', 'bar'})
+ vim.api.nvim_win_set_cursor(0, { 1, 0 })
+ vim.cmd.normal('v')
+ vim.api.nvim_win_set_cursor(0, { 2, 3 })
+ vim.lsp.buf.format({ bufnr = bufnr, false })
+ return server.messages
+ ]])
+ eq("textDocument/rangeFormatting", result[3].method)
+ local expected_range = {
+ start = { line = 0, character = 0 },
+ ['end'] = { line = 1, character = 4 },
+ }
+ eq(expected_range, result[3].params.range)
+ end)
+ it('Aborts with notify if no clients support requested method', function()
+ exec_lua(create_server_definition)
+ exec_lua([[
+ vim.notify = function(msg, _)
+ notify_msg = msg
+ end
+ ]])
+ local fail_msg = "[LSP] Format request failed, no matching language servers."
+ local function check_notify(name, formatting, range_formatting)
+ local timeout_msg = "[LSP][" .. name .. "] timeout"
+ exec_lua([[
+ local formatting, range_formatting, name = ...
+ local server = _create_server({ capabilities = {
+ documentFormattingProvider = formatting,
+ documentRangeFormattingProvider = range_formatting,
+ }})
+ vim.lsp.start({ name = name, cmd = server.cmd })
+ notify_msg = nil
+ vim.lsp.buf.format({ name = name, timeout_ms = 1 })
+ ]], formatting, range_formatting, name)
+ eq(formatting and timeout_msg or fail_msg, exec_lua('return notify_msg'))
+ exec_lua([[
+ notify_msg = nil
+ vim.lsp.buf.format({ name = name, timeout_ms = 1, range = {start={1, 0}, ['end']={1, 0}}})
+ ]])
+ eq(range_formatting and timeout_msg or fail_msg, exec_lua('return notify_msg'))
+ end
+ check_notify("none", false, false)
+ check_notify("formatting", true, false)
+ check_notify("rangeFormatting", false, true)
+ check_notify("both", true, true)
+ end)
+ end)
+ describe('cmd', function()
+ it('can connect to lsp server via rpc.connect', function()
+ local result = exec_lua [[
+ local uv = vim.loop
+ local server = uv.new_tcp()
+ local init = nil
+ server:bind('127.0.0.1', 0)
+ server:listen(127, function(err)
+ assert(not err, err)
+ local socket = uv.new_tcp()
+ server:accept(socket)
+ socket:read_start(require('vim.lsp.rpc').create_read_loop(function(body)
+ init = body
+ socket:close()
+ end))
+ end)
+ local port = server:getsockname().port
+ vim.lsp.start({ name = 'dummy', cmd = vim.lsp.rpc.connect('127.0.0.1', port) })
+ vim.wait(1000, function() return init ~= nil end)
+ assert(init, "server must receive `initialize` request")
+ server:close()
+ server:shutdown()
+ return vim.json.decode(init)
+ ]]
+ eq(result.method, "initialize")
+ end)
+ end)
+
+ describe('handlers', function()
+ it('handler can return false as response', function()
+ local result = exec_lua [[
+ local uv = vim.loop
+ local server = uv.new_tcp()
+ local messages = {}
+ local responses = {}
+ server:bind('127.0.0.1', 0)
+ server:listen(127, function(err)
+ assert(not err, err)
+ local socket = uv.new_tcp()
+ server:accept(socket)
+ socket:read_start(require('vim.lsp.rpc').create_read_loop(function(body)
+ local payload = vim.json.decode(body)
+ if payload.method then
+ table.insert(messages, payload.method)
+ if payload.method == 'initialize' then
+ local msg = vim.json.encode({
+ id = payload.id,
+ jsonrpc = '2.0',
+ result = {
+ capabilities = {}
+ },
+ })
+ socket:write(table.concat({'Content-Length: ', tostring(#msg), '\r\n\r\n', msg}))
+ elseif payload.method == 'initialized' then
+ local msg = vim.json.encode({
+ id = 10,
+ jsonrpc = '2.0',
+ method = 'dummy',
+ params = {},
+ })
+ socket:write(table.concat({'Content-Length: ', tostring(#msg), '\r\n\r\n', msg}))
+ end
+ else
+ table.insert(responses, payload)
+ socket:close()
+ end
+ end))
+ end)
+ local port = server:getsockname().port
+ local handler_called = false
+ vim.lsp.handlers['dummy'] = function(err, result)
+ handler_called = true
+ return false
+ end
+ local client_id = vim.lsp.start({ name = 'dummy', cmd = vim.lsp.rpc.connect('127.0.0.1', port) })
+ local client = vim.lsp.get_client_by_id(client_id)
+ vim.wait(1000, function() return #messages == 2 and handler_called and #responses == 1 end)
+ server:close()
+ server:shutdown()
+ return {
+ messages = messages,
+ handler_called = handler_called,
+ responses = responses }
+ ]]
+ local expected = {
+ messages = { 'initialize', 'initialized' },
+ handler_called = true,
+ responses = {
+ {
+ id = 10,
+ jsonrpc = '2.0',
+ result = false
+ }
+ }
+ }
+ eq(expected, result)
+ end)
end)
end)
diff --git a/test/functional/plugin/man_spec.lua b/test/functional/plugin/man_spec.lua
index c8da5a711f..c6c7d2b03d 100644
--- a/test/functional/plugin/man_spec.lua
+++ b/test/functional/plugin/man_spec.lua
@@ -1,10 +1,21 @@
local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
-local command, eval, rawfeed = helpers.command, helpers.eval, helpers.rawfeed
+local command, rawfeed = helpers.command, helpers.rawfeed
local clear = helpers.clear
+local exec_lua = helpers.exec_lua
local funcs = helpers.funcs
local nvim_prog = helpers.nvim_prog
local matches = helpers.matches
+local write_file = helpers.write_file
+local tmpname = helpers.tmpname
+local skip = helpers.skip
+local is_ci = helpers.is_ci
+
+clear()
+if funcs.executable('man') == 0 then
+ pending('missing "man" command', function() end)
+ return
+end
describe(':Man', function()
before_each(function()
@@ -44,7 +55,7 @@ describe(':Man', function()
|
]]}
- eval('man#init_pager()')
+ exec_lua[[require'man'.init_pager()]]
screen:expect([[
^this {b:is} {b:a} test |
@@ -68,7 +79,7 @@ describe(':Man', function()
|
]=]}
- eval('man#init_pager()')
+ exec_lua[[require'man'.init_pager()]]
screen:expect([[
^this {b:is }{bi:a }{biu:test} |
@@ -83,7 +94,7 @@ describe(':Man', function()
rawfeed([[
ithis i<C-v><C-h>is<C-v><C-h>s あ<C-v><C-h>あ test
with _<C-v><C-h>ö_<C-v><C-h>v_<C-v><C-h>e_<C-v><C-h>r_<C-v><C-h>s_<C-v><C-h>t_<C-v><C-h>r_<C-v><C-h>u_<C-v><C-h>̃_<C-v><C-h>c_<C-v><C-h>k te<C-v><ESC>[3mxt¶<C-v><ESC>[0m<ESC>]])
- eval('man#init_pager()')
+ exec_lua[[require'man'.init_pager()]]
screen:expect([[
^this {b:is} {b:あ} test |
@@ -99,7 +110,7 @@ describe(':Man', function()
i_<C-v><C-h>_b<C-v><C-h>be<C-v><C-h>eg<C-v><C-h>gi<C-v><C-h>in<C-v><C-h>ns<C-v><C-h>s
m<C-v><C-h>mi<C-v><C-h>id<C-v><C-h>d_<C-v><C-h>_d<C-v><C-h>dl<C-v><C-h>le<C-v><C-h>e
_<C-v><C-h>m_<C-v><C-h>i_<C-v><C-h>d_<C-v><C-h>__<C-v><C-h>d_<C-v><C-h>l_<C-v><C-h>e<ESC>]])
- eval('man#init_pager()')
+ exec_lua[[require'man'.init_pager()]]
screen:expect([[
{b:^_begins} |
@@ -115,7 +126,7 @@ describe(':Man', function()
i· ·<C-v><C-h>·
+<C-v><C-h>o
+<C-v><C-h>+<C-v><C-h>o<C-v><C-h>o double<ESC>]])
- eval('man#init_pager()')
+ exec_lua[[require'man'.init_pager()]]
screen:expect([[
^· {b:·} |
@@ -132,7 +143,7 @@ describe(':Man', function()
<C-v><C-[>[44m 4 <C-v><C-[>[45m 5 <C-v><C-[>[46m 6 <C-v><C-[>[47m 7 <C-v><C-[>[100m 8 <C-v><C-[>[101m 9
<C-v><C-[>[102m 10 <C-v><C-[>[103m 11 <C-v><C-[>[104m 12 <C-v><C-[>[105m 13 <C-v><C-[>[106m 14 <C-v><C-[>[107m 15
<C-v><C-[>[48:5:16m 16 <ESC>]])
- eval('man#init_pager()')
+ exec_lua[[require'man'.init_pager()]]
screen:expect([[
^ 0 1 2 3 |
@@ -149,4 +160,17 @@ describe(':Man', function()
local args = {nvim_prog, '--headless', '+autocmd VimLeave * echo "quit works!!"', '+Man!', '+call nvim_input("q")'}
matches('quit works!!', funcs.system(args, {'manpage contents'}))
end)
+
+ it('reports non-existent man pages for absolute paths', function()
+ skip(is_ci('cirrus'))
+ local actual_file = tmpname()
+ -- actual_file must be an absolute path to an existent file for us to test against it
+ matches('^/.+', actual_file)
+ write_file(actual_file, '')
+ local args = {nvim_prog, '--headless', '+:Man ' .. actual_file, '+q'}
+ matches(('Error detected while processing command line:\r\n' ..
+ 'man.lua: "no manual entry for %s"'):format(actual_file),
+ funcs.system(args, {''}))
+ os.remove(actual_file)
+ end)
end)
diff --git a/test/functional/plugin/shada_spec.lua b/test/functional/plugin/shada_spec.lua
index 6f22f865e6..93cf6d2b77 100644
--- a/test/functional/plugin/shada_spec.lua
+++ b/test/functional/plugin/shada_spec.lua
@@ -1,4 +1,5 @@
local helpers = require('test.functional.helpers')(after_each)
+local Screen = require('test.functional.ui.screen')
local clear = helpers.clear
local eq, meths, nvim_eval, nvim_command, nvim, exc_exec, funcs, nvim_feed, curbuf =
helpers.eq, helpers.meths, helpers.eval, helpers.command, helpers.nvim, helpers.exc_exec,
@@ -2139,7 +2140,7 @@ end)
describe('plugin/shada.vim', function()
local epoch = os.date('%Y-%m-%dT%H:%M:%S', 0)
- local eol = helpers.iswin() and '\r\n' or '\n'
+ local eol = helpers.is_os('win') and '\r\n' or '\n'
before_each(function()
-- Note: reset() is called explicitly in each test.
os.remove(fname)
@@ -2538,13 +2539,26 @@ describe('ftplugin/shada.vim', function()
end)
describe('syntax/shada.vim', function()
- local epoch = os.date('%Y-%m-%dT%H:%M:%S', 0)
+ local epoch = os.date('!%Y-%m-%dT%H:%M:%S', 0)
before_each(reset)
it('works', function()
nvim_command('syntax on')
nvim_command('setlocal syntax=shada')
nvim_command('set laststatus&')
+ local screen = Screen.new(60, 37)
+ screen:set_default_attr_ids {
+ [1] = {bold = true, foreground = Screen.colors.Brown};
+ [2] = {foreground = tonumber('0x6a0dad')};
+ [3] = {foreground = Screen.colors.Fuchsia};
+ [4] = {foreground = Screen.colors.Blue1};
+ [5] = {bold = true, foreground = Screen.colors.SeaGreen4};
+ [6] = {foreground = Screen.colors.SlateBlue};
+ [7] = {bold = true, reverse = true};
+ [8] = {bold = true, foreground = Screen.colors.Blue};
+ }
+ screen:attach()
+
curbuf('set_lines', 0, 1, true, {
'Header with timestamp ' .. epoch .. ':',
' % Key Value',
@@ -2580,6 +2594,46 @@ describe('syntax/shada.vim', function()
' % Key Description________ Value',
' + se place cursor at end TRUE',
})
+ screen:expect{grid=[=[
+ {1:^Header} with timestamp 1970{1:-}01{1:-}01{1:T}00{1::}00{1::}00: |
+ {2: % Key Value} |
+ {1: +} {3:t } {1:"}{3:test}{1:"} |
+ {1:Jump} with timestamp 1970{1:-}01{1:-}01{1:T}00{1::}00{1::}00: |
+ {2: % Key________ Description Value} |
+ {1: +} {3:n } {4:name } {3:'A'} |
+ {1: +} {3:f } {4:file name } {1:["}{3:foo}{1:"]} |
+ {1: +} {3:l } {4:line number} {3:2} |
+ {1: +} {3:c } {4:column } {3:-200} |
+ {1:Register} with timestamp 1970{1:-}01{1:-}01{1:T}00{1::}00{1::}00: |
+ {2: % Key Description Value} |
+ {1: +} {3:rc } {4:contents } {1:@} |
+ {1: | -} {1:{"}{3:abcdefghijklmnopqrstuvwxyz}{1:":} {3:1.0}{1:}} |
+ {1: +} {3:rt } {4:type } {1:CHARACTERWISE} |
+ {1: +} {3:rt } {4:type } {1:LINEWISE} |
+ {1: +} {3:rt } {4:type } {1:BLOCKWISE} |
+ {1:Replacement string} with timestamp 1970{1:-}01{1:-}01{1:T}00{1::}00{1::}00: |
+ {2: @ Description__________ Value} |
+ {1: -} {4::s replacement string} {1:CMD} |
+ {1: -} {4::s replacement string} {1:SEARCH} |
+ {1: -} {4::s replacement string} {1:EXPR} |
+ {1: -} {4::s replacement string} {1:INPUT} |
+ {1: -} {4::s replacement string} {1:DEBUG} |
+ {1:Buffer list} with timestamp 1970{1:-}01{1:-}01{1:T}00{1::}00{1::}00: |
+ {4: # Expected array of maps} |
+ = {1:[{="}{3:a}{1:":} {1:+(}{5:10}{1:)"}{3:ac}{6:\0}{3:df}{6:\n}{3:gi}{6:\"}{3:tt\.}{1:",} {1:TRUE:} {1:FALSE},} {1:[NIL,} {1:+(}{5:-}|
+ {5:10}{1:)""]]} |
+ {1:Buffer list} with timestamp 1970{1:-}01{1:-}01{1:T}00{1::}00{1::}00: |
+ {2: % Key Description Value} |
+ |
+ {2: % Key Description Value} |
+ {1:Header} with timestamp 1970{1:-}01{1:-}01{1:T}00{1::}00{1::}00: |
+ {2: % Key Description________ Value} |
+ {1: +} {3:se } {4:place cursor at end} {1:TRUE} |
+ {8:~ }|
+ {7:[No Name] [+] }|
+ |
+ ]=]}
+
nvim_command([[
function GetSyntax()
let lines = []
@@ -2613,7 +2667,7 @@ describe('syntax/shada.vim', function()
year = htsnum(os.date('%Y', 0)),
month = htsnum(os.date('%m', 0)),
day = htsnum(os.date('%d', 0)),
- hour = htsnum(os.date('%H', 0)),
+ hour = htsnum(os.date('!%H', 0)),
minute = htsnum(os.date('%M', 0)),
second = htsnum(os.date('%S', 0)),
}
@@ -2768,9 +2822,8 @@ describe('syntax/shada.vim', function()
{{'ShaDaEntryArray', 'ShaDaMsgpackShaDaKeyword'}, 'INPUT'},
},
{
- {{'ShaDaEntryArrayEntryStart'}, ' - '},
- {{'ShaDaEntryArrayDescription'}, ':s replacement string '},
- {{'ShaDaMsgpackShaDaKeyword'}, 'DEBUG'},
+ as(), ad(':s replacement string '),
+ {{'ShaDaEntryArray', 'ShaDaMsgpackShaDaKeyword'}, 'DEBUG'},
},
{
hname('Buffer list'), h(' with timestamp '),
@@ -2872,10 +2925,10 @@ describe('syntax/shada.vim', function()
mlh(' % Key Description________ Value'),
},
{
- {{'ShaDaEntryMapLongEntryStart'}, ' + '},
- {{'ShaDaEntryMapLongKey'}, 'se '},
- {{'ShaDaEntryMapLongDescription'}, 'place cursor at end '},
- {{'ShaDaMsgpackKeyword'}, 'TRUE'},
+ mles(' + '),
+ mlk('se '),
+ mld('place cursor at end '),
+ {{'ShaDaEntryMapLong', 'ShaDaMsgpackKeyword'}, 'TRUE'},
},
}
eq(exp, act)