@@ -390,7 +390,10 @@ MiniCompletion.completefunc_lsp = function(findstart, base)
390
390
-- End completion and wait for LSP callback to re-trigger this
391
391
return findstart == 1 and - 3 or {}
392
392
else
393
- if findstart == 1 then return H .get_completion_start (H .completion .lsp .result ) end
393
+ if findstart == 1 then
394
+ H .completion .start_pos = H .get_completion_start (H .completion .lsp .result )
395
+ return H .completion .start_pos [2 ]
396
+ end
394
397
395
398
local process_items , is_incomplete = H .get_config ().lsp_completion .process_items , false
396
399
process_items = process_items or MiniCompletion .default_process_items
@@ -479,6 +482,7 @@ H.completion = {
479
482
text_changed_id = 0 ,
480
483
timer = vim .loop .new_timer (),
481
484
lsp = { id = 0 , status = nil , result = nil , cancel_fun = nil },
485
+ start_pos = {},
482
486
}
483
487
484
488
-- Cache for completion item info
@@ -697,8 +701,16 @@ H.on_completedonepre = function()
697
701
-- visible and pressing keys first hides it with 'CompleteDonePre' event.
698
702
if H .completion .lsp .status == ' received' then return end
699
703
700
- -- Try to apply additional text edits
701
- H .apply_additional_text_edits ()
704
+ -- Do extra actions for LSP completion items
705
+ local lsp_data = H .table_get (vim .v .completed_item , { ' user_data' , ' nvim' , ' lsp' })
706
+ if lsp_data ~= nil then
707
+ -- Prefer resolved item over the one from 'textDocument/completion'
708
+ local resolved = (H .info .lsp .result or {})[lsp_data .client_id ]
709
+ local item = (resolved == nil or resolved .err ) and lsp_data .completion_item or resolved .result
710
+
711
+ -- Try to apply additional text edits
712
+ H .apply_additional_text_edits (item .additionalTextEdits , lsp_data .client_id )
713
+ end
702
714
703
715
-- Stop processes
704
716
MiniCompletion .stop ({ ' completion' , ' info' })
@@ -959,40 +971,30 @@ H.get_completion_word = function(item)
959
971
return H .table_get (item , { ' textEdit' , ' newText' }) or item .insertText or item .label or ' '
960
972
end
961
973
962
- H .apply_additional_text_edits = function ()
963
- -- Code originally.inspired by https://github.com/neovim/neovim/issues/12310
964
-
965
- -- Try to get `additionalTextEdits`. First from 'completionItem/resolve';
966
- -- then - from selected item. The reason for this is inconsistency in how
967
- -- servers provide `additionTextEdits`: on 'textDocument/completion' or
968
- -- 'completionItem/resolve'.
969
- local resolve_data = H .process_lsp_response (H .info .lsp .result , function (response , client_id )
970
- -- Return nested table because this will be a second argument of
971
- -- `vim.list_extend()` and the whole inner table is a target value here.
972
- return { { edits = response .additionalTextEdits , client_id = client_id } }
973
- end )
974
- local edits , client_id
975
- if # resolve_data >= 1 then
976
- edits , client_id = resolve_data [1 ].edits , resolve_data [1 ].client_id
977
- else
978
- local lsp_data = H .table_get (vim .v .completed_item , { ' user_data' , ' nvim' , ' lsp' }) or {}
979
- edits = H .table_get (lsp_data , { ' completion_item' , ' additionalTextEdits' })
980
- client_id = lsp_data .client_id
981
- end
982
-
974
+ H .apply_additional_text_edits = function (edits , client_id )
975
+ -- Code originally inspired by https://github.com/neovim/neovim/issues/12310
983
976
if edits == nil then return end
984
977
client_id = client_id or 0
985
978
986
- -- Use extmark to track relevant cursor position after text edits
979
+ -- Prepare extmarks to track relevant positions after text edits
980
+ local start_pos = H .completion .start_pos
981
+ local start_extmark_id = vim .api .nvim_buf_set_extmark (0 , H .ns_id , start_pos [1 ] - 1 , start_pos [2 ], {})
982
+
987
983
local cur_pos = vim .api .nvim_win_get_cursor (0 )
988
- local extmark_id = vim .api .nvim_buf_set_extmark (0 , H .ns_id , cur_pos [1 ] - 1 , cur_pos [2 ], {})
984
+ local cursor_extmark_id = vim .api .nvim_buf_set_extmark (0 , H .ns_id , cur_pos [1 ] - 1 , cur_pos [2 ], {})
989
985
986
+ -- Do text edits
990
987
local offset_encoding = vim .lsp .get_client_by_id (client_id ).offset_encoding
991
988
vim .lsp .util .apply_text_edits (edits , vim .api .nvim_get_current_buf (), offset_encoding )
992
989
993
- local extmark_data = vim .api .nvim_buf_get_extmark_by_id (0 , H .ns_id , extmark_id , {})
994
- pcall (vim .api .nvim_buf_del_extmark , 0 , H .ns_id , extmark_id )
995
- pcall (vim .api .nvim_win_set_cursor , 0 , { extmark_data [1 ] + 1 , extmark_data [2 ] })
990
+ -- Restore relevant positions
991
+ local start_data = vim .api .nvim_buf_get_extmark_by_id (0 , H .ns_id , start_extmark_id , {})
992
+ H .completion .start_pos = { start_data [1 ] + 1 , start_data [2 ] }
993
+ pcall (vim .api .nvim_buf_del_extmark , 0 , H .ns_id , start_extmark_id )
994
+
995
+ local cursor_data = vim .api .nvim_buf_get_extmark_by_id (0 , H .ns_id , cursor_extmark_id , {})
996
+ pcall (vim .api .nvim_win_set_cursor , 0 , { cursor_data [1 ] + 1 , cursor_data [2 ] })
997
+ pcall (vim .api .nvim_buf_del_extmark , 0 , H .ns_id , cursor_extmark_id )
996
998
end
997
999
998
1000
-- Completion item info -------------------------------------------------------
@@ -1038,29 +1040,23 @@ end
1038
1040
1039
1041
H .info_window_lines = function (info_id )
1040
1042
local completed_item = H .table_get (H .info , { ' event' , ' completed_item' }) or {}
1043
+ local lsp_data = H .table_get (completed_item , { ' user_data' , ' nvim' , ' lsp' })
1044
+ local info = completed_item .info or ' '
1041
1045
1042
1046
-- If popup is not from LSP, try using 'info' field of completion item
1043
- if H .completion .source ~= ' lsp' then
1044
- local text = completed_item .info or ' '
1045
- return (not H .is_whitespace (text )) and vim .split (text , ' \n ' ) or nil
1046
- end
1047
+ if lsp_data == nil then return vim .split (info , ' \n ' ) end
1047
1048
1048
- -- Try to get documentation from LSP's latest completion result
1049
+ -- Try to get documentation from LSP's latest resolved info
1049
1050
if H .info .lsp .status == ' received' then
1050
- local lines = H .process_lsp_response (H .info .lsp .result , H .normalize_item_doc )
1051
+ local lines = H .process_lsp_response (H .info .lsp .result , function ( x ) return H .normalize_item_doc ( x , info ) end )
1051
1052
H .info .lsp .status = ' done'
1052
1053
return lines
1053
1054
end
1054
1055
1055
1056
-- If server doesn't support resolving completion item, reuse first response
1056
- local lsp_data = H .table_get (completed_item , { ' user_data' , ' nvim' , ' lsp' })
1057
- -- NOTE: If there is no LSP's completion item, then there is no point to
1058
- -- proceed as it should serve as parameters to LSP request
1059
- if lsp_data .completion_item == nil then return end
1060
-
1061
1057
local client = vim .lsp .get_client_by_id (lsp_data .client_id ) or {}
1062
1058
local can_resolve = H .table_get (client .server_capabilities , { ' completionProvider' , ' resolveProvider' })
1063
- if not can_resolve then return H .normalize_item_doc (lsp_data .completion_item ) end
1059
+ if not can_resolve then return H .normalize_item_doc (lsp_data .completion_item , info ) end
1064
1060
1065
1061
-- Finally, request to resolve current completion to add more documentation
1066
1062
local bufnr = vim .api .nvim_get_current_buf ()
@@ -1405,17 +1401,16 @@ end
1405
1401
H .pumvisible = function () return vim .fn .pumvisible () > 0 end
1406
1402
1407
1403
H .get_completion_start = function (lsp_result )
1408
- local pos = vim .api .nvim_win_get_cursor (0 )
1409
-
1410
1404
-- Prefer completion start from LSP response(s)
1411
1405
for _ , response_data in pairs (lsp_result or {}) do
1412
- local server_start = H .get_completion_start_server (response_data , pos [ 1 ] - 1 )
1406
+ local server_start = H .get_completion_start_server (response_data )
1413
1407
if server_start ~= nil then return server_start end
1414
1408
end
1415
1409
1416
1410
-- Fall back to start position of latest keyword
1411
+ local pos = vim .api .nvim_win_get_cursor (0 )
1417
1412
local line = vim .api .nvim_get_current_line ()
1418
- return vim .fn .match (line :sub (1 , pos [2 ]), ' \\ k*$' )
1413
+ return { pos [ 1 ], vim .fn .match (line :sub (1 , pos [2 ]), ' \\ k*$' ) }
1419
1414
end
1420
1415
1421
1416
H .get_completion_start_server = function (response_data , line_num )
@@ -1426,7 +1421,7 @@ H.get_completion_start_server = function(response_data, line_num)
1426
1421
-- NOTE: As per LSP spec, `textEdit` can be either `TextEdit` or `InsertReplaceEdit`
1427
1422
local range = type (item .textEdit .range ) == ' table' and item .textEdit .range or item .textEdit .insert
1428
1423
-- NOTE: Return immediately, ignoring possibly several conflicting starts
1429
- return range .start .character
1424
+ return { range .start .line + 1 , range . start . character }
1430
1425
end
1431
1426
end
1432
1427
end
@@ -1498,17 +1493,19 @@ H.map = function(mode, lhs, rhs, opts)
1498
1493
vim .keymap .set (mode , lhs , rhs , opts )
1499
1494
end
1500
1495
1501
- H .normalize_item_doc = function (completion_item )
1496
+ H .normalize_item_doc = function (completion_item , fallback_info )
1502
1497
local detail , doc = completion_item .detail , completion_item .documentation
1498
+ -- Fall back to explicit info only of there is no data in completion item
1499
+ -- Assume that explicit info is a code that needs highlighting
1500
+ detail = (detail == nil and doc == nil ) and fallback_info or detail
1503
1501
if detail == nil and doc == nil then return {} end
1504
1502
1505
1503
-- Extract string content. Treat markdown and plain kinds the same.
1506
1504
-- Show both `detail` and `documentation` if the first provides new info.
1507
1505
detail , doc = detail or ' ' , (type (doc ) == ' table' and doc .value or doc ) or ' '
1508
- detail = (H .is_whitespace (detail ) or doc :find (detail , 1 , true ) ~= nil ) and ' '
1509
- -- Wrap details in language's code block to (usually) improve highlighting
1510
- -- This approach seems to work in 'hrsh7th/nvim-cmp'
1511
- or string.format (' ```%s\n %s\n ```\n ' , vim .bo .filetype :match (' ^[^%.]*' ), vim .trim (detail ))
1506
+ -- Wrap details in language's code block to (usually) improve highlighting
1507
+ -- This approach seems to work in 'hrsh7th/nvim-cmp'
1508
+ detail = (H .is_whitespace (detail ) or doc :find (detail , 1 , true ) ~= nil ) and ' ' or (H .wrap_in_codeblock (detail ) .. ' \n ' )
1512
1509
local text = detail .. doc
1513
1510
1514
1511
-- Ensure consistent line separators
@@ -1520,9 +1517,12 @@ H.normalize_item_doc = function(completion_item)
1520
1517
-- Remove padding around code blocks as they are concealed and appear empty
1521
1518
text = text :gsub (' \n *(\n ```%S+\n )' , ' %1' ):gsub (' (\n ```\n ?)\n *' , ' %1' )
1522
1519
1520
+ if text == ' ' and fallback_info ~= ' ' then text = H .wrap_in_codeblock (fallback_info ) end
1523
1521
return text == ' ' and {} or vim .split (text , ' \n ' )
1524
1522
end
1525
1523
1524
+ H .wrap_in_codeblock = function (x ) return string.format (' ```%s\n %s\n ```' , vim .bo .filetype :match (' ^[^%.]*' ), vim .trim (x )) end
1525
+
1526
1526
-- TODO: Remove after compatibility with Neovim=0.9 is dropped
1527
1527
H .islist = vim .fn .has (' nvim-0.10' ) == 1 and vim .islist or vim .tbl_islist
1528
1528
0 commit comments