completion_spec.lua 26 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045
  1. ---@diagnostic disable: no-unknown
  2. local t = require('test.testutil')
  3. local t_lsp = require('test.functional.plugin.lsp.testutil')
  4. local n = require('test.functional.testnvim')()
  5. local clear = n.clear
  6. local eq = t.eq
  7. local neq = t.neq
  8. local exec_lua = n.exec_lua
  9. local feed = n.feed
  10. local retry = t.retry
  11. local create_server_definition = t_lsp.create_server_definition
  12. --- Convert completion results.
  13. ---
  14. ---@param line string line contents. Mark cursor position with `|`
  15. ---@param candidates lsp.CompletionList|lsp.CompletionItem[]
  16. ---@param lnum? integer 0-based, defaults to 0
  17. ---@return {items: table[], server_start_boundary: integer?}
  18. local function complete(line, candidates, lnum, server_boundary)
  19. lnum = lnum or 0
  20. -- nvim_win_get_cursor returns 0 based column, line:find returns 1 based
  21. local cursor_col = line:find('|') - 1
  22. line = line:gsub('|', '')
  23. return exec_lua(function(result)
  24. local line_to_cursor = line:sub(1, cursor_col)
  25. local client_start_boundary = vim.fn.match(line_to_cursor, '\\k*$')
  26. local items, new_server_boundary = require('vim.lsp.completion')._convert_results(
  27. line,
  28. lnum,
  29. cursor_col,
  30. 1,
  31. client_start_boundary,
  32. server_boundary,
  33. result,
  34. 'utf-16'
  35. )
  36. return {
  37. items = items,
  38. server_start_boundary = new_server_boundary,
  39. }
  40. end, candidates)
  41. end
  42. describe('vim.lsp.completion: item conversion', function()
  43. before_each(n.clear)
  44. -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
  45. it('prefers textEdit over label as word', function()
  46. local range0 = {
  47. start = { line = 0, character = 0 },
  48. ['end'] = { line = 0, character = 0 },
  49. }
  50. local completion_list = {
  51. -- resolves into label
  52. { label = 'foobar', sortText = 'a', documentation = 'documentation' },
  53. {
  54. label = 'foobar',
  55. sortText = 'b',
  56. documentation = { value = 'documentation' },
  57. },
  58. -- resolves into insertText
  59. { label = 'foocar', sortText = 'c', insertText = 'foobar' },
  60. { label = 'foocar', sortText = 'd', insertText = 'foobar' },
  61. -- resolves into textEdit.newText
  62. {
  63. label = 'foocar',
  64. sortText = 'e',
  65. insertText = 'foodar',
  66. textEdit = { newText = 'foobar', range = range0 },
  67. },
  68. { label = 'foocar', sortText = 'f', textEdit = { newText = 'foobar', range = range0 } },
  69. -- plain text
  70. {
  71. label = 'foocar',
  72. sortText = 'g',
  73. insertText = 'foodar(${1:var1})',
  74. insertTextFormat = 1,
  75. },
  76. {
  77. label = '•INT16_C(c)',
  78. insertText = 'INT16_C(${1:c})',
  79. insertTextFormat = 2,
  80. filterText = 'INT16_C',
  81. sortText = 'h',
  82. textEdit = {
  83. newText = 'INT16_C(${1:c})',
  84. range = range0,
  85. },
  86. },
  87. }
  88. local expected = {
  89. {
  90. abbr = 'foobar',
  91. word = 'foobar',
  92. },
  93. {
  94. abbr = 'foobar',
  95. word = 'foobar',
  96. },
  97. {
  98. abbr = 'foocar',
  99. word = 'foobar',
  100. },
  101. {
  102. abbr = 'foocar',
  103. word = 'foobar',
  104. },
  105. {
  106. abbr = 'foocar',
  107. word = 'foobar',
  108. },
  109. {
  110. abbr = 'foocar',
  111. word = 'foobar',
  112. },
  113. {
  114. abbr = 'foocar',
  115. word = 'foodar(${1:var1})', -- marked as PlainText, text is used as is
  116. },
  117. {
  118. abbr = '•INT16_C(c)',
  119. word = 'INT16_C',
  120. },
  121. }
  122. local result = complete('|', completion_list)
  123. result = vim.tbl_map(function(x)
  124. return {
  125. abbr = x.abbr,
  126. word = x.word,
  127. }
  128. end, result.items)
  129. eq(expected, result)
  130. end)
  131. it('does not filter if there is a textEdit', function()
  132. local range0 = {
  133. start = { line = 0, character = 0 },
  134. ['end'] = { line = 0, character = 0 },
  135. }
  136. local completion_list = {
  137. { label = 'foo', textEdit = { newText = 'foo', range = range0 } },
  138. { label = 'bar', textEdit = { newText = 'bar', range = range0 } },
  139. }
  140. local result = complete('fo|', completion_list)
  141. local expected = {
  142. {
  143. abbr = 'foo',
  144. word = 'foo',
  145. },
  146. {
  147. abbr = 'bar',
  148. word = 'bar',
  149. },
  150. }
  151. result = vim.tbl_map(function(x)
  152. return {
  153. abbr = x.abbr,
  154. word = x.word,
  155. }
  156. end, result.items)
  157. local sorter = function(a, b)
  158. return a.word > b.word
  159. end
  160. table.sort(expected, sorter)
  161. table.sort(result, sorter)
  162. eq(expected, result)
  163. end)
  164. ---@param prefix string
  165. ---@param items lsp.CompletionItem[]
  166. ---@param expected table[]
  167. local assert_completion_matches = function(prefix, items, expected)
  168. local result = complete(prefix .. '|', items)
  169. result = vim.tbl_map(function(x)
  170. return {
  171. abbr = x.abbr,
  172. word = x.word,
  173. }
  174. end, result.items)
  175. local sorter = function(a, b)
  176. return a.word > b.word
  177. end
  178. table.sort(expected, sorter)
  179. table.sort(result, sorter)
  180. eq(expected, result)
  181. end
  182. describe('when completeopt has fuzzy matching enabled', function()
  183. before_each(function()
  184. exec_lua(function()
  185. vim.opt.completeopt:append('fuzzy')
  186. end)
  187. end)
  188. after_each(function()
  189. exec_lua(function()
  190. vim.opt.completeopt:remove('fuzzy')
  191. end)
  192. end)
  193. it('fuzzy matches on filterText', function()
  194. assert_completion_matches('fo', {
  195. { label = '?.foo', filterText = 'foo' },
  196. { label = 'faz other', filterText = 'faz other' },
  197. { label = 'bar', filterText = 'bar' },
  198. }, {
  199. {
  200. abbr = 'faz other',
  201. word = 'faz other',
  202. },
  203. {
  204. abbr = '?.foo',
  205. word = '?.foo',
  206. },
  207. })
  208. end)
  209. it('fuzzy matches on label when filterText is missing', function()
  210. assert_completion_matches('fo', {
  211. { label = 'foo' },
  212. { label = 'faz other' },
  213. { label = 'bar' },
  214. }, {
  215. {
  216. abbr = 'faz other',
  217. word = 'faz other',
  218. },
  219. {
  220. abbr = 'foo',
  221. word = 'foo',
  222. },
  223. })
  224. end)
  225. end)
  226. describe('when smartcase is enabled', function()
  227. before_each(function()
  228. exec_lua(function()
  229. vim.opt.smartcase = true
  230. end)
  231. end)
  232. after_each(function()
  233. exec_lua(function()
  234. vim.opt.smartcase = false
  235. end)
  236. end)
  237. it('matches filterText case sensitively', function()
  238. assert_completion_matches('Fo', {
  239. { label = 'foo', filterText = 'foo' },
  240. { label = '?.Foo', filterText = 'Foo' },
  241. { label = 'Faz other', filterText = 'Faz other' },
  242. { label = 'faz other', filterText = 'faz other' },
  243. { label = 'bar', filterText = 'bar' },
  244. }, {
  245. {
  246. abbr = '?.Foo',
  247. word = '?.Foo',
  248. },
  249. })
  250. end)
  251. it('matches label case sensitively when filterText is missing', function()
  252. assert_completion_matches('Fo', {
  253. { label = 'foo' },
  254. { label = 'Foo' },
  255. { label = 'Faz other' },
  256. { label = 'faz other' },
  257. { label = 'bar' },
  258. }, {
  259. {
  260. abbr = 'Foo',
  261. word = 'Foo',
  262. },
  263. })
  264. end)
  265. describe('when ignorecase is enabled', function()
  266. before_each(function()
  267. exec_lua(function()
  268. vim.opt.ignorecase = true
  269. end)
  270. end)
  271. after_each(function()
  272. exec_lua(function()
  273. vim.opt.ignorecase = false
  274. end)
  275. end)
  276. it('matches filterText case insensitively if prefix is lowercase', function()
  277. assert_completion_matches('fo', {
  278. { label = '?.foo', filterText = 'foo' },
  279. { label = '?.Foo', filterText = 'Foo' },
  280. { label = 'Faz other', filterText = 'Faz other' },
  281. { label = 'faz other', filterText = 'faz other' },
  282. { label = 'bar', filterText = 'bar' },
  283. }, {
  284. {
  285. abbr = '?.Foo',
  286. word = '?.Foo',
  287. },
  288. {
  289. abbr = '?.foo',
  290. word = '?.foo',
  291. },
  292. })
  293. end)
  294. it(
  295. 'matches label case insensitively if prefix is lowercase and filterText is missing',
  296. function()
  297. assert_completion_matches('fo', {
  298. { label = 'foo' },
  299. { label = 'Foo' },
  300. { label = 'Faz other' },
  301. { label = 'faz other' },
  302. { label = 'bar' },
  303. }, {
  304. {
  305. abbr = 'Foo',
  306. word = 'Foo',
  307. },
  308. {
  309. abbr = 'foo',
  310. word = 'foo',
  311. },
  312. })
  313. end
  314. )
  315. it('matches filterText case sensitively if prefix has uppercase letters', function()
  316. assert_completion_matches('Fo', {
  317. { label = 'foo', filterText = 'foo' },
  318. { label = '?.Foo', filterText = 'Foo' },
  319. { label = 'Faz other', filterText = 'Faz other' },
  320. { label = 'faz other', filterText = 'faz other' },
  321. { label = 'bar', filterText = 'bar' },
  322. }, {
  323. {
  324. abbr = '?.Foo',
  325. word = '?.Foo',
  326. },
  327. })
  328. end)
  329. it(
  330. 'matches label case sensitively if prefix has uppercase letters and filterText is missing',
  331. function()
  332. assert_completion_matches('Fo', {
  333. { label = 'foo' },
  334. { label = 'Foo' },
  335. { label = 'Faz other' },
  336. { label = 'faz other' },
  337. { label = 'bar' },
  338. }, {
  339. {
  340. abbr = 'Foo',
  341. word = 'Foo',
  342. },
  343. })
  344. end
  345. )
  346. end)
  347. end)
  348. describe('when ignorecase is enabled', function()
  349. before_each(function()
  350. exec_lua(function()
  351. vim.opt.ignorecase = true
  352. end)
  353. end)
  354. after_each(function()
  355. exec_lua(function()
  356. vim.opt.ignorecase = false
  357. end)
  358. end)
  359. it('matches filterText case insensitively', function()
  360. assert_completion_matches('Fo', {
  361. { label = '?.foo', filterText = 'foo' },
  362. { label = '?.Foo', filterText = 'Foo' },
  363. { label = 'Faz other', filterText = 'Faz other' },
  364. { label = 'faz other', filterText = 'faz other' },
  365. { label = 'bar', filterText = 'bar' },
  366. }, {
  367. {
  368. abbr = '?.Foo',
  369. word = '?.Foo',
  370. },
  371. {
  372. abbr = '?.foo',
  373. word = '?.foo',
  374. },
  375. })
  376. end)
  377. it('matches label case insensitively when filterText is missing', function()
  378. assert_completion_matches('Fo', {
  379. { label = 'foo' },
  380. { label = 'Foo' },
  381. { label = 'Faz other' },
  382. { label = 'faz other' },
  383. { label = 'bar' },
  384. }, {
  385. {
  386. abbr = 'Foo',
  387. word = 'Foo',
  388. },
  389. {
  390. abbr = 'foo',
  391. word = 'foo',
  392. },
  393. })
  394. end)
  395. end)
  396. it('works on non word prefix', function()
  397. local completion_list = {
  398. { label = ' foo', insertText = '->foo' },
  399. }
  400. local result = complete('wp.|', completion_list, 0, 2)
  401. local expected = {
  402. {
  403. abbr = ' foo',
  404. word = '->foo',
  405. },
  406. }
  407. result = vim.tbl_map(function(x)
  408. return {
  409. abbr = x.abbr,
  410. word = x.word,
  411. }
  412. end, result.items)
  413. eq(expected, result)
  414. end)
  415. it('trims trailing newline or tab from textEdit', function()
  416. local range0 = {
  417. start = { line = 0, character = 0 },
  418. ['end'] = { line = 0, character = 0 },
  419. }
  420. local items = {
  421. {
  422. detail = 'ansible.builtin',
  423. filterText = 'lineinfile ansible.builtin.lineinfile builtin ansible',
  424. kind = 7,
  425. label = 'ansible.builtin.lineinfile',
  426. sortText = '2_ansible.builtin.lineinfile',
  427. textEdit = {
  428. newText = 'ansible.builtin.lineinfile:\n ',
  429. range = range0,
  430. },
  431. },
  432. }
  433. local result = complete('|', items)
  434. result = vim.tbl_map(function(x)
  435. return {
  436. abbr = x.abbr,
  437. word = x.word,
  438. }
  439. end, result.items)
  440. local expected = {
  441. {
  442. abbr = 'ansible.builtin.lineinfile',
  443. word = 'ansible.builtin.lineinfile:',
  444. },
  445. }
  446. eq(expected, result)
  447. end)
  448. it('prefers wordlike components for snippets', function()
  449. -- There are two goals here:
  450. --
  451. -- 1. The `word` should match what the user started typing, so that vim.fn.complete() doesn't
  452. -- filter it away, preventing snippet expansion
  453. --
  454. -- For example, if they type `items@ins`, luals returns `table.insert(items, $0)` as
  455. -- textEdit.newText and `insert` as label.
  456. -- There would be no prefix match if textEdit.newText is used as `word`
  457. --
  458. -- 2. If users do not expand a snippet, but continue typing, they should see a somewhat reasonable
  459. -- `word` getting inserted.
  460. --
  461. -- For example in:
  462. --
  463. -- insertText: "testSuites ${1:Env}"
  464. -- label: "testSuites"
  465. --
  466. -- "testSuites" should have priority as `word`, as long as the full snippet gets expanded on accept (<c-y>)
  467. local range0 = {
  468. start = { line = 0, character = 0 },
  469. ['end'] = { line = 0, character = 0 },
  470. }
  471. local completion_list = {
  472. -- luals postfix snippet (typed text: items@ins|)
  473. {
  474. label = 'insert',
  475. insertTextFormat = 2,
  476. textEdit = {
  477. newText = 'table.insert(items, $0)',
  478. range = range0,
  479. },
  480. },
  481. -- eclipse.jdt.ls `new` snippet
  482. {
  483. label = 'new',
  484. insertTextFormat = 2,
  485. textEdit = {
  486. newText = '${1:Object} ${2:foo} = new ${1}(${3});\n${0}',
  487. range = range0,
  488. },
  489. textEditText = '${1:Object} ${2:foo} = new ${1}(${3});\n${0}',
  490. },
  491. -- eclipse.jdt.ls `List.copyO` function call completion
  492. {
  493. label = 'copyOf(Collection<? extends E> coll) : List<E>',
  494. insertTextFormat = 2,
  495. insertText = 'copyOf',
  496. textEdit = {
  497. newText = 'copyOf(${1:coll})',
  498. range = range0,
  499. },
  500. },
  501. }
  502. local expected = {
  503. {
  504. abbr = 'copyOf(Collection<? extends E> coll) : List<E>',
  505. word = 'copyOf',
  506. },
  507. {
  508. abbr = 'insert',
  509. word = 'insert',
  510. },
  511. {
  512. abbr = 'new',
  513. word = 'new',
  514. },
  515. }
  516. local result = complete('|', completion_list)
  517. result = vim.tbl_map(function(x)
  518. return {
  519. abbr = x.abbr,
  520. word = x.word,
  521. }
  522. end, result.items)
  523. eq(expected, result)
  524. end)
  525. it('uses correct start boundary', function()
  526. local completion_list = {
  527. isIncomplete = false,
  528. items = {
  529. {
  530. filterText = 'this_thread',
  531. insertText = 'this_thread',
  532. insertTextFormat = 1,
  533. kind = 9,
  534. label = ' this_thread',
  535. score = 1.3205767869949,
  536. sortText = '4056f757this_thread',
  537. textEdit = {
  538. newText = 'this_thread',
  539. range = {
  540. start = { line = 0, character = 7 },
  541. ['end'] = { line = 0, character = 11 },
  542. },
  543. },
  544. },
  545. },
  546. }
  547. local expected = {
  548. abbr = ' this_thread',
  549. dup = 1,
  550. empty = 1,
  551. icase = 1,
  552. info = '',
  553. kind = 'Module',
  554. menu = '',
  555. abbr_hlgroup = '',
  556. word = 'this_thread',
  557. }
  558. local result = complete(' std::this|', completion_list)
  559. eq(7, result.server_start_boundary)
  560. local item = result.items[1]
  561. item.user_data = nil
  562. eq(expected, item)
  563. end)
  564. it('should search from start boundary to cursor position', function()
  565. local completion_list = {
  566. isIncomplete = false,
  567. items = {
  568. {
  569. filterText = 'this_thread',
  570. insertText = 'this_thread',
  571. insertTextFormat = 1,
  572. kind = 9,
  573. label = ' this_thread',
  574. score = 1.3205767869949,
  575. sortText = '4056f757this_thread',
  576. textEdit = {
  577. newText = 'this_thread',
  578. range = {
  579. start = { line = 0, character = 7 },
  580. ['end'] = { line = 0, character = 11 },
  581. },
  582. },
  583. },
  584. {
  585. filterText = 'no_match',
  586. insertText = 'notthis_thread',
  587. insertTextFormat = 1,
  588. kind = 9,
  589. label = ' notthis_thread',
  590. score = 1.3205767869949,
  591. sortText = '4056f757this_thread',
  592. textEdit = {
  593. newText = 'notthis_thread',
  594. range = {
  595. start = { line = 0, character = 7 },
  596. ['end'] = { line = 0, character = 11 },
  597. },
  598. },
  599. },
  600. },
  601. }
  602. local expected = {
  603. abbr = ' this_thread',
  604. dup = 1,
  605. empty = 1,
  606. icase = 1,
  607. info = '',
  608. kind = 'Module',
  609. menu = '',
  610. abbr_hlgroup = '',
  611. word = 'this_thread',
  612. }
  613. local result = complete(' std::this|is', completion_list)
  614. eq(1, #result.items)
  615. local item = result.items[1]
  616. item.user_data = nil
  617. eq(expected, item)
  618. end)
  619. it('uses defaults from itemDefaults', function()
  620. --- @type lsp.CompletionList
  621. local completion_list = {
  622. isIncomplete = false,
  623. itemDefaults = {
  624. editRange = {
  625. start = { line = 1, character = 1 },
  626. ['end'] = { line = 1, character = 4 },
  627. },
  628. insertTextFormat = 2,
  629. data = 'foobar',
  630. },
  631. items = {
  632. {
  633. label = 'hello',
  634. data = 'item-property-has-priority',
  635. textEditText = 'hello',
  636. },
  637. },
  638. }
  639. local result = complete('|', completion_list)
  640. eq(1, #result.items)
  641. local item = result.items[1].user_data.nvim.lsp.completion_item --- @type lsp.CompletionItem
  642. eq(2, item.insertTextFormat)
  643. eq('item-property-has-priority', item.data)
  644. eq({ line = 1, character = 1 }, item.textEdit.range.start)
  645. end)
  646. it(
  647. 'uses insertText as textEdit.newText if there are editRange defaults but no textEditText',
  648. function()
  649. --- @type lsp.CompletionList
  650. local completion_list = {
  651. isIncomplete = false,
  652. itemDefaults = {
  653. editRange = {
  654. start = { line = 1, character = 1 },
  655. ['end'] = { line = 1, character = 4 },
  656. },
  657. insertTextFormat = 2,
  658. data = 'foobar',
  659. },
  660. items = {
  661. {
  662. insertText = 'the-insertText',
  663. label = 'hello',
  664. data = 'item-property-has-priority',
  665. },
  666. },
  667. }
  668. local result = complete('|', completion_list)
  669. eq(1, #result.items)
  670. local text = result.items[1].user_data.nvim.lsp.completion_item.textEdit.newText
  671. eq('the-insertText', text)
  672. end
  673. )
  674. it(
  675. 'defaults to label as textEdit.newText if insertText or textEditText are not present',
  676. function()
  677. local completion_list = {
  678. isIncomplete = false,
  679. itemDefaults = {
  680. editRange = {
  681. start = { line = 1, character = 1 },
  682. ['end'] = { line = 1, character = 4 },
  683. },
  684. insertTextFormat = 2,
  685. data = 'foobar',
  686. },
  687. items = {
  688. {
  689. label = 'hello',
  690. data = 'item-property-has-priority',
  691. },
  692. },
  693. }
  694. local result = complete('|', completion_list)
  695. eq(1, #result.items)
  696. local text = result.items[1].user_data.nvim.lsp.completion_item.textEdit.newText
  697. eq('hello', text)
  698. end
  699. )
  700. end)
  701. --- @param name string
  702. --- @param completion_result lsp.CompletionList
  703. --- @return integer
  704. local function create_server(name, completion_result)
  705. return exec_lua(function()
  706. local server = _G._create_server({
  707. capabilities = {
  708. completionProvider = {
  709. triggerCharacters = { '.' },
  710. },
  711. },
  712. handlers = {
  713. ['textDocument/completion'] = function(_, _, callback)
  714. callback(nil, completion_result)
  715. end,
  716. },
  717. })
  718. local bufnr = vim.api.nvim_get_current_buf()
  719. vim.api.nvim_win_set_buf(0, bufnr)
  720. return vim.lsp.start({
  721. name = name,
  722. cmd = server.cmd,
  723. on_attach = function(client, bufnr0)
  724. vim.lsp.completion.enable(true, client.id, bufnr0, {
  725. convert = function(item)
  726. return { abbr = item.label:gsub('%b()', '') }
  727. end,
  728. })
  729. end,
  730. })
  731. end)
  732. end
  733. describe('vim.lsp.completion: protocol', function()
  734. before_each(function()
  735. clear()
  736. exec_lua(create_server_definition)
  737. exec_lua(function()
  738. _G.capture = {}
  739. --- @diagnostic disable-next-line:duplicate-set-field
  740. vim.fn.complete = function(col, matches)
  741. _G.capture.col = col
  742. _G.capture.matches = matches
  743. end
  744. end)
  745. end)
  746. after_each(clear)
  747. local function assert_matches(fn)
  748. retry(nil, nil, function()
  749. fn(exec_lua('return _G.capture.matches'))
  750. end)
  751. end
  752. --- @param pos [integer, integer]
  753. local function trigger_at_pos(pos)
  754. exec_lua(function()
  755. local win = vim.api.nvim_get_current_win()
  756. vim.api.nvim_win_set_cursor(win, pos)
  757. vim.lsp.completion.trigger()
  758. end)
  759. retry(nil, nil, function()
  760. neq(nil, exec_lua('return _G.capture.col'))
  761. end)
  762. end
  763. it('fetches completions and shows them using complete on trigger', function()
  764. create_server('dummy', {
  765. isIncomplete = false,
  766. items = {
  767. {
  768. label = 'hello',
  769. },
  770. {
  771. label = 'hercules',
  772. tags = { 1 }, -- 1 represents Deprecated tag
  773. },
  774. {
  775. label = 'hero',
  776. deprecated = true,
  777. },
  778. },
  779. })
  780. feed('ih')
  781. trigger_at_pos({ 1, 1 })
  782. assert_matches(function(matches)
  783. eq({
  784. {
  785. abbr = 'hello',
  786. dup = 1,
  787. empty = 1,
  788. icase = 1,
  789. info = '',
  790. kind = 'Unknown',
  791. menu = '',
  792. abbr_hlgroup = '',
  793. user_data = {
  794. nvim = {
  795. lsp = {
  796. client_id = 1,
  797. completion_item = {
  798. label = 'hello',
  799. },
  800. },
  801. },
  802. },
  803. word = 'hello',
  804. },
  805. {
  806. abbr = 'hercules',
  807. dup = 1,
  808. empty = 1,
  809. icase = 1,
  810. info = '',
  811. kind = 'Unknown',
  812. menu = '',
  813. abbr_hlgroup = 'DiagnosticDeprecated',
  814. user_data = {
  815. nvim = {
  816. lsp = {
  817. client_id = 1,
  818. completion_item = {
  819. label = 'hercules',
  820. tags = { 1 },
  821. },
  822. },
  823. },
  824. },
  825. word = 'hercules',
  826. },
  827. {
  828. abbr = 'hero',
  829. dup = 1,
  830. empty = 1,
  831. icase = 1,
  832. info = '',
  833. kind = 'Unknown',
  834. menu = '',
  835. abbr_hlgroup = 'DiagnosticDeprecated',
  836. user_data = {
  837. nvim = {
  838. lsp = {
  839. client_id = 1,
  840. completion_item = {
  841. label = 'hero',
  842. deprecated = true,
  843. },
  844. },
  845. },
  846. },
  847. word = 'hero',
  848. },
  849. }, matches)
  850. end)
  851. end)
  852. it('merges results from multiple clients', function()
  853. create_server('dummy1', {
  854. isIncomplete = false,
  855. items = {
  856. {
  857. label = 'hello',
  858. },
  859. },
  860. })
  861. create_server('dummy2', {
  862. isIncomplete = false,
  863. items = {
  864. {
  865. label = 'hallo',
  866. },
  867. },
  868. })
  869. feed('ih')
  870. trigger_at_pos({ 1, 1 })
  871. assert_matches(function(matches)
  872. eq(2, #matches)
  873. eq('hello', matches[1].word)
  874. eq('hallo', matches[2].word)
  875. end)
  876. end)
  877. it('executes commands', function()
  878. local completion_list = {
  879. isIncomplete = false,
  880. items = {
  881. {
  882. label = 'hello',
  883. command = {
  884. arguments = { '1', '0' },
  885. command = 'dummy',
  886. title = '',
  887. },
  888. },
  889. },
  890. }
  891. local client_id = create_server('dummy', completion_list)
  892. exec_lua(function()
  893. _G.called = false
  894. local client = assert(vim.lsp.get_client_by_id(client_id))
  895. client.commands.dummy = function()
  896. _G.called = true
  897. end
  898. end)
  899. feed('ih')
  900. trigger_at_pos({ 1, 1 })
  901. local item = completion_list.items[1]
  902. exec_lua(function()
  903. vim.v.completed_item = {
  904. user_data = {
  905. nvim = {
  906. lsp = {
  907. client_id = client_id,
  908. completion_item = item,
  909. },
  910. },
  911. },
  912. }
  913. end)
  914. feed('<C-x><C-o><C-y>')
  915. assert_matches(function(matches)
  916. eq(1, #matches)
  917. eq('hello', matches[1].word)
  918. eq(true, exec_lua('return _G.called'))
  919. end)
  920. end)
  921. it('enable(…,{convert=fn}) custom word/abbr format', function()
  922. create_server('dummy', {
  923. isIncomplete = false,
  924. items = {
  925. {
  926. label = 'foo(bar)',
  927. },
  928. },
  929. })
  930. feed('ifo')
  931. trigger_at_pos({ 1, 1 })
  932. assert_matches(function(matches)
  933. eq('foo', matches[1].abbr)
  934. end)
  935. end)
  936. end)
  937. describe('vim.lsp.completion: integration', function()
  938. before_each(function()
  939. clear()
  940. exec_lua(create_server_definition)
  941. exec_lua(function()
  942. vim.fn.complete = vim.schedule_wrap(vim.fn.complete)
  943. end)
  944. end)
  945. after_each(clear)
  946. it('puts cursor at the end of completed word', function()
  947. local completion_list = {
  948. isIncomplete = false,
  949. items = {
  950. {
  951. label = 'hello',
  952. insertText = '${1:hello} friends',
  953. insertTextFormat = 2,
  954. },
  955. },
  956. }
  957. exec_lua(function()
  958. vim.o.completeopt = 'menuone,noselect'
  959. end)
  960. create_server('dummy', completion_list)
  961. feed('i world<esc>0ih<c-x><c-o>')
  962. retry(nil, nil, function()
  963. eq(
  964. 1,
  965. exec_lua(function()
  966. return vim.fn.pumvisible()
  967. end)
  968. )
  969. end)
  970. feed('<C-n><C-y>')
  971. eq(
  972. { true, { 'hello friends world' } },
  973. exec_lua(function()
  974. return {
  975. vim.snippet.active({ direction = 1 }),
  976. vim.api.nvim_buf_get_lines(0, 0, -1, true),
  977. }
  978. end)
  979. )
  980. feed('<tab>')
  981. eq(
  982. #'hello friends',
  983. exec_lua(function()
  984. return vim.api.nvim_win_get_cursor(0)[2]
  985. end)
  986. )
  987. end)
  988. end)