aboutsummaryrefslogtreecommitdiff
path: root/test/functional/ui/screen.lua
diff options
context:
space:
mode:
Diffstat (limited to 'test/functional/ui/screen.lua')
-rw-r--r--test/functional/ui/screen.lua800
1 files changed, 692 insertions, 108 deletions
diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua
index 7607131e9b..af036913d8 100644
--- a/test/functional/ui/screen.lua
+++ b/test/functional/ui/screen.lua
@@ -71,24 +71,35 @@
-- To help write screen tests, see Screen:snapshot_util().
-- To debug screen tests, see Screen:redraw_debug().
+local global_helpers = require('test.helpers')
+local shallowcopy = global_helpers.shallowcopy
local helpers = require('test.functional.helpers')(nil)
local request, run, uimeths = helpers.request, helpers.run, helpers.uimeths
+local eq = helpers.eq
local dedent = helpers.dedent
+local inspect = require('inspect')
+
+local function isempty(v)
+ return type(v) == 'table' and next(v) == nil
+end
+
local Screen = {}
Screen.__index = Screen
local debug_screen
-local default_screen_timeout = 3500
+local default_timeout_factor = 1
if os.getenv('VALGRIND') then
- default_screen_timeout = default_screen_timeout * 3
+ default_timeout_factor = default_timeout_factor * 3
end
if os.getenv('CI') then
- default_screen_timeout = default_screen_timeout * 3
+ default_timeout_factor = default_timeout_factor * 3
end
+local default_screen_timeout = default_timeout_factor * 3500
+
do
local spawn, nvim_prog = helpers.spawn, helpers.nvim_prog
local session = spawn({nvim_prog, '-u', 'NONE', '-i', 'NONE', '-N', '--embed'})
@@ -138,16 +149,26 @@ function Screen.new(width, height)
suspended = false,
mode = 'normal',
options = {},
+ popupmenu = nil,
+ cmdline = {},
+ cmdline_block = {},
+ wildmenu_items = nil,
+ wildmenu_selected = nil,
_default_attr_ids = nil,
_default_attr_ignore = nil,
_mouse_enabled = true,
_attrs = {},
+ _hl_info = {},
+ _attr_table = {[0]={{},{}}},
+ _clear_attrs = {},
+ _new_attrs = false,
+ _width = width,
+ _height = height,
_cursor = {
row = 1, col = 1
},
_busy = false
}, Screen)
- self:_handle_resize(width, height)
return self
end
@@ -159,11 +180,26 @@ function Screen:set_default_attr_ignore(attr_ignore)
self._default_attr_ignore = attr_ignore
end
+function Screen:set_hlstate_cterm(val)
+ self._hlstate_cterm = val
+end
+
function Screen:attach(options)
if options == nil then
- options = {rgb=true}
+ options = {}
end
+ if options.ext_linegrid == nil then
+ options.ext_linegrid = true
+ end
+ self._options = options
+ self._clear_attrs = (options.ext_linegrid and {{},{}}) or {}
+ self:_handle_resize(self._width, self._height)
uimeths.attach(self._width, self._height, options)
+ if self._options.rgb == nil then
+ -- nvim defaults to rgb=true internally,
+ -- simplify test code by doing the same.
+ self._options.rgb = true
+ end
end
function Screen:detach()
@@ -176,41 +212,123 @@ end
function Screen:set_option(option, value)
uimeths.set_option(option, value)
+ self._options[option] = value
end
--- Asserts that `expected` eventually matches the screen state.
+-- canonical order of ext keys, used to generate asserts
+local ext_keys = {
+ 'popupmenu', 'cmdline', 'cmdline_block', 'wildmenu_items', 'wildmenu_pos'
+}
+
+-- Asserts that the screen state eventually matches an expected state
+--
+-- This function can either be called with the positional forms
+--
+-- screen:expect(grid, [attr_ids, attr_ignore])
+-- screen:expect(condition)
+--
+-- or to use additional arguments (or grid and condition at the same time)
+-- the keyword form has to be used:
--
--- expected: Expected screen state (string). Each line represents a screen
+-- 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.
--- Used as `condition` if NOT a string; must be the ONLY arg then.
-- 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.
--- attr_ignore: Ignored text attributes, or `true` to ignore all.
--- condition: Function asserting some arbitrary condition.
--- any: true: Succeed if `expected` matches ANY screen line(s).
--- false (default): `expected` must match screen exactly.
-function Screen:expect(expected, attr_ids, attr_ignore, condition, any)
+-- Use screen:set_default_attr_ids() to define attributes for many
+-- expect() calls.
+-- attr_ignore: Ignored text attributes, or `true` to ignore all. By default
+-- nothing is ignored.
+-- 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
+function Screen:expect(expected, attr_ids, attr_ignore)
+ local grid, condition = nil, nil
local expected_rows = {}
- if type(expected) ~= "string" then
- assert(not (attr_ids or attr_ignore or condition or any))
+ if type(expected) == "table" then
+ assert(not (attr_ids ~= nil or attr_ignore ~= nil))
+ local is_key = {grid=true, attr_ids=true, attr_ignore=true, condition=true,
+ any=true, mode=true, unchanged=true, intermediate=true,
+ reset=true, timeout=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.."'")
+ end
+ end
+ grid = expected.grid
+ attr_ids = expected.attr_ids
+ attr_ignore = expected.attr_ignore
+ condition = expected.condition
+ assert(not (expected.any ~= nil and grid ~= nil))
+ elseif type(expected) == "string" then
+ grid = expected
+ expected = {}
+ elseif type(expected) == "function" then
+ assert(not (attr_ids ~= nil or attr_ignore ~= nil))
condition = expected
- expected = nil
+ expected = {}
else
+ assert(false)
+ end
+
+ if grid ~= nil then
-- Remove the last line and dedent. Note that gsub returns more then one
-- value.
- expected = dedent(expected:gsub('\n[ ]+$', ''), 0)
- for row in expected:gmatch('[^\n]+') do
+ grid = dedent(grid:gsub('\n[ ]+$', ''), 0)
+ for row in grid:gmatch('[^\n]+') do
row = row:sub(1, #row - 1) -- Last char must be the screen delimiter.
table.insert(expected_rows, row)
end
end
- local ids = attr_ids or self._default_attr_ids
- local ignore = attr_ignore or self._default_attr_ignore
- self:wait(function()
+ local attr_state = {
+ ids = attr_ids or self._default_attr_ids,
+ ignore = attr_ignore or self._default_attr_ignore,
+ }
+ if self._options.ext_hlstate then
+ attr_state.id_to_index = self:hlstate_check_attrs(attr_state.ids or {})
+ end
+ self._new_attrs = false
+ self:_wait(function()
if condition ~= nil then
local status, res = pcall(condition)
if not status then
@@ -218,28 +336,32 @@ function Screen:expect(expected, attr_ids, attr_ignore, condition, any)
end
end
- if expected and not any and self._height ~= #expected_rows then
+ if grid ~= nil and self._height ~= #expected_rows then
return ("Expected screen state's row count(" .. #expected_rows
.. ') differs from configured height(' .. self._height .. ') of Screen.')
end
+ if self._options.ext_hlstate and self._new_attrs then
+ attr_state.id_to_index = self:hlstate_check_attrs(attr_state.ids or {})
+ end
+
local actual_rows = {}
for i = 1, self._height do
- actual_rows[i] = self:_row_repr(self._rows[i], ids, ignore)
+ actual_rows[i] = self:_row_repr(self._rows[i], attr_state)
end
- if expected == nil then
- return
- elseif any then
- -- Search for `expected` anywhere in the screen lines.
+ if expected.any ~= nil 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) then
+ if nil == string.find(actual_screen_str, expected.any) then
return (
'Failed to match any screen lines.\n'
- .. 'Expected (anywhere): "' .. expected .. '"\n'
+ .. 'Expected (anywhere): "' .. expected.any .. '"\n'
.. 'Actual:\n |' .. table.concat(actual_rows, '|\n |') .. '|\n\n')
end
- else
+ end
+
+ if grid ~= nil then
-- `expected` must match the screen lines exactly.
for i = 1, self._height do
if expected_rows[i] ~= actual_rows[i] then
@@ -259,21 +381,87 @@ screen:redraw_debug() to show all intermediate screen states. ]])
end
end
end
- end)
+
+ -- Extension features. The default expectations should cover the case of
+ -- the ext_ feature being disabled, or the feature currently not activated
+ -- (for instance no external cmdline visible). Some extensions require
+ -- preprocessing to represent highlights in a reproducible way.
+ local extstate = self:_extstate_repr(attr_state)
+
+ -- convert assertion errors into invalid screen state descriptions
+ local status, res = pcall(function()
+ for _, k in ipairs(ext_keys) do
+ -- Empty states is considered the default and need not be mentioned
+ if not (expected[k] == nil and isempty(extstate[k])) then
+ eq(expected[k], extstate[k], k)
+ end
+ end
+ if expected.mode ~= nil then
+ eq(expected.mode, self.mode, "mode")
+ end
+ end)
+ if not status then
+ return tostring(res)
+ end
+ end, expected)
end
-function Screen:wait(check, timeout)
- local err, checked = false
+function Screen:_wait(check, flags)
+ local err, checked = false, false
local success_seen = false
local failure_after_success = false
+ local did_flush = true
+ local warn_immediate = not (flags.unchanged or flags.intermediate)
+
+ if flags.intermediate and flags.unchanged then
+ error("Choose only one of 'intermediate' and 'unchanged', not both")
+ end
+
+ if flags.reset then
+ -- throw away all state, we expect it to be retransmitted
+ self:_reset()
+ end
+
+ -- Maximum timeout, after which a incorrect state will be regarded as a
+ -- failure
+ local timeout = flags.timeout or self.timeout
+
+ -- Minimal timeout before the loop is allowed to be stopped so we
+ -- always do some check for failure after success.
+ local minimal_timeout = default_timeout_factor * 2
+
+ local immediate_seen, intermediate_seen = false, false
+ if not check() then
+ minimal_timeout = default_timeout_factor * 20
+ immediate_seen = true
+ end
+
+ -- for an unchanged test, flags.timeout means the time during the state is
+ -- expected to be unchanged, so always wait this full time.
+ if (flags.unchanged or flags.intermediate) and flags.timeout ~= nil then
+ minimal_timeout = timeout
+ end
+
+ assert(timeout >= minimal_timeout)
+ local did_miminal_timeout = false
+
local function notification_cb(method, args)
assert(method == 'redraw')
- self:_redraw(args)
+ did_flush = self:_redraw(args)
+ if not did_flush then
+ return
+ end
err = check()
checked = true
+ if err and immediate_seen then
+ intermediate_seen = true
+ end
+
if not err then
success_seen = true
- helpers.stop()
+ if did_miminal_timeout then
+ helpers.stop()
+ end
elseif success_seen and #args > 0 then
failure_after_success = true
--print(require('inspect')(args))
@@ -281,37 +469,88 @@ function Screen:wait(check, timeout)
return true
end
- run(nil, notification_cb, nil, timeout or self.timeout)
- if not checked then
+ run(nil, notification_cb, nil, minimal_timeout)
+ if not did_flush then
+ err = "no flush received"
+ elseif not checked then
err = check()
+ if not err and flags.unchanged then
+ -- expecting NO screen change: use a shorter timout
+ success_seen = true
+ end
+ end
+
+ if not success_seen then
+ did_miminal_timeout = true
+ run(nil, notification_cb, nil, timeout-minimal_timeout)
+ end
+
+ local did_warn = false
+ if warn_immediate and immediate_seen then
+ print([[
+
+Warning: A screen test has immediate success. Try to avoid this unless the
+purpose of the test really requires it.]])
+ if intermediate_seen then
+ print([[
+There are intermediate states between the two identical expects.
+Use screen:snapshot_util() or screen:redraw_debug() to find them, and add them
+to the test if they make sense.
+]])
+ else
+ print([[If necessary, silence this warning by
+supplying the 'unchanged' argument to screen:expect.]])
+ end
+ did_warn = true
end
if failure_after_success then
print([[
Warning: Screen changes were received after the expected state. This indicates
-indeterminism in the test. Try adding wait() (or screen:expect(...)) between
+indeterminism in the test. Try adding screen:expect(...) (or wait()) between
asynchronous (feed(), nvim_input()) and synchronous API calls.
- - Use Screen:redraw_debug() to investigate the problem.
+ - Use Screen:redraw_debug() to investigate the problem. It might find
+ relevant intermediate states that should be added to the test to make it
+ more robust.
+ - If the point of the test is to assert the state after some user input
+ sent with feed(...), also adding an screen:expect(...) before the feed(...)
+ will help ensure the input is sent to nvim when nvim is in a predictable
+ state. This is preferable to using wait(), as it is more closely emulates
+ real user interaction.
- wait() can trigger redraws and consequently generate more indeterminism.
In that case try removing every wait().
]])
+ did_warn = true
+ end
+
+
+ if err then
+ assert(false, err)
+ elseif did_warn then
local tb = debug.traceback()
local index = string.find(tb, '\n%s*%[C]')
print(string.sub(tb,1,index))
end
- if err then
- assert(false, err)
+ if flags.intermediate then
+ assert(intermediate_seen, "expected intermediate screen state before final screen state")
+ elseif flags.unchanged then
+ assert(not intermediate_seen, "expected screen state to be unchanged")
end
end
function Screen:sleep(ms)
- pcall(function() self:wait(function() return "error" end, ms) end)
+ local function notification_cb(method, args)
+ assert(method == 'redraw')
+ self:_redraw(args)
+ end
+ run(nil, notification_cb, nil, ms)
end
function Screen:_redraw(updates)
- for _, update in ipairs(updates) do
+ local did_flush = false
+ for k, update in ipairs(updates) do
-- print('--')
-- print(require('inspect')(update))
local method = update[1]
@@ -326,8 +565,11 @@ function Screen:_redraw(updates)
self._on_event(method, update[i])
end
end
- -- print(self:_current_screen())
+ if k == #updates and method == "flush" then
+ did_flush = true
+ end
end
+ return did_flush
end
function Screen:set_on_event_handler(callback)
@@ -339,7 +581,7 @@ function Screen:_handle_resize(width, height)
for _ = 1, height do
local cols = {}
for _ = 1, width do
- table.insert(cols, {text = ' ', attrs = {}})
+ table.insert(cols, {text = ' ', attrs = self._clear_attrs, hl_id = 0})
end
table.insert(rows, cols)
end
@@ -353,14 +595,59 @@ function Screen:_handle_resize(width, height)
}
end
+function Screen:_handle_flush()
+end
+
+function Screen:_handle_grid_resize(grid, width, height)
+ assert(grid == 1)
+ self:_handle_resize(width, height)
+end
+
+function Screen:_reset()
+ -- TODO: generalize to multigrid later
+ self:_handle_grid_clear(1)
+
+ -- TODO: share with initialization, so it generalizes?
+ self.popupmenu = nil
+ self.cmdline = {}
+ self.cmdline_block = {}
+ self.wildmenu_items = nil
+ self.wildmenu_pos = nil
+end
+
+
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
+ -- attr IDs are not stable, but their value should be
+ if item.attr_id ~= nil then
+ item.attr = self._attr_table[item.attr_id][1]
+ item.attr_id = nil
+ end
+ if item.attr_id_lm ~= nil then
+ item.attr_lm = self._attr_table[item.attr_id_lm][1]
+ item.attr_id_lm = nil
+ end
+ end
self._mode_info = mode_info
end
function Screen:_handle_clear()
- self:_clear_block(self._scroll_region.top, self._scroll_region.bot,
- self._scroll_region.left, self._scroll_region.right)
+ -- the first implemented UI protocol clients (python-gui and builitin TUI)
+ -- allowed the cleared region to be restricted by setting the scroll region.
+ -- this was never used by nvim tough, and not documented and implemented by
+ -- newer clients, to check we remain compatible with both kind of clients,
+ -- ensure the scroll region is in a reset state.
+ local expected_region = {
+ top = 1, bot = self._height, left = 1, right = self._width
+ }
+ eq(expected_region, self._scroll_region)
+ self:_clear_block(1, self._height, 1, self._width)
+end
+
+function Screen:_handle_grid_clear(grid)
+ assert(grid == 1)
+ self:_clear_block(1, self._height, 1, self._width)
end
function Screen:_handle_eol_clear()
@@ -373,6 +660,12 @@ function Screen:_handle_cursor_goto(row, col)
self._cursor.col = col + 1
end
+function Screen:_handle_grid_cursor_goto(grid, row, col)
+ assert(grid == 1)
+ self._cursor.row = row + 1
+ self._cursor.col = col + 1
+end
+
function Screen:_handle_busy_start()
self._busy = true
end
@@ -406,45 +699,85 @@ function Screen:_handle_scroll(count)
local bot = self._scroll_region.bot
local left = self._scroll_region.left
local right = self._scroll_region.right
+ self:_handle_grid_scroll(1, top-1, bot, left-1, right, count, 0)
+end
+
+function Screen:_handle_grid_scroll(grid, top, bot, left, right, rows, cols)
+ top = top+1
+ left = left+1
+ assert(grid == 1)
+ assert(cols == 0)
local start, stop, step
- if count > 0 then
+ if rows > 0 then
start = top
- stop = bot - count
+ stop = bot - rows
step = 1
else
start = bot
- stop = top - count
+ stop = top - rows
step = -1
end
-- shift scroll region
for i = start, stop, step do
local target = self._rows[i]
- local source = self._rows[i + count]
+ local source = self._rows[i + rows]
for j = left, right do
target[j].text = source[j].text
target[j].attrs = source[j].attrs
+ target[j].hl_id = source[j].hl_id
end
end
-- clear invalid rows
- for i = stop + step, stop + count, step do
+ for i = stop + step, stop + rows, step do
self:_clear_row_section(i, left, right)
end
end
+function Screen:_handle_hl_attr_define(id, rgb_attrs, cterm_attrs, info)
+ self._attr_table[id] = {rgb_attrs, cterm_attrs}
+ self._hl_info[id] = info
+ self._new_attrs = true
+end
+
function Screen:_handle_highlight_set(attrs)
self._attrs = attrs
end
function Screen:_handle_put(str)
+ assert(not self._options.ext_linegrid)
local cell = self._rows[self._cursor.row][self._cursor.col]
cell.text = str
cell.attrs = self._attrs
+ cell.hl_id = -1
self._cursor.col = self._cursor.col + 1
end
+function Screen:_handle_grid_line(grid, row, col, items)
+ assert(self._options.ext_linegrid)
+ assert(grid == 1)
+ local line = self._rows[row+1]
+ local colpos = col+1
+ local hl = self._clear_attrs
+ local hl_id = 0
+ for _,item in ipairs(items) do
+ local text, hl_id_cell, count = unpack(item)
+ if hl_id_cell ~= nil then
+ hl_id = hl_id_cell
+ hl = self._attr_table[hl_id]
+ end
+ for _ = 1, (count or 1) do
+ local cell = line[colpos]
+ cell.text = text
+ cell.hl_id = hl_id
+ cell.attrs = hl
+ colpos = colpos+1
+ end
+ end
+end
+
function Screen:_handle_bell()
self.bell = true
end
@@ -453,7 +786,14 @@ function Screen:_handle_visual_bell()
self.visual_bell = true
end
-function Screen:_handle_default_colors_set()
+function Screen:_handle_default_colors_set(rgb_fg, rgb_bg, rgb_sp, cterm_fg, cterm_bg)
+ self.default_colors = {
+ rgb_fg=rgb_fg,
+ rgb_bg=rgb_bg,
+ rgb_sp=rgb_sp,
+ cterm_fg=cterm_fg,
+ cterm_bg=cterm_bg
+ }
end
function Screen:_handle_update_fg(fg)
@@ -488,6 +828,63 @@ function Screen:_handle_option_set(name, value)
self.options[name] = value
end
+function Screen:_handle_popupmenu_show(items, selected, row, col)
+ self.popupmenu = {items=items,pos=selected, anchor={row, col}}
+end
+
+function Screen:_handle_popupmenu_select(selected)
+ self.popupmenu.pos = selected
+end
+
+function Screen:_handle_popupmenu_hide()
+ self.popupmenu = nil
+end
+
+function Screen:_handle_cmdline_show(content, pos, firstc, prompt, indent, level)
+ if firstc == '' then firstc = nil end
+ if prompt == '' then prompt = nil end
+ if indent == 0 then indent = nil end
+ self.cmdline[level] = {content=content, pos=pos, firstc=firstc,
+ prompt=prompt, indent=indent}
+end
+
+function Screen:_handle_cmdline_hide(level)
+ self.cmdline[level] = nil
+end
+
+function Screen:_handle_cmdline_special_char(char, shift, level)
+ -- cleared by next cmdline_show on the same level
+ self.cmdline[level].special = {char, shift}
+end
+
+function Screen:_handle_cmdline_pos(pos, level)
+ self.cmdline[level].pos = pos
+end
+
+function Screen:_handle_cmdline_block_show(block)
+ self.cmdline_block = block
+end
+
+function Screen:_handle_cmdline_block_append(item)
+ self.cmdline_block[#self.cmdline_block+1] = item
+end
+
+function Screen:_handle_cmdline_block_hide()
+ self.cmdline_block = {}
+end
+
+function Screen:_handle_wildmenu_show(items)
+ self.wildmenu_items = items
+end
+
+function Screen:_handle_wildmenu_select(pos)
+ self.wildmenu_pos = pos
+end
+
+function Screen:_handle_wildmenu_hide()
+ self.wildmenu_items, self.wildmenu_pos = nil, nil
+end
+
function Screen:_clear_block(top, bot, left, right)
for i = top, bot do
self:_clear_row_section(i, left, right)
@@ -498,15 +895,19 @@ function Screen:_clear_row_section(rownum, startcol, stopcol)
local row = self._rows[rownum]
for i = startcol, stopcol do
row[i].text = ' '
- row[i].attrs = {}
+ row[i].attrs = self._clear_attrs
end
end
-function Screen:_row_repr(row, attr_ids, attr_ignore)
+function Screen:_row_repr(row, attr_state)
local rv = {}
local current_attr_id
for i = 1, self._width do
- local attr_id = self:_get_attr_id(attr_ids, attr_ignore, row[i].attrs)
+ local attrs = row[i].attrs
+ if self._options.ext_linegrid then
+ attrs = attrs[(self._options.rgb and 1) or 2]
+ end
+ local attr_id = self:_get_attr_id(attr_state, attrs, row[i].hl_id)
if current_attr_id and attr_id ~= current_attr_id then
-- close current attribute bracket, add it before any whitespace
-- up to the current cell
@@ -532,14 +933,42 @@ function Screen:_row_repr(row, attr_ids, attr_ignore)
return table.concat(rv, '')--:gsub('%s+$', '')
end
+function Screen:_extstate_repr(attr_state)
+ local cmdline = {}
+ for i, entry in pairs(self.cmdline) do
+ entry = shallowcopy(entry)
+ entry.content = self:_chunks_repr(entry.content, attr_state)
+ cmdline[i] = entry
+ end
+
+ local cmdline_block = {}
+ for i, entry in ipairs(self.cmdline_block) do
+ cmdline_block[i] = self:_chunks_repr(entry, attr_state)
+ end
-function Screen:_current_screen()
- -- get a string that represents the current screen state(debugging helper)
- local rv = {}
- for i = 1, self._height do
- table.insert(rv, "'"..self:_row_repr(self._rows[i]).."'")
+ return {
+ popupmenu=self.popupmenu,
+ cmdline=cmdline,
+ cmdline_block=cmdline_block,
+ wildmenu_items=self.wildmenu_items,
+ wildmenu_pos=self.wildmenu_pos,
+ }
+end
+
+function Screen:_chunks_repr(chunks, attr_state)
+ local repr_chunks = {}
+ for i, chunk in ipairs(chunks) do
+ local hl, text = unpack(chunk)
+ local attrs
+ if self._options.ext_linegrid then
+ attrs = self._attr_table[hl][1]
+ else
+ attrs = hl
+ end
+ local attr_id = self:_get_attr_id(attr_state, attrs, hl)
+ repr_chunks[i] = {text, attr_id}
end
- return table.concat(rv, '\n')
+ return repr_chunks
end
-- Generates tests. Call it where Screen:expect() would be. Waits briefly, then
@@ -570,56 +999,179 @@ function Screen:redraw_debug(attrs, ignore, timeout)
end
function Screen:print_snapshot(attrs, ignore)
+ attrs = attrs or self._default_attr_ids
if ignore == nil then
ignore = self._default_attr_ignore
end
- if attrs == nil then
- attrs = {}
- if self._default_attr_ids ~= nil then
- for i, a in pairs(self._default_attr_ids) do
- attrs[i] = a
+ local attr_state = {
+ ids = {},
+ ignore = ignore,
+ mutable = true, -- allow _row_repr to add missing highlights
+ }
+
+ if attrs ~= nil then
+ for i, a in pairs(attrs) do
+ attr_state.ids[i] = a
+ end
+ end
+ if self._options.ext_hlstate then
+ attr_state.id_to_index = self:hlstate_check_attrs(attr_state.ids)
+ end
+
+ local lines = {}
+ for i = 1, self._height do
+ table.insert(lines, " "..self:_row_repr(self._rows[i], attr_state).."|")
+ end
+
+ local ext_state = self:_extstate_repr(attr_state)
+ local keys = false
+ for k, v in pairs(ext_state) do
+ if isempty(v) then
+ ext_state[k] = nil -- deleting keys while iterating is ok
+ else
+ keys = true
+ end
+ end
+
+ local attrstr = ""
+ if attr_state.modified then
+ local attrstrs = {}
+ for i, a in pairs(attr_state.ids) do
+ local dict
+ if self._options.ext_hlstate then
+ dict = self:_pprint_hlstate(a)
+ else
+ dict = "{"..self:_pprint_attrs(a).."}"
end
+ local keyval = (type(i) == "number") and "["..tostring(i).."]" or i
+ table.insert(attrstrs, " "..keyval.." = "..dict..",")
end
+ attrstr = (", "..(keys and "attr_ids=" or "")
+ .."{\n"..table.concat(attrstrs, "\n").."\n}")
+ end
+ print( "\nscreen:expect"..(keys and "{grid=" or "(").."[[")
+ print( table.concat(lines, '\n'))
+ io.stdout:write( "]]"..attrstr)
+ for _, k in ipairs(ext_keys) do
+ if ext_state[k] ~= nil then
+ io.stdout:write(", "..k.."="..inspect(ext_state[k]))
+ end
+ end
+ print((keys and "}" or ")").."\n")
+ io.stdout:flush()
+end
- if ignore ~= true then
- for i = 1, self._height do
- local row = self._rows[i]
- for j = 1, self._width do
- local attr = row[j].attrs
- if self:_attr_index(attrs, attr) == nil and self:_attr_index(ignore, attr) == nil then
- if not self:_equal_attrs(attr, {}) then
- table.insert(attrs, attr)
+function Screen:_insert_hl_id(attr_state, hl_id)
+ if attr_state.id_to_index[hl_id] ~= nil then
+ return attr_state.id_to_index[hl_id]
+ end
+ local raw_info = self._hl_info[hl_id]
+ local info = {}
+ if #raw_info > 1 then
+ for i, item in ipairs(raw_info) do
+ info[i] = self:_insert_hl_id(attr_state, item.id)
+ end
+ else
+ info[1] = {}
+ for k, v in pairs(raw_info[1]) do
+ if k ~= "id" then
+ info[1][k] = v
+ end
+ end
+ end
+
+ local entry = self._attr_table[hl_id]
+ local attrval
+ if self._hlstate_cterm then
+ attrval = {entry[1], entry[2], info} -- unpack() doesn't work
+ else
+ attrval = {entry[1], info}
+ end
+
+
+ table.insert(attr_state.ids, attrval)
+ attr_state.id_to_index[hl_id] = #attr_state.ids
+ return #attr_state.ids
+end
+
+function Screen:hlstate_check_attrs(attrs)
+ local id_to_index = {}
+ for i = 1,#self._attr_table do
+ local iinfo = self._hl_info[i]
+ local matchinfo = {}
+ if #iinfo > 1 then
+ for k,item in ipairs(iinfo) do
+ matchinfo[k] = id_to_index[item.id]
+ end
+ else
+ matchinfo = iinfo
+ end
+ for k,v in pairs(attrs) do
+ local attr, info, attr_rgb, attr_cterm
+ if self._hlstate_cterm then
+ attr_rgb, attr_cterm, info = unpack(v)
+ attr = {attr_rgb, attr_cterm}
+ else
+ attr, info = unpack(v)
+ end
+ if self:_equal_attr_def(attr, self._attr_table[i]) then
+ if #info == #matchinfo then
+ local match = false
+ if #info == 1 then
+ if self:_equal_info(info[1],matchinfo[1]) then
+ match = true
+ end
+ else
+ match = true
+ for j = 1,#info do
+ if info[j] ~= matchinfo[j] then
+ match = false
+ end
end
end
+ if match then
+ id_to_index[i] = k
+ end
end
end
end
end
+ return id_to_index
+end
- local rv = {}
- for i = 1, self._height do
- table.insert(rv, " "..self:_row_repr(self._rows[i],attrs, ignore).."|")
- end
- local attrstrs = {}
- local alldefault = true
- for i, a in ipairs(attrs) do
- if self._default_attr_ids == nil or self._default_attr_ids[i] ~= a then
- alldefault = false
- end
- local dict = "{"..self:_pprint_attrs(a).."}"
- table.insert(attrstrs, "["..tostring(i).."] = "..dict)
- end
- local attrstr = "{"..table.concat(attrstrs, ", ").."}"
- print( "\nscreen:expect([[")
- print( table.concat(rv, '\n'))
- if alldefault then
- print( "]])\n")
+
+function Screen:_pprint_hlstate(item)
+ --print(require('inspect')(item))
+ local attrdict = "{"..self:_pprint_attrs(item[1]).."}, "
+ local attrdict2, hlinfo
+ if self._hlstate_cterm then
+ attrdict2 = "{"..self:_pprint_attrs(item[2]).."}, "
+ hlinfo = item[3]
+ else
+ attrdict2 = ""
+ hlinfo = item[2]
+ end
+ local descdict = "{"..self:_pprint_hlinfo(hlinfo).."}"
+ return "{"..attrdict..attrdict2..descdict.."}"
+end
+
+function Screen:_pprint_hlinfo(states)
+ if #states == 1 then
+ local items = {}
+ for f, v in pairs(states[1]) do
+ local desc = tostring(v)
+ if type(v) == type("") then
+ desc = '"'..desc..'"'
+ end
+ table.insert(items, f.." = "..desc)
+ end
+ return "{"..table.concat(items, ", ").."}"
else
- print( "]], "..attrstr..")\n")
+ return table.concat(states, ", ")
end
- io.stdout:flush()
end
+
function Screen:_pprint_attrs(attrs)
local items = {}
for f, v in pairs(attrs) do
@@ -643,32 +1195,64 @@ local function backward_find_meaningful(tbl, from) -- luacheck: no unused
return from
end
-function Screen:_get_attr_id(attr_ids, ignore, attrs)
- if not attr_ids then
+function Screen:_get_attr_id(attr_state, attrs, hl_id)
+ if not attr_state.ids then
return
end
- for id, a in pairs(attr_ids) do
- if self:_equal_attrs(a, attrs) then
- return id
- end
+
+ if self._options.ext_hlstate then
+ local id = attr_state.id_to_index[hl_id]
+ if id ~= nil or hl_id == 0 then
+ return id
+ end
+ if attr_state.mutable then
+ id = self:_insert_hl_id(attr_state, hl_id)
+ attr_state.modified = true
+ return id
+ end
+ return "UNEXPECTED "..self:_pprint_attrs(self._attr_table[hl_id][1])
+ else
+ for id, a in pairs(attr_state.ids) do
+ if self:_equal_attrs(a, attrs) then
+ return id
+ end
+ end
+ if self:_equal_attrs(attrs, {}) or
+ attr_state.ignore == true or
+ self:_attr_index(attr_state.ignore, attrs) ~= nil then
+ -- ignore this attrs
+ return nil
+ end
+ if attr_state.mutable then
+ table.insert(attr_state.ids, attrs)
+ attr_state.modified = true
+ return #attr_state.ids
+ end
+ return "UNEXPECTED "..self:_pprint_attrs(attrs)
end
- if self:_equal_attrs(attrs, {}) or
- ignore == true or self:_attr_index(ignore, attrs) ~= nil then
- -- ignore this attrs
- return nil
+end
+
+function Screen:_equal_attr_def(a, b)
+ if self._hlstate_cterm then
+ return self:_equal_attrs(a[1],b[1]) and self:_equal_attrs(a[2],b[2])
+ else
+ return self:_equal_attrs(a,b[1])
end
- return "UNEXPECTED "..self:_pprint_attrs(attrs)
end
function Screen:_equal_attrs(a, b)
return a.bold == b.bold and a.standout == b.standout and
a.underline == b.underline and a.undercurl == b.undercurl and
a.italic == b.italic and a.reverse == b.reverse and
- a.foreground == b.foreground and
- a.background == b.background and
+ a.foreground == b.foreground and a.background == b.background and
a.special == b.special
end
+function Screen:_equal_info(a, b)
+ return a.kind == b.kind and a.hi_name == b.hi_name and
+ a.ui_name == b.ui_name
+end
+
function Screen:_attr_index(attrs, attr)
if not attrs then
return nil