tinymist/editors/neovim/spec/helpers.lua
Myriad-Dreamin c03898cd3d
Some checks failed
tinymist::ci / Duplicate Actions Detection (push) Has been cancelled
tinymist::ci / Check Clippy, Formatting, Completion, Documentation, and Tests (Linux) (push) Has been cancelled
tinymist::ci / Check Minimum Rust version and Tests (Windows) (push) Has been cancelled
tinymist::ci / prepare-build (push) Has been cancelled
tinymist::gh_pages / build-gh-pages (push) Has been cancelled
tinymist::ci / build-vsc-assets (push) Has been cancelled
tinymist::ci / build-vscode (push) Has been cancelled
tinymist::ci / build-vscode-others (push) Has been cancelled
tinymist::ci / publish-vscode (push) Has been cancelled
tinymist::ci / build-binary (push) Has been cancelled
tinymist::ci / E2E Tests (darwin-arm64 on macos-latest) (push) Has been cancelled
tinymist::ci / E2E Tests (linux-x64 on ubuntu-22.04) (push) Has been cancelled
tinymist::ci / E2E Tests (linux-x64 on ubuntu-latest) (push) Has been cancelled
tinymist::ci / E2E Tests (win32-x64 on windows-2019) (push) Has been cancelled
tinymist::ci / E2E Tests (win32-x64 on windows-latest) (push) Has been cancelled
feat: add a neovim plugin as the canonical lsp client implementation (#1842)
* fix: bad link

* feat(neovim): init lsp

* feat(neovim): add bootstrap script

* build: add notice
2025-06-25 22:12:55 +08:00

294 lines
9.2 KiB
Lua

local Tab = require 'std.nvim.tab'
local Window = require 'std.nvim.window'
local assert = require 'luassert'
local text = require 'std.text'
local fixtures = require 'spec.fixtures'
-- local progress = require 'lean.progress'
-- local util = require 'lean._util'
---@class LeanClientCapabilities : lsp.ClientCapabilities
---@field silentDiagnosticSupport? boolean Whether the client supports `DiagnosticWith.isSilent = true`.
---@class LeanClientConfig : vim.lsp.ClientConfig
---@field lean? LeanClientCapabilities
---Find the `vim.lsp.Client` attached to the given buffer.
---@param bufnr? number
---@return vim.lsp.Client?
function client_for(bufnr)
local clients = vim.lsp.get_clients { name = 'tinymist', bufnr = bufnr or 0 }
return clients[1]
end
local helpers = { _clean_buffer_counter = 1 }
---Feed some keystrokes into the current buffer, replacing termcodes.
function helpers.feed(contents, feed_opts)
feed_opts = feed_opts or 'mtx'
local to_feed = vim.api.nvim_replace_termcodes(contents, true, false, true)
vim.api.nvim_feedkeys(to_feed, feed_opts, true)
end
---Insert some text into the current buffer.
function helpers.insert(contents, feed_opts)
feed_opts = feed_opts or 'x'
helpers.feed('i' .. contents, feed_opts)
end
function helpers.all_lean_extmarks(buffer, start, end_)
local extmarks = {}
for namespace, ns_id in pairs(vim.api.nvim_get_namespaces()) do
if namespace:match '^lean.' then
vim.list_extend(
extmarks,
vim.api.nvim_buf_get_extmarks(buffer, ns_id, start, end_, { details = true })
)
end
end
return extmarks
end
---Move the cursor to a new location.
---
---Ideally this function wouldn't exist, and one would call `set_cursor`
---directly, but it does not fire `CursorMoved` autocmds.
---This function exists therefore to make tests which have slightly
---less implementation details in them (the manual firing of that autocmd).
---
---@param opts MoveCursorOpts
function helpers.move_cursor(opts)
local window = opts.window or Window:current()
window:move_cursor(opts.to)
end
---Search forward in the buffer for the given text.
---
---Fires `CursorMoved` if the cursor moves and fails if it does not.
function helpers.search(string)
local cursor = vim.api.nvim_win_get_cursor(0)
vim.fn.search(string)
assert.are_not.same(cursor, vim.api.nvim_win_get_cursor(0), 'Cursor did not move!')
vim.api.nvim_exec_autocmds('CursorMoved', {})
end
function helpers.wait_for_ready_lsp()
local succeeded, _ = vim.wait(15000, function()
local client = client_for(0)
return client and client.initialized or false
end)
assert.message('LSP server was never ready.').True(succeeded)
end
---Wait until a window that isn't one of the known ones shows up.
---@param known table
function helpers.wait_for_new_window(known)
local ids = vim
.iter(known)
:map(function(window)
return window.id
end)
:totable()
local new_window
local succeeded = vim.wait(1000, function()
new_window = vim.iter(vim.api.nvim_tabpage_list_wins(0)):find(function(window)
return not vim.tbl_contains(ids, window)
end)
return new_window
end)
assert.message('Never found a new window').is_true(succeeded)
return Window:from_id(new_window)
end
-- Even though we can delete a buffer, so should be able to reuse names,
-- we do this to ensure if a test fails, future ones still get new "files".
local function set_unique_name_so_we_always_have_a_separate_fake_file(bufnr)
local counter = helpers._clean_buffer_counter
helpers._clean_buffer_counter = helpers._clean_buffer_counter + 1
local unique_name = fixtures.project.child(('unittest-%d.typ'):format(counter))
vim.api.nvim_buf_set_name(bufnr, unique_name)
end
---Create a clean Lean buffer with the given contents.
---
---Waits for the LSP to be ready before proceeding with a given callback.
--
---Yes c(lean) may be a double entendre, and no I don't feel bad.
function helpers.clean_buffer(contents, callback)
local lines
-- Support a 1-arg version where we assume the contents is an empty buffer.
if callback == nil then
callback = contents
lines = {}
else
lines = vim.split(text.dedent(contents:gsub('^\n', '')):gsub('\n$', ''), '\n')
end
return function()
local bufnr = vim.api.nvim_create_buf(false, false)
set_unique_name_so_we_always_have_a_separate_fake_file(bufnr)
vim.bo[bufnr].swapfile = false
vim.bo[bufnr].filetype = 'typst'
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
-- isn't it fun how fragile the order of the below lines is, and how
-- BufWinEnter seems automatically called by `nvim_set_current_buf`, but
-- `BufEnter` seems not automatically called by `nvim_buf_call` so we
-- manually trigger it?
vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_exec_autocmds('BufEnter', { buffer = bufnr })
vim.api.nvim_buf_call(bufnr, callback)
-- FIXME: Deleting buffers seems good for keeping our tests clean, but is
-- broken on 0.11 with impossible to diagnose invalid buffer errors.
-- vim.api.nvim_buf_delete(bufnr, { force = true })
end
end
---Wait a few seconds for line diagnostics, erroring if none arrive.
function helpers.wait_for_line_diagnostics()
local params = vim.lsp.util.make_position_params(0, 'utf-16')
local succeeded, _ = vim.wait(15000, function()
if progress.at(params) == progress.Kind.processing then
return false
end
local diagnostics = util.lean_lsp_diagnostics { lnum = params.position.line }
return #diagnostics > 0
end)
assert.message('Waited for line diagnostics but none came.').True(succeeded)
end
function helpers.wait_for_filetype()
local result, _ = vim.wait(15000, function()
return vim.bo.filetype == 'typst'
end)
assert.message('filetype was never set').is_truthy(result)
end
---Assert about the current word.
local function has_current_word(_, arguments)
assert.is.equal(arguments[1], vim.fn.expand '<cword>')
return true
end
assert:register('assertion', 'current_word', has_current_word)
---Assert about the current line.
local function has_current_line(_, arguments)
assert.is.equal(arguments[1], vim.api.nvim_get_current_line())
return true
end
assert:register('assertion', 'current_line', has_current_line)
---Assert about the current cursor location.
local function has_current_cursor(_, arguments)
local window = arguments[1].window
if not window then
window = Window:current()
elseif type(window) == 'number' then
window = Window:from_id(window)
end
local got = window:cursor()
local column = arguments[1][2] or arguments[1].column or 0
local expected = { arguments[1][1] or got[1], column }
assert.are.same(expected, got)
return true
end
assert:register('assertion', 'current_cursor', has_current_cursor)
---Assert about the current tabpage.
local function has_current_tabpage(_, arguments)
assert.are.same(Tab:current(), arguments[1])
return true
end
assert:register('assertion', 'current_tabpage', has_current_tabpage)
---Assert about the current window.
local function has_current_window(_, arguments)
assert.are.same(Window:current(), arguments[1])
return true
end
assert:register('assertion', 'current_window', has_current_window)
local function _expected(arguments)
local expected = arguments[1][1] or arguments[1]
-- Handle cases where we're indeed checking for a real trailing newline.
local dedented = text.dedent(expected)
if dedented ~= expected then
expected = dedented:gsub('\n$', '')
end
return expected
end
---Assert about the entire buffer contents.
local function has_buf_contents(_, arguments)
local bufnr = arguments[1].bufnr or 0
local got = table.concat(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), '\n')
assert.is.equal(_expected(arguments), got)
return true
end
assert:register('assertion', 'contents', has_buf_contents)
assert:register('assertion', 'diff_contents', has_diff_contents)
local function has_highlighted_text(_, arguments)
local inspected = vim.inspect_pos(0)
local highlight = vim.iter(inspected.extmarks):find(function(mark)
return mark.opts.hl_group == 'widgetElementHighlight'
end)
assert.is_not_nil(highlight, ('No highlighted text found in %s'):format(vim.inspect(inspected)))
local got = vim.api.nvim_buf_get_text(
0,
highlight.row,
highlight.col,
highlight.end_row,
highlight.end_col,
{}
)[1]
assert.are.same(arguments[1], got)
return true
end
assert:register('assertion', 'highlighted_text', has_highlighted_text)
local function has_all(_, arguments)
local contents = arguments[1]
if type(contents) == 'table' then
contents = table.concat(contents, '\n')
end
local expected = arguments[2]
for _, string in pairs(expected) do
assert.has_match(string, contents, nil, true)
end
return true
end
assert:register('assertion', 'has_all', has_all)
---Assert a tabpage has the given windows open in it.
local function has_open_windows(_, arguments)
local expected
if arguments.n == 1 and type(arguments[1]) == 'table' then
expected = arguments[1]
expected.n = #expected
else
expected = arguments
end
local got = vim.api.nvim_tabpage_list_wins(0)
got.n = #got
table.sort(expected)
table.sort(got)
assert.are.same(expected, got)
return true
end
assert:register('assertion', 'windows', has_open_windows)
return helpers