inccommand_user_spec.lua 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. local t = require('test.testutil')
  2. local n = require('test.functional.testnvim')()
  3. local Screen = require('test.functional.ui.screen')
  4. local api = n.api
  5. local clear = n.clear
  6. local eq = t.eq
  7. local exec_lua = n.exec_lua
  8. local insert = n.insert
  9. local feed = n.feed
  10. local command = n.command
  11. local assert_alive = n.assert_alive
  12. -- Implements a :Replace command that works like :substitute and has multibuffer support.
  13. local setup_replace_cmd = [[
  14. local function show_replace_preview(use_preview_win, preview_ns, preview_buf, matches)
  15. -- Find the width taken by the largest line number, used for padding the line numbers
  16. local highest_lnum = math.max(matches[#matches][1], 1)
  17. local highest_lnum_width = math.floor(math.log10(highest_lnum))
  18. local preview_buf_line = 0
  19. local multibuffer = #matches > 1
  20. for _, match in ipairs(matches) do
  21. local buf = match[1]
  22. local buf_matches = match[2]
  23. if multibuffer and #buf_matches > 0 and use_preview_win then
  24. local bufname = vim.api.nvim_buf_get_name(buf)
  25. if bufname == "" then
  26. bufname = string.format("Buffer #%d", buf)
  27. end
  28. vim.api.nvim_buf_set_lines(
  29. preview_buf,
  30. preview_buf_line,
  31. preview_buf_line,
  32. 0,
  33. { bufname .. ':' }
  34. )
  35. preview_buf_line = preview_buf_line + 1
  36. end
  37. for _, buf_match in ipairs(buf_matches) do
  38. local lnum = buf_match[1]
  39. local line_matches = buf_match[2]
  40. local prefix
  41. if use_preview_win then
  42. prefix = string.format(
  43. '|%s%d| ',
  44. string.rep(' ', highest_lnum_width - math.floor(math.log10(lnum))),
  45. lnum
  46. )
  47. vim.api.nvim_buf_set_lines(
  48. preview_buf,
  49. preview_buf_line,
  50. preview_buf_line,
  51. 0,
  52. { prefix .. vim.api.nvim_buf_get_lines(buf, lnum - 1, lnum, false)[1] }
  53. )
  54. end
  55. for _, line_match in ipairs(line_matches) do
  56. vim.api.nvim_buf_add_highlight(
  57. buf,
  58. preview_ns,
  59. 'Substitute',
  60. lnum - 1,
  61. line_match[1],
  62. line_match[2]
  63. )
  64. if use_preview_win then
  65. vim.api.nvim_buf_add_highlight(
  66. preview_buf,
  67. preview_ns,
  68. 'Substitute',
  69. preview_buf_line,
  70. #prefix + line_match[1],
  71. #prefix + line_match[2]
  72. )
  73. end
  74. end
  75. preview_buf_line = preview_buf_line + 1
  76. end
  77. end
  78. if use_preview_win then
  79. return 2
  80. else
  81. return 1
  82. end
  83. end
  84. local function do_replace(opts, preview, preview_ns, preview_buf)
  85. local pat1 = opts.fargs[1]
  86. if not pat1 then return end
  87. local pat2 = opts.fargs[2] or ''
  88. local line1 = opts.line1
  89. local line2 = opts.line2
  90. local matches = {}
  91. -- Get list of valid and listed buffers
  92. local buffers = vim.tbl_filter(
  93. function(buf)
  94. if not (vim.api.nvim_buf_is_valid(buf) and vim.bo[buf].buflisted and buf ~= preview_buf)
  95. then
  96. return false
  97. end
  98. -- Check if there's at least one window using the buffer
  99. for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
  100. if vim.api.nvim_win_get_buf(win) == buf then
  101. return true
  102. end
  103. end
  104. return false
  105. end,
  106. vim.api.nvim_list_bufs()
  107. )
  108. for _, buf in ipairs(buffers) do
  109. local lines = vim.api.nvim_buf_get_lines(buf, line1 - 1, line2, false)
  110. local buf_matches = {}
  111. for i, line in ipairs(lines) do
  112. local startidx, endidx = 0, 0
  113. local line_matches = {}
  114. local num = 1
  115. while startidx ~= -1 do
  116. local match = vim.fn.matchstrpos(line, pat1, 0, num)
  117. startidx, endidx = match[2], match[3]
  118. if startidx ~= -1 then
  119. line_matches[#line_matches+1] = { startidx, endidx }
  120. end
  121. num = num + 1
  122. end
  123. if #line_matches > 0 then
  124. buf_matches[#buf_matches+1] = { line1 + i - 1, line_matches }
  125. end
  126. end
  127. local new_lines = {}
  128. for _, buf_match in ipairs(buf_matches) do
  129. local lnum = buf_match[1]
  130. local line_matches = buf_match[2]
  131. local line = lines[lnum - line1 + 1]
  132. local pat_width_differences = {}
  133. -- If previewing, only replace the text in current buffer if pat2 isn't empty
  134. -- Otherwise, always replace the text
  135. if pat2 ~= '' or not preview then
  136. if preview then
  137. for _, line_match in ipairs(line_matches) do
  138. local startidx, endidx = unpack(line_match)
  139. local pat_match = line:sub(startidx + 1, endidx)
  140. pat_width_differences[#pat_width_differences+1] =
  141. #vim.fn.substitute(pat_match, pat1, pat2, 'g') - #pat_match
  142. end
  143. end
  144. new_lines[lnum] = vim.fn.substitute(line, pat1, pat2, 'g')
  145. end
  146. -- Highlight the matches if previewing
  147. if preview then
  148. local idx_offset = 0
  149. for i, line_match in ipairs(line_matches) do
  150. local startidx, endidx = unpack(line_match)
  151. -- Starting index of replacement text
  152. local repl_startidx = startidx + idx_offset
  153. -- Ending index of the replacement text (if pat2 isn't empty)
  154. local repl_endidx
  155. if pat2 ~= '' then
  156. repl_endidx = endidx + idx_offset + pat_width_differences[i]
  157. else
  158. repl_endidx = endidx + idx_offset
  159. end
  160. if pat2 ~= '' then
  161. idx_offset = idx_offset + pat_width_differences[i]
  162. end
  163. line_matches[i] = { repl_startidx, repl_endidx }
  164. end
  165. end
  166. end
  167. for lnum, line in pairs(new_lines) do
  168. vim.api.nvim_buf_set_lines(buf, lnum - 1, lnum, false, { line })
  169. end
  170. matches[#matches+1] = { buf, buf_matches }
  171. end
  172. if preview then
  173. local lnum = vim.api.nvim_win_get_cursor(0)[1]
  174. -- Use preview window only if preview buffer is provided and range isn't just the current line
  175. local use_preview_win = (preview_buf ~= nil) and (line1 ~= lnum or line2 ~= lnum)
  176. return show_replace_preview(use_preview_win, preview_ns, preview_buf, matches)
  177. end
  178. end
  179. local function replace(opts)
  180. do_replace(opts, false)
  181. end
  182. local function replace_preview(opts, preview_ns, preview_buf)
  183. return do_replace(opts, true, preview_ns, preview_buf)
  184. end
  185. -- ":<range>Replace <pat1> <pat2>"
  186. -- Replaces all occurrences of <pat1> in <range> with <pat2>
  187. vim.api.nvim_create_user_command(
  188. 'Replace',
  189. replace,
  190. { nargs = '*', range = '%', addr = 'lines',
  191. preview = replace_preview }
  192. )
  193. ]]
  194. describe("'inccommand' for user commands", function()
  195. local screen
  196. before_each(function()
  197. clear()
  198. screen = Screen.new(40, 17)
  199. exec_lua(setup_replace_cmd)
  200. command('set cmdwinheight=5')
  201. insert [[
  202. text on line 1
  203. more text on line 2
  204. oh no, even more text
  205. will the text ever stop
  206. oh well
  207. did the text stop
  208. why won't it stop
  209. make the text stop
  210. ]]
  211. end)
  212. it("can preview 'nomodifiable' buffer", function()
  213. exec_lua([[
  214. vim.api.nvim_create_user_command("PreviewTest", function() end, {
  215. preview = function(ev)
  216. vim.bo.modifiable = true
  217. vim.api.nvim_buf_set_lines(0, 0, -1, false, {"cats"})
  218. return 2
  219. end,
  220. })
  221. ]])
  222. command('set inccommand=split')
  223. command('set nomodifiable')
  224. eq(false, api.nvim_get_option_value('modifiable', { buf = 0 }))
  225. feed(':PreviewTest')
  226. screen:expect([[
  227. cats |
  228. {1:~ }|*8
  229. {3:[No Name] [+] }|
  230. |
  231. {1:~ }|*4
  232. {2:[Preview] }|
  233. :PreviewTest^ |
  234. ]])
  235. feed('<Esc>')
  236. screen:expect([[
  237. text on line 1 |
  238. more text on line 2 |
  239. oh no, even more text |
  240. will the text ever stop |
  241. oh well |
  242. did the text stop |
  243. why won't it stop |
  244. make the text stop |
  245. ^ |
  246. {1:~ }|*7
  247. |
  248. ]])
  249. eq(false, api.nvim_get_option_value('modifiable', { buf = 0 }))
  250. end)
  251. it('works with inccommand=nosplit', function()
  252. command('set inccommand=nosplit')
  253. feed(':Replace text cats')
  254. screen:expect([[
  255. {10:cats} on line 1 |
  256. more {10:cats} on line 2 |
  257. oh no, even more {10:cats} |
  258. will the {10:cats} ever stop |
  259. oh well |
  260. did the {10:cats} stop |
  261. why won't it stop |
  262. make the {10:cats} stop |
  263. |
  264. {1:~ }|*7
  265. :Replace text cats^ |
  266. ]])
  267. end)
  268. it('works with inccommand=split', function()
  269. command('set inccommand=split')
  270. feed(':Replace text cats')
  271. screen:expect([[
  272. {10:cats} on line 1 |
  273. more {10:cats} on line 2 |
  274. oh no, even more {10:cats} |
  275. will the {10:cats} ever stop |
  276. oh well |
  277. did the {10:cats} stop |
  278. why won't it stop |
  279. make the {10:cats} stop |
  280. |
  281. {3:[No Name] [+] }|
  282. |1| {10:cats} on line 1 |
  283. |2| more {10:cats} on line 2 |
  284. |3| oh no, even more {10:cats} |
  285. |4| will the {10:cats} ever stop |
  286. |6| did the {10:cats} stop |
  287. {2:[Preview] }|
  288. :Replace text cats^ |
  289. ]])
  290. end)
  291. it('properly closes preview when inccommand=split', function()
  292. command('set inccommand=split')
  293. feed(':Replace text cats<Esc>')
  294. screen:expect([[
  295. text on line 1 |
  296. more text on line 2 |
  297. oh no, even more text |
  298. will the text ever stop |
  299. oh well |
  300. did the text stop |
  301. why won't it stop |
  302. make the text stop |
  303. ^ |
  304. {1:~ }|*7
  305. |
  306. ]])
  307. end)
  308. it('properly executes command when inccommand=split', function()
  309. command('set inccommand=split')
  310. feed(':Replace text cats<CR>')
  311. screen:expect([[
  312. cats on line 1 |
  313. more cats on line 2 |
  314. oh no, even more cats |
  315. will the cats ever stop |
  316. oh well |
  317. did the cats stop |
  318. why won't it stop |
  319. make the cats stop |
  320. ^ |
  321. {1:~ }|*7
  322. :Replace text cats |
  323. ]])
  324. end)
  325. it('shows preview window only when range is not current line', function()
  326. command('set inccommand=split')
  327. feed('gg:.Replace text cats')
  328. screen:expect([[
  329. {10:cats} on line 1 |
  330. more text on line 2 |
  331. oh no, even more text |
  332. will the text ever stop |
  333. oh well |
  334. did the text stop |
  335. why won't it stop |
  336. make the text stop |
  337. |
  338. {1:~ }|*7
  339. :.Replace text cats^ |
  340. ]])
  341. end)
  342. it('does not crash on ambiguous command #18825', function()
  343. command('set inccommand=split')
  344. command('command Reply echo 1')
  345. feed(':R')
  346. assert_alive()
  347. feed('e')
  348. assert_alive()
  349. end)
  350. it('no crash if preview callback changes inccommand option', function()
  351. command('set inccommand=nosplit')
  352. exec_lua([[
  353. vim.api.nvim_create_user_command('Replace', function() end, {
  354. nargs = '*',
  355. preview = function()
  356. vim.api.nvim_set_option_value('inccommand', 'split', {})
  357. return 2
  358. end,
  359. })
  360. ]])
  361. feed(':R')
  362. assert_alive()
  363. feed('e')
  364. assert_alive()
  365. end)
  366. it('no crash when adding highlight after :substitute #21495', function()
  367. command('set inccommand=nosplit')
  368. exec_lua([[
  369. vim.api.nvim_create_user_command("Crash", function() end, {
  370. preview = function(_, preview_ns, _)
  371. vim.cmd("%s/text/cats/g")
  372. vim.api.nvim_buf_add_highlight(0, preview_ns, "Search", 0, 0, -1)
  373. return 1
  374. end,
  375. })
  376. ]])
  377. feed(':C')
  378. screen:expect([[
  379. {10: cats on line 1} |
  380. more cats on line 2 |
  381. oh no, even more cats |
  382. will the cats ever stop |
  383. oh well |
  384. did the cats stop |
  385. why won't it stop |
  386. make the cats stop |
  387. |
  388. {1:~ }|*7
  389. :C^ |
  390. ]])
  391. assert_alive()
  392. end)
  393. it('no crash if preview callback executes undo #20036', function()
  394. command('set inccommand=nosplit')
  395. exec_lua([[
  396. vim.api.nvim_create_user_command('Foo', function() end, {
  397. nargs = '?',
  398. preview = function(_, _, _)
  399. vim.cmd.undo()
  400. end,
  401. })
  402. ]])
  403. -- Clear undo history
  404. command('set undolevels=-1')
  405. feed('ggyyp')
  406. command('set undolevels=1000')
  407. feed('yypp:Fo')
  408. assert_alive()
  409. feed('<Esc>:Fo')
  410. assert_alive()
  411. end)
  412. local function test_preview_break_undo()
  413. command('set inccommand=nosplit')
  414. exec_lua([[
  415. vim.api.nvim_create_user_command('Test', function() end, {
  416. nargs = 1,
  417. preview = function(opts, _, _)
  418. vim.cmd('norm i' .. opts.args)
  419. return 1
  420. end
  421. })
  422. ]])
  423. feed(':Test a.a.a.a.')
  424. screen:expect([[
  425. text on line 1 |
  426. more text on line 2 |
  427. oh no, even more text |
  428. will the text ever stop |
  429. oh well |
  430. did the text stop |
  431. why won't it stop |
  432. make the text stop |
  433. a.a.a.a. |
  434. {1:~ }|*7
  435. :Test a.a.a.a.^ |
  436. ]])
  437. feed('<C-V><Esc>u')
  438. screen:expect([[
  439. text on line 1 |
  440. more text on line 2 |
  441. oh no, even more text |
  442. will the text ever stop |
  443. oh well |
  444. did the text stop |
  445. why won't it stop |
  446. make the text stop |
  447. a.a.a. |
  448. {1:~ }|*7
  449. :Test a.a.a.a.{18:^[}u^ |
  450. ]])
  451. feed('<Esc>')
  452. screen:expect([[
  453. text on line 1 |
  454. more text on line 2 |
  455. oh no, even more text |
  456. will the text ever stop |
  457. oh well |
  458. did the text stop |
  459. why won't it stop |
  460. make the text stop |
  461. ^ |
  462. {1:~ }|*7
  463. |
  464. ]])
  465. end
  466. describe('breaking undo chain in Insert mode works properly', function()
  467. it('when using i_CTRL-G_u #20248', function()
  468. command('inoremap . .<C-G>u')
  469. test_preview_break_undo()
  470. end)
  471. it('when setting &l:undolevels to itself #24575', function()
  472. command('inoremap . .<Cmd>let &l:undolevels = &l:undolevels<CR>')
  473. test_preview_break_undo()
  474. end)
  475. end)
  476. it('disables preview if preview buffer cannot be created #27086', function()
  477. command('set inccommand=split')
  478. api.nvim_buf_set_name(0, '[Preview]')
  479. exec_lua([[
  480. vim.api.nvim_create_user_command('Test', function() end, {
  481. nargs = '*',
  482. preview = function(_, _, _)
  483. return 2
  484. end
  485. })
  486. ]])
  487. eq('split', api.nvim_get_option_value('inccommand', {}))
  488. feed(':Test')
  489. eq('nosplit', api.nvim_get_option_value('inccommand', {}))
  490. end)
  491. it('does not flush intermediate cursor position at end of message grid', function()
  492. exec_lua([[
  493. vim.api.nvim_create_user_command('Test', function() end, {
  494. nargs = '*',
  495. preview = function(_, _, _)
  496. vim.api.nvim_buf_set_text(0, 0, 0, 1, -1, { "Preview" })
  497. vim.cmd.sleep("1m")
  498. return 1
  499. end
  500. })
  501. ]])
  502. local cursor_goto = screen._handle_grid_cursor_goto
  503. screen._handle_grid_cursor_goto = function(...)
  504. cursor_goto(...)
  505. assert(screen._cursor.col < 12)
  506. end
  507. feed(':Test baz<Left><Left>arb')
  508. screen:expect({
  509. grid = [[
  510. Preview |
  511. oh no, even more text |
  512. will the text ever stop |
  513. oh well |
  514. did the text stop |
  515. why won't it stop |
  516. make the text stop |
  517. |
  518. {1:~ }|*8
  519. :Test barb^az |
  520. ]],
  521. })
  522. end)
  523. end)
  524. describe("'inccommand' with multiple buffers", function()
  525. local screen
  526. before_each(function()
  527. clear()
  528. screen = Screen.new(40, 17)
  529. exec_lua(setup_replace_cmd)
  530. command('set cmdwinheight=10')
  531. insert [[
  532. foo bar baz
  533. bar baz foo
  534. baz foo bar
  535. ]]
  536. command('vsplit | enew')
  537. insert [[
  538. bar baz foo
  539. baz foo bar
  540. foo bar baz
  541. ]]
  542. end)
  543. it('works', function()
  544. command('set inccommand=nosplit')
  545. feed(':Replace foo bar')
  546. screen:expect([[
  547. bar baz {10:bar} │ {10:bar} bar baz |
  548. baz {10:bar} bar │ bar baz {10:bar} |
  549. {10:bar} bar baz │ baz {10:bar} bar |
  550. │ |
  551. {1:~ }│{1:~ }|*11
  552. {3:[No Name] [+] }{2:[No Name] [+] }|
  553. :Replace foo bar^ |
  554. ]])
  555. feed('<CR>')
  556. screen:expect([[
  557. bar baz bar │ bar bar baz |
  558. baz bar bar │ bar baz bar |
  559. bar bar baz │ baz bar bar |
  560. ^ │ |
  561. {1:~ }│{1:~ }|*11
  562. {3:[No Name] [+] }{2:[No Name] [+] }|
  563. :Replace foo bar |
  564. ]])
  565. end)
  566. it('works with inccommand=split', function()
  567. command('set inccommand=split')
  568. feed(':Replace foo bar')
  569. screen:expect([[
  570. bar baz {10:bar} │ {10:bar} bar baz |
  571. baz {10:bar} bar │ bar baz {10:bar} |
  572. {10:bar} bar baz │ baz {10:bar} bar |
  573. │ |
  574. {3:[No Name] [+] }{2:[No Name] [+] }|
  575. Buffer #1: |
  576. |1| {10:bar} bar baz |
  577. |2| bar baz {10:bar} |
  578. |3| baz {10:bar} bar |
  579. Buffer #2: |
  580. |1| bar baz {10:bar} |
  581. |2| baz {10:bar} bar |
  582. |3| {10:bar} bar baz |
  583. |
  584. {1:~ }|
  585. {2:[Preview] }|
  586. :Replace foo bar^ |
  587. ]])
  588. feed('<CR>')
  589. screen:expect([[
  590. bar baz bar │ bar bar baz |
  591. baz bar bar │ bar baz bar |
  592. bar bar baz │ baz bar bar |
  593. ^ │ |
  594. {1:~ }│{1:~ }|*11
  595. {3:[No Name] [+] }{2:[No Name] [+] }|
  596. :Replace foo bar |
  597. ]])
  598. end)
  599. end)