12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271 |
- -- This module contains the Screen class, a complete Nvim UI implementation
- -- designed for functional testing (verifying screen state, in particular).
- --
- -- Screen:expect() takes a string representing the expected screen state and an
- -- optional set of attribute identifiers for checking highlighted characters.
- --
- -- Example usage:
- --
- -- local screen = Screen.new(25, 10)
- -- -- Attach the screen to the current Nvim instance.
- -- screen:attach()
- -- -- Enter insert-mode and type some text.
- -- feed('ihello screen')
- -- -- Assert the expected screen state.
- -- screen:expect([[
- -- hello screen |
- -- ~ |
- -- ~ |
- -- ~ |
- -- ~ |
- -- ~ |
- -- ~ |
- -- ~ |
- -- ~ |
- -- -- INSERT -- |
- -- ]]) -- <- Last line is stripped
- --
- -- Since screen updates are received asynchronously, expect() actually specifies
- -- the _eventual_ screen state.
- --
- -- This is how expect() works:
- -- * It starts the event loop with a timeout.
- -- * Each time it receives an update it checks that against the expected state.
- -- * If the expected state matches the current state, the event loop will be
- -- stopped and expect() will return.
- -- * If the timeout expires, the last match error will be reported and the
- -- test will fail.
- --
- -- Continuing the above example, say we want to assert that "-- INSERT --" is
- -- highlighted with the bold attribute. The expect() call should look like this:
- --
- -- NonText = Screen.colors.Blue
- -- screen:expect([[
- -- hello screen |
- -- ~ |
- -- ~ |
- -- ~ |
- -- ~ |
- -- ~ |
- -- ~ |
- -- ~ |
- -- ~ |
- -- {b:-- INSERT --} |
- -- ]], {b = {bold = true}}, {{bold = true, foreground = NonText}})
- --
- -- In this case "b" is a string associated with the set composed of one
- -- attribute: bold. Note that since the {b:} markup is not a real part of the
- -- screen, the delimiter "|" moved to the right. Also, the highlighting of the
- -- NonText markers "~" is ignored in this test.
- --
- -- Tests will often share a group of attribute sets to expect(). Those can be
- -- defined at the beginning of a test:
- --
- -- NonText = Screen.colors.Blue
- -- screen:set_default_attr_ids( {
- -- [1] = {reverse = true, bold = true},
- -- [2] = {reverse = true}
- -- })
- -- screen:set_default_attr_ignore( {{}, {bold=true, foreground=NonText}} )
- --
- -- To help write screen tests, see Screen:snapshot_util().
- -- To debug screen tests, see Screen:redraw_debug().
- local global_helpers = require('test.helpers')
- local deepcopy = global_helpers.deepcopy
- 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_timeout_factor = 1
- if os.getenv('VALGRIND') then
- default_timeout_factor = default_timeout_factor * 3
- end
- if os.getenv('CI') then
- 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'})
- local status, rv = session:request('nvim_get_color_map')
- if not status then
- print('failed to get color map')
- os.exit(1)
- end
- local colors = rv
- local colornames = {}
- 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
- colornames[rgb] = name
- end
- session:close()
- Screen.colors = colors
- Screen.colornames = colornames
- end
- function Screen.debug(command)
- if not command then
- command = 'pynvim -n -c '
- end
- command = command .. request('vim_eval', '$NVIM_LISTEN_ADDRESS')
- if debug_screen then
- debug_screen:close()
- end
- debug_screen = io.popen(command, 'r')
- debug_screen:read()
- end
- function Screen.new(width, height)
- if not width then
- width = 53
- end
- if not height then
- height = 14
- end
- local self = setmetatable({
- timeout = default_screen_timeout,
- title = '',
- icon = '',
- bell = false,
- update_menu = false,
- visual_bell = false,
- 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)
- return self
- end
- function Screen:set_default_attr_ids(attr_ids)
- self._default_attr_ids = attr_ids
- end
- function Screen:get_default_attr_ids()
- return deepcopy(self._default_attr_ids)
- end
- 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 = {}
- 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()
- uimeths.detach()
- end
- function Screen:try_resize(columns, rows)
- uimeths.try_resize(columns, rows)
- end
- function Screen:set_option(option, value)
- uimeths.set_option(option, value)
- self._options[option] = value
- end
- -- 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:
- --
- -- 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.
- -- 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 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) == "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 = {}
- else
- assert(false)
- end
- if grid ~= nil then
- -- Remove the last line and dedent. Note that gsub returns more then one
- -- value.
- 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 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
- return tostring(res)
- end
- end
- 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], attr_state)
- end
- 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.any) then
- return (
- 'Failed to match any screen lines.\n'
- .. 'Expected (anywhere): "' .. expected.any .. '"\n'
- .. 'Actual:\n |' .. table.concat(actual_rows, '|\n |') .. '|\n\n')
- end
- 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
- local msg_expected_rows = {}
- for j = 1, #expected_rows do
- msg_expected_rows[j] = expected_rows[j]
- end
- msg_expected_rows[i] = '*' .. msg_expected_rows[i]
- actual_rows[i] = '*' .. actual_rows[i]
- return (
- 'Row ' .. tostring(i) .. ' did not match.\n'
- ..'Expected:\n |'..table.concat(msg_expected_rows, '|\n |')..'|\n'
- ..'Actual:\n |'..table.concat(actual_rows, '|\n |')..'|\n\n'..[[
- To print the expect() call that would assert the current screen state, use
- screen:snapshot_util(). In case of non-deterministic failures, use
- screen:redraw_debug() to show all intermediate screen states. ]])
- 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, 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')
- 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
- if did_miminal_timeout then
- helpers.stop()
- end
- elseif success_seen and #args > 0 then
- failure_after_success = true
- --print(require('inspect')(args))
- end
- return true
- end
- 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: Screen test succeeded immediately. 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 with 'unchanged' argument of 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 screen:expect(...) (or wait()) between
- asynchronous (feed(), nvim_input()) and synchronous API calls.
- - Use screen:redraw_debug() to investigate; it may find relevant intermediate
- states that should be added to the test to make it more robust.
- - If the purpose of the test is to assert state after some user input sent
- with feed(), adding screen:expect() before the feed() will help to ensure
- the input is sent when Nvim is in a predictable state. This is preferable
- to wait(), for being closer to real user interaction.
- - wait() can trigger redraws and consequently generate more indeterminism.
- Try removing 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 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)
- local function notification_cb(method, args)
- assert(method == 'redraw')
- self:_redraw(args)
- end
- run(nil, notification_cb, nil, ms)
- end
- function Screen:_redraw(updates)
- local did_flush = false
- for k, update in ipairs(updates) do
- -- print('--')
- -- print(require('inspect')(update))
- local method = update[1]
- for i = 2, #update do
- local handler_name = '_handle_'..method
- local handler = self[handler_name]
- if handler ~= nil then
- handler(self, unpack(update[i]))
- else
- assert(self._on_event,
- "Add Screen:"..handler_name.." or call Screen:set_on_event_handler")
- self._on_event(method, update[i])
- end
- end
- if k == #updates and method == "flush" then
- did_flush = true
- end
- end
- return did_flush
- end
- function Screen:set_on_event_handler(callback)
- self._on_event = callback
- end
- function Screen:_handle_resize(width, height)
- local rows = {}
- for _ = 1, height do
- local cols = {}
- for _ = 1, width do
- table.insert(cols, {text = ' ', attrs = self._clear_attrs, hl_id = 0})
- end
- table.insert(rows, cols)
- end
- self._cursor.row = 1
- self._cursor.col = 1
- self._rows = rows
- self._width = width
- self._height = height
- self._scroll_region = {
- top = 1, bot = height, left = 1, right = width
- }
- 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()
- -- 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()
- local row, col = self._cursor.row, self._cursor.col
- self:_clear_block(row, row, col, self._scroll_region.right)
- end
- function Screen:_handle_cursor_goto(row, col)
- self._cursor.row = row + 1
- 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
- function Screen:_handle_busy_stop()
- self._busy = false
- end
- function Screen:_handle_mouse_on()
- self._mouse_enabled = true
- end
- function Screen:_handle_mouse_off()
- self._mouse_enabled = false
- end
- function Screen:_handle_mode_change(mode, idx)
- assert(mode == self._mode_info[idx+1].name)
- self.mode = mode
- end
- function Screen:_handle_set_scroll_region(top, bot, left, right)
- self._scroll_region.top = top + 1
- self._scroll_region.bot = bot + 1
- self._scroll_region.left = left + 1
- self._scroll_region.right = right + 1
- end
- function Screen:_handle_scroll(count)
- local top = self._scroll_region.top
- 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 rows > 0 then
- start = top
- stop = bot - rows
- step = 1
- else
- start = bot
- 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 + 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 + 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
- function Screen:_handle_visual_bell()
- self.visual_bell = true
- end
- 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)
- self._fg = fg
- end
- function Screen:_handle_update_bg(bg)
- self._bg = bg
- end
- function Screen:_handle_update_sp(sp)
- self._sp = sp
- end
- function Screen:_handle_suspend()
- self.suspended = true
- end
- function Screen:_handle_update_menu()
- self.update_menu = true
- end
- function Screen:_handle_set_title(title)
- self.title = title
- end
- function Screen:_handle_set_icon(icon)
- self.icon = icon
- end
- 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)
- end
- end
- function Screen:_clear_row_section(rownum, startcol, stopcol)
- local row = self._rows[rownum]
- for i = startcol, stopcol do
- row[i].text = ' '
- row[i].attrs = self._clear_attrs
- end
- end
- function Screen:_row_repr(row, attr_state)
- local rv = {}
- local current_attr_id
- for i = 1, self._width do
- 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
- -- table.insert(rv, backward_find_meaningful(rv, i), '}')
- table.insert(rv, '}')
- current_attr_id = nil
- end
- if not current_attr_id and attr_id then
- -- open a new attribute bracket
- table.insert(rv, '{' .. attr_id .. ':')
- current_attr_id = attr_id
- end
- if not self._busy and self._rows[self._cursor.row] == row and self._cursor.col == i then
- table.insert(rv, '^')
- end
- table.insert(rv, row[i].text)
- end
- if current_attr_id then
- table.insert(rv, '}')
- end
- -- return the line representation, but remove empty attribute brackets and
- -- trailing whitespace
- 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
- 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 repr_chunks
- end
- -- Generates tests. Call it where Screen:expect() would be. Waits briefly, then
- -- dumps the current screen state in the form of Screen:expect().
- -- Use snapshot_util({},true) to generate a text-only (no attributes) test.
- --
- -- @see Screen:redraw_debug()
- function Screen:snapshot_util(attrs, ignore)
- self:sleep(250)
- self:print_snapshot(attrs, ignore)
- end
- function Screen:redraw_debug(attrs, ignore, timeout)
- self:print_snapshot(attrs, ignore)
- local function notification_cb(method, args)
- assert(method == 'redraw')
- for _, update in ipairs(args) do
- print(require('inspect')(update))
- end
- self:_redraw(args)
- self:print_snapshot(attrs, ignore)
- return true
- end
- if timeout == nil then
- timeout = 250
- end
- run(nil, notification_cb, nil, 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
- 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
- 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
- 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
- return table.concat(states, ", ")
- end
- end
- function Screen:_pprint_attrs(attrs)
- local items = {}
- for f, v in pairs(attrs) do
- local desc = tostring(v)
- if f == "foreground" or f == "background" or f == "special" then
- if Screen.colornames[v] ~= nil then
- desc = "Screen.colors."..Screen.colornames[v]
- end
- end
- table.insert(items, f.." = "..desc)
- end
- return table.concat(items, ", ")
- end
- local function backward_find_meaningful(tbl, from) -- luacheck: no unused
- for i = from or #tbl, 1, -1 do
- if tbl[i] ~= ' ' then
- return i + 1
- end
- end
- return from
- end
- function Screen:_get_attr_id(attr_state, attrs, hl_id)
- if not attr_state.ids then
- return
- 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
- 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
- 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.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
- end
- for i,a in pairs(attrs) do
- if self:_equal_attrs(a, attr) then
- return i
- end
- end
- return nil
- end
- return Screen
|