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
-- vim.fn.{func}(...)
---@private
---@nodoc
vim.fn = setmetatable({}, {
__index = function(t, key)
local _fn

View File

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

View File

@ -88,7 +88,28 @@ local function isempty(v)
return type(v) == 'table' and next(v) == nil
end
--- @class test.functional.ui.screen.Grid
--- @field rows table[][]
--- @field width integer
--- @field height integer
--- @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 = {}
Screen.__index = Screen
@ -103,13 +124,14 @@ end
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')
if not status then
error('failed to get color map')
end
local colors = rv
local colornames = {}
local colors = rv --- @type table<string,integer>
local colornames = {} --- @type table<integer,string>
for name, rgb in pairs(colors) do
-- we disregard the case that colornames might not be unique, as
-- 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
end
--- @param width? integer
--- @param height? integer
--- @return test.functional.ui.screen
function Screen.new(width, height)
if not Screen.colors then
Screen._init_colors(get_session())
_init_colors()
end
if not width then
width = 53
end
if not height then
height = 14
end
local self = setmetatable({
timeout = default_screen_timeout,
title = '',
@ -166,8 +185,8 @@ function Screen.new(width, height)
_attr_table = { [0] = { {}, {} } },
_clear_attrs = nil,
_new_attrs = false,
_width = width,
_height = height,
_width = width or 53,
_height = height or 14,
_grids = {},
_grid_win_extmarks = {},
_cursor = {
@ -177,6 +196,7 @@ function Screen.new(width, height)
},
_busy = false,
}, Screen)
local function ui(method, ...)
if self.rpc_async then
self._session:notify('nvim_ui_' .. method, ...)
@ -187,7 +207,9 @@ function Screen.new(width, height)
end
end
end
self.uimeths = create_callindex(ui)
return self
end
@ -203,16 +225,25 @@ function Screen:set_rgb_cterm(val)
self._rgb_cterm = val
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)
if session == nil then
session = get_session()
end
if options == nil then
options = {}
end
session = session or get_session()
options = options or {}
if options.ext_linegrid == nil then
options.ext_linegrid = true
end
self._session = session
self._options = options
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)
end
--- @param option 'ext_linegrid'|'ext_multigrid'|'ext_popupmenu'|'ext_wildmenu'|'rgb'
--- @param value boolean
function Screen:set_option(option, value)
self.uimeths.set_option(option, value)
--- @diagnostic disable-next-line:no-unknown
self._options[option] = value
end
@ -264,109 +298,154 @@ local ext_keys = {
'win_viewport',
}
-- 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}
--
--
-- grid: Expected screen state (string). Each line represents a screen
-- row. Last character of each row (typically "|") is stripped.
-- Common indentation is stripped.
-- "{MATCH:x}" in a line is matched against Lua pattern `x`.
-- "*n" at the end of a line means it repeats `n` times.
-- 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
-- "{K:S}", where K is a key in `attr_ids`. Any unexpected
-- attributes in the final state are an error.
-- Use an empty table for a text-only (no attributes) expectation.
-- Use screen:set_default_attr_ids() to define attributes for many
-- expect() calls.
-- extmarks: Expected win_extmarks accumulated for the grids. For each grid,
-- the win_extmark messages are accumulated into an array.
-- condition: Function asserting some arbitrary condition. Return value is
-- ignored, throw an error (use eq() or similar) to signal failure.
-- any: Lua pattern string expected to match a screen line. NB: the
-- following chars are magic characters
-- ( ) . % + - * ? [ ^ $
-- and must be escaped with a preceding % for a literal match.
-- mode: Expected mode as signaled by "mode_change" event
-- unchanged: Test that the screen state is unchanged since the previous
-- expect(...). Any flush event resulting in a different state is
-- considered an error. Not observing any events until timeout
-- is acceptable.
-- intermediate: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.
-- reset: 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.
-- timeout: 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)
--
-- 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.
--
-- popupmenu: Expected ext_popupmenu state,
-- cmdline: Expected ext_cmdline state, as an array of cmdlines of
-- different level.
-- cmdline_block: Expected ext_cmdline block (for function definitions)
-- wildmenu_items: Expected items for ext_wildmenu
-- wildmenu_pos: Expected position for ext_wildmenu
local expect_keys = {
grid = true,
attr_ids = true,
condition = true,
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
expect_keys[v] = true
end
--- @class test.function.ui.screen.Expect
---
--- Expected screen state (string). Each line represents a screen
--- row. Last character of each row (typically "|") is stripped.
--- Common indentation is stripped.
--- "{MATCH:x}" in a line is matched against Lua pattern `x`.
--- "*n" at the end of a line means it repeats `n` times.
--- @field grid? string
---
--- 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
--- "{K:S}", where K is a key in `attr_ids`. Any unexpected
--- attributes in the final state are an error.
--- Use an empty table for a text-only (no attributes) expectation.
--- Use screen:set_default_attr_ids() to define attributes for many
--- expect() calls.
--- @field attr_ids? table<integer,table<string,any>>
---
--- Expected win_extmarks accumulated for the grids. For each grid,
--- the win_extmark messages are accumulated into an array.
--- @field extmarks? table<integer,table>
---
--- Function asserting some arbitrary condition. Return value is
--- ignored, throw an error (use eq() or similar) to signal failure.
--- @field condition? fun()
---
--- Lua pattern string expected to match a screen line. NB: the
--- following chars are magic characters
--- ( ) . % + - * ? [ ^ $
--- and must be escaped with a preceding % for a literal match.
--- @field any? string
---
--- Expected mode as signaled by "mode_change" event
--- @field mode? string
---
--- Test that the screen state is unchanged since the previous
--- 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, ...)
local grid, condition = nil, nil
local expected_rows = {}
--- @type string, fun()
local grid, condition
assert(next({ ... }) == nil, 'invalid args to expect()')
if type(expected) == 'table' then
assert(not (attr_ids ~= nil))
local is_key = {
grid = true,
attr_ids = true,
condition = true,
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
assert(attr_ids == nil)
for k, _ in
pairs(expected --[[@as table<string,any>]])
do
if not expect_keys[k] then
error("Screen:expect: Unknown keyword argument '" .. k .. "'")
end
end
grid = expected.grid
attr_ids = expected.attr_ids
condition = expected.condition
assert(not (expected.any ~= nil and grid ~= nil))
assert(expected.any == nil or grid == nil)
elseif type(expected) == 'string' then
grid = expected
expected = {}
elseif type(expected) == 'function' then
assert(not (attr_ids ~= nil))
assert(attr_ids == nil)
condition = expected
expected = {}
else
assert(false)
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
-- value.
grid = dedent(grid:gsub('\n[ ]+$', ''), 0)
@ -374,18 +453,23 @@ function Screen:expect(expected, attr_ids, ...)
table.insert(expected_rows, row)
end
end
local attr_state = {
ids = attr_ids or self._default_attr_ids,
}
if isempty(attr_ids) then
attr_state.ids = nil
end
if self._options.ext_linegrid then
attr_state.id_to_index = self:linegrid_check_attrs(attr_state.ids or {})
end
self._new_attrs = false
self:_wait(function()
if condition ~= nil then
if condition then
--- @type boolean, string
local status, res = pcall(condition)
if not status then
return tostring(res)
@ -398,10 +482,10 @@ function Screen:expect(expected, attr_ids, ...)
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.
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 (
'Failed to match any screen lines.\n'
.. 'Expected (anywhere): "'
@ -414,9 +498,9 @@ function Screen:expect(expected, attr_ids, ...)
end
end
if grid ~= nil then
if grid then
for i, row in ipairs(expected_rows) do
local count
local count --- @type integer?
row, count = row:match('^(.*%|)%*(%d+)$')
if row then
count = tonumber(count)
@ -438,7 +522,7 @@ function Screen:expect(expected, attr_ids, ...)
local msg_expected_rows = shallowcopy(expected_rows)
local msg_actual_rows = shallowcopy(actual_rows)
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
local after = row
while true do
@ -447,6 +531,7 @@ function Screen:expect(expected, attr_ids, ...)
pat = pat and (pat .. pesc(after))
break
end
--- @type string
pat = (pat or '') .. pesc(after:sub(1, s - 1)) .. m
after = after:sub(e + 1)
end
@ -568,8 +653,12 @@ function Screen:expect_unchanged(intermediate, waittime_ms, ignore_attrs)
self:expect(kwargs)
end
--- @private
--- @param check fun(): string
--- @param flags table<string,any>
function Screen:_wait(check, flags)
local err, checked = false, false
local err --- @type string?
local checked = false
local success_seen = false
local failure_after_success = false
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
-- 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
end
@ -717,6 +806,8 @@ function Screen:sleep(ms, request_cb)
run_session(self._session, request_cb, notification_cb, nil, ms)
end
--- @private
--- @param updates {[1]:string, [integer]:any[]}[]
function Screen:_redraw(updates)
local did_flush = false
for k, update in ipairs(updates) do
@ -724,6 +815,7 @@ function Screen:_redraw(updates)
local method = update[1]
for i = 2, #update do
local handler_name = '_handle_' .. method
--- @type function
local handler = self[handler_name]
assert(handler ~= nil, 'missing handler: Screen:' .. handler_name)
local status, res = pcall(handler, self, unpack(update[i]))
@ -815,6 +907,8 @@ function Screen:_reset()
self._grid_win_extmarks = {}
end
--- @param cursor_style_enabled boolean
--- @param mode_info table[]
function Screen:_handle_mode_info_set(cursor_style_enabled, mode_info)
self._cursor_style_enabled = cursor_style_enabled
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)
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)
top = top + 1
left = left + 1
assert(cols == 0)
local grid = self._grids[g]
--- @type integer, integer, integer
local start, stop, step
if rows > 0 then
@ -1013,6 +1115,8 @@ function Screen:_handle_hl_attr_define(id, rgb_attrs, cterm_attrs, info)
self._new_attrs = true
end
--- @param name string
--- @param id integer
function Screen:_handle_hl_group_set(name, id)
self.hl_groups[name] = id
end
@ -1020,9 +1124,8 @@ end
function Screen:get_hl(val)
if self._options.ext_newgrid then
return self._attr_table[val][1]
else
return val
end
return val
end
function Screen:_handle_highlight_set(attrs)
@ -1038,6 +1141,10 @@ function Screen:_handle_put(str)
self._cursor.col = self._cursor.col + 1
end
--- @param grid integer
--- @param row integer
--- @param col integer
--- @param items integer[][]
function Screen:_handle_grid_line(grid, row, col, items)
assert(self._options.ext_linegrid)
assert(#items > 0)
@ -1045,7 +1152,7 @@ function Screen:_handle_grid_line(grid, row, col, items)
local colpos = col + 1
local hl_id = 0
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
hl_id = hl_id_cell
end
@ -1379,6 +1486,10 @@ function Screen:redraw_debug(attrs, ignore, timeout)
run_session(self._session, nil, notification_cb, nil, timeout)
end
--- @param headers boolean
--- @param attr_state any
--- @param preview? boolean
--- @return string[]
function Screen:render(headers, attr_state, preview)
headers = headers and (self._options.ext_multigrid or self._options._debug_float)
local rv = {}