test: typing for screen.lua

Very rough buts resolves most diagnostic errors and should provide
some useful hovers.
This commit is contained in:
Lewis Russell 2024-01-23 00:53:15 +00:00 committed by Lewis Russell
parent 89ffdebd20
commit 0054c18500
3 changed files with 230 additions and 117 deletions

View File

@ -352,7 +352,7 @@ function vim.schedule_wrap(fn)
end end
-- vim.fn.{func}(...) -- vim.fn.{func}(...)
---@private ---@nodoc
vim.fn = setmetatable({}, { vim.fn = setmetatable({}, {
__index = function(t, key) __index = function(t, key)
local _fn local _fn

View File

@ -251,9 +251,9 @@ function module.set_method_error(err)
end end
--- @param lsession test.Session --- @param lsession test.Session
--- @param request_cb function --- @param request_cb function?
--- @param notification_cb function --- @param notification_cb function?
--- @param setup_cb function --- @param setup_cb function?
--- @param timeout integer --- @param timeout integer
--- @return {[1]: integer, [2]: string} --- @return {[1]: integer, [2]: string}
function module.run_session(lsession, request_cb, notification_cb, setup_cb, timeout) function module.run_session(lsession, request_cb, notification_cb, setup_cb, timeout)
@ -642,6 +642,8 @@ function module.set_shell_powershell(fake)
return found return found
end end
---@param func function
---@return table<string,function>
function module.create_callindex(func) function module.create_callindex(func)
return setmetatable({}, { return setmetatable({}, {
--- @param tbl table<any,function> --- @param tbl table<any,function>

View File

@ -88,7 +88,28 @@ local function isempty(v)
return type(v) == 'table' and next(v) == nil return type(v) == 'table' and next(v) == nil
end end
--- @class test.functional.ui.screen.Grid
--- @field rows table[][]
--- @field width integer
--- @field height integer
--- @class test.functional.ui.screen --- @class test.functional.ui.screen
--- @field colors table<string,integer>
--- @field colornames table<integer,string>
--- @field uimeths table<string,function>
--- @field options? table<string,any>
--- @field timeout integer
--- @field win_position table<integer,table<string,integer>>
--- @field float_pos table<integer,table>
--- @field cmdline table<integer,table>
--- @field cmdline_block table[]
--- @field hl_groups table<string,integer>
--- @field messages table<integer,table>
--- @field private _cursor {grid:integer,row:integer,col:integer}
--- @field private _grids table<integer,test.functional.ui.screen.Grid>
--- @field private _grid_win_extmarks table<integer,table>
--- @field private _attr_table table<integer,table>
--- @field private _hl_info table<integer,table>
local Screen = {} local Screen = {}
Screen.__index = Screen Screen.__index = Screen
@ -103,13 +124,14 @@ end
local default_screen_timeout = default_timeout_factor * 3500 local default_screen_timeout = default_timeout_factor * 3500
function Screen._init_colors(session) local function _init_colors()
local session = get_session()
local status, rv = session:request('nvim_get_color_map') local status, rv = session:request('nvim_get_color_map')
if not status then if not status then
error('failed to get color map') error('failed to get color map')
end end
local colors = rv local colors = rv --- @type table<string,integer>
local colornames = {} local colornames = {} --- @type table<integer,string>
for name, rgb in pairs(colors) do for name, rgb in pairs(colors) do
-- we disregard the case that colornames might not be unique, as -- we disregard the case that colornames might not be unique, as
-- this is just a helper to get any canonical name of a color -- this is just a helper to get any canonical name of a color
@ -119,17 +141,14 @@ function Screen._init_colors(session)
Screen.colornames = colornames Screen.colornames = colornames
end end
--- @param width? integer
--- @param height? integer
--- @return test.functional.ui.screen
function Screen.new(width, height) function Screen.new(width, height)
if not Screen.colors then if not Screen.colors then
Screen._init_colors(get_session()) _init_colors()
end end
if not width then
width = 53
end
if not height then
height = 14
end
local self = setmetatable({ local self = setmetatable({
timeout = default_screen_timeout, timeout = default_screen_timeout,
title = '', title = '',
@ -166,8 +185,8 @@ function Screen.new(width, height)
_attr_table = { [0] = { {}, {} } }, _attr_table = { [0] = { {}, {} } },
_clear_attrs = nil, _clear_attrs = nil,
_new_attrs = false, _new_attrs = false,
_width = width, _width = width or 53,
_height = height, _height = height or 14,
_grids = {}, _grids = {},
_grid_win_extmarks = {}, _grid_win_extmarks = {},
_cursor = { _cursor = {
@ -177,6 +196,7 @@ function Screen.new(width, height)
}, },
_busy = false, _busy = false,
}, Screen) }, Screen)
local function ui(method, ...) local function ui(method, ...)
if self.rpc_async then if self.rpc_async then
self._session:notify('nvim_ui_' .. method, ...) self._session:notify('nvim_ui_' .. method, ...)
@ -187,7 +207,9 @@ function Screen.new(width, height)
end end
end end
end end
self.uimeths = create_callindex(ui) self.uimeths = create_callindex(ui)
return self return self
end end
@ -203,16 +225,25 @@ function Screen:set_rgb_cterm(val)
self._rgb_cterm = val self._rgb_cterm = val
end end
--- @class test.functional.ui.screen.Opts
--- @field ext_linegrid? boolean
--- @field ext_multigrid? boolean
--- @field ext_newgrid? boolean
--- @field ext_popupmenu? boolean
--- @field ext_wildmenu? boolean
--- @field rgb? boolean
--- @field _debug_float? boolean
--- @param options test.functional.ui.screen.Opts
--- @param session? test.Session
function Screen:attach(options, session) function Screen:attach(options, session)
if session == nil then session = session or get_session()
session = get_session() options = options or {}
end
if options == nil then
options = {}
end
if options.ext_linegrid == nil then if options.ext_linegrid == nil then
options.ext_linegrid = true options.ext_linegrid = true
end end
self._session = session self._session = session
self._options = options self._options = options
self._clear_attrs = (not options.ext_linegrid) and {} or nil self._clear_attrs = (not options.ext_linegrid) and {} or nil
@ -243,8 +274,11 @@ function Screen:try_resize_grid(grid, columns, rows)
self.uimeths.try_resize_grid(grid, columns, rows) self.uimeths.try_resize_grid(grid, columns, rows)
end end
--- @param option 'ext_linegrid'|'ext_multigrid'|'ext_popupmenu'|'ext_wildmenu'|'rgb'
--- @param value boolean
function Screen:set_option(option, value) function Screen:set_option(option, value)
self.uimeths.set_option(option, value) self.uimeths.set_option(option, value)
--- @diagnostic disable-next-line:no-unknown
self._options[option] = value self._options[option] = value
end end
@ -264,109 +298,154 @@ local ext_keys = {
'win_viewport', 'win_viewport',
} }
-- Asserts that the screen state eventually matches an expected state. local expect_keys = {
-- grid = true,
-- Can be called with positional args: attr_ids = true,
-- screen:expect(grid, [attr_ids]) condition = true,
-- screen:expect(condition) mouse_enabled = true,
-- or keyword args (supports more options): any = true,
-- screen:expect{grid=[[...]], cmdline={...}, condition=function() ... end} mode = true,
-- unchanged = true,
-- intermediate = true,
-- grid: Expected screen state (string). Each line represents a screen reset = true,
-- row. Last character of each row (typically "|") is stripped. timeout = true,
-- Common indentation is stripped. request_cb = true,
-- "{MATCH:x}" in a line is matched against Lua pattern `x`. hl_groups = true,
-- "*n" at the end of a line means it repeats `n` times. extmarks = true,
-- attr_ids: Expected text attributes. Screen rows are transformed according }
-- to this table, as follows: each substring S composed of
-- characters having the same attributes will be substituted by for _, v in ipairs(ext_keys) do
-- "{K:S}", where K is a key in `attr_ids`. Any unexpected expect_keys[v] = true
-- attributes in the final state are an error. end
-- Use an empty table for a text-only (no attributes) expectation.
-- Use screen:set_default_attr_ids() to define attributes for many --- @class test.function.ui.screen.Expect
-- expect() calls. ---
-- extmarks: Expected win_extmarks accumulated for the grids. For each grid, --- Expected screen state (string). Each line represents a screen
-- the win_extmark messages are accumulated into an array. --- row. Last character of each row (typically "|") is stripped.
-- condition: Function asserting some arbitrary condition. Return value is --- Common indentation is stripped.
-- ignored, throw an error (use eq() or similar) to signal failure. --- "{MATCH:x}" in a line is matched against Lua pattern `x`.
-- any: Lua pattern string expected to match a screen line. NB: the --- "*n" at the end of a line means it repeats `n` times.
-- following chars are magic characters --- @field grid? string
-- ( ) . % + - * ? [ ^ $ ---
-- and must be escaped with a preceding % for a literal match. --- Expected text attributes. Screen rows are transformed according
-- mode: Expected mode as signaled by "mode_change" event --- to this table, as follows: each substring S composed of
-- unchanged: Test that the screen state is unchanged since the previous --- characters having the same attributes will be substituted by
-- expect(...). Any flush event resulting in a different state is --- "{K:S}", where K is a key in `attr_ids`. Any unexpected
-- considered an error. Not observing any events until timeout --- attributes in the final state are an error.
-- is acceptable. --- Use an empty table for a text-only (no attributes) expectation.
-- intermediate:Test that the final state is the same as the previous expect, --- Use screen:set_default_attr_ids() to define attributes for many
-- but expect an intermediate state that is different. If possible --- expect() calls.
-- it is better to use an explicit screen:expect(...) for this --- @field attr_ids? table<integer,table<string,any>>
-- intermediate state. ---
-- reset: Reset the state internal to the test Screen before starting to --- Expected win_extmarks accumulated for the grids. For each grid,
-- receive updates. This should be used after command("redraw!") --- the win_extmark messages are accumulated into an array.
-- or some other mechanism that will invoke "redraw!", to check --- @field extmarks? table<integer,table>
-- that all screen state is transmitted again. This includes ---
-- state related to ext_ features as mentioned below. --- Function asserting some arbitrary condition. Return value is
-- timeout: maximum time that will be waited until the expected state is --- ignored, throw an error (use eq() or similar) to signal failure.
-- seen (or maximum time to observe an incorrect change when --- @field condition? fun()
-- `unchanged` flag is used) ---
-- --- Lua pattern string expected to match a screen line. NB: the
-- The following keys should be used to expect the state of various ext_ --- following chars are magic characters
-- features. Note that an absent key will assert that the item is currently --- ( ) . % + - * ? [ ^ $
-- NOT present on the screen, also when positional form is used. --- and must be escaped with a preceding % for a literal match.
-- --- @field any? string
-- popupmenu: Expected ext_popupmenu state, ---
-- cmdline: Expected ext_cmdline state, as an array of cmdlines of --- Expected mode as signaled by "mode_change" event
-- different level. --- @field mode? string
-- cmdline_block: Expected ext_cmdline block (for function definitions) ---
-- wildmenu_items: Expected items for ext_wildmenu --- Test that the screen state is unchanged since the previous
-- wildmenu_pos: Expected position for ext_wildmenu --- expect(...). Any flush event resulting in a different state is
--- considered an error. Not observing any events until timeout
--- is acceptable.
--- @field unchanged? boolean
---
--- Test that the final state is the same as the previous expect,
--- but expect an intermediate state that is different. If possible
--- it is better to use an explicit screen:expect(...) for this
--- intermediate state.
--- @field intermediate? boolean
---
--- Reset the state internal to the test Screen before starting to
--- receive updates. This should be used after command("redraw!")
--- or some other mechanism that will invoke "redraw!", to check
--- that all screen state is transmitted again. This includes
--- state related to ext_ features as mentioned below.
--- @field reset? boolean
---
--- maximum time that will be waited until the expected state is
--- seen (or maximum time to observe an incorrect change when
--- `unchanged` flag is used)
--- @field timeout? integer
---
--- @field mouse_enabled? boolean
---
--- @field win_viewport? table<integer,table<string,integer>>
--- @field float_pos? {[1]:integer,[2]:integer}
--- @field hl_groups? table<string,integer>
---
--- The following keys should be used to expect the state of various ext_
--- features. Note that an absent key will assert that the item is currently
--- NOT present on the screen, also when positional form is used.
---
--- Expected ext_popupmenu state,
--- @field popupmenu? table
---
--- Expected ext_cmdline state, as an array of cmdlines of
--- different level.
--- @field cmdline? table
---
--- Expected ext_cmdline block (for function definitions)
--- @field cmdline_block? table
---
--- items for ext_wildmenu
--- @field wildmenu_items? table
---
--- position for ext_wildmenu
--- @field wildmenu_pos? table
--- Asserts that the screen state eventually matches an expected state.
---
--- Can be called with positional args:
--- screen:expect(grid, [attr_ids])
--- screen:expect(condition)
--- or keyword args (supports more options):
--- screen:expect{grid=[[...]], cmdline={...}, condition=function() ... end}
---
--- @param expected string|function|test.function.ui.screen.Expect
--- @param attr_ids? table<integer,table<string,any>>
function Screen:expect(expected, attr_ids, ...) function Screen:expect(expected, attr_ids, ...)
local grid, condition = nil, nil --- @type string, fun()
local expected_rows = {} local grid, condition
assert(next({ ... }) == nil, 'invalid args to expect()') assert(next({ ... }) == nil, 'invalid args to expect()')
if type(expected) == 'table' then if type(expected) == 'table' then
assert(not (attr_ids ~= nil)) assert(attr_ids == nil)
local is_key = { for k, _ in
grid = true, pairs(expected --[[@as table<string,any>]])
attr_ids = true, do
condition = true, if not expect_keys[k] then
mouse_enabled = true,
any = true,
mode = true,
unchanged = true,
intermediate = true,
reset = true,
timeout = true,
request_cb = true,
hl_groups = true,
extmarks = true,
}
for _, v in ipairs(ext_keys) do
is_key[v] = true
end
for k, _ in pairs(expected) do
if not is_key[k] then
error("Screen:expect: Unknown keyword argument '" .. k .. "'") error("Screen:expect: Unknown keyword argument '" .. k .. "'")
end end
end end
grid = expected.grid grid = expected.grid
attr_ids = expected.attr_ids attr_ids = expected.attr_ids
condition = expected.condition condition = expected.condition
assert(not (expected.any ~= nil and grid ~= nil)) assert(expected.any == nil or grid == nil)
elseif type(expected) == 'string' then elseif type(expected) == 'string' then
grid = expected grid = expected
expected = {} expected = {}
elseif type(expected) == 'function' then elseif type(expected) == 'function' then
assert(not (attr_ids ~= nil)) assert(attr_ids == nil)
condition = expected condition = expected
expected = {} expected = {}
else else
assert(false) assert(false)
end end
if grid ~= nil then local expected_rows = {} --- @type string[]
if grid then
-- Remove the last line and dedent. Note that gsub returns more then one -- Remove the last line and dedent. Note that gsub returns more then one
-- value. -- value.
grid = dedent(grid:gsub('\n[ ]+$', ''), 0) grid = dedent(grid:gsub('\n[ ]+$', ''), 0)
@ -374,18 +453,23 @@ function Screen:expect(expected, attr_ids, ...)
table.insert(expected_rows, row) table.insert(expected_rows, row)
end end
end end
local attr_state = { local attr_state = {
ids = attr_ids or self._default_attr_ids, ids = attr_ids or self._default_attr_ids,
} }
if isempty(attr_ids) then if isempty(attr_ids) then
attr_state.ids = nil attr_state.ids = nil
end end
if self._options.ext_linegrid then if self._options.ext_linegrid then
attr_state.id_to_index = self:linegrid_check_attrs(attr_state.ids or {}) attr_state.id_to_index = self:linegrid_check_attrs(attr_state.ids or {})
end end
self._new_attrs = false self._new_attrs = false
self:_wait(function() self:_wait(function()
if condition ~= nil then if condition then
--- @type boolean, string
local status, res = pcall(condition) local status, res = pcall(condition)
if not status then if not status then
return tostring(res) return tostring(res)
@ -398,10 +482,10 @@ function Screen:expect(expected, attr_ids, ...)
local actual_rows = self:render(not expected.any, attr_state) local actual_rows = self:render(not expected.any, attr_state)
if expected.any ~= nil then if expected.any then
-- Search for `any` anywhere in the screen lines. -- Search for `any` anywhere in the screen lines.
local actual_screen_str = table.concat(actual_rows, '\n') local actual_screen_str = table.concat(actual_rows, '\n')
if nil == string.find(actual_screen_str, expected.any) then if not actual_screen_str:find(expected.any) then
return ( return (
'Failed to match any screen lines.\n' 'Failed to match any screen lines.\n'
.. 'Expected (anywhere): "' .. 'Expected (anywhere): "'
@ -414,9 +498,9 @@ function Screen:expect(expected, attr_ids, ...)
end end
end end
if grid ~= nil then if grid then
for i, row in ipairs(expected_rows) do for i, row in ipairs(expected_rows) do
local count local count --- @type integer?
row, count = row:match('^(.*%|)%*(%d+)$') row, count = row:match('^(.*%|)%*(%d+)$')
if row then if row then
count = tonumber(count) count = tonumber(count)
@ -438,7 +522,7 @@ function Screen:expect(expected, attr_ids, ...)
local msg_expected_rows = shallowcopy(expected_rows) local msg_expected_rows = shallowcopy(expected_rows)
local msg_actual_rows = shallowcopy(actual_rows) local msg_actual_rows = shallowcopy(actual_rows)
for i, row in ipairs(expected_rows) do for i, row in ipairs(expected_rows) do
local pat = nil local pat = nil --- @type string?
if actual_rows[i] and row ~= actual_rows[i] then if actual_rows[i] and row ~= actual_rows[i] then
local after = row local after = row
while true do while true do
@ -447,6 +531,7 @@ function Screen:expect(expected, attr_ids, ...)
pat = pat and (pat .. pesc(after)) pat = pat and (pat .. pesc(after))
break break
end end
--- @type string
pat = (pat or '') .. pesc(after:sub(1, s - 1)) .. m pat = (pat or '') .. pesc(after:sub(1, s - 1)) .. m
after = after:sub(e + 1) after = after:sub(e + 1)
end end
@ -568,8 +653,12 @@ function Screen:expect_unchanged(intermediate, waittime_ms, ignore_attrs)
self:expect(kwargs) self:expect(kwargs)
end end
--- @private
--- @param check fun(): string
--- @param flags table<string,any>
function Screen:_wait(check, flags) function Screen:_wait(check, flags)
local err, checked = false, false local err --- @type string?
local checked = false
local success_seen = false local success_seen = false
local failure_after_success = false local failure_after_success = false
local did_flush = true local did_flush = true
@ -600,7 +689,7 @@ function Screen:_wait(check, flags)
-- For an "unchanged" test, flags.timeout is the time during which the state -- For an "unchanged" test, flags.timeout is the time during which the state
-- must not change, so always wait this full time. -- must not change, so always wait this full time.
if (flags.unchanged or flags.intermediate) and flags.timeout ~= nil then if (flags.unchanged or flags.intermediate) and flags.timeout then
minimal_timeout = timeout minimal_timeout = timeout
end end
@ -717,6 +806,8 @@ function Screen:sleep(ms, request_cb)
run_session(self._session, request_cb, notification_cb, nil, ms) run_session(self._session, request_cb, notification_cb, nil, ms)
end end
--- @private
--- @param updates {[1]:string, [integer]:any[]}[]
function Screen:_redraw(updates) function Screen:_redraw(updates)
local did_flush = false local did_flush = false
for k, update in ipairs(updates) do for k, update in ipairs(updates) do
@ -724,6 +815,7 @@ function Screen:_redraw(updates)
local method = update[1] local method = update[1]
for i = 2, #update do for i = 2, #update do
local handler_name = '_handle_' .. method local handler_name = '_handle_' .. method
--- @type function
local handler = self[handler_name] local handler = self[handler_name]
assert(handler ~= nil, 'missing handler: Screen:' .. handler_name) assert(handler ~= nil, 'missing handler: Screen:' .. handler_name)
local status, res = pcall(handler, self, unpack(update[i])) local status, res = pcall(handler, self, unpack(update[i]))
@ -815,6 +907,8 @@ function Screen:_reset()
self._grid_win_extmarks = {} self._grid_win_extmarks = {}
end end
--- @param cursor_style_enabled boolean
--- @param mode_info table[]
function Screen:_handle_mode_info_set(cursor_style_enabled, mode_info) function Screen:_handle_mode_info_set(cursor_style_enabled, mode_info)
self._cursor_style_enabled = cursor_style_enabled self._cursor_style_enabled = cursor_style_enabled
for _, item in pairs(mode_info) do for _, item in pairs(mode_info) do
@ -973,11 +1067,19 @@ function Screen:_handle_scroll(count)
self:_handle_grid_scroll(1, top - 1, bot, left - 1, right, count, 0) self:_handle_grid_scroll(1, top - 1, bot, left - 1, right, count, 0)
end end
--- @param g any
--- @param top integer
--- @param bot integer
--- @param left integer
--- @param right integer
--- @param rows integer
--- @param cols integer
function Screen:_handle_grid_scroll(g, top, bot, left, right, rows, cols) function Screen:_handle_grid_scroll(g, top, bot, left, right, rows, cols)
top = top + 1 top = top + 1
left = left + 1 left = left + 1
assert(cols == 0) assert(cols == 0)
local grid = self._grids[g] local grid = self._grids[g]
--- @type integer, integer, integer
local start, stop, step local start, stop, step
if rows > 0 then if rows > 0 then
@ -1013,6 +1115,8 @@ function Screen:_handle_hl_attr_define(id, rgb_attrs, cterm_attrs, info)
self._new_attrs = true self._new_attrs = true
end end
--- @param name string
--- @param id integer
function Screen:_handle_hl_group_set(name, id) function Screen:_handle_hl_group_set(name, id)
self.hl_groups[name] = id self.hl_groups[name] = id
end end
@ -1020,9 +1124,8 @@ end
function Screen:get_hl(val) function Screen:get_hl(val)
if self._options.ext_newgrid then if self._options.ext_newgrid then
return self._attr_table[val][1] return self._attr_table[val][1]
else
return val
end end
return val
end end
function Screen:_handle_highlight_set(attrs) function Screen:_handle_highlight_set(attrs)
@ -1038,6 +1141,10 @@ function Screen:_handle_put(str)
self._cursor.col = self._cursor.col + 1 self._cursor.col = self._cursor.col + 1
end end
--- @param grid integer
--- @param row integer
--- @param col integer
--- @param items integer[][]
function Screen:_handle_grid_line(grid, row, col, items) function Screen:_handle_grid_line(grid, row, col, items)
assert(self._options.ext_linegrid) assert(self._options.ext_linegrid)
assert(#items > 0) assert(#items > 0)
@ -1045,7 +1152,7 @@ function Screen:_handle_grid_line(grid, row, col, items)
local colpos = col + 1 local colpos = col + 1
local hl_id = 0 local hl_id = 0
for _, item in ipairs(items) do for _, item in ipairs(items) do
local text, hl_id_cell, count = unpack(item) local text, hl_id_cell, count = item[1], item[2], item[3]
if hl_id_cell ~= nil then if hl_id_cell ~= nil then
hl_id = hl_id_cell hl_id = hl_id_cell
end end
@ -1379,6 +1486,10 @@ function Screen:redraw_debug(attrs, ignore, timeout)
run_session(self._session, nil, notification_cb, nil, timeout) run_session(self._session, nil, notification_cb, nil, timeout)
end end
--- @param headers boolean
--- @param attr_state any
--- @param preview? boolean
--- @return string[]
function Screen:render(headers, attr_state, preview) function Screen:render(headers, attr_state, preview)
headers = headers and (self._options.ext_multigrid or self._options._debug_float) headers = headers and (self._options.ext_multigrid or self._options._debug_float)
local rv = {} local rv = {}