screen.lua 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997
  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. -- -- Attach a screen to the current Nvim instance.
  10. -- local screen = Screen.new(25, 10)
  11. -- -- Enter insert-mode and type some text.
  12. -- feed('ihello screen')
  13. -- -- Assert the expected screen state.
  14. -- screen:expect([[
  15. -- hello screen^ |
  16. -- {1:~ }|*8
  17. -- {5:-- INSERT --} |
  18. -- ]]) -- <- Last line is stripped
  19. --
  20. -- Since screen updates are received asynchronously, expect() actually specifies
  21. -- the _eventual_ screen state.
  22. --
  23. -- This is how expect() works:
  24. -- * It starts the event loop with a timeout.
  25. -- * Each time it receives an update it checks that against the expected state.
  26. -- * If the expected state matches the current state, the event loop will be
  27. -- stopped and expect() will return.
  28. -- * If the timeout expires, the last match error will be reported and the
  29. -- test will fail.
  30. --
  31. -- The 30 most common highlight groups are predefined, see init_colors() below.
  32. -- In this case "5" is a predefined highlight associated with the set composed of one
  33. -- attribute: bold. Note that since the {5:} markup is not a real part of the
  34. -- screen, the delimiter "|" moved to the right. Also, the highlighting of the
  35. -- NonText markers "~" is visible.
  36. --
  37. -- Tests will often share a group of extra attribute sets to expect(). Those can be
  38. -- defined at the beginning of a test:
  39. --
  40. -- screen:add_extra_attr_ids({
  41. -- [100] = { background = Screen.colors.Plum1, underline = true },
  42. -- [101] = { background = Screen.colors.Red1, bold = true, underline = true },
  43. -- })
  44. --
  45. -- To help write screen tests, see Screen:snapshot_util().
  46. -- To debug screen tests, see Screen:redraw_debug().
  47. local t = require('test.testutil')
  48. local n = require('test.functional.testnvim')()
  49. local busted = require('busted')
  50. local deepcopy = vim.deepcopy
  51. local shallowcopy = t.shallowcopy
  52. local concat_tables = t.concat_tables
  53. local pesc = vim.pesc
  54. local run_session = n.run_session
  55. local eq = t.eq
  56. local dedent = t.dedent
  57. local get_session = n.get_session
  58. local create_callindex = n.create_callindex
  59. local inspect = vim.inspect
  60. local function isempty(v)
  61. return type(v) == 'table' and next(v) == nil
  62. end
  63. --- @class test.functional.ui.screen.Grid
  64. --- @field rows table[][]
  65. --- @field width integer
  66. --- @field height integer
  67. --- @class test.functional.ui.screen
  68. --- @field colors table<string,integer>
  69. --- @field colornames table<integer,string>
  70. --- @field uimeths table<string,function>
  71. --- @field options? table<string,any>
  72. --- @field timeout integer
  73. --- @field win_position table<integer,table<string,integer>>
  74. --- @field float_pos table<integer,table>
  75. --- @field cmdline table<integer,table>
  76. --- @field cmdline_hide_level integer?
  77. --- @field cmdline_block table[]
  78. --- @field hl_groups table<string,integer>
  79. --- @field messages table<integer,table>
  80. --- @field private _cursor {grid:integer,row:integer,col:integer}
  81. --- @field private _grids table<integer,test.functional.ui.screen.Grid>
  82. --- @field private _grid_win_extmarks table<integer,table>
  83. --- @field private _attr_table table<integer,table>
  84. --- @field private _hl_info table<integer,table>
  85. local Screen = {}
  86. Screen.__index = 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. local function _init_colors()
  96. local session = get_session()
  97. local status, rv = session:request('nvim_get_color_map')
  98. if not status then
  99. error('failed to get color map')
  100. end
  101. local colors = rv --- @type table<string,integer>
  102. local colornames = {} --- @type table<integer,string>
  103. for name, rgb in pairs(colors) do
  104. -- we disregard the case that colornames might not be unique, as
  105. -- this is just a helper to get any canonical name of a color
  106. colornames[rgb] = name
  107. end
  108. Screen.colors = colors
  109. Screen.colornames = colornames
  110. Screen._global_default_attr_ids = {
  111. [1] = { foreground = Screen.colors.Blue1, bold = true },
  112. [2] = { reverse = true },
  113. [3] = { bold = true, reverse = true },
  114. [4] = { background = Screen.colors.LightMagenta },
  115. [5] = { bold = true },
  116. [6] = { foreground = Screen.colors.SeaGreen, bold = true },
  117. [7] = { background = Screen.colors.Gray, foreground = Screen.colors.DarkBlue },
  118. [8] = { foreground = Screen.colors.Brown },
  119. [9] = { background = Screen.colors.Red, foreground = Screen.colors.Grey100 },
  120. [10] = { background = Screen.colors.Yellow },
  121. [11] = {
  122. foreground = Screen.colors.Blue1,
  123. background = Screen.colors.LightMagenta,
  124. bold = true,
  125. },
  126. [12] = { background = Screen.colors.Gray },
  127. [13] = { background = Screen.colors.LightGrey, foreground = Screen.colors.DarkBlue },
  128. [14] = { background = Screen.colors.DarkGray, foreground = Screen.colors.LightGrey },
  129. [15] = { foreground = Screen.colors.Brown, bold = true },
  130. [16] = { foreground = Screen.colors.SlateBlue },
  131. [17] = { background = Screen.colors.LightGrey, foreground = Screen.colors.Black },
  132. [18] = { foreground = Screen.colors.Blue1 },
  133. [19] = { foreground = Screen.colors.Red },
  134. [20] = { background = Screen.colors.Yellow, foreground = Screen.colors.Red },
  135. [21] = { background = Screen.colors.Grey90 },
  136. [22] = { background = Screen.colors.LightBlue },
  137. [23] = { foreground = Screen.colors.Blue1, background = Screen.colors.LightCyan, bold = true },
  138. [24] = { background = Screen.colors.LightGrey, underline = true },
  139. [25] = { foreground = Screen.colors.Cyan4 },
  140. [26] = { foreground = Screen.colors.Fuchsia },
  141. [27] = { background = Screen.colors.Red, bold = true },
  142. [28] = { foreground = Screen.colors.SlateBlue, underline = true },
  143. [29] = { foreground = Screen.colors.SlateBlue, bold = true },
  144. [30] = { background = Screen.colors.Red },
  145. }
  146. end
  147. --- @class test.functional.ui.screen.Opts
  148. --- @field ext_linegrid? boolean
  149. --- @field ext_multigrid? boolean
  150. --- @field ext_newgrid? boolean
  151. --- @field ext_popupmenu? boolean
  152. --- @field ext_wildmenu? boolean
  153. --- @field rgb? boolean
  154. --- @field _debug_float? boolean
  155. --- @param width? integer
  156. --- @param height? integer
  157. --- @param options? test.functional.ui.screen.Opts
  158. --- @param session? test.Session|false
  159. --- @return test.functional.ui.screen
  160. function Screen.new(width, height, options, session)
  161. if not Screen.colors then
  162. _init_colors()
  163. end
  164. options = options or {}
  165. if options.ext_linegrid == nil then
  166. options.ext_linegrid = true
  167. end
  168. local self = setmetatable({
  169. timeout = default_screen_timeout,
  170. title = '',
  171. icon = '',
  172. bell = false,
  173. update_menu = false,
  174. visual_bell = false,
  175. suspended = false,
  176. mode = 'normal',
  177. options = {},
  178. pwd = '',
  179. popupmenu = nil,
  180. cmdline = {},
  181. cmdline_block = {},
  182. wildmenu_items = nil,
  183. wildmenu_selected = nil,
  184. win_position = {},
  185. win_viewport = {},
  186. win_viewport_margins = {},
  187. float_pos = {},
  188. msg_grid = nil,
  189. msg_grid_pos = nil,
  190. _session = nil,
  191. rpc_async = false,
  192. messages = {},
  193. msg_history = {},
  194. showmode = {},
  195. showcmd = {},
  196. ruler = {},
  197. hl_groups = {},
  198. _default_attr_ids = nil,
  199. mouse_enabled = true,
  200. _attrs = {},
  201. _hl_info = { [0] = {} },
  202. _attr_table = { [0] = { {}, {} } },
  203. _clear_attrs = nil,
  204. _new_attrs = false,
  205. _width = width or 53,
  206. _height = height or 14,
  207. _options = options,
  208. _grids = {},
  209. _grid_win_extmarks = {},
  210. _cursor = {
  211. grid = 1,
  212. row = 1,
  213. col = 1,
  214. },
  215. _busy = false,
  216. }, Screen)
  217. local function ui(method, ...)
  218. if self.rpc_async then
  219. self._session:notify('nvim_ui_' .. method, ...)
  220. else
  221. local status, rv = self._session:request('nvim_ui_' .. method, ...)
  222. if not status then
  223. error(rv[2])
  224. end
  225. end
  226. end
  227. self.uimeths = create_callindex(ui)
  228. -- session is often nil, which implies the default session
  229. if session ~= false then
  230. self:attach(session)
  231. end
  232. return self
  233. end
  234. function Screen:set_default_attr_ids(attr_ids)
  235. self._default_attr_ids = attr_ids
  236. self._attrs_overridden = true
  237. end
  238. function Screen:add_extra_attr_ids(extra_attr_ids)
  239. local attr_ids = vim.deepcopy(Screen._global_default_attr_ids)
  240. for id, attr in pairs(extra_attr_ids) do
  241. if type(id) == 'number' and id < 100 then
  242. error('extra attr ids should be at least 100 or be strings')
  243. end
  244. attr_ids[id] = attr
  245. end
  246. self._default_attr_ids = attr_ids
  247. end
  248. function Screen:get_default_attr_ids()
  249. return deepcopy(self._default_attr_ids)
  250. end
  251. function Screen:set_rgb_cterm(val)
  252. self._rgb_cterm = val
  253. end
  254. --- @param session? test.Session
  255. function Screen:attach(session)
  256. session = session or get_session()
  257. local options = self._options
  258. if options.ext_linegrid == nil then
  259. options.ext_linegrid = true
  260. end
  261. self._session = session
  262. self._options = options
  263. self._clear_attrs = (not options.ext_linegrid) and {} or nil
  264. self:_handle_resize(self._width, self._height)
  265. self.uimeths.attach(self._width, self._height, options)
  266. if self._options.rgb == nil then
  267. -- nvim defaults to rgb=true internally,
  268. -- simplify test code by doing the same.
  269. self._options.rgb = true
  270. end
  271. if self._options.ext_multigrid then
  272. self._options.ext_linegrid = true
  273. end
  274. if self._default_attr_ids == nil then
  275. self._default_attr_ids = Screen._global_default_attr_ids
  276. end
  277. end
  278. function Screen:detach()
  279. self.uimeths.detach()
  280. self._session = nil
  281. end
  282. function Screen:try_resize(columns, rows)
  283. self._width = columns
  284. self._height = rows
  285. self.uimeths.try_resize(columns, rows)
  286. end
  287. function Screen:try_resize_grid(grid, columns, rows)
  288. self.uimeths.try_resize_grid(grid, columns, rows)
  289. end
  290. --- @param option 'ext_linegrid'|'ext_multigrid'|'ext_popupmenu'|'ext_wildmenu'|'rgb'
  291. --- @param value boolean
  292. function Screen:set_option(option, value)
  293. self.uimeths.set_option(option, value)
  294. --- @diagnostic disable-next-line:no-unknown
  295. self._options[option] = value
  296. end
  297. -- canonical order of ext keys, used to generate asserts
  298. local ext_keys = {
  299. 'popupmenu',
  300. 'cmdline',
  301. 'cmdline_block',
  302. 'wildmenu_items',
  303. 'wildmenu_pos',
  304. 'messages',
  305. 'msg_history',
  306. 'showmode',
  307. 'showcmd',
  308. 'ruler',
  309. 'float_pos',
  310. 'win_viewport',
  311. 'win_viewport_margins',
  312. }
  313. local expect_keys = {
  314. grid = true,
  315. attr_ids = true,
  316. condition = true,
  317. mouse_enabled = true,
  318. any = true,
  319. mode = true,
  320. unchanged = true,
  321. intermediate = true,
  322. reset = true,
  323. timeout = true,
  324. request_cb = true,
  325. hl_groups = true,
  326. extmarks = true,
  327. }
  328. for _, v in ipairs(ext_keys) do
  329. expect_keys[v] = true
  330. end
  331. --- @class test.function.ui.screen.Expect
  332. ---
  333. --- Expected screen state (string). Each line represents a screen
  334. --- row. Last character of each row (typically "|") is stripped.
  335. --- Common indentation is stripped.
  336. --- "{MATCH:x}" in a line is matched against Lua pattern `x`.
  337. --- "*n" at the end of a line means it repeats `n` times.
  338. --- @field grid? string
  339. ---
  340. --- Expected text attributes. Screen rows are transformed according
  341. --- to this table, as follows: each substring S composed of
  342. --- characters having the same attributes will be substituted by
  343. --- "{K:S}", where K is a key in `attr_ids`. Any unexpected
  344. --- attributes in the final state are an error.
  345. --- Use an empty table for a text-only (no attributes) expectation.
  346. --- Use screen:set_default_attr_ids() to define attributes for many
  347. --- expect() calls.
  348. --- @field attr_ids? table<integer,table<string,any>>
  349. ---
  350. --- Expected win_extmarks accumulated for the grids. For each grid,
  351. --- the win_extmark messages are accumulated into an array.
  352. --- @field extmarks? table<integer,table>
  353. ---
  354. --- Function asserting some arbitrary condition. Return value is
  355. --- ignored, throw an error (use eq() or similar) to signal failure.
  356. --- @field condition? fun()
  357. ---
  358. --- Lua pattern string expected to match a screen line. NB: the
  359. --- following chars are magic characters
  360. --- ( ) . % + - * ? [ ^ $
  361. --- and must be escaped with a preceding % for a literal match.
  362. --- @field any? string
  363. ---
  364. --- Expected mode as signaled by "mode_change" event
  365. --- @field mode? string
  366. ---
  367. --- Test that the screen state is unchanged since the previous
  368. --- expect(...). Any flush event resulting in a different state is
  369. --- considered an error. Not observing any events until timeout
  370. --- is acceptable.
  371. --- @field unchanged? boolean
  372. ---
  373. --- Test that the final state is the same as the previous expect,
  374. --- but expect an intermediate state that is different. If possible
  375. --- it is better to use an explicit screen:expect(...) for this
  376. --- intermediate state.
  377. --- @field intermediate? boolean
  378. ---
  379. --- Reset the state internal to the test Screen before starting to
  380. --- receive updates. This should be used after command("redraw!")
  381. --- or some other mechanism that will invoke "redraw!", to check
  382. --- that all screen state is transmitted again. This includes
  383. --- state related to ext_ features as mentioned below.
  384. --- @field reset? boolean
  385. ---
  386. --- maximum time that will be waited until the expected state is
  387. --- seen (or maximum time to observe an incorrect change when
  388. --- `unchanged` flag is used)
  389. --- @field timeout? integer
  390. ---
  391. --- @field mouse_enabled? boolean
  392. ---
  393. --- @field win_viewport? table<integer,table<string,integer>>
  394. --- @field float_pos? [integer,integer]
  395. --- @field hl_groups? table<string,integer>
  396. ---
  397. --- The following keys should be used to expect the state of various ext_
  398. --- features. Note that an absent key will assert that the item is currently
  399. --- NOT present on the screen, also when positional form is used.
  400. ---
  401. --- Expected ext_popupmenu state,
  402. --- @field popupmenu? table
  403. ---
  404. --- Expected ext_cmdline state, as an array of cmdlines of
  405. --- different level.
  406. --- @field cmdline? table
  407. ---
  408. --- Expected ext_cmdline block (for function definitions)
  409. --- @field cmdline_block? table
  410. ---
  411. --- items for ext_wildmenu
  412. --- @field wildmenu_items? table
  413. ---
  414. --- position for ext_wildmenu
  415. --- @field wildmenu_pos? table
  416. --- Asserts that the screen state eventually matches an expected state.
  417. ---
  418. --- Can be called with positional args:
  419. --- screen:expect(grid, [attr_ids])
  420. --- screen:expect(condition)
  421. --- or keyword args (supports more options):
  422. --- screen:expect({ grid=[[...]], cmdline={...}, condition=function() ... end })
  423. ---
  424. --- @param expected string|function|test.function.ui.screen.Expect
  425. --- @param attr_ids? table<integer,table<string,any>>
  426. function Screen:expect(expected, attr_ids, ...)
  427. --- @type string, fun()
  428. local grid, condition
  429. assert(next({ ... }) == nil, 'invalid args to expect()')
  430. if type(expected) == 'table' then
  431. assert(attr_ids == nil)
  432. for k, _ in
  433. pairs(expected --[[@as table<string,any>]])
  434. do
  435. if not expect_keys[k] then
  436. error("Screen:expect: Unknown keyword argument '" .. k .. "'")
  437. end
  438. end
  439. grid = expected.grid
  440. attr_ids = expected.attr_ids
  441. condition = expected.condition
  442. assert(expected.any == nil or grid == nil)
  443. elseif type(expected) == 'string' then
  444. grid = expected
  445. expected = {}
  446. elseif type(expected) == 'function' then
  447. assert(attr_ids == nil)
  448. condition = expected
  449. expected = {}
  450. else
  451. assert(false)
  452. end
  453. local expected_rows = {} --- @type string[]
  454. if grid then
  455. -- Remove the last line and dedent. Note that gsub returns more then one
  456. -- value.
  457. grid = dedent(grid:gsub('\n[ ]+$', ''), 0)
  458. for row in grid:gmatch('[^\n]+') do
  459. table.insert(expected_rows, row)
  460. end
  461. end
  462. local attr_state = {
  463. ids = attr_ids or self._default_attr_ids,
  464. }
  465. if isempty(attr_ids) then
  466. attr_state.ids = nil
  467. end
  468. if self._options.ext_linegrid then
  469. attr_state.id_to_index = self:linegrid_check_attrs(attr_state.ids or {})
  470. end
  471. self._new_attrs = false
  472. self:_wait(function()
  473. if condition then
  474. --- @type boolean, string
  475. local status, res = pcall(condition)
  476. if not status then
  477. return tostring(res)
  478. end
  479. end
  480. if self._options.ext_linegrid and self._new_attrs then
  481. attr_state.id_to_index = self:linegrid_check_attrs(attr_state.ids or {})
  482. end
  483. local actual_rows
  484. if expected.any or grid then
  485. actual_rows = self:render(not expected.any, attr_state)
  486. end
  487. if expected.any then
  488. -- Search for `any` anywhere in the screen lines.
  489. local actual_screen_str = table.concat(actual_rows, '\n')
  490. if not actual_screen_str:find(expected.any) then
  491. return (
  492. 'Failed to match any screen lines.\n'
  493. .. 'Expected (anywhere): "'
  494. .. expected.any
  495. .. '"\n'
  496. .. 'Actual:\n |'
  497. .. table.concat(actual_rows, '\n |')
  498. .. '\n\n'
  499. )
  500. end
  501. end
  502. if grid then
  503. for i, row in ipairs(expected_rows) do
  504. local count --- @type integer?
  505. row, count = row:match('^(.*%|)%*(%d+)$')
  506. if row then
  507. count = tonumber(count)
  508. table.remove(expected_rows, i)
  509. for _ = 1, count do
  510. table.insert(expected_rows, i, row)
  511. end
  512. end
  513. end
  514. local err_msg = nil
  515. -- `expected` must match the screen lines exactly.
  516. if #actual_rows ~= #expected_rows then
  517. err_msg = 'Expected screen height '
  518. .. #expected_rows
  519. .. ' differs from actual height '
  520. .. #actual_rows
  521. .. '.'
  522. end
  523. local msg_expected_rows = shallowcopy(expected_rows)
  524. local msg_actual_rows = shallowcopy(actual_rows)
  525. for i, row in ipairs(expected_rows) do
  526. local pat = nil --- @type string?
  527. if actual_rows[i] and row ~= actual_rows[i] then
  528. local after = row
  529. while true do
  530. local s, e, m = after:find('{MATCH:(.-)}')
  531. if not s then
  532. pat = pat and (pat .. pesc(after))
  533. break
  534. end
  535. --- @type string
  536. pat = (pat or '') .. pesc(after:sub(1, s - 1)) .. m
  537. after = after:sub(e + 1)
  538. end
  539. end
  540. if row ~= actual_rows[i] and (not pat or not actual_rows[i]:match(pat)) then
  541. msg_expected_rows[i] = '*' .. msg_expected_rows[i]
  542. if i <= #actual_rows then
  543. msg_actual_rows[i] = '*' .. msg_actual_rows[i]
  544. end
  545. if err_msg == nil then
  546. err_msg = 'Row ' .. tostring(i) .. ' did not match.'
  547. end
  548. end
  549. end
  550. if err_msg ~= nil then
  551. return (
  552. err_msg
  553. .. '\nExpected:\n |'
  554. .. table.concat(msg_expected_rows, '\n |')
  555. .. '\n'
  556. .. 'Actual:\n |'
  557. .. table.concat(msg_actual_rows, '\n |')
  558. .. '\n\n'
  559. .. [[
  560. To print the expect() call that would assert the current screen state, use
  561. screen:snapshot_util(). In case of non-deterministic failures, use
  562. screen:redraw_debug() to show all intermediate screen states.]]
  563. )
  564. end
  565. end
  566. -- UI extensions. The default expectations should cover the case of
  567. -- the ext_ feature being disabled, or the feature currently not activated
  568. -- (e.g. no external cmdline visible). Some extensions require
  569. -- preprocessing to represent highlights in a reproducible way.
  570. local extstate = self:_extstate_repr(attr_state)
  571. if expected.mode ~= nil then
  572. extstate.mode = self.mode
  573. end
  574. if expected.mouse_enabled ~= nil then
  575. extstate.mouse_enabled = self.mouse_enabled
  576. end
  577. if expected.win_viewport == nil then
  578. extstate.win_viewport = nil
  579. end
  580. if expected.win_viewport_margins == nil then
  581. extstate.win_viewport_margins = nil
  582. end
  583. if expected.float_pos then
  584. expected.float_pos = deepcopy(expected.float_pos)
  585. for _, v in pairs(expected.float_pos) do
  586. if not v.external and v[7] == nil then
  587. v[7] = 50
  588. end
  589. end
  590. end
  591. -- Convert assertion errors into invalid screen state descriptions.
  592. for _, k in ipairs(concat_tables(ext_keys, { 'mode', 'mouse_enabled' })) do
  593. -- Empty states are considered the default and need not be mentioned.
  594. if not (expected[k] == nil and isempty(extstate[k])) then
  595. local status, res = pcall(eq, expected[k], extstate[k], k)
  596. if not status then
  597. return (
  598. tostring(res)
  599. .. '\nHint: full state of "'
  600. .. k
  601. .. '":\n '
  602. .. inspect(extstate[k])
  603. )
  604. end
  605. end
  606. end
  607. -- Only test the abort state of a cmdline level once.
  608. if self.cmdline_hide_level ~= nil then
  609. self.cmdline[self.cmdline_hide_level] = nil
  610. self.cmdline_hide_level = nil
  611. end
  612. if expected.hl_groups ~= nil then
  613. for name, id in pairs(expected.hl_groups) do
  614. local expected_hl = attr_state.ids[id]
  615. local actual_hl = self._attr_table[self.hl_groups[name]][(self._options.rgb and 1) or 2]
  616. local status, res = pcall(eq, expected_hl, actual_hl, 'highlight ' .. name)
  617. if not status then
  618. return tostring(res)
  619. end
  620. end
  621. end
  622. if expected.extmarks ~= nil then
  623. for gridid, expected_marks in pairs(expected.extmarks) do
  624. local stored_marks = self._grid_win_extmarks[gridid]
  625. if stored_marks == nil then
  626. return 'no win_extmark for grid ' .. tostring(gridid)
  627. end
  628. local status, res =
  629. pcall(eq, expected_marks, stored_marks, 'extmarks for grid ' .. tostring(gridid))
  630. if not status then
  631. return tostring(res)
  632. end
  633. end
  634. for gridid, _ in pairs(self._grid_win_extmarks) do
  635. local expected_marks = expected.extmarks[gridid]
  636. if expected_marks == nil then
  637. return 'unexpected win_extmark for grid ' .. tostring(gridid)
  638. end
  639. end
  640. end
  641. end, expected)
  642. end
  643. function Screen:expect_unchanged(intermediate, waittime_ms)
  644. -- Collect the current screen state.
  645. local kwargs = self:get_snapshot()
  646. if intermediate then
  647. kwargs.intermediate = true
  648. else
  649. kwargs.unchanged = true
  650. end
  651. kwargs.timeout = waittime_ms
  652. -- Check that screen state does not change.
  653. self:expect(kwargs)
  654. end
  655. --- @private
  656. --- @param check fun(): string
  657. --- @param flags table<string,any>
  658. function Screen:_wait(check, flags)
  659. local err --- @type string?
  660. local checked = false
  661. local success_seen = false
  662. local failure_after_success = false
  663. local did_flush = true
  664. local warn_immediate = not (flags.unchanged or flags.intermediate)
  665. if flags.intermediate and flags.unchanged then
  666. error("Choose only one of 'intermediate' and 'unchanged', not both")
  667. end
  668. if flags.reset then
  669. -- throw away all state, we expect it to be retransmitted
  670. self:_reset()
  671. end
  672. -- Maximum timeout, after which a incorrect state will be regarded as a
  673. -- failure
  674. local timeout = flags.timeout or self.timeout
  675. -- Minimal timeout before the loop is allowed to be stopped so we
  676. -- always do some check for failure after success.
  677. local minimal_timeout = default_timeout_factor * 2
  678. local immediate_seen, intermediate_seen = false, false
  679. if not check() then
  680. minimal_timeout = default_timeout_factor * 20
  681. immediate_seen = true
  682. end
  683. -- For an "unchanged" test, flags.timeout is the time during which the state
  684. -- must not change, so always wait this full time.
  685. if flags.unchanged then
  686. minimal_timeout = flags.timeout or default_timeout_factor * 20
  687. end
  688. assert(timeout >= minimal_timeout)
  689. local did_minimal_timeout = false
  690. local function notification_cb(method, args)
  691. assert(
  692. method == 'redraw',
  693. string.format('notification_cb: unexpected method (%s, args=%s)', method, inspect(args))
  694. )
  695. did_flush = self:_redraw(args)
  696. if not did_flush then
  697. return
  698. end
  699. err = check()
  700. checked = true
  701. if err and immediate_seen then
  702. intermediate_seen = true
  703. end
  704. if not err and (not flags.intermediate or intermediate_seen) then
  705. success_seen = true
  706. if did_minimal_timeout then
  707. self._session:stop()
  708. end
  709. elseif err and success_seen and #args > 0 then
  710. success_seen = false
  711. failure_after_success = true
  712. -- print(inspect(args))
  713. end
  714. return true
  715. end
  716. local eof = run_session(self._session, flags.request_cb, notification_cb, nil, minimal_timeout)
  717. if not did_flush then
  718. if eof then
  719. err = 'no flush received'
  720. end
  721. elseif not checked then
  722. err = check()
  723. if not err and flags.unchanged then
  724. -- expecting NO screen change: use a shorter timeout
  725. success_seen = true
  726. end
  727. end
  728. if not success_seen and not eof then
  729. did_minimal_timeout = true
  730. eof =
  731. run_session(self._session, flags.request_cb, notification_cb, nil, timeout - minimal_timeout)
  732. if not did_flush then
  733. err = 'no flush received'
  734. end
  735. end
  736. local did_warn = false
  737. if warn_immediate and immediate_seen then
  738. print([[
  739. warning: Screen test succeeded immediately. Try to avoid this unless the
  740. purpose of the test really requires it.]])
  741. if intermediate_seen then
  742. print([[
  743. There are intermediate states between the two identical expects.
  744. Use screen:snapshot_util() or screen:redraw_debug() to find them, and add them
  745. to the test if they make sense.
  746. ]])
  747. else
  748. print([[If necessary, silence this warning with 'unchanged' argument of screen:expect.]])
  749. end
  750. did_warn = true
  751. end
  752. if failure_after_success then
  753. print([[
  754. warning: Screen changes were received after the expected state. This indicates
  755. indeterminism in the test. Try adding screen:expect(...) (or poke_eventloop())
  756. between asynchronous (feed(), nvim_input()) and synchronous API calls.
  757. - Use screen:redraw_debug() to investigate; it may find relevant intermediate
  758. states that should be added to the test to make it more robust.
  759. - If the purpose of the test is to assert state after some user input sent
  760. with feed(), adding screen:expect() before the feed() will help to ensure
  761. the input is sent when Nvim is in a predictable state. This is preferable
  762. to poke_eventloop(), for being closer to real user interaction.
  763. - poke_eventloop() can trigger redraws and thus generate more indeterminism.
  764. Try removing poke_eventloop().
  765. ]])
  766. did_warn = true
  767. end
  768. if err then
  769. if eof then
  770. err = err .. '\n\n' .. eof[2]
  771. end
  772. busted.fail(err .. '\n\nSnapshot:\n' .. self:_print_snapshot(), 3)
  773. elseif did_warn then
  774. if eof then
  775. print(eof[2])
  776. end
  777. local tb = debug.traceback()
  778. local index = string.find(tb, '\n%s*%[C]')
  779. print(string.sub(tb, 1, index))
  780. end
  781. if flags.intermediate then
  782. assert(intermediate_seen, 'expected intermediate screen state before final screen state')
  783. elseif flags.unchanged then
  784. assert(not intermediate_seen, 'expected screen state to be unchanged')
  785. end
  786. end
  787. function Screen:sleep(ms, request_cb)
  788. local function notification_cb(method, args)
  789. assert(method == 'redraw')
  790. self:_redraw(args)
  791. end
  792. run_session(self._session, request_cb, notification_cb, nil, ms)
  793. end
  794. --- @private
  795. --- @param updates {[1]:string, [integer]:any[]}[]
  796. function Screen:_redraw(updates)
  797. local did_flush = false
  798. for k, update in ipairs(updates) do
  799. -- print('--', inspect(update))
  800. local method = update[1]
  801. for i = 2, #update do
  802. local handler_name = '_handle_' .. method
  803. --- @type function
  804. local handler = self[handler_name]
  805. assert(handler ~= nil, 'missing handler: Screen:' .. handler_name)
  806. local status, res = pcall(handler, self, unpack(update[i]))
  807. if not status then
  808. error(
  809. handler_name
  810. .. ' failed'
  811. .. '\n payload: '
  812. .. inspect(update)
  813. .. '\n error: '
  814. .. tostring(res)
  815. )
  816. end
  817. end
  818. if k == #updates and method == 'flush' then
  819. did_flush = true
  820. end
  821. end
  822. return did_flush
  823. end
  824. function Screen:_handle_resize(width, height)
  825. self:_handle_grid_resize(1, width, height)
  826. self._scroll_region = {
  827. top = 1,
  828. bot = height,
  829. left = 1,
  830. right = width,
  831. }
  832. self._grid = self._grids[1]
  833. end
  834. local function min(x, y)
  835. if x < y then
  836. return x
  837. else
  838. return y
  839. end
  840. end
  841. function Screen:_handle_grid_resize(grid, width, height)
  842. local rows = {}
  843. for _ = 1, height do
  844. local cols = {}
  845. for _ = 1, width do
  846. table.insert(cols, { text = ' ', attrs = self._clear_attrs, hl_id = 0 })
  847. end
  848. table.insert(rows, cols)
  849. end
  850. if grid > 1 and self._grids[grid] ~= nil then
  851. local old = self._grids[grid]
  852. for i = 1, min(height, old.height) do
  853. for j = 1, min(width, old.width) do
  854. rows[i][j] = old.rows[i][j]
  855. end
  856. end
  857. end
  858. if self._cursor.grid == grid then
  859. self._cursor.row = 1 -- -1 ?
  860. self._cursor.col = 1
  861. end
  862. self._grids[grid] = {
  863. rows = rows,
  864. width = width,
  865. height = height,
  866. }
  867. end
  868. function Screen:_handle_msg_set_pos(grid, row, scrolled, char)
  869. self.msg_grid = grid
  870. self.msg_grid_pos = row
  871. self.msg_scrolled = scrolled
  872. self.msg_sep_char = char
  873. end
  874. function Screen:_handle_flush() end
  875. function Screen:_reset()
  876. -- TODO: generalize to multigrid later
  877. self:_handle_grid_clear(1)
  878. -- TODO: share with initialization, so it generalizes?
  879. self.popupmenu = nil
  880. self.cmdline = {}
  881. self.cmdline_block = {}
  882. self.wildmenu_items = nil
  883. self.wildmenu_pos = nil
  884. self._grid_win_extmarks = {}
  885. end
  886. --- @param cursor_style_enabled boolean
  887. --- @param mode_info table[]
  888. function Screen:_handle_mode_info_set(cursor_style_enabled, mode_info)
  889. self._cursor_style_enabled = cursor_style_enabled
  890. for _, item in pairs(mode_info) do
  891. -- attr IDs are not stable, but their value should be
  892. if item.attr_id ~= nil and self._attr_table[item.attr_id] ~= nil then
  893. item.attr = self._attr_table[item.attr_id][1]
  894. item.attr_id = nil
  895. end
  896. if item.attr_id_lm ~= nil and self._attr_table[item.attr_id_lm] ~= nil then
  897. item.attr_lm = self._attr_table[item.attr_id_lm][1]
  898. item.attr_id_lm = nil
  899. end
  900. end
  901. self._mode_info = mode_info
  902. end
  903. function Screen:_handle_clear()
  904. -- the first implemented UI protocol clients (python-gui and builitin TUI)
  905. -- allowed the cleared region to be restricted by setting the scroll region.
  906. -- this was never used by nvim tough, and not documented and implemented by
  907. -- newer clients, to check we remain compatible with both kind of clients,
  908. -- ensure the scroll region is in a reset state.
  909. local expected_region = {
  910. top = 1,
  911. bot = self._grid.height,
  912. left = 1,
  913. right = self._grid.width,
  914. }
  915. eq(expected_region, self._scroll_region)
  916. self:_handle_grid_clear(1)
  917. end
  918. function Screen:_handle_grid_clear(grid)
  919. self:_clear_block(self._grids[grid], 1, self._grids[grid].height, 1, self._grids[grid].width)
  920. end
  921. function Screen:_handle_grid_destroy(grid)
  922. self._grids[grid] = nil
  923. if self._options.ext_multigrid then
  924. self.win_position[grid] = nil
  925. self.win_viewport[grid] = nil
  926. self.win_viewport_margins[grid] = nil
  927. end
  928. end
  929. function Screen:_handle_eol_clear()
  930. local row, col = self._cursor.row, self._cursor.col
  931. self:_clear_block(self._grid, row, row, col, self._grid.width)
  932. end
  933. function Screen:_handle_cursor_goto(row, col)
  934. self._cursor.row = row + 1
  935. self._cursor.col = col + 1
  936. end
  937. function Screen:_handle_grid_cursor_goto(grid, row, col)
  938. self._cursor.grid = grid
  939. assert(row >= 0 and col >= 0)
  940. self._cursor.row = row + 1
  941. self._cursor.col = col + 1
  942. end
  943. function Screen:_handle_win_pos(grid, win, startrow, startcol, width, height)
  944. self.win_position[grid] = {
  945. win = win,
  946. startrow = startrow,
  947. startcol = startcol,
  948. width = width,
  949. height = height,
  950. }
  951. self.float_pos[grid] = nil
  952. end
  953. function Screen:_handle_win_viewport(
  954. grid,
  955. win,
  956. topline,
  957. botline,
  958. curline,
  959. curcol,
  960. linecount,
  961. scroll_delta
  962. )
  963. -- accumulate scroll delta
  964. local last_scroll_delta = self.win_viewport[grid] and self.win_viewport[grid].sum_scroll_delta
  965. or 0
  966. self.win_viewport[grid] = {
  967. win = win,
  968. topline = topline,
  969. botline = botline,
  970. curline = curline,
  971. curcol = curcol,
  972. linecount = linecount,
  973. sum_scroll_delta = scroll_delta + last_scroll_delta,
  974. }
  975. end
  976. function Screen:_handle_win_viewport_margins(grid, win, top, bottom, left, right)
  977. self.win_viewport_margins[grid] = {
  978. win = win,
  979. top = top,
  980. bottom = bottom,
  981. left = left,
  982. right = right,
  983. }
  984. end
  985. function Screen:_handle_win_float_pos(grid, ...)
  986. self.win_position[grid] = nil
  987. self.float_pos[grid] = { ... }
  988. end
  989. function Screen:_handle_win_external_pos(grid)
  990. self.win_position[grid] = nil
  991. self.float_pos[grid] = { external = true }
  992. end
  993. function Screen:_handle_win_hide(grid)
  994. self.win_position[grid] = nil
  995. self.float_pos[grid] = nil
  996. end
  997. function Screen:_handle_win_close(grid)
  998. self.float_pos[grid] = nil
  999. end
  1000. function Screen:_handle_win_extmark(grid, ...)
  1001. if self._grid_win_extmarks[grid] == nil then
  1002. self._grid_win_extmarks[grid] = {}
  1003. end
  1004. table.insert(self._grid_win_extmarks[grid], { ... })
  1005. end
  1006. function Screen:_handle_busy_start()
  1007. self._busy = true
  1008. end
  1009. function Screen:_handle_busy_stop()
  1010. self._busy = false
  1011. end
  1012. function Screen:_handle_mouse_on()
  1013. self.mouse_enabled = true
  1014. end
  1015. function Screen:_handle_mouse_off()
  1016. self.mouse_enabled = false
  1017. end
  1018. function Screen:_handle_mode_change(mode, idx)
  1019. assert(mode == self._mode_info[idx + 1].name)
  1020. self.mode = mode
  1021. end
  1022. function Screen:_handle_set_scroll_region(top, bot, left, right)
  1023. self._scroll_region.top = top + 1
  1024. self._scroll_region.bot = bot + 1
  1025. self._scroll_region.left = left + 1
  1026. self._scroll_region.right = right + 1
  1027. end
  1028. function Screen:_handle_scroll(count)
  1029. local top = self._scroll_region.top
  1030. local bot = self._scroll_region.bot
  1031. local left = self._scroll_region.left
  1032. local right = self._scroll_region.right
  1033. self:_handle_grid_scroll(1, top - 1, bot, left - 1, right, count, 0)
  1034. end
  1035. --- @param g any
  1036. --- @param top integer
  1037. --- @param bot integer
  1038. --- @param left integer
  1039. --- @param right integer
  1040. --- @param rows integer
  1041. --- @param cols integer
  1042. function Screen:_handle_grid_scroll(g, top, bot, left, right, rows, cols)
  1043. top = top + 1
  1044. left = left + 1
  1045. assert(cols == 0)
  1046. local grid = self._grids[g]
  1047. --- @type integer, integer, integer
  1048. local start, stop, step
  1049. if rows > 0 then
  1050. start = top
  1051. stop = bot - rows
  1052. step = 1
  1053. else
  1054. start = bot
  1055. stop = top - rows
  1056. step = -1
  1057. end
  1058. -- shift scroll region
  1059. for i = start, stop, step do
  1060. local target = grid.rows[i]
  1061. local source = grid.rows[i + rows]
  1062. for j = left, right do
  1063. target[j].text = source[j].text
  1064. target[j].attrs = source[j].attrs
  1065. target[j].hl_id = source[j].hl_id
  1066. end
  1067. end
  1068. -- clear invalid rows
  1069. for i = stop + step, stop + rows, step do
  1070. self:_clear_row_section(grid, i, left, right, true)
  1071. end
  1072. end
  1073. function Screen:_handle_hl_attr_define(id, rgb_attrs, cterm_attrs, info)
  1074. self._attr_table[id] = { rgb_attrs, cterm_attrs }
  1075. self._hl_info[id] = info
  1076. self._new_attrs = true
  1077. end
  1078. --- @param name string
  1079. --- @param id integer
  1080. function Screen:_handle_hl_group_set(name, id)
  1081. self.hl_groups[name] = id
  1082. end
  1083. function Screen:get_hl(val)
  1084. if self._options.ext_newgrid then
  1085. return self._attr_table[val][1]
  1086. end
  1087. return val
  1088. end
  1089. function Screen:_handle_highlight_set(attrs)
  1090. self._attrs = attrs
  1091. end
  1092. function Screen:_handle_put(str)
  1093. assert(not self._options.ext_linegrid)
  1094. local cell = self._grid.rows[self._cursor.row][self._cursor.col]
  1095. cell.text = str
  1096. cell.attrs = self._attrs
  1097. cell.hl_id = -1
  1098. self._cursor.col = self._cursor.col + 1
  1099. end
  1100. --- @param grid integer
  1101. --- @param row integer
  1102. --- @param col integer
  1103. --- @param items integer[][]
  1104. function Screen:_handle_grid_line(grid, row, col, items)
  1105. assert(self._options.ext_linegrid)
  1106. assert(#items > 0)
  1107. local line = self._grids[grid].rows[row + 1]
  1108. local colpos = col + 1
  1109. local hl_id = 0
  1110. for _, item in ipairs(items) do
  1111. local text, hl_id_cell, count = item[1], item[2], item[3]
  1112. if hl_id_cell ~= nil then
  1113. hl_id = hl_id_cell
  1114. end
  1115. for _ = 1, (count or 1) do
  1116. local cell = line[colpos]
  1117. cell.text = text
  1118. cell.hl_id = hl_id
  1119. colpos = colpos + 1
  1120. end
  1121. end
  1122. end
  1123. function Screen:_handle_bell()
  1124. self.bell = true
  1125. end
  1126. function Screen:_handle_visual_bell()
  1127. self.visual_bell = true
  1128. end
  1129. function Screen:_handle_default_colors_set(rgb_fg, rgb_bg, rgb_sp, cterm_fg, cterm_bg)
  1130. self.default_colors = {
  1131. rgb_fg = rgb_fg,
  1132. rgb_bg = rgb_bg,
  1133. rgb_sp = rgb_sp,
  1134. cterm_fg = cterm_fg,
  1135. cterm_bg = cterm_bg,
  1136. }
  1137. end
  1138. function Screen:_handle_update_fg(fg)
  1139. self._fg = fg
  1140. end
  1141. function Screen:_handle_update_bg(bg)
  1142. self._bg = bg
  1143. end
  1144. function Screen:_handle_update_sp(sp)
  1145. self._sp = sp
  1146. end
  1147. function Screen:_handle_suspend()
  1148. self.suspended = true
  1149. end
  1150. function Screen:_handle_update_menu()
  1151. self.update_menu = true
  1152. end
  1153. function Screen:_handle_set_title(title)
  1154. self.title = title
  1155. end
  1156. function Screen:_handle_set_icon(icon)
  1157. self.icon = icon
  1158. end
  1159. function Screen:_handle_option_set(name, value)
  1160. self.options[name] = value
  1161. end
  1162. function Screen:_handle_chdir(path)
  1163. self.pwd = vim.fs.normalize(path, { expand_env = false })
  1164. end
  1165. function Screen:_handle_popupmenu_show(items, selected, row, col, grid)
  1166. self.popupmenu = { items = items, pos = selected, anchor = { grid, row, col } }
  1167. end
  1168. function Screen:_handle_popupmenu_select(selected)
  1169. self.popupmenu.pos = selected
  1170. end
  1171. function Screen:_handle_popupmenu_hide()
  1172. self.popupmenu = nil
  1173. end
  1174. function Screen:_handle_cmdline_show(content, pos, firstc, prompt, indent, level, hl_id)
  1175. if firstc == '' then
  1176. firstc = nil
  1177. end
  1178. if prompt == '' then
  1179. prompt = nil
  1180. end
  1181. if indent == 0 then
  1182. indent = nil
  1183. end
  1184. -- check position is valid #10000
  1185. local len = 0
  1186. for _, chunk in ipairs(content) do
  1187. len = len + string.len(chunk[2])
  1188. end
  1189. assert(pos <= len)
  1190. self.cmdline[level] = {
  1191. content = content,
  1192. pos = pos,
  1193. firstc = firstc,
  1194. prompt = prompt,
  1195. indent = indent,
  1196. hl_id = prompt and hl_id,
  1197. }
  1198. end
  1199. function Screen:_handle_cmdline_hide(level, abort)
  1200. self.cmdline[level] = { abort = abort }
  1201. self.cmdline_hide_level = level
  1202. end
  1203. function Screen:_handle_cmdline_special_char(char, shift, level)
  1204. -- cleared by next cmdline_show on the same level
  1205. self.cmdline[level].special = { char, shift }
  1206. end
  1207. function Screen:_handle_cmdline_pos(pos, level)
  1208. self.cmdline[level].pos = pos
  1209. end
  1210. function Screen:_handle_cmdline_block_show(block)
  1211. self.cmdline_block = block
  1212. end
  1213. function Screen:_handle_cmdline_block_append(item)
  1214. self.cmdline_block[#self.cmdline_block + 1] = item
  1215. end
  1216. function Screen:_handle_cmdline_block_hide()
  1217. self.cmdline_block = {}
  1218. end
  1219. function Screen:_handle_wildmenu_show(items)
  1220. self.wildmenu_items = items
  1221. end
  1222. function Screen:_handle_wildmenu_select(pos)
  1223. self.wildmenu_pos = pos
  1224. end
  1225. function Screen:_handle_wildmenu_hide()
  1226. self.wildmenu_items, self.wildmenu_pos = nil, nil
  1227. end
  1228. function Screen:_handle_msg_show(kind, chunks, replace_last, history)
  1229. local pos = #self.messages
  1230. if not replace_last or pos == 0 then
  1231. pos = pos + 1
  1232. end
  1233. self.messages[pos] = { kind = kind, content = chunks, history = history }
  1234. end
  1235. function Screen:_handle_msg_clear()
  1236. self.messages = {}
  1237. end
  1238. function Screen:_handle_msg_showcmd(msg)
  1239. self.showcmd = msg
  1240. end
  1241. function Screen:_handle_msg_showmode(msg)
  1242. self.showmode = msg
  1243. end
  1244. function Screen:_handle_msg_ruler(msg)
  1245. self.ruler = msg
  1246. end
  1247. function Screen:_handle_msg_history_show(entries)
  1248. self.msg_history = entries
  1249. end
  1250. function Screen:_handle_msg_history_clear()
  1251. self.msg_history = {}
  1252. end
  1253. function Screen:_clear_block(grid, top, bot, left, right)
  1254. for i = top, bot do
  1255. self:_clear_row_section(grid, i, left, right)
  1256. end
  1257. end
  1258. function Screen:_clear_row_section(grid, rownum, startcol, stopcol, invalid)
  1259. local row = grid.rows[rownum]
  1260. for i = startcol, stopcol do
  1261. row[i].text = (invalid and '�' or ' ')
  1262. row[i].attrs = self._clear_attrs
  1263. row[i].hl_id = 0
  1264. end
  1265. end
  1266. function Screen:_row_repr(gridnr, rownr, attr_state, cursor)
  1267. local rv = {}
  1268. local current_attr_id
  1269. local i = 1
  1270. local has_windows = self._options.ext_multigrid and gridnr == 1
  1271. local row = self._grids[gridnr].rows[rownr]
  1272. if has_windows and self.msg_grid and self.msg_grid_pos < rownr then
  1273. return '[' .. self.msg_grid .. ':' .. string.rep('-', #row) .. ']'
  1274. end
  1275. while i <= #row do
  1276. local did_window = false
  1277. if has_windows then
  1278. for id, pos in pairs(self.win_position) do
  1279. if
  1280. i - 1 == pos.startcol
  1281. and pos.startrow <= rownr - 1
  1282. and rownr - 1 < pos.startrow + pos.height
  1283. then
  1284. if current_attr_id then
  1285. -- close current attribute bracket
  1286. table.insert(rv, '}')
  1287. current_attr_id = nil
  1288. end
  1289. table.insert(rv, '[' .. id .. ':' .. string.rep('-', pos.width) .. ']')
  1290. i = i + pos.width
  1291. did_window = true
  1292. end
  1293. end
  1294. end
  1295. if not did_window then
  1296. local attr_id = self:_get_attr_id(attr_state, row[i].attrs, row[i].hl_id)
  1297. if current_attr_id and attr_id ~= current_attr_id then
  1298. -- close current attribute bracket
  1299. table.insert(rv, '}')
  1300. current_attr_id = nil
  1301. end
  1302. if not current_attr_id and attr_id then
  1303. -- open a new attribute bracket
  1304. table.insert(rv, '{' .. attr_id .. ':')
  1305. current_attr_id = attr_id
  1306. end
  1307. if not self._busy and cursor and self._cursor.col == i then
  1308. table.insert(rv, '^')
  1309. end
  1310. table.insert(rv, row[i].text)
  1311. i = i + 1
  1312. end
  1313. end
  1314. if current_attr_id then
  1315. table.insert(rv, '}')
  1316. end
  1317. -- return the line representation, but remove empty attribute brackets and
  1318. -- trailing whitespace
  1319. return table.concat(rv, '') --:gsub('%s+$', '')
  1320. end
  1321. function Screen:_extstate_repr(attr_state)
  1322. local cmdline = {}
  1323. for i, entry in pairs(self.cmdline) do
  1324. entry = shallowcopy(entry)
  1325. if entry.content ~= nil then
  1326. entry.content = self:_chunks_repr(entry.content, attr_state)
  1327. end
  1328. cmdline[i] = entry
  1329. end
  1330. local cmdline_block = {}
  1331. for i, entry in ipairs(self.cmdline_block) do
  1332. cmdline_block[i] = self:_chunks_repr(entry, attr_state)
  1333. end
  1334. local messages = {}
  1335. for i, entry in ipairs(self.messages) do
  1336. messages[i] = {
  1337. kind = entry.kind,
  1338. content = self:_chunks_repr(entry.content, attr_state),
  1339. history = entry.history,
  1340. }
  1341. end
  1342. local msg_history = {}
  1343. for i, entry in ipairs(self.msg_history) do
  1344. msg_history[i] = { kind = entry[1], content = self:_chunks_repr(entry[2], attr_state) }
  1345. end
  1346. local win_viewport = (next(self.win_viewport) and self.win_viewport) or nil
  1347. local win_viewport_margins = (next(self.win_viewport_margins) and self.win_viewport_margins)
  1348. or nil
  1349. return {
  1350. popupmenu = self.popupmenu,
  1351. cmdline = cmdline,
  1352. cmdline_block = cmdline_block,
  1353. wildmenu_items = self.wildmenu_items,
  1354. wildmenu_pos = self.wildmenu_pos,
  1355. messages = messages,
  1356. showmode = self:_chunks_repr(self.showmode, attr_state),
  1357. showcmd = self:_chunks_repr(self.showcmd, attr_state),
  1358. ruler = self:_chunks_repr(self.ruler, attr_state),
  1359. msg_history = msg_history,
  1360. float_pos = self.float_pos,
  1361. win_viewport = win_viewport,
  1362. win_viewport_margins = win_viewport_margins,
  1363. }
  1364. end
  1365. function Screen:_chunks_repr(chunks, attr_state)
  1366. local repr_chunks = {}
  1367. for i, chunk in ipairs(chunks) do
  1368. local hl, text, id = unpack(chunk)
  1369. local attrs
  1370. if self._options.ext_linegrid then
  1371. attrs = self._attr_table[hl][1]
  1372. else
  1373. attrs = hl
  1374. end
  1375. local attr_id = self:_get_attr_id(attr_state, attrs, hl)
  1376. repr_chunks[i] = { text, attr_id, attr_id and id or nil }
  1377. end
  1378. return repr_chunks
  1379. end
  1380. -- Generates tests. Call it where Screen:expect() would be. Waits briefly, then
  1381. -- dumps the current screen state in the form of Screen:expect().
  1382. -- Use snapshot_util({}) to generate a text-only (no attributes) test.
  1383. --
  1384. -- @see Screen:redraw_debug()
  1385. function Screen:snapshot_util(request_cb)
  1386. -- TODO: simplify this later when existing tests have been updated
  1387. self:sleep(250, request_cb)
  1388. self:print_snapshot()
  1389. end
  1390. function Screen:redraw_debug(timeout)
  1391. self:print_snapshot()
  1392. local function notification_cb(method, args)
  1393. assert(method == 'redraw')
  1394. for _, update in ipairs(args) do
  1395. -- mode_info_set is quite verbose, comment out the condition to debug it.
  1396. if update[1] ~= 'mode_info_set' then
  1397. print(inspect(update))
  1398. end
  1399. end
  1400. self:_redraw(args)
  1401. self:print_snapshot()
  1402. return true
  1403. end
  1404. if timeout == nil then
  1405. timeout = 250
  1406. end
  1407. run_session(self._session, nil, notification_cb, nil, timeout)
  1408. end
  1409. --- @param headers boolean
  1410. --- @param attr_state any
  1411. --- @param preview? boolean
  1412. --- @return string[]
  1413. function Screen:render(headers, attr_state, preview)
  1414. headers = headers and (self._options.ext_multigrid or self._options._debug_float)
  1415. local rv = {}
  1416. for igrid, grid in vim.spairs(self._grids) do
  1417. if headers then
  1418. local suffix = ''
  1419. if
  1420. igrid > 1
  1421. and self.win_position[igrid] == nil
  1422. and self.float_pos[igrid] == nil
  1423. and self.msg_grid ~= igrid
  1424. then
  1425. suffix = ' (hidden)'
  1426. end
  1427. table.insert(rv, '## grid ' .. igrid .. suffix)
  1428. end
  1429. local height = grid.height
  1430. if igrid == self.msg_grid then
  1431. height = self._grids[1].height - self.msg_grid_pos
  1432. end
  1433. for i = 1, height do
  1434. local cursor = self._cursor.grid == igrid and self._cursor.row == i
  1435. local prefix = (headers or preview) and ' ' or ''
  1436. table.insert(rv, prefix .. self:_row_repr(igrid, i, attr_state, cursor) .. '|')
  1437. end
  1438. end
  1439. return rv
  1440. end
  1441. -- Returns the current screen state in the form of a screen:expect()
  1442. -- keyword-args map.
  1443. function Screen:get_snapshot()
  1444. local attr_state = {
  1445. ids = {},
  1446. mutable = true, -- allow _row_repr to add missing highlights
  1447. }
  1448. local attrs = self._default_attr_ids
  1449. if attrs ~= nil then
  1450. for i, a in pairs(attrs) do
  1451. attr_state.ids[i] = a
  1452. end
  1453. end
  1454. if self._options.ext_linegrid then
  1455. attr_state.id_to_index = self:linegrid_check_attrs(attr_state.ids or {})
  1456. end
  1457. local lines = self:render(true, attr_state, true)
  1458. for i, row in ipairs(lines) do
  1459. local count = 1
  1460. while i < #lines and lines[i + 1] == row do
  1461. count = count + 1
  1462. table.remove(lines, i + 1)
  1463. end
  1464. if count > 1 then
  1465. lines[i] = lines[i] .. '*' .. count
  1466. end
  1467. end
  1468. local ext_state = self:_extstate_repr(attr_state)
  1469. for k, v in pairs(ext_state) do
  1470. if isempty(v) then
  1471. ext_state[k] = nil -- deleting keys while iterating is ok
  1472. end
  1473. end
  1474. -- Build keyword-args for screen:expect().
  1475. local kwargs = {}
  1476. if attr_state.modified then
  1477. kwargs['attr_ids'] = {}
  1478. for i, a in pairs(attr_state.ids) do
  1479. kwargs['attr_ids'][i] = a
  1480. end
  1481. end
  1482. kwargs['grid'] = table.concat(lines, '\n')
  1483. for _, k in ipairs(ext_keys) do
  1484. if ext_state[k] ~= nil then
  1485. kwargs[k] = ext_state[k]
  1486. end
  1487. end
  1488. return kwargs, ext_state, attr_state
  1489. end
  1490. local function fmt_ext_state(name, state)
  1491. local function remove_all_metatables(item, path)
  1492. if path[#path] ~= inspect.METATABLE then
  1493. return item
  1494. end
  1495. end
  1496. if name == 'win_viewport' then
  1497. local str = '{\n'
  1498. for k, v in pairs(state) do
  1499. str = (
  1500. str
  1501. .. ' ['
  1502. .. k
  1503. .. '] = {win = '
  1504. .. v.win
  1505. .. ', topline = '
  1506. .. v.topline
  1507. .. ', botline = '
  1508. .. v.botline
  1509. .. ', curline = '
  1510. .. v.curline
  1511. .. ', curcol = '
  1512. .. v.curcol
  1513. .. ', linecount = '
  1514. .. v.linecount
  1515. .. ', sum_scroll_delta = '
  1516. .. v.sum_scroll_delta
  1517. .. '};\n'
  1518. )
  1519. end
  1520. return str .. '}'
  1521. elseif name == 'float_pos' then
  1522. local str = '{\n'
  1523. for k, v in pairs(state) do
  1524. str = str .. ' [' .. k .. '] = {' .. v[1]
  1525. for i = 2, #v do
  1526. str = str .. ', ' .. inspect(v[i])
  1527. end
  1528. str = str .. '};\n'
  1529. end
  1530. return str .. '}'
  1531. else
  1532. -- TODO(bfredl): improve formatting of more states
  1533. return inspect(state, { process = remove_all_metatables })
  1534. end
  1535. end
  1536. function Screen:_print_snapshot()
  1537. local kwargs, ext_state, attr_state = self:get_snapshot()
  1538. local attrstr = ''
  1539. local modify_attrs = not self._attrs_overridden
  1540. if attr_state.modified then
  1541. local attrstrs = {}
  1542. for i, a in pairs(attr_state.ids) do
  1543. local dict
  1544. if self._options.ext_linegrid then
  1545. dict = self:_pprint_hlitem(a)
  1546. else
  1547. dict = '{ ' .. self:_pprint_attrs(a) .. ' }'
  1548. end
  1549. local keyval = (type(i) == 'number') and '[' .. tostring(i) .. ']' or i
  1550. if not (type(i) == 'number' and modify_attrs and i <= 30) then
  1551. table.insert(attrstrs, ' ' .. keyval .. ' = ' .. dict .. ',')
  1552. end
  1553. if modify_attrs then
  1554. self._default_attr_ids = attr_state.ids
  1555. end
  1556. end
  1557. local fn_name = modify_attrs and 'add_extra_attr_ids' or 'set_default_attr_ids'
  1558. attrstr = ('screen:' .. fn_name .. '({\n' .. table.concat(attrstrs, '\n') .. '\n})\n\n')
  1559. end
  1560. local extstr = ''
  1561. for _, k in ipairs(ext_keys) do
  1562. if ext_state[k] ~= nil and not (k == 'win_viewport' and not self.options.ext_multigrid) then
  1563. extstr = extstr .. '\n ' .. k .. ' = ' .. fmt_ext_state(k, ext_state[k]) .. ','
  1564. end
  1565. end
  1566. return ('%sscreen:expect(%s%s%s%s%s'):format(
  1567. attrstr,
  1568. #extstr > 0 and '{\n grid = [[\n ' or '[[\n',
  1569. #extstr > 0 and kwargs.grid:gsub('\n', '\n ') or kwargs.grid,
  1570. #extstr > 0 and '\n ]],' or '\n]]',
  1571. extstr,
  1572. #extstr > 0 and '\n})' or ')'
  1573. )
  1574. end
  1575. function Screen:print_snapshot()
  1576. print('\n' .. self:_print_snapshot() .. '\n')
  1577. io.stdout:flush()
  1578. end
  1579. function Screen:_insert_hl_id(attr_state, hl_id)
  1580. if attr_state.id_to_index[hl_id] ~= nil then
  1581. return attr_state.id_to_index[hl_id]
  1582. end
  1583. local raw_info = self._hl_info[hl_id]
  1584. local info = nil
  1585. if self._options.ext_hlstate then
  1586. info = {}
  1587. if #raw_info > 1 then
  1588. for i, item in ipairs(raw_info) do
  1589. info[i] = self:_insert_hl_id(attr_state, item.id)
  1590. end
  1591. else
  1592. info[1] = {}
  1593. for k, v in pairs(raw_info[1]) do
  1594. if k ~= 'id' then
  1595. info[1][k] = v
  1596. end
  1597. end
  1598. end
  1599. end
  1600. local entry = self._attr_table[hl_id]
  1601. local attrval
  1602. if self._rgb_cterm then
  1603. attrval = { entry[1], entry[2], info } -- unpack() doesn't work
  1604. elseif self._options.ext_hlstate then
  1605. attrval = { entry[1], info }
  1606. else
  1607. attrval = self._options.rgb and entry[1] or entry[2]
  1608. end
  1609. table.insert(attr_state.ids, attrval)
  1610. attr_state.id_to_index[hl_id] = #attr_state.ids
  1611. return #attr_state.ids
  1612. end
  1613. function Screen:linegrid_check_attrs(attrs)
  1614. local id_to_index = {}
  1615. for i, def_attr in pairs(self._attr_table) do
  1616. local iinfo = self._hl_info[i]
  1617. local matchinfo = {}
  1618. if #iinfo > 1 then
  1619. for k, item in ipairs(iinfo) do
  1620. matchinfo[k] = id_to_index[item.id]
  1621. end
  1622. else
  1623. matchinfo = iinfo
  1624. end
  1625. for k, v in pairs(attrs) do
  1626. local attr, info, attr_rgb, attr_cterm
  1627. if self._rgb_cterm then
  1628. attr_rgb, attr_cterm, info = unpack(v)
  1629. attr = { attr_rgb, attr_cterm }
  1630. info = info or {}
  1631. elseif self._options.ext_hlstate then
  1632. attr, info = unpack(v)
  1633. else
  1634. attr = v
  1635. info = {}
  1636. end
  1637. if self:_equal_attr_def(attr, def_attr) then
  1638. if #info == #matchinfo then
  1639. local match = false
  1640. if #info == 1 then
  1641. if self:_equal_info(info[1], matchinfo[1]) then
  1642. match = true
  1643. end
  1644. else
  1645. match = true
  1646. for j = 1, #info do
  1647. if info[j] ~= matchinfo[j] then
  1648. match = false
  1649. end
  1650. end
  1651. end
  1652. if match then
  1653. id_to_index[i] = k
  1654. end
  1655. end
  1656. end
  1657. end
  1658. if
  1659. self:_equal_attr_def(self._rgb_cterm and { {}, {} } or {}, def_attr)
  1660. and #self._hl_info[i] == 0
  1661. then
  1662. id_to_index[i] = ''
  1663. end
  1664. end
  1665. return id_to_index
  1666. end
  1667. function Screen:_pprint_hlitem(item)
  1668. -- print(inspect(item))
  1669. local multi = self._rgb_cterm or self._options.ext_hlstate
  1670. local cterm = (not self._rgb_cterm and not self._options.rgb)
  1671. local attrdict = '{ ' .. self:_pprint_attrs(multi and item[1] or item, cterm) .. ' }'
  1672. local attrdict2, hlinfo
  1673. local descdict = ''
  1674. if self._rgb_cterm then
  1675. attrdict2 = ', { ' .. self:_pprint_attrs(item[2], true) .. ' }'
  1676. hlinfo = item[3]
  1677. else
  1678. attrdict2 = ''
  1679. hlinfo = item[2]
  1680. end
  1681. if self._options.ext_hlstate then
  1682. descdict = ', { ' .. self:_pprint_hlinfo(hlinfo) .. ' }'
  1683. end
  1684. return (multi and '{ ' or '') .. attrdict .. attrdict2 .. descdict .. (multi and ' }' or '')
  1685. end
  1686. function Screen:_pprint_hlinfo(states)
  1687. if #states == 1 then
  1688. local items = {}
  1689. for f, v in pairs(states[1]) do
  1690. local desc = tostring(v)
  1691. if type(v) == type('') then
  1692. desc = '"' .. desc .. '"'
  1693. end
  1694. table.insert(items, f .. ' = ' .. desc)
  1695. end
  1696. return '{' .. table.concat(items, ', ') .. '}'
  1697. else
  1698. return table.concat(states, ', ')
  1699. end
  1700. end
  1701. function Screen:_pprint_attrs(attrs, cterm)
  1702. local items = {}
  1703. for f, v in pairs(attrs) do
  1704. local desc = tostring(v)
  1705. if f == 'foreground' or f == 'background' or f == 'special' then
  1706. if Screen.colornames[v] ~= nil then
  1707. desc = 'Screen.colors.' .. Screen.colornames[v]
  1708. elseif cterm then
  1709. desc = tostring(v)
  1710. else
  1711. desc = string.format("tonumber('0x%06x')", v)
  1712. end
  1713. end
  1714. table.insert(items, f .. ' = ' .. desc)
  1715. end
  1716. return table.concat(items, ', ')
  1717. end
  1718. ---@diagnostic disable-next-line: unused-local, unused-function
  1719. local function backward_find_meaningful(tbl, from) -- luacheck: no unused
  1720. for i = from or #tbl, 1, -1 do
  1721. if tbl[i] ~= ' ' then
  1722. return i + 1
  1723. end
  1724. end
  1725. return from
  1726. end
  1727. function Screen:_get_attr_id(attr_state, attrs, hl_id)
  1728. if not attr_state.ids then
  1729. return
  1730. end
  1731. if self._options.ext_linegrid then
  1732. local id = attr_state.id_to_index[hl_id]
  1733. if id == '' then -- sentinel for empty it
  1734. return nil
  1735. elseif id ~= nil then
  1736. return id
  1737. end
  1738. if attr_state.mutable then
  1739. id = self:_insert_hl_id(attr_state, hl_id)
  1740. attr_state.modified = true
  1741. return id
  1742. end
  1743. local kind = self._options.rgb and 1 or 2
  1744. return 'UNEXPECTED ' .. self:_pprint_attrs(self._attr_table[hl_id][kind])
  1745. else
  1746. if self:_equal_attrs(attrs, {}) then
  1747. -- ignore this attrs
  1748. return nil
  1749. end
  1750. for id, a in pairs(attr_state.ids) do
  1751. if self:_equal_attrs(a, attrs) then
  1752. return id
  1753. end
  1754. end
  1755. if attr_state.mutable then
  1756. table.insert(attr_state.ids, attrs)
  1757. attr_state.modified = true
  1758. return #attr_state.ids
  1759. end
  1760. return 'UNEXPECTED ' .. self:_pprint_attrs(attrs)
  1761. end
  1762. end
  1763. function Screen:_equal_attr_def(a, b)
  1764. if self._rgb_cterm then
  1765. return self:_equal_attrs(a[1], b[1]) and self:_equal_attrs(a[2], b[2])
  1766. elseif self._options.rgb then
  1767. return self:_equal_attrs(a, b[1])
  1768. else
  1769. return self:_equal_attrs(a, b[2])
  1770. end
  1771. end
  1772. function Screen:_equal_attrs(a, b)
  1773. return a.bold == b.bold
  1774. and a.standout == b.standout
  1775. and a.underline == b.underline
  1776. and a.undercurl == b.undercurl
  1777. and a.underdouble == b.underdouble
  1778. and a.underdotted == b.underdotted
  1779. and a.underdashed == b.underdashed
  1780. and a.italic == b.italic
  1781. and a.reverse == b.reverse
  1782. and a.foreground == b.foreground
  1783. and a.background == b.background
  1784. and a.special == b.special
  1785. and a.blend == b.blend
  1786. and a.strikethrough == b.strikethrough
  1787. and a.fg_indexed == b.fg_indexed
  1788. and a.bg_indexed == b.bg_indexed
  1789. and a.url == b.url
  1790. end
  1791. function Screen:_equal_info(a, b)
  1792. return a.kind == b.kind and a.hi_name == b.hi_name and a.ui_name == b.ui_name
  1793. end
  1794. function Screen:_attr_index(attrs, attr)
  1795. if not attrs then
  1796. return nil
  1797. end
  1798. for i, a in pairs(attrs) do
  1799. if self:_equal_attrs(a, attr) then
  1800. return i
  1801. end
  1802. end
  1803. return nil
  1804. end
  1805. return Screen