inccommand_user_spec.lua 23 KB

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