screen.lua 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271
  1. -- This module contains the Screen class, a complete Nvim UI implementation
  2. -- designed for functional testing (verifying screen state, in particular).
  3. --
  4. -- Screen:expect() takes a string representing the expected screen state and an
  5. -- optional set of attribute identifiers for checking highlighted characters.
  6. --
  7. -- Example usage:
  8. --
  9. -- local screen = Screen.new(25, 10)
  10. -- -- Attach the screen to the current Nvim instance.
  11. -- screen:attach()
  12. -- -- Enter insert-mode and type some text.
  13. -- feed('ihello screen')
  14. -- -- Assert the expected screen state.
  15. -- screen:expect([[
  16. -- hello screen |
  17. -- ~ |
  18. -- ~ |
  19. -- ~ |
  20. -- ~ |
  21. -- ~ |
  22. -- ~ |
  23. -- ~ |
  24. -- ~ |
  25. -- -- INSERT -- |
  26. -- ]]) -- <- Last line is stripped
  27. --
  28. -- Since screen updates are received asynchronously, expect() actually specifies
  29. -- the _eventual_ screen state.
  30. --
  31. -- This is how expect() works:
  32. -- * It starts the event loop with a timeout.
  33. -- * Each time it receives an update it checks that against the expected state.
  34. -- * If the expected state matches the current state, the event loop will be
  35. -- stopped and expect() will return.
  36. -- * If the timeout expires, the last match error will be reported and the
  37. -- test will fail.
  38. --
  39. -- Continuing the above example, say we want to assert that "-- INSERT --" is
  40. -- highlighted with the bold attribute. The expect() call should look like this:
  41. --
  42. -- NonText = Screen.colors.Blue
  43. -- screen:expect([[
  44. -- hello screen |
  45. -- ~ |
  46. -- ~ |
  47. -- ~ |
  48. -- ~ |
  49. -- ~ |
  50. -- ~ |
  51. -- ~ |
  52. -- ~ |
  53. -- {b:-- INSERT --} |
  54. -- ]], {b = {bold = true}}, {{bold = true, foreground = NonText}})
  55. --
  56. -- In this case "b" is a string associated with the set composed of one
  57. -- attribute: bold. Note that since the {b:} markup is not a real part of the
  58. -- screen, the delimiter "|" moved to the right. Also, the highlighting of the
  59. -- NonText markers "~" is ignored in this test.
  60. --
  61. -- Tests will often share a group of attribute sets to expect(). Those can be
  62. -- defined at the beginning of a test:
  63. --
  64. -- NonText = Screen.colors.Blue
  65. -- screen:set_default_attr_ids( {
  66. -- [1] = {reverse = true, bold = true},
  67. -- [2] = {reverse = true}
  68. -- })
  69. -- screen:set_default_attr_ignore( {{}, {bold=true, foreground=NonText}} )
  70. --
  71. -- To help write screen tests, see Screen:snapshot_util().
  72. -- To debug screen tests, see Screen:redraw_debug().
  73. local global_helpers = require('test.helpers')
  74. local deepcopy = global_helpers.deepcopy
  75. local shallowcopy = global_helpers.shallowcopy
  76. local helpers = require('test.functional.helpers')(nil)
  77. local request, run, uimeths = helpers.request, helpers.run, helpers.uimeths
  78. local eq = helpers.eq
  79. local dedent = helpers.dedent
  80. local inspect = require('inspect')
  81. local function isempty(v)
  82. return type(v) == 'table' and next(v) == nil
  83. end
  84. local Screen = {}
  85. Screen.__index = Screen
  86. local debug_screen
  87. local default_timeout_factor = 1
  88. if os.getenv('VALGRIND') then
  89. default_timeout_factor = default_timeout_factor * 3
  90. end
  91. if os.getenv('CI') then
  92. default_timeout_factor = default_timeout_factor * 3
  93. end
  94. local default_screen_timeout = default_timeout_factor * 3500
  95. do
  96. local spawn, nvim_prog = helpers.spawn, helpers.nvim_prog
  97. local session = spawn({nvim_prog, '-u', 'NONE', '-i', 'NONE', '-N', '--embed'})
  98. local status, rv = session:request('nvim_get_color_map')
  99. if not status then
  100. print('failed to get color map')
  101. os.exit(1)
  102. end
  103. local colors = rv
  104. local colornames = {}
  105. for name, rgb in pairs(colors) do
  106. -- we disregard the case that colornames might not be unique, as
  107. -- this is just a helper to get any canonical name of a color
  108. colornames[rgb] = name
  109. end
  110. session:close()
  111. Screen.colors = colors
  112. Screen.colornames = colornames
  113. end
  114. function Screen.debug(command)
  115. if not command then
  116. command = 'pynvim -n -c '
  117. end
  118. command = command .. request('vim_eval', '$NVIM_LISTEN_ADDRESS')
  119. if debug_screen then
  120. debug_screen:close()
  121. end
  122. debug_screen = io.popen(command, 'r')
  123. debug_screen:read()
  124. end
  125. function Screen.new(width, height)
  126. if not width then
  127. width = 53
  128. end
  129. if not height then
  130. height = 14
  131. end
  132. local self = setmetatable({
  133. timeout = default_screen_timeout,
  134. title = '',
  135. icon = '',
  136. bell = false,
  137. update_menu = false,
  138. visual_bell = false,
  139. suspended = false,
  140. mode = 'normal',
  141. options = {},
  142. popupmenu = nil,
  143. cmdline = {},
  144. cmdline_block = {},
  145. wildmenu_items = nil,
  146. wildmenu_selected = nil,
  147. _default_attr_ids = nil,
  148. _default_attr_ignore = nil,
  149. _mouse_enabled = true,
  150. _attrs = {},
  151. _hl_info = {},
  152. _attr_table = {[0]={{},{}}},
  153. _clear_attrs = {},
  154. _new_attrs = false,
  155. _width = width,
  156. _height = height,
  157. _cursor = {
  158. row = 1, col = 1
  159. },
  160. _busy = false
  161. }, Screen)
  162. return self
  163. end
  164. function Screen:set_default_attr_ids(attr_ids)
  165. self._default_attr_ids = attr_ids
  166. end
  167. function Screen:get_default_attr_ids()
  168. return deepcopy(self._default_attr_ids)
  169. end
  170. function Screen:set_default_attr_ignore(attr_ignore)
  171. self._default_attr_ignore = attr_ignore
  172. end
  173. function Screen:set_hlstate_cterm(val)
  174. self._hlstate_cterm = val
  175. end
  176. function Screen:attach(options)
  177. if options == nil then
  178. options = {}
  179. end
  180. if options.ext_linegrid == nil then
  181. options.ext_linegrid = true
  182. end
  183. self._options = options
  184. self._clear_attrs = (options.ext_linegrid and {{},{}}) or {}
  185. self:_handle_resize(self._width, self._height)
  186. uimeths.attach(self._width, self._height, options)
  187. if self._options.rgb == nil then
  188. -- nvim defaults to rgb=true internally,
  189. -- simplify test code by doing the same.
  190. self._options.rgb = true
  191. end
  192. end
  193. function Screen:detach()
  194. uimeths.detach()
  195. end
  196. function Screen:try_resize(columns, rows)
  197. uimeths.try_resize(columns, rows)
  198. end
  199. function Screen:set_option(option, value)
  200. uimeths.set_option(option, value)
  201. self._options[option] = value
  202. end
  203. -- canonical order of ext keys, used to generate asserts
  204. local ext_keys = {
  205. 'popupmenu', 'cmdline', 'cmdline_block', 'wildmenu_items', 'wildmenu_pos'
  206. }
  207. -- Asserts that the screen state eventually matches an expected state
  208. --
  209. -- This function can either be called with the positional forms
  210. --
  211. -- screen:expect(grid, [attr_ids, attr_ignore])
  212. -- screen:expect(condition)
  213. --
  214. -- or to use additional arguments (or grid and condition at the same time)
  215. -- the keyword form has to be used:
  216. --
  217. -- screen:expect{grid=[[...]], cmdline={...}, condition=function() ... end}
  218. --
  219. --
  220. -- grid: Expected screen state (string). Each line represents a screen
  221. -- row. Last character of each row (typically "|") is stripped.
  222. -- Common indentation is stripped.
  223. -- attr_ids: Expected text attributes. Screen rows are transformed according
  224. -- to this table, as follows: each substring S composed of
  225. -- characters having the same attributes will be substituted by
  226. -- "{K:S}", where K is a key in `attr_ids`. Any unexpected
  227. -- attributes in the final state are an error.
  228. -- Use screen:set_default_attr_ids() to define attributes for many
  229. -- expect() calls.
  230. -- attr_ignore: Ignored text attributes, or `true` to ignore all. By default
  231. -- nothing is ignored.
  232. -- condition: Function asserting some arbitrary condition. Return value is
  233. -- ignored, throw an error (use eq() or similar) to signal failure.
  234. -- any: Lua pattern string expected to match a screen line. NB: the
  235. -- following chars are magic characters
  236. -- ( ) . % + - * ? [ ^ $
  237. -- and must be escaped with a preceding % for a literal match.
  238. -- mode: Expected mode as signaled by "mode_change" event
  239. -- unchanged: Test that the screen state is unchanged since the previous
  240. -- expect(...). Any flush event resulting in a different state is
  241. -- considered an error. Not observing any events until timeout
  242. -- is acceptable.
  243. -- intermediate:Test that the final state is the same as the previous expect,
  244. -- but expect an intermediate state that is different. If possible
  245. -- it is better to use an explicit screen:expect(...) for this
  246. -- intermediate state.
  247. -- reset: Reset the state internal to the test Screen before starting to
  248. -- receive updates. This should be used after command("redraw!")
  249. -- or some other mechanism that will invoke "redraw!", to check
  250. -- that all screen state is transmitted again. This includes
  251. -- state related to ext_ features as mentioned below.
  252. -- timeout: maximum time that will be waited until the expected state is
  253. -- seen (or maximum time to observe an incorrect change when
  254. -- `unchanged` flag is used)
  255. --
  256. -- The following keys should be used to expect the state of various ext_
  257. -- features. Note that an absent key will assert that the item is currently
  258. -- NOT present on the screen, also when positional form is used.
  259. --
  260. -- popupmenu: Expected ext_popupmenu state,
  261. -- cmdline: Expected ext_cmdline state, as an array of cmdlines of
  262. -- different level.
  263. -- cmdline_block: Expected ext_cmdline block (for function definitions)
  264. -- wildmenu_items: Expected items for ext_wildmenu
  265. -- wildmenu_pos: Expected position for ext_wildmenu
  266. function Screen:expect(expected, attr_ids, attr_ignore)
  267. local grid, condition = nil, nil
  268. local expected_rows = {}
  269. if type(expected) == "table" then
  270. assert(not (attr_ids ~= nil or attr_ignore ~= nil))
  271. local is_key = {grid=true, attr_ids=true, attr_ignore=true, condition=true,
  272. any=true, mode=true, unchanged=true, intermediate=true,
  273. reset=true, timeout=true}
  274. for _, v in ipairs(ext_keys) do
  275. is_key[v] = true
  276. end
  277. for k, _ in pairs(expected) do
  278. if not is_key[k] then
  279. error("Screen:expect: Unknown keyword argument '"..k.."'")
  280. end
  281. end
  282. grid = expected.grid
  283. attr_ids = expected.attr_ids
  284. attr_ignore = expected.attr_ignore
  285. condition = expected.condition
  286. assert(not (expected.any ~= nil and grid ~= nil))
  287. elseif type(expected) == "string" then
  288. grid = expected
  289. expected = {}
  290. elseif type(expected) == "function" then
  291. assert(not (attr_ids ~= nil or attr_ignore ~= nil))
  292. condition = expected
  293. expected = {}
  294. else
  295. assert(false)
  296. end
  297. if grid ~= nil then
  298. -- Remove the last line and dedent. Note that gsub returns more then one
  299. -- value.
  300. grid = dedent(grid:gsub('\n[ ]+$', ''), 0)
  301. for row in grid:gmatch('[^\n]+') do
  302. row = row:sub(1, #row - 1) -- Last char must be the screen delimiter.
  303. table.insert(expected_rows, row)
  304. end
  305. end
  306. local attr_state = {
  307. ids = attr_ids or self._default_attr_ids,
  308. ignore = attr_ignore or self._default_attr_ignore,
  309. }
  310. if self._options.ext_hlstate then
  311. attr_state.id_to_index = self:hlstate_check_attrs(attr_state.ids or {})
  312. end
  313. self._new_attrs = false
  314. self:_wait(function()
  315. if condition ~= nil then
  316. local status, res = pcall(condition)
  317. if not status then
  318. return tostring(res)
  319. end
  320. end
  321. if grid ~= nil and self._height ~= #expected_rows then
  322. return ("Expected screen state's row count(" .. #expected_rows
  323. .. ') differs from configured height(' .. self._height .. ') of Screen.')
  324. end
  325. if self._options.ext_hlstate and self._new_attrs then
  326. attr_state.id_to_index = self:hlstate_check_attrs(attr_state.ids or {})
  327. end
  328. local actual_rows = {}
  329. for i = 1, self._height do
  330. actual_rows[i] = self:_row_repr(self._rows[i], attr_state)
  331. end
  332. if expected.any ~= nil then
  333. -- Search for `any` anywhere in the screen lines.
  334. local actual_screen_str = table.concat(actual_rows, '\n')
  335. if nil == string.find(actual_screen_str, expected.any) then
  336. return (
  337. 'Failed to match any screen lines.\n'
  338. .. 'Expected (anywhere): "' .. expected.any .. '"\n'
  339. .. 'Actual:\n |' .. table.concat(actual_rows, '|\n |') .. '|\n\n')
  340. end
  341. end
  342. if grid ~= nil then
  343. -- `expected` must match the screen lines exactly.
  344. for i = 1, self._height do
  345. if expected_rows[i] ~= actual_rows[i] then
  346. local msg_expected_rows = {}
  347. for j = 1, #expected_rows do
  348. msg_expected_rows[j] = expected_rows[j]
  349. end
  350. msg_expected_rows[i] = '*' .. msg_expected_rows[i]
  351. actual_rows[i] = '*' .. actual_rows[i]
  352. return (
  353. 'Row ' .. tostring(i) .. ' did not match.\n'
  354. ..'Expected:\n |'..table.concat(msg_expected_rows, '|\n |')..'|\n'
  355. ..'Actual:\n |'..table.concat(actual_rows, '|\n |')..'|\n\n'..[[
  356. To print the expect() call that would assert the current screen state, use
  357. screen:snapshot_util(). In case of non-deterministic failures, use
  358. screen:redraw_debug() to show all intermediate screen states. ]])
  359. end
  360. end
  361. end
  362. -- Extension features. The default expectations should cover the case of
  363. -- the ext_ feature being disabled, or the feature currently not activated
  364. -- (for instance no external cmdline visible). Some extensions require
  365. -- preprocessing to represent highlights in a reproducible way.
  366. local extstate = self:_extstate_repr(attr_state)
  367. -- convert assertion errors into invalid screen state descriptions
  368. local status, res = pcall(function()
  369. for _, k in ipairs(ext_keys) do
  370. -- Empty states is considered the default and need not be mentioned
  371. if not (expected[k] == nil and isempty(extstate[k])) then
  372. eq(expected[k], extstate[k], k)
  373. end
  374. end
  375. if expected.mode ~= nil then
  376. eq(expected.mode, self.mode, "mode")
  377. end
  378. end)
  379. if not status then
  380. return tostring(res)
  381. end
  382. end, expected)
  383. end
  384. function Screen:_wait(check, flags)
  385. local err, checked = false, false
  386. local success_seen = false
  387. local failure_after_success = false
  388. local did_flush = true
  389. local warn_immediate = not (flags.unchanged or flags.intermediate)
  390. if flags.intermediate and flags.unchanged then
  391. error("Choose only one of 'intermediate' and 'unchanged', not both")
  392. end
  393. if flags.reset then
  394. -- throw away all state, we expect it to be retransmitted
  395. self:_reset()
  396. end
  397. -- Maximum timeout, after which a incorrect state will be regarded as a
  398. -- failure
  399. local timeout = flags.timeout or self.timeout
  400. -- Minimal timeout before the loop is allowed to be stopped so we
  401. -- always do some check for failure after success.
  402. local minimal_timeout = default_timeout_factor * 2
  403. local immediate_seen, intermediate_seen = false, false
  404. if not check() then
  405. minimal_timeout = default_timeout_factor * 20
  406. immediate_seen = true
  407. end
  408. -- for an unchanged test, flags.timeout means the time during the state is
  409. -- expected to be unchanged, so always wait this full time.
  410. if (flags.unchanged or flags.intermediate) and flags.timeout ~= nil then
  411. minimal_timeout = timeout
  412. end
  413. assert(timeout >= minimal_timeout)
  414. local did_miminal_timeout = false
  415. local function notification_cb(method, args)
  416. assert(method == 'redraw')
  417. did_flush = self:_redraw(args)
  418. if not did_flush then
  419. return
  420. end
  421. err = check()
  422. checked = true
  423. if err and immediate_seen then
  424. intermediate_seen = true
  425. end
  426. if not err then
  427. success_seen = true
  428. if did_miminal_timeout then
  429. helpers.stop()
  430. end
  431. elseif success_seen and #args > 0 then
  432. failure_after_success = true
  433. --print(require('inspect')(args))
  434. end
  435. return true
  436. end
  437. run(nil, notification_cb, nil, minimal_timeout)
  438. if not did_flush then
  439. err = "no flush received"
  440. elseif not checked then
  441. err = check()
  442. if not err and flags.unchanged then
  443. -- expecting NO screen change: use a shorter timout
  444. success_seen = true
  445. end
  446. end
  447. if not success_seen then
  448. did_miminal_timeout = true
  449. run(nil, notification_cb, nil, timeout-minimal_timeout)
  450. end
  451. local did_warn = false
  452. if warn_immediate and immediate_seen then
  453. print([[
  454. warning: Screen test succeeded immediately. Try to avoid this unless the
  455. purpose of the test really requires it.]])
  456. if intermediate_seen then
  457. print([[
  458. There are intermediate states between the two identical expects.
  459. Use screen:snapshot_util() or screen:redraw_debug() to find them, and add them
  460. to the test if they make sense.
  461. ]])
  462. else
  463. print([[If necessary, silence this warning with 'unchanged' argument of screen:expect.]])
  464. end
  465. did_warn = true
  466. end
  467. if failure_after_success then
  468. print([[
  469. warning: Screen changes were received after the expected state. This indicates
  470. indeterminism in the test. Try adding screen:expect(...) (or wait()) between
  471. asynchronous (feed(), nvim_input()) and synchronous API calls.
  472. - Use screen:redraw_debug() to investigate; it may find relevant intermediate
  473. states that should be added to the test to make it more robust.
  474. - If the purpose of the test is to assert state after some user input sent
  475. with feed(), adding screen:expect() before the feed() will help to ensure
  476. the input is sent when Nvim is in a predictable state. This is preferable
  477. to wait(), for being closer to real user interaction.
  478. - wait() can trigger redraws and consequently generate more indeterminism.
  479. Try removing wait().
  480. ]])
  481. did_warn = true
  482. end
  483. if err then
  484. assert(false, err)
  485. elseif did_warn then
  486. local tb = debug.traceback()
  487. local index = string.find(tb, '\n%s*%[C]')
  488. print(string.sub(tb,1,index))
  489. end
  490. if flags.intermediate then
  491. assert(intermediate_seen, "expected intermediate screen state before final screen state")
  492. elseif flags.unchanged then
  493. assert(not intermediate_seen, "expected screen state to be unchanged")
  494. end
  495. end
  496. function Screen:sleep(ms)
  497. local function notification_cb(method, args)
  498. assert(method == 'redraw')
  499. self:_redraw(args)
  500. end
  501. run(nil, notification_cb, nil, ms)
  502. end
  503. function Screen:_redraw(updates)
  504. local did_flush = false
  505. for k, update in ipairs(updates) do
  506. -- print('--')
  507. -- print(require('inspect')(update))
  508. local method = update[1]
  509. for i = 2, #update do
  510. local handler_name = '_handle_'..method
  511. local handler = self[handler_name]
  512. if handler ~= nil then
  513. handler(self, unpack(update[i]))
  514. else
  515. assert(self._on_event,
  516. "Add Screen:"..handler_name.." or call Screen:set_on_event_handler")
  517. self._on_event(method, update[i])
  518. end
  519. end
  520. if k == #updates and method == "flush" then
  521. did_flush = true
  522. end
  523. end
  524. return did_flush
  525. end
  526. function Screen:set_on_event_handler(callback)
  527. self._on_event = callback
  528. end
  529. function Screen:_handle_resize(width, height)
  530. local rows = {}
  531. for _ = 1, height do
  532. local cols = {}
  533. for _ = 1, width do
  534. table.insert(cols, {text = ' ', attrs = self._clear_attrs, hl_id = 0})
  535. end
  536. table.insert(rows, cols)
  537. end
  538. self._cursor.row = 1
  539. self._cursor.col = 1
  540. self._rows = rows
  541. self._width = width
  542. self._height = height
  543. self._scroll_region = {
  544. top = 1, bot = height, left = 1, right = width
  545. }
  546. end
  547. function Screen:_handle_flush()
  548. end
  549. function Screen:_handle_grid_resize(grid, width, height)
  550. assert(grid == 1)
  551. self:_handle_resize(width, height)
  552. end
  553. function Screen:_reset()
  554. -- TODO: generalize to multigrid later
  555. self:_handle_grid_clear(1)
  556. -- TODO: share with initialization, so it generalizes?
  557. self.popupmenu = nil
  558. self.cmdline = {}
  559. self.cmdline_block = {}
  560. self.wildmenu_items = nil
  561. self.wildmenu_pos = nil
  562. end
  563. function Screen:_handle_mode_info_set(cursor_style_enabled, mode_info)
  564. self._cursor_style_enabled = cursor_style_enabled
  565. for _, item in pairs(mode_info) do
  566. -- attr IDs are not stable, but their value should be
  567. if item.attr_id ~= nil then
  568. item.attr = self._attr_table[item.attr_id][1]
  569. item.attr_id = nil
  570. end
  571. if item.attr_id_lm ~= nil then
  572. item.attr_lm = self._attr_table[item.attr_id_lm][1]
  573. item.attr_id_lm = nil
  574. end
  575. end
  576. self._mode_info = mode_info
  577. end
  578. function Screen:_handle_clear()
  579. -- the first implemented UI protocol clients (python-gui and builitin TUI)
  580. -- allowed the cleared region to be restricted by setting the scroll region.
  581. -- this was never used by nvim tough, and not documented and implemented by
  582. -- newer clients, to check we remain compatible with both kind of clients,
  583. -- ensure the scroll region is in a reset state.
  584. local expected_region = {
  585. top = 1, bot = self._height, left = 1, right = self._width
  586. }
  587. eq(expected_region, self._scroll_region)
  588. self:_clear_block(1, self._height, 1, self._width)
  589. end
  590. function Screen:_handle_grid_clear(grid)
  591. assert(grid == 1)
  592. self:_clear_block(1, self._height, 1, self._width)
  593. end
  594. function Screen:_handle_eol_clear()
  595. local row, col = self._cursor.row, self._cursor.col
  596. self:_clear_block(row, row, col, self._scroll_region.right)
  597. end
  598. function Screen:_handle_cursor_goto(row, col)
  599. self._cursor.row = row + 1
  600. self._cursor.col = col + 1
  601. end
  602. function Screen:_handle_grid_cursor_goto(grid, row, col)
  603. assert(grid == 1)
  604. self._cursor.row = row + 1
  605. self._cursor.col = col + 1
  606. end
  607. function Screen:_handle_busy_start()
  608. self._busy = true
  609. end
  610. function Screen:_handle_busy_stop()
  611. self._busy = false
  612. end
  613. function Screen:_handle_mouse_on()
  614. self._mouse_enabled = true
  615. end
  616. function Screen:_handle_mouse_off()
  617. self._mouse_enabled = false
  618. end
  619. function Screen:_handle_mode_change(mode, idx)
  620. assert(mode == self._mode_info[idx+1].name)
  621. self.mode = mode
  622. end
  623. function Screen:_handle_set_scroll_region(top, bot, left, right)
  624. self._scroll_region.top = top + 1
  625. self._scroll_region.bot = bot + 1
  626. self._scroll_region.left = left + 1
  627. self._scroll_region.right = right + 1
  628. end
  629. function Screen:_handle_scroll(count)
  630. local top = self._scroll_region.top
  631. local bot = self._scroll_region.bot
  632. local left = self._scroll_region.left
  633. local right = self._scroll_region.right
  634. self:_handle_grid_scroll(1, top-1, bot, left-1, right, count, 0)
  635. end
  636. function Screen:_handle_grid_scroll(grid, top, bot, left, right, rows, cols)
  637. top = top+1
  638. left = left+1
  639. assert(grid == 1)
  640. assert(cols == 0)
  641. local start, stop, step
  642. if rows > 0 then
  643. start = top
  644. stop = bot - rows
  645. step = 1
  646. else
  647. start = bot
  648. stop = top - rows
  649. step = -1
  650. end
  651. -- shift scroll region
  652. for i = start, stop, step do
  653. local target = self._rows[i]
  654. local source = self._rows[i + rows]
  655. for j = left, right do
  656. target[j].text = source[j].text
  657. target[j].attrs = source[j].attrs
  658. target[j].hl_id = source[j].hl_id
  659. end
  660. end
  661. -- clear invalid rows
  662. for i = stop + step, stop + rows, step do
  663. self:_clear_row_section(i, left, right)
  664. end
  665. end
  666. function Screen:_handle_hl_attr_define(id, rgb_attrs, cterm_attrs, info)
  667. self._attr_table[id] = {rgb_attrs, cterm_attrs}
  668. self._hl_info[id] = info
  669. self._new_attrs = true
  670. end
  671. function Screen:_handle_highlight_set(attrs)
  672. self._attrs = attrs
  673. end
  674. function Screen:_handle_put(str)
  675. assert(not self._options.ext_linegrid)
  676. local cell = self._rows[self._cursor.row][self._cursor.col]
  677. cell.text = str
  678. cell.attrs = self._attrs
  679. cell.hl_id = -1
  680. self._cursor.col = self._cursor.col + 1
  681. end
  682. function Screen:_handle_grid_line(grid, row, col, items)
  683. assert(self._options.ext_linegrid)
  684. assert(grid == 1)
  685. local line = self._rows[row+1]
  686. local colpos = col+1
  687. local hl = self._clear_attrs
  688. local hl_id = 0
  689. for _,item in ipairs(items) do
  690. local text, hl_id_cell, count = unpack(item)
  691. if hl_id_cell ~= nil then
  692. hl_id = hl_id_cell
  693. hl = self._attr_table[hl_id]
  694. end
  695. for _ = 1, (count or 1) do
  696. local cell = line[colpos]
  697. cell.text = text
  698. cell.hl_id = hl_id
  699. cell.attrs = hl
  700. colpos = colpos+1
  701. end
  702. end
  703. end
  704. function Screen:_handle_bell()
  705. self.bell = true
  706. end
  707. function Screen:_handle_visual_bell()
  708. self.visual_bell = true
  709. end
  710. function Screen:_handle_default_colors_set(rgb_fg, rgb_bg, rgb_sp, cterm_fg, cterm_bg)
  711. self.default_colors = {
  712. rgb_fg=rgb_fg,
  713. rgb_bg=rgb_bg,
  714. rgb_sp=rgb_sp,
  715. cterm_fg=cterm_fg,
  716. cterm_bg=cterm_bg
  717. }
  718. end
  719. function Screen:_handle_update_fg(fg)
  720. self._fg = fg
  721. end
  722. function Screen:_handle_update_bg(bg)
  723. self._bg = bg
  724. end
  725. function Screen:_handle_update_sp(sp)
  726. self._sp = sp
  727. end
  728. function Screen:_handle_suspend()
  729. self.suspended = true
  730. end
  731. function Screen:_handle_update_menu()
  732. self.update_menu = true
  733. end
  734. function Screen:_handle_set_title(title)
  735. self.title = title
  736. end
  737. function Screen:_handle_set_icon(icon)
  738. self.icon = icon
  739. end
  740. function Screen:_handle_option_set(name, value)
  741. self.options[name] = value
  742. end
  743. function Screen:_handle_popupmenu_show(items, selected, row, col)
  744. self.popupmenu = {items=items,pos=selected, anchor={row, col}}
  745. end
  746. function Screen:_handle_popupmenu_select(selected)
  747. self.popupmenu.pos = selected
  748. end
  749. function Screen:_handle_popupmenu_hide()
  750. self.popupmenu = nil
  751. end
  752. function Screen:_handle_cmdline_show(content, pos, firstc, prompt, indent, level)
  753. if firstc == '' then firstc = nil end
  754. if prompt == '' then prompt = nil end
  755. if indent == 0 then indent = nil end
  756. self.cmdline[level] = {content=content, pos=pos, firstc=firstc,
  757. prompt=prompt, indent=indent}
  758. end
  759. function Screen:_handle_cmdline_hide(level)
  760. self.cmdline[level] = nil
  761. end
  762. function Screen:_handle_cmdline_special_char(char, shift, level)
  763. -- cleared by next cmdline_show on the same level
  764. self.cmdline[level].special = {char, shift}
  765. end
  766. function Screen:_handle_cmdline_pos(pos, level)
  767. self.cmdline[level].pos = pos
  768. end
  769. function Screen:_handle_cmdline_block_show(block)
  770. self.cmdline_block = block
  771. end
  772. function Screen:_handle_cmdline_block_append(item)
  773. self.cmdline_block[#self.cmdline_block+1] = item
  774. end
  775. function Screen:_handle_cmdline_block_hide()
  776. self.cmdline_block = {}
  777. end
  778. function Screen:_handle_wildmenu_show(items)
  779. self.wildmenu_items = items
  780. end
  781. function Screen:_handle_wildmenu_select(pos)
  782. self.wildmenu_pos = pos
  783. end
  784. function Screen:_handle_wildmenu_hide()
  785. self.wildmenu_items, self.wildmenu_pos = nil, nil
  786. end
  787. function Screen:_clear_block(top, bot, left, right)
  788. for i = top, bot do
  789. self:_clear_row_section(i, left, right)
  790. end
  791. end
  792. function Screen:_clear_row_section(rownum, startcol, stopcol)
  793. local row = self._rows[rownum]
  794. for i = startcol, stopcol do
  795. row[i].text = ' '
  796. row[i].attrs = self._clear_attrs
  797. end
  798. end
  799. function Screen:_row_repr(row, attr_state)
  800. local rv = {}
  801. local current_attr_id
  802. for i = 1, self._width do
  803. local attrs = row[i].attrs
  804. if self._options.ext_linegrid then
  805. attrs = attrs[(self._options.rgb and 1) or 2]
  806. end
  807. local attr_id = self:_get_attr_id(attr_state, attrs, row[i].hl_id)
  808. if current_attr_id and attr_id ~= current_attr_id then
  809. -- close current attribute bracket, add it before any whitespace
  810. -- up to the current cell
  811. -- table.insert(rv, backward_find_meaningful(rv, i), '}')
  812. table.insert(rv, '}')
  813. current_attr_id = nil
  814. end
  815. if not current_attr_id and attr_id then
  816. -- open a new attribute bracket
  817. table.insert(rv, '{' .. attr_id .. ':')
  818. current_attr_id = attr_id
  819. end
  820. if not self._busy and self._rows[self._cursor.row] == row and self._cursor.col == i then
  821. table.insert(rv, '^')
  822. end
  823. table.insert(rv, row[i].text)
  824. end
  825. if current_attr_id then
  826. table.insert(rv, '}')
  827. end
  828. -- return the line representation, but remove empty attribute brackets and
  829. -- trailing whitespace
  830. return table.concat(rv, '')--:gsub('%s+$', '')
  831. end
  832. function Screen:_extstate_repr(attr_state)
  833. local cmdline = {}
  834. for i, entry in pairs(self.cmdline) do
  835. entry = shallowcopy(entry)
  836. entry.content = self:_chunks_repr(entry.content, attr_state)
  837. cmdline[i] = entry
  838. end
  839. local cmdline_block = {}
  840. for i, entry in ipairs(self.cmdline_block) do
  841. cmdline_block[i] = self:_chunks_repr(entry, attr_state)
  842. end
  843. return {
  844. popupmenu=self.popupmenu,
  845. cmdline=cmdline,
  846. cmdline_block=cmdline_block,
  847. wildmenu_items=self.wildmenu_items,
  848. wildmenu_pos=self.wildmenu_pos,
  849. }
  850. end
  851. function Screen:_chunks_repr(chunks, attr_state)
  852. local repr_chunks = {}
  853. for i, chunk in ipairs(chunks) do
  854. local hl, text = unpack(chunk)
  855. local attrs
  856. if self._options.ext_linegrid then
  857. attrs = self._attr_table[hl][1]
  858. else
  859. attrs = hl
  860. end
  861. local attr_id = self:_get_attr_id(attr_state, attrs, hl)
  862. repr_chunks[i] = {text, attr_id}
  863. end
  864. return repr_chunks
  865. end
  866. -- Generates tests. Call it where Screen:expect() would be. Waits briefly, then
  867. -- dumps the current screen state in the form of Screen:expect().
  868. -- Use snapshot_util({},true) to generate a text-only (no attributes) test.
  869. --
  870. -- @see Screen:redraw_debug()
  871. function Screen:snapshot_util(attrs, ignore)
  872. self:sleep(250)
  873. self:print_snapshot(attrs, ignore)
  874. end
  875. function Screen:redraw_debug(attrs, ignore, timeout)
  876. self:print_snapshot(attrs, ignore)
  877. local function notification_cb(method, args)
  878. assert(method == 'redraw')
  879. for _, update in ipairs(args) do
  880. print(require('inspect')(update))
  881. end
  882. self:_redraw(args)
  883. self:print_snapshot(attrs, ignore)
  884. return true
  885. end
  886. if timeout == nil then
  887. timeout = 250
  888. end
  889. run(nil, notification_cb, nil, timeout)
  890. end
  891. function Screen:print_snapshot(attrs, ignore)
  892. attrs = attrs or self._default_attr_ids
  893. if ignore == nil then
  894. ignore = self._default_attr_ignore
  895. end
  896. local attr_state = {
  897. ids = {},
  898. ignore = ignore,
  899. mutable = true, -- allow _row_repr to add missing highlights
  900. }
  901. if attrs ~= nil then
  902. for i, a in pairs(attrs) do
  903. attr_state.ids[i] = a
  904. end
  905. end
  906. if self._options.ext_hlstate then
  907. attr_state.id_to_index = self:hlstate_check_attrs(attr_state.ids)
  908. end
  909. local lines = {}
  910. for i = 1, self._height do
  911. table.insert(lines, " "..self:_row_repr(self._rows[i], attr_state).."|")
  912. end
  913. local ext_state = self:_extstate_repr(attr_state)
  914. local keys = false
  915. for k, v in pairs(ext_state) do
  916. if isempty(v) then
  917. ext_state[k] = nil -- deleting keys while iterating is ok
  918. else
  919. keys = true
  920. end
  921. end
  922. local attrstr = ""
  923. if attr_state.modified then
  924. local attrstrs = {}
  925. for i, a in pairs(attr_state.ids) do
  926. local dict
  927. if self._options.ext_hlstate then
  928. dict = self:_pprint_hlstate(a)
  929. else
  930. dict = "{"..self:_pprint_attrs(a).."}"
  931. end
  932. local keyval = (type(i) == "number") and "["..tostring(i).."]" or i
  933. table.insert(attrstrs, " "..keyval.." = "..dict..",")
  934. end
  935. attrstr = (", "..(keys and "attr_ids=" or "")
  936. .."{\n"..table.concat(attrstrs, "\n").."\n}")
  937. end
  938. print( "\nscreen:expect"..(keys and "{grid=" or "(").."[[")
  939. print( table.concat(lines, '\n'))
  940. io.stdout:write( "]]"..attrstr)
  941. for _, k in ipairs(ext_keys) do
  942. if ext_state[k] ~= nil then
  943. io.stdout:write(", "..k.."="..inspect(ext_state[k]))
  944. end
  945. end
  946. print((keys and "}" or ")").."\n")
  947. io.stdout:flush()
  948. end
  949. function Screen:_insert_hl_id(attr_state, hl_id)
  950. if attr_state.id_to_index[hl_id] ~= nil then
  951. return attr_state.id_to_index[hl_id]
  952. end
  953. local raw_info = self._hl_info[hl_id]
  954. local info = {}
  955. if #raw_info > 1 then
  956. for i, item in ipairs(raw_info) do
  957. info[i] = self:_insert_hl_id(attr_state, item.id)
  958. end
  959. else
  960. info[1] = {}
  961. for k, v in pairs(raw_info[1]) do
  962. if k ~= "id" then
  963. info[1][k] = v
  964. end
  965. end
  966. end
  967. local entry = self._attr_table[hl_id]
  968. local attrval
  969. if self._hlstate_cterm then
  970. attrval = {entry[1], entry[2], info} -- unpack() doesn't work
  971. else
  972. attrval = {entry[1], info}
  973. end
  974. table.insert(attr_state.ids, attrval)
  975. attr_state.id_to_index[hl_id] = #attr_state.ids
  976. return #attr_state.ids
  977. end
  978. function Screen:hlstate_check_attrs(attrs)
  979. local id_to_index = {}
  980. for i = 1,#self._attr_table do
  981. local iinfo = self._hl_info[i]
  982. local matchinfo = {}
  983. if #iinfo > 1 then
  984. for k,item in ipairs(iinfo) do
  985. matchinfo[k] = id_to_index[item.id]
  986. end
  987. else
  988. matchinfo = iinfo
  989. end
  990. for k,v in pairs(attrs) do
  991. local attr, info, attr_rgb, attr_cterm
  992. if self._hlstate_cterm then
  993. attr_rgb, attr_cterm, info = unpack(v)
  994. attr = {attr_rgb, attr_cterm}
  995. else
  996. attr, info = unpack(v)
  997. end
  998. if self:_equal_attr_def(attr, self._attr_table[i]) then
  999. if #info == #matchinfo then
  1000. local match = false
  1001. if #info == 1 then
  1002. if self:_equal_info(info[1],matchinfo[1]) then
  1003. match = true
  1004. end
  1005. else
  1006. match = true
  1007. for j = 1,#info do
  1008. if info[j] ~= matchinfo[j] then
  1009. match = false
  1010. end
  1011. end
  1012. end
  1013. if match then
  1014. id_to_index[i] = k
  1015. end
  1016. end
  1017. end
  1018. end
  1019. end
  1020. return id_to_index
  1021. end
  1022. function Screen:_pprint_hlstate(item)
  1023. --print(require('inspect')(item))
  1024. local attrdict = "{"..self:_pprint_attrs(item[1]).."}, "
  1025. local attrdict2, hlinfo
  1026. if self._hlstate_cterm then
  1027. attrdict2 = "{"..self:_pprint_attrs(item[2]).."}, "
  1028. hlinfo = item[3]
  1029. else
  1030. attrdict2 = ""
  1031. hlinfo = item[2]
  1032. end
  1033. local descdict = "{"..self:_pprint_hlinfo(hlinfo).."}"
  1034. return "{"..attrdict..attrdict2..descdict.."}"
  1035. end
  1036. function Screen:_pprint_hlinfo(states)
  1037. if #states == 1 then
  1038. local items = {}
  1039. for f, v in pairs(states[1]) do
  1040. local desc = tostring(v)
  1041. if type(v) == type("") then
  1042. desc = '"'..desc..'"'
  1043. end
  1044. table.insert(items, f.." = "..desc)
  1045. end
  1046. return "{"..table.concat(items, ", ").."}"
  1047. else
  1048. return table.concat(states, ", ")
  1049. end
  1050. end
  1051. function Screen:_pprint_attrs(attrs)
  1052. local items = {}
  1053. for f, v in pairs(attrs) do
  1054. local desc = tostring(v)
  1055. if f == "foreground" or f == "background" or f == "special" then
  1056. if Screen.colornames[v] ~= nil then
  1057. desc = "Screen.colors."..Screen.colornames[v]
  1058. end
  1059. end
  1060. table.insert(items, f.." = "..desc)
  1061. end
  1062. return table.concat(items, ", ")
  1063. end
  1064. local function backward_find_meaningful(tbl, from) -- luacheck: no unused
  1065. for i = from or #tbl, 1, -1 do
  1066. if tbl[i] ~= ' ' then
  1067. return i + 1
  1068. end
  1069. end
  1070. return from
  1071. end
  1072. function Screen:_get_attr_id(attr_state, attrs, hl_id)
  1073. if not attr_state.ids then
  1074. return
  1075. end
  1076. if self._options.ext_hlstate then
  1077. local id = attr_state.id_to_index[hl_id]
  1078. if id ~= nil or hl_id == 0 then
  1079. return id
  1080. end
  1081. if attr_state.mutable then
  1082. id = self:_insert_hl_id(attr_state, hl_id)
  1083. attr_state.modified = true
  1084. return id
  1085. end
  1086. return "UNEXPECTED "..self:_pprint_attrs(self._attr_table[hl_id][1])
  1087. else
  1088. for id, a in pairs(attr_state.ids) do
  1089. if self:_equal_attrs(a, attrs) then
  1090. return id
  1091. end
  1092. end
  1093. if self:_equal_attrs(attrs, {}) or
  1094. attr_state.ignore == true or
  1095. self:_attr_index(attr_state.ignore, attrs) ~= nil then
  1096. -- ignore this attrs
  1097. return nil
  1098. end
  1099. if attr_state.mutable then
  1100. table.insert(attr_state.ids, attrs)
  1101. attr_state.modified = true
  1102. return #attr_state.ids
  1103. end
  1104. return "UNEXPECTED "..self:_pprint_attrs(attrs)
  1105. end
  1106. end
  1107. function Screen:_equal_attr_def(a, b)
  1108. if self._hlstate_cterm then
  1109. return self:_equal_attrs(a[1],b[1]) and self:_equal_attrs(a[2],b[2])
  1110. else
  1111. return self:_equal_attrs(a,b[1])
  1112. end
  1113. end
  1114. function Screen:_equal_attrs(a, b)
  1115. return a.bold == b.bold and a.standout == b.standout and
  1116. a.underline == b.underline and a.undercurl == b.undercurl and
  1117. a.italic == b.italic and a.reverse == b.reverse and
  1118. a.foreground == b.foreground and a.background == b.background and
  1119. a.special == b.special
  1120. end
  1121. function Screen:_equal_info(a, b)
  1122. return a.kind == b.kind and a.hi_name == b.hi_name and
  1123. a.ui_name == b.ui_name
  1124. end
  1125. function Screen:_attr_index(attrs, attr)
  1126. if not attrs then
  1127. return nil
  1128. end
  1129. for i,a in pairs(attrs) do
  1130. if self:_equal_attrs(a, attr) then
  1131. return i
  1132. end
  1133. end
  1134. return nil
  1135. end
  1136. return Screen