util.lua 66 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933
  1. local protocol = require 'vim.lsp.protocol'
  2. local snippet = require 'vim.lsp._snippet'
  3. local vim = vim
  4. local validate = vim.validate
  5. local api = vim.api
  6. local list_extend = vim.list_extend
  7. local highlight = require 'vim.highlight'
  8. local uv = vim.loop
  9. local npcall = vim.F.npcall
  10. local split = vim.split
  11. local M = {}
  12. local default_border = {
  13. {"", "NormalFloat"},
  14. {"", "NormalFloat"},
  15. {"", "NormalFloat"},
  16. {" ", "NormalFloat"},
  17. {"", "NormalFloat"},
  18. {"", "NormalFloat"},
  19. {"", "NormalFloat"},
  20. {" ", "NormalFloat"},
  21. }
  22. ---@private
  23. --- Check the border given by opts or the default border for the additional
  24. --- size it adds to a float.
  25. ---@param opts (table, optional) options for the floating window
  26. --- - border (string or table) the border
  27. ---@returns (table) size of border in the form of { height = height, width = width }
  28. local function get_border_size(opts)
  29. local border = opts and opts.border or default_border
  30. local height = 0
  31. local width = 0
  32. if type(border) == 'string' then
  33. local border_size = {none = {0, 0}, single = {2, 2}, double = {2, 2}, rounded = {2, 2}, solid = {2, 2}, shadow = {1, 1}}
  34. if border_size[border] == nil then
  35. error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border)))
  36. end
  37. height, width = unpack(border_size[border])
  38. else
  39. if 8 % #border ~= 0 then
  40. error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border)))
  41. end
  42. ---@private
  43. local function border_width(id)
  44. id = (id - 1) % #border + 1
  45. if type(border[id]) == "table" then
  46. -- border specified as a table of <character, highlight group>
  47. return vim.fn.strdisplaywidth(border[id][1])
  48. elseif type(border[id]) == "string" then
  49. -- border specified as a list of border characters
  50. return vim.fn.strdisplaywidth(border[id])
  51. end
  52. error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border)))
  53. end
  54. ---@private
  55. local function border_height(id)
  56. id = (id - 1) % #border + 1
  57. if type(border[id]) == "table" then
  58. -- border specified as a table of <character, highlight group>
  59. return #border[id][1] > 0 and 1 or 0
  60. elseif type(border[id]) == "string" then
  61. -- border specified as a list of border characters
  62. return #border[id] > 0 and 1 or 0
  63. end
  64. error(string.format("invalid floating preview border: %s. :help vim.api.nvim_open_win()", vim.inspect(border)))
  65. end
  66. height = height + border_height(2) -- top
  67. height = height + border_height(6) -- bottom
  68. width = width + border_width(4) -- right
  69. width = width + border_width(8) -- left
  70. end
  71. return { height = height, width = width }
  72. end
  73. ---@private
  74. local function split_lines(value)
  75. return split(value, '\n', true)
  76. end
  77. --- Convert byte index to `encoding` index.
  78. --- Convenience wrapper around vim.str_utfindex
  79. ---@param line string line to be indexed
  80. ---@param index number byte index (utf-8), or `nil` for length
  81. ---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16
  82. ---@return number `encoding` index of `index` in `line`
  83. function M._str_utfindex_enc(line, index, encoding)
  84. if not encoding then encoding = 'utf-16' end
  85. if encoding == 'utf-8' then
  86. if index then return index else return #line end
  87. elseif encoding == 'utf-16' then
  88. local _, col16 = vim.str_utfindex(line, index)
  89. return col16
  90. elseif encoding == 'utf-32' then
  91. local col32, _ = vim.str_utfindex(line, index)
  92. return col32
  93. else
  94. error("Invalid encoding: " .. vim.inspect(encoding))
  95. end
  96. end
  97. --- Convert UTF index to `encoding` index.
  98. --- Convenience wrapper around vim.str_byteindex
  99. ---Alternative to vim.str_byteindex that takes an encoding.
  100. ---@param line string line to be indexed
  101. ---@param index number UTF index
  102. ---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16
  103. ---@return number byte (utf-8) index of `encoding` index `index` in `line`
  104. function M._str_byteindex_enc(line, index, encoding)
  105. if not encoding then encoding = 'utf-16' end
  106. if encoding == 'utf-8' then
  107. if index then return index else return #line end
  108. elseif encoding == 'utf-16' then
  109. return vim.str_byteindex(line, index, true)
  110. elseif encoding == 'utf-32' then
  111. return vim.str_byteindex(line, index)
  112. else
  113. error("Invalid encoding: " .. vim.inspect(encoding))
  114. end
  115. end
  116. local _str_utfindex_enc = M._str_utfindex_enc
  117. local _str_byteindex_enc = M._str_byteindex_enc
  118. --- Replaces text in a range with new text.
  119. ---
  120. --- CAUTION: Changes in-place!
  121. ---
  122. ---@param lines (table) Original list of strings
  123. ---@param A (table) Start position; a 2-tuple of {line, col} numbers
  124. ---@param B (table) End position; a 2-tuple of {line, col} numbers
  125. ---@param new_lines A list of strings to replace the original
  126. ---@returns (table) The modified {lines} object
  127. function M.set_lines(lines, A, B, new_lines)
  128. -- 0-indexing to 1-indexing
  129. local i_0 = A[1] + 1
  130. -- If it extends past the end, truncate it to the end. This is because the
  131. -- way the LSP describes the range including the last newline is by
  132. -- specifying a line number after what we would call the last line.
  133. local i_n = math.min(B[1] + 1, #lines)
  134. if not (i_0 >= 1 and i_0 <= #lines + 1 and i_n >= 1 and i_n <= #lines) then
  135. error("Invalid range: "..vim.inspect{A = A; B = B; #lines, new_lines})
  136. end
  137. local prefix = ""
  138. local suffix = lines[i_n]:sub(B[2]+1)
  139. if A[2] > 0 then
  140. prefix = lines[i_0]:sub(1, A[2])
  141. end
  142. local n = i_n - i_0 + 1
  143. if n ~= #new_lines then
  144. for _ = 1, n - #new_lines do table.remove(lines, i_0) end
  145. for _ = 1, #new_lines - n do table.insert(lines, i_0, '') end
  146. end
  147. for i = 1, #new_lines do
  148. lines[i - 1 + i_0] = new_lines[i]
  149. end
  150. if #suffix > 0 then
  151. local i = i_0 + #new_lines - 1
  152. lines[i] = lines[i]..suffix
  153. end
  154. if #prefix > 0 then
  155. lines[i_0] = prefix..lines[i_0]
  156. end
  157. return lines
  158. end
  159. ---@private
  160. local function sort_by_key(fn)
  161. return function(a,b)
  162. local ka, kb = fn(a), fn(b)
  163. assert(#ka == #kb)
  164. for i = 1, #ka do
  165. if ka[i] ~= kb[i] then
  166. return ka[i] < kb[i]
  167. end
  168. end
  169. -- every value must have been equal here, which means it's not less than.
  170. return false
  171. end
  172. end
  173. ---@private
  174. --- Gets the zero-indexed lines from the given buffer.
  175. --- Works on unloaded buffers by reading the file using libuv to bypass buf reading events.
  176. --- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI.
  177. ---
  178. ---@param bufnr number bufnr to get the lines from
  179. ---@param rows number[] zero-indexed line numbers
  180. ---@return table<number string> a table mapping rows to lines
  181. local function get_lines(bufnr, rows)
  182. rows = type(rows) == "table" and rows or { rows }
  183. -- This is needed for bufload and bufloaded
  184. if bufnr == 0 then
  185. bufnr = vim.api.nvim_get_current_buf()
  186. end
  187. ---@private
  188. local function buf_lines()
  189. local lines = {}
  190. for _, row in pairs(rows) do
  191. lines[row] = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { "" })[1]
  192. end
  193. return lines
  194. end
  195. local uri = vim.uri_from_bufnr(bufnr)
  196. -- load the buffer if this is not a file uri
  197. -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds.
  198. if uri:sub(1, 4) ~= "file" then
  199. vim.fn.bufload(bufnr)
  200. return buf_lines()
  201. end
  202. -- use loaded buffers if available
  203. if vim.fn.bufloaded(bufnr) == 1 then
  204. return buf_lines()
  205. end
  206. local filename = api.nvim_buf_get_name(bufnr)
  207. -- get the data from the file
  208. local fd = uv.fs_open(filename, "r", 438)
  209. if not fd then return "" end
  210. local stat = uv.fs_fstat(fd)
  211. local data = uv.fs_read(fd, stat.size, 0)
  212. uv.fs_close(fd)
  213. local lines = {} -- rows we need to retrieve
  214. local need = 0 -- keep track of how many unique rows we need
  215. for _, row in pairs(rows) do
  216. if not lines[row] then
  217. need = need + 1
  218. end
  219. lines[row] = true
  220. end
  221. local found = 0
  222. local lnum = 0
  223. for line in string.gmatch(data, "([^\n]*)\n?") do
  224. if lines[lnum] == true then
  225. lines[lnum] = line
  226. found = found + 1
  227. if found == need then break end
  228. end
  229. lnum = lnum + 1
  230. end
  231. -- change any lines we didn't find to the empty string
  232. for i, line in pairs(lines) do
  233. if line == true then
  234. lines[i] = ""
  235. end
  236. end
  237. return lines
  238. end
  239. ---@private
  240. --- Gets the zero-indexed line from the given buffer.
  241. --- Works on unloaded buffers by reading the file using libuv to bypass buf reading events.
  242. --- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI.
  243. ---
  244. ---@param bufnr number
  245. ---@param row number zero-indexed line number
  246. ---@return string the line at row in filename
  247. local function get_line(bufnr, row)
  248. return get_lines(bufnr, { row })[row]
  249. end
  250. ---@private
  251. --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
  252. --- Returns a zero-indexed column, since set_lines() does the conversion to
  253. ---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to utf-16
  254. --- 1-indexed
  255. local function get_line_byte_from_position(bufnr, position, offset_encoding)
  256. -- LSP's line and characters are 0-indexed
  257. -- Vim's line and columns are 1-indexed
  258. local col = position.character
  259. -- When on the first character, we can ignore the difference between byte and
  260. -- character
  261. if col > 0 then
  262. local line = get_line(bufnr, position.line)
  263. local ok, result
  264. ok, result = pcall(_str_byteindex_enc, line, col, offset_encoding)
  265. if ok then
  266. return result
  267. end
  268. return math.min(#line, col)
  269. end
  270. return col
  271. end
  272. --- Process and return progress reports from lsp server
  273. ---@private
  274. function M.get_progress_messages()
  275. local new_messages = {}
  276. local msg_remove = {}
  277. local progress_remove = {}
  278. for _, client in ipairs(vim.lsp.get_active_clients()) do
  279. local messages = client.messages
  280. local data = messages
  281. for token, ctx in pairs(data.progress) do
  282. local new_report = {
  283. name = data.name,
  284. title = ctx.title or "empty title",
  285. message = ctx.message,
  286. percentage = ctx.percentage,
  287. done = ctx.done,
  288. progress = true,
  289. }
  290. table.insert(new_messages, new_report)
  291. if ctx.done then
  292. table.insert(progress_remove, {client = client, token = token})
  293. end
  294. end
  295. for i, msg in ipairs(data.messages) do
  296. if msg.show_once then
  297. msg.shown = msg.shown + 1
  298. if msg.shown > 1 then
  299. table.insert(msg_remove, {client = client, idx = i})
  300. end
  301. end
  302. table.insert(new_messages, {name = data.name, content = msg.content})
  303. end
  304. if next(data.status) ~= nil then
  305. table.insert(new_messages, {
  306. name = data.name,
  307. content = data.status.content,
  308. uri = data.status.uri,
  309. status = true
  310. })
  311. end
  312. for _, item in ipairs(msg_remove) do
  313. table.remove(client.messages, item.idx)
  314. end
  315. end
  316. for _, item in ipairs(progress_remove) do
  317. item.client.messages.progress[item.token] = nil
  318. end
  319. return new_messages
  320. end
  321. --- Applies a list of text edits to a buffer.
  322. ---@param text_edits table list of `TextEdit` objects
  323. ---@param bufnr number Buffer id
  324. ---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to encoding of first client of `bufnr`
  325. ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
  326. function M.apply_text_edits(text_edits, bufnr, offset_encoding)
  327. validate {
  328. text_edits = { text_edits, 't', false };
  329. bufnr = { bufnr, 'number', false };
  330. offset_encoding = { offset_encoding, 'string', true };
  331. }
  332. offset_encoding = offset_encoding or M._get_offset_encoding(bufnr)
  333. if not next(text_edits) then return end
  334. if not api.nvim_buf_is_loaded(bufnr) then
  335. vim.fn.bufload(bufnr)
  336. end
  337. api.nvim_buf_set_option(bufnr, 'buflisted', true)
  338. -- Fix reversed range and indexing each text_edits
  339. local index = 0
  340. text_edits = vim.tbl_map(function(text_edit)
  341. index = index + 1
  342. text_edit._index = index
  343. if text_edit.range.start.line > text_edit.range['end'].line or text_edit.range.start.line == text_edit.range['end'].line and text_edit.range.start.character > text_edit.range['end'].character then
  344. local start = text_edit.range.start
  345. text_edit.range.start = text_edit.range['end']
  346. text_edit.range['end'] = start
  347. end
  348. return text_edit
  349. end, text_edits)
  350. -- Sort text_edits
  351. table.sort(text_edits, function(a, b)
  352. if a.range.start.line ~= b.range.start.line then
  353. return a.range.start.line > b.range.start.line
  354. end
  355. if a.range.start.character ~= b.range.start.character then
  356. return a.range.start.character > b.range.start.character
  357. end
  358. if a._index ~= b._index then
  359. return a._index > b._index
  360. end
  361. end)
  362. -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here.
  363. local has_eol_text_edit = false
  364. local max = vim.api.nvim_buf_line_count(bufnr)
  365. local len = _str_utfindex_enc(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '', nil, offset_encoding)
  366. text_edits = vim.tbl_map(function(text_edit)
  367. if max <= text_edit.range.start.line then
  368. text_edit.range.start.line = max - 1
  369. text_edit.range.start.character = len
  370. text_edit.newText = '\n' .. text_edit.newText
  371. has_eol_text_edit = true
  372. end
  373. if max <= text_edit.range['end'].line then
  374. text_edit.range['end'].line = max - 1
  375. text_edit.range['end'].character = len
  376. has_eol_text_edit = true
  377. end
  378. return text_edit
  379. end, text_edits)
  380. -- Some LSP servers are depending on the VSCode behavior.
  381. -- The VSCode will re-locate the cursor position after applying TextEdit so we also do it.
  382. local is_current_buf = vim.api.nvim_get_current_buf() == bufnr
  383. local cursor = (function()
  384. if not is_current_buf then
  385. return {
  386. row = -1,
  387. col = -1,
  388. }
  389. end
  390. local cursor = vim.api.nvim_win_get_cursor(0)
  391. return {
  392. row = cursor[1] - 1,
  393. col = cursor[2],
  394. }
  395. end)()
  396. -- Apply text edits.
  397. local is_cursor_fixed = false
  398. for _, text_edit in ipairs(text_edits) do
  399. local e = {
  400. start_row = text_edit.range.start.line,
  401. start_col = get_line_byte_from_position(bufnr, text_edit.range.start),
  402. end_row = text_edit.range['end'].line,
  403. end_col = get_line_byte_from_position(bufnr, text_edit.range['end']),
  404. text = vim.split(text_edit.newText, '\n', true),
  405. }
  406. vim.api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text)
  407. local row_count = (e.end_row - e.start_row) + 1
  408. if e.end_row < cursor.row then
  409. cursor.row = cursor.row + (#e.text - row_count)
  410. is_cursor_fixed = true
  411. elseif e.end_row == cursor.row and e.end_col <= cursor.col then
  412. cursor.row = cursor.row + (#e.text - row_count)
  413. cursor.col = #e.text[#e.text] + (cursor.col - e.end_col)
  414. if #e.text == 1 then
  415. cursor.col = cursor.col + e.start_col
  416. end
  417. is_cursor_fixed = true
  418. end
  419. end
  420. if is_cursor_fixed then
  421. local is_valid_cursor = true
  422. is_valid_cursor = is_valid_cursor and cursor.row < vim.api.nvim_buf_line_count(bufnr)
  423. is_valid_cursor = is_valid_cursor and cursor.col <= #(vim.api.nvim_buf_get_lines(bufnr, cursor.row, cursor.row + 1, false)[1] or '')
  424. if is_valid_cursor then
  425. vim.api.nvim_win_set_cursor(0, { cursor.row + 1, cursor.col })
  426. end
  427. end
  428. -- Remove final line if needed
  429. local fix_eol = has_eol_text_edit
  430. fix_eol = fix_eol and api.nvim_buf_get_option(bufnr, 'fixeol')
  431. fix_eol = fix_eol and (vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '') == ''
  432. if fix_eol then
  433. vim.api.nvim_buf_set_lines(bufnr, -2, -1, false, {})
  434. end
  435. end
  436. -- local valid_windows_path_characters = "[^<>:\"/\\|?*]"
  437. -- local valid_unix_path_characters = "[^/]"
  438. -- https://github.com/davidm/lua-glob-pattern
  439. -- https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
  440. -- function M.glob_to_regex(glob)
  441. -- end
  442. --- Can be used to extract the completion items from a
  443. --- `textDocument/completion` request, which may return one of
  444. --- `CompletionItem[]`, `CompletionList` or null.
  445. ---@param result (table) The result of a `textDocument/completion` request
  446. ---@returns (table) List of completion items
  447. ---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
  448. function M.extract_completion_items(result)
  449. if type(result) == 'table' and result.items then
  450. -- result is a `CompletionList`
  451. return result.items
  452. elseif result ~= nil then
  453. -- result is `CompletionItem[]`
  454. return result
  455. else
  456. -- result is `null`
  457. return {}
  458. end
  459. end
  460. --- Applies a `TextDocumentEdit`, which is a list of changes to a single
  461. --- document.
  462. ---
  463. ---@param text_document_edit table: a `TextDocumentEdit` object
  464. ---@param index number: Optional index of the edit, if from a list of edits (or nil, if not from a list)
  465. ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit
  466. function M.apply_text_document_edit(text_document_edit, index)
  467. local text_document = text_document_edit.textDocument
  468. local bufnr = vim.uri_to_bufnr(text_document.uri)
  469. -- For lists of text document edits,
  470. -- do not check the version after the first edit.
  471. local should_check_version = true
  472. if index and index > 1 then
  473. should_check_version = false
  474. end
  475. -- `VersionedTextDocumentIdentifier`s version may be null
  476. -- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier
  477. if should_check_version and (text_document.version
  478. and text_document.version > 0
  479. and M.buf_versions[bufnr]
  480. and M.buf_versions[bufnr] > text_document.version) then
  481. print("Buffer ", text_document.uri, " newer than edits.")
  482. return
  483. end
  484. M.apply_text_edits(text_document_edit.edits, bufnr)
  485. end
  486. --- Parses snippets in a completion entry.
  487. ---
  488. ---@param input string unparsed snippet
  489. ---@returns string parsed snippet
  490. function M.parse_snippet(input)
  491. local ok, parsed = pcall(function()
  492. return tostring(snippet.parse(input))
  493. end)
  494. if not ok then
  495. return input
  496. end
  497. return parsed
  498. end
  499. ---@private
  500. --- Sorts by CompletionItem.sortText.
  501. ---
  502. --see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
  503. local function sort_completion_items(items)
  504. table.sort(items, function(a, b)
  505. return (a.sortText or a.label) < (b.sortText or b.label)
  506. end)
  507. end
  508. ---@private
  509. --- Returns text that should be inserted when selecting completion item. The
  510. --- precedence is as follows: textEdit.newText > insertText > label
  511. --see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
  512. local function get_completion_word(item)
  513. if item.textEdit ~= nil and item.textEdit.newText ~= nil and item.textEdit.newText ~= "" then
  514. local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
  515. if insert_text_format == "PlainText" or insert_text_format == nil then
  516. return item.textEdit.newText
  517. else
  518. return M.parse_snippet(item.textEdit.newText)
  519. end
  520. elseif item.insertText ~= nil and item.insertText ~= "" then
  521. local insert_text_format = protocol.InsertTextFormat[item.insertTextFormat]
  522. if insert_text_format == "PlainText" or insert_text_format == nil then
  523. return item.insertText
  524. else
  525. return M.parse_snippet(item.insertText)
  526. end
  527. end
  528. return item.label
  529. end
  530. ---@private
  531. --- Some language servers return complementary candidates whose prefixes do not
  532. --- match are also returned. So we exclude completion candidates whose prefix
  533. --- does not match.
  534. local function remove_unmatch_completion_items(items, prefix)
  535. return vim.tbl_filter(function(item)
  536. local word = get_completion_word(item)
  537. return vim.startswith(word, prefix)
  538. end, items)
  539. end
  540. --- According to LSP spec, if the client set `completionItemKind.valueSet`,
  541. --- the client must handle it properly even if it receives a value outside the
  542. --- specification.
  543. ---
  544. ---@param completion_item_kind (`vim.lsp.protocol.completionItemKind`)
  545. ---@returns (`vim.lsp.protocol.completionItemKind`)
  546. ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
  547. function M._get_completion_item_kind_name(completion_item_kind)
  548. return protocol.CompletionItemKind[completion_item_kind] or "Unknown"
  549. end
  550. --- Turns the result of a `textDocument/completion` request into vim-compatible
  551. --- |complete-items|.
  552. ---
  553. ---@param result The result of a `textDocument/completion` call, e.g. from
  554. ---|vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`,
  555. --- `CompletionList` or `null`
  556. ---@param prefix (string) the prefix to filter the completion items
  557. ---@returns { matches = complete-items table, incomplete = bool }
  558. ---@see |complete-items|
  559. function M.text_document_completion_list_to_complete_items(result, prefix)
  560. local items = M.extract_completion_items(result)
  561. if vim.tbl_isempty(items) then
  562. return {}
  563. end
  564. items = remove_unmatch_completion_items(items, prefix)
  565. sort_completion_items(items)
  566. local matches = {}
  567. for _, completion_item in ipairs(items) do
  568. local info = ' '
  569. local documentation = completion_item.documentation
  570. if documentation then
  571. if type(documentation) == 'string' and documentation ~= '' then
  572. info = documentation
  573. elseif type(documentation) == 'table' and type(documentation.value) == 'string' then
  574. info = documentation.value
  575. -- else
  576. -- TODO(ashkan) Validation handling here?
  577. end
  578. end
  579. local word = get_completion_word(completion_item)
  580. table.insert(matches, {
  581. word = word,
  582. abbr = completion_item.label,
  583. kind = M._get_completion_item_kind_name(completion_item.kind),
  584. menu = completion_item.detail or '',
  585. info = info,
  586. icase = 1,
  587. dup = 1,
  588. empty = 1,
  589. user_data = {
  590. nvim = {
  591. lsp = {
  592. completion_item = completion_item
  593. }
  594. }
  595. },
  596. })
  597. end
  598. return matches
  599. end
  600. --- Rename old_fname to new_fname
  601. ---
  602. ---@param opts (table)
  603. -- overwrite? bool
  604. -- ignoreIfExists? bool
  605. function M.rename(old_fname, new_fname, opts)
  606. opts = opts or {}
  607. local target_exists = vim.loop.fs_stat(new_fname) ~= nil
  608. if target_exists and not opts.overwrite or opts.ignoreIfExists then
  609. vim.notify('Rename target already exists. Skipping rename.')
  610. return
  611. end
  612. local oldbuf = vim.fn.bufadd(old_fname)
  613. vim.fn.bufload(oldbuf)
  614. -- The there may be pending changes in the buffer
  615. api.nvim_buf_call(oldbuf, function()
  616. vim.cmd('w!')
  617. end)
  618. local ok, err = os.rename(old_fname, new_fname)
  619. assert(ok, err)
  620. local newbuf = vim.fn.bufadd(new_fname)
  621. for _, win in pairs(api.nvim_list_wins()) do
  622. if api.nvim_win_get_buf(win) == oldbuf then
  623. api.nvim_win_set_buf(win, newbuf)
  624. end
  625. end
  626. api.nvim_buf_delete(oldbuf, { force = true })
  627. end
  628. ---@private
  629. local function create_file(change)
  630. local opts = change.options or {}
  631. -- from spec: Overwrite wins over `ignoreIfExists`
  632. local fname = vim.uri_to_fname(change.uri)
  633. if not opts.ignoreIfExists or opts.overwrite then
  634. local file = io.open(fname, 'w')
  635. file:close()
  636. end
  637. vim.fn.bufadd(fname)
  638. end
  639. ---@private
  640. local function delete_file(change)
  641. local opts = change.options or {}
  642. local fname = vim.uri_to_fname(change.uri)
  643. local stat = vim.loop.fs_stat(fname)
  644. if opts.ignoreIfNotExists and not stat then
  645. return
  646. end
  647. assert(stat, "Cannot delete not existing file or folder " .. fname)
  648. local flags
  649. if stat and stat.type == 'directory' then
  650. flags = opts.recursive and 'rf' or 'd'
  651. else
  652. flags = ''
  653. end
  654. local bufnr = vim.fn.bufadd(fname)
  655. local result = tonumber(vim.fn.delete(fname, flags))
  656. assert(result == 0, 'Could not delete file: ' .. fname .. ', stat: ' .. vim.inspect(stat))
  657. api.nvim_buf_delete(bufnr, { force = true })
  658. end
  659. --- Applies a `WorkspaceEdit`.
  660. ---
  661. ---@param workspace_edit (table) `WorkspaceEdit`
  662. --see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
  663. function M.apply_workspace_edit(workspace_edit)
  664. if workspace_edit.documentChanges then
  665. for idx, change in ipairs(workspace_edit.documentChanges) do
  666. if change.kind == "rename" then
  667. M.rename(
  668. vim.uri_to_fname(change.oldUri),
  669. vim.uri_to_fname(change.newUri),
  670. change.options
  671. )
  672. elseif change.kind == 'create' then
  673. create_file(change)
  674. elseif change.kind == 'delete' then
  675. delete_file(change)
  676. elseif change.kind then
  677. error(string.format("Unsupported change: %q", vim.inspect(change)))
  678. else
  679. M.apply_text_document_edit(change, idx)
  680. end
  681. end
  682. return
  683. end
  684. local all_changes = workspace_edit.changes
  685. if not (all_changes and not vim.tbl_isempty(all_changes)) then
  686. return
  687. end
  688. for uri, changes in pairs(all_changes) do
  689. local bufnr = vim.uri_to_bufnr(uri)
  690. M.apply_text_edits(changes, bufnr)
  691. end
  692. end
  693. --- Converts any of `MarkedString` | `MarkedString[]` | `MarkupContent` into
  694. --- a list of lines containing valid markdown. Useful to populate the hover
  695. --- window for `textDocument/hover`, for parsing the result of
  696. --- `textDocument/signatureHelp`, and potentially others.
  697. ---
  698. ---@param input (`MarkedString` | `MarkedString[]` | `MarkupContent`)
  699. ---@param contents (table, optional, default `{}`) List of strings to extend with converted lines
  700. ---@returns {contents}, extended with lines of converted markdown.
  701. ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
  702. function M.convert_input_to_markdown_lines(input, contents)
  703. contents = contents or {}
  704. -- MarkedString variation 1
  705. if type(input) == 'string' then
  706. list_extend(contents, split_lines(input))
  707. else
  708. assert(type(input) == 'table', "Expected a table for Hover.contents")
  709. -- MarkupContent
  710. if input.kind then
  711. -- The kind can be either plaintext or markdown.
  712. -- If it's plaintext, then wrap it in a <text></text> block
  713. -- Some servers send input.value as empty, so let's ignore this :(
  714. local value = input.value or ''
  715. if input.kind == "plaintext" then
  716. -- wrap this in a <text></text> block so that stylize_markdown
  717. -- can properly process it as plaintext
  718. value = string.format("<text>\n%s\n</text>", value)
  719. end
  720. -- assert(type(value) == 'string')
  721. list_extend(contents, split_lines(value))
  722. -- MarkupString variation 2
  723. elseif input.language then
  724. -- Some servers send input.value as empty, so let's ignore this :(
  725. -- assert(type(input.value) == 'string')
  726. table.insert(contents, "```"..input.language)
  727. list_extend(contents, split_lines(input.value or ''))
  728. table.insert(contents, "```")
  729. -- By deduction, this must be MarkedString[]
  730. else
  731. -- Use our existing logic to handle MarkedString
  732. for _, marked_string in ipairs(input) do
  733. M.convert_input_to_markdown_lines(marked_string, contents)
  734. end
  735. end
  736. end
  737. if (contents[1] == '' or contents[1] == nil) and #contents == 1 then
  738. return {}
  739. end
  740. return contents
  741. end
  742. --- Converts `textDocument/SignatureHelp` response to markdown lines.
  743. ---
  744. ---@param signature_help Response of `textDocument/SignatureHelp`
  745. ---@param ft optional filetype that will be use as the `lang` for the label markdown code block
  746. ---@param triggers optional list of trigger characters from the lsp server. used to better determine parameter offsets
  747. ---@returns list of lines of converted markdown.
  748. ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
  749. function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers)
  750. if not signature_help.signatures then
  751. return
  752. end
  753. --The active signature. If omitted or the value lies outside the range of
  754. --`signatures` the value defaults to zero or is ignored if `signatures.length
  755. --=== 0`. Whenever possible implementors should make an active decision about
  756. --the active signature and shouldn't rely on a default value.
  757. local contents = {}
  758. local active_hl
  759. local active_signature = signature_help.activeSignature or 0
  760. -- If the activeSignature is not inside the valid range, then clip it.
  761. if active_signature >= #signature_help.signatures then
  762. active_signature = 0
  763. end
  764. local signature = signature_help.signatures[active_signature + 1]
  765. if not signature then
  766. return
  767. end
  768. local label = signature.label
  769. if ft then
  770. -- wrap inside a code block so stylize_markdown can render it properly
  771. label = ("```%s\n%s\n```"):format(ft, label)
  772. end
  773. vim.list_extend(contents, vim.split(label, '\n', true))
  774. if signature.documentation then
  775. M.convert_input_to_markdown_lines(signature.documentation, contents)
  776. end
  777. if signature.parameters and #signature.parameters > 0 then
  778. local active_parameter = (signature.activeParameter or signature_help.activeParameter or 0)
  779. if active_parameter < 0
  780. then active_parameter = 0
  781. end
  782. -- If the activeParameter is > #parameters, then set it to the last
  783. -- NOTE: this is not fully according to the spec, but a client-side interpretation
  784. if active_parameter >= #signature.parameters then
  785. active_parameter = #signature.parameters - 1
  786. end
  787. local parameter = signature.parameters[active_parameter + 1]
  788. if parameter then
  789. --[=[
  790. --Represents a parameter of a callable-signature. A parameter can
  791. --have a label and a doc-comment.
  792. interface ParameterInformation {
  793. --The label of this parameter information.
  794. --
  795. --Either a string or an inclusive start and exclusive end offsets within its containing
  796. --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16
  797. --string representation as `Position` and `Range` does.
  798. --
  799. --*Note*: a label of type string should be a substring of its containing signature label.
  800. --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`.
  801. label: string | [number, number];
  802. --The human-readable doc-comment of this parameter. Will be shown
  803. --in the UI but can be omitted.
  804. documentation?: string | MarkupContent;
  805. }
  806. --]=]
  807. if parameter.label then
  808. if type(parameter.label) == "table" then
  809. active_hl = parameter.label
  810. else
  811. local offset = 1
  812. -- try to set the initial offset to the first found trigger character
  813. for _, t in ipairs(triggers or {}) do
  814. local trigger_offset = signature.label:find(t, 1, true)
  815. if trigger_offset and (offset == 1 or trigger_offset < offset) then
  816. offset = trigger_offset
  817. end
  818. end
  819. for p, param in pairs(signature.parameters) do
  820. offset = signature.label:find(param.label, offset, true)
  821. if not offset then break end
  822. if p == active_parameter + 1 then
  823. active_hl = {offset - 1, offset + #parameter.label - 1}
  824. break
  825. end
  826. offset = offset + #param.label + 1
  827. end
  828. end
  829. end
  830. if parameter.documentation then
  831. M.convert_input_to_markdown_lines(parameter.documentation, contents)
  832. end
  833. end
  834. end
  835. return contents, active_hl
  836. end
  837. --- Creates a table with sensible default options for a floating window. The
  838. --- table can be passed to |nvim_open_win()|.
  839. ---
  840. ---@param width (number) window width (in character cells)
  841. ---@param height (number) window height (in character cells)
  842. ---@param opts (table, optional)
  843. --- - offset_x (number) offset to add to `col`
  844. --- - offset_y (number) offset to add to `row`
  845. --- - border (string or table) override `border`
  846. --- - focusable (string or table) override `focusable`
  847. --- - zindex (string or table) override `zindex`, defaults to 50
  848. ---@returns (table) Options
  849. function M.make_floating_popup_options(width, height, opts)
  850. validate {
  851. opts = { opts, 't', true };
  852. }
  853. opts = opts or {}
  854. validate {
  855. ["opts.offset_x"] = { opts.offset_x, 'n', true };
  856. ["opts.offset_y"] = { opts.offset_y, 'n', true };
  857. }
  858. local anchor = ''
  859. local row, col
  860. local lines_above = vim.fn.winline() - 1
  861. local lines_below = vim.fn.winheight(0) - lines_above
  862. if lines_above < lines_below then
  863. anchor = anchor..'N'
  864. height = math.min(lines_below, height)
  865. row = 1
  866. else
  867. anchor = anchor..'S'
  868. height = math.min(lines_above, height)
  869. row = 0
  870. end
  871. if vim.fn.wincol() + width + (opts.offset_x or 0) <= api.nvim_get_option('columns') then
  872. anchor = anchor..'W'
  873. col = 0
  874. else
  875. anchor = anchor..'E'
  876. col = 1
  877. end
  878. return {
  879. anchor = anchor,
  880. col = col + (opts.offset_x or 0),
  881. height = height,
  882. focusable = opts.focusable,
  883. relative = 'cursor',
  884. row = row + (opts.offset_y or 0),
  885. style = 'minimal',
  886. width = width,
  887. border = opts.border or default_border,
  888. zindex = opts.zindex or 50,
  889. }
  890. end
  891. --- Jumps to a location.
  892. ---
  893. ---@param location (`Location`|`LocationLink`)
  894. ---@returns `true` if the jump succeeded
  895. function M.jump_to_location(location)
  896. -- location may be Location or LocationLink
  897. local uri = location.uri or location.targetUri
  898. if uri == nil then return end
  899. local bufnr = vim.uri_to_bufnr(uri)
  900. -- Save position in jumplist
  901. vim.cmd "normal! m'"
  902. -- Push a new item into tagstack
  903. local from = {vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0}
  904. local items = {{tagname=vim.fn.expand('<cword>'), from=from}}
  905. vim.fn.settagstack(vim.fn.win_getid(), {items=items}, 't')
  906. --- Jump to new location (adjusting for UTF-16 encoding of characters)
  907. api.nvim_set_current_buf(bufnr)
  908. api.nvim_buf_set_option(bufnr, 'buflisted', true)
  909. local range = location.range or location.targetSelectionRange
  910. local row = range.start.line
  911. local col = get_line_byte_from_position(bufnr, range.start)
  912. api.nvim_win_set_cursor(0, {row + 1, col})
  913. -- Open folds under the cursor
  914. vim.cmd("normal! zv")
  915. return true
  916. end
  917. --- Previews a location in a floating window
  918. ---
  919. --- behavior depends on type of location:
  920. --- - for Location, range is shown (e.g., function definition)
  921. --- - for LocationLink, targetRange is shown (e.g., body of function definition)
  922. ---
  923. ---@param location a single `Location` or `LocationLink`
  924. ---@returns (bufnr,winnr) buffer and window number of floating window or nil
  925. function M.preview_location(location, opts)
  926. -- location may be LocationLink or Location (more useful for the former)
  927. local uri = location.targetUri or location.uri
  928. if uri == nil then return end
  929. local bufnr = vim.uri_to_bufnr(uri)
  930. if not api.nvim_buf_is_loaded(bufnr) then
  931. vim.fn.bufload(bufnr)
  932. end
  933. local range = location.targetRange or location.range
  934. local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range["end"].line+1, false)
  935. local syntax = api.nvim_buf_get_option(bufnr, 'syntax')
  936. if syntax == "" then
  937. -- When no syntax is set, we use filetype as fallback. This might not result
  938. -- in a valid syntax definition. See also ft detection in stylize_markdown.
  939. -- An empty syntax is more common now with TreeSitter, since TS disables syntax.
  940. syntax = api.nvim_buf_get_option(bufnr, 'filetype')
  941. end
  942. opts = opts or {}
  943. opts.focus_id = "location"
  944. return M.open_floating_preview(contents, syntax, opts)
  945. end
  946. ---@private
  947. local function find_window_by_var(name, value)
  948. for _, win in ipairs(api.nvim_list_wins()) do
  949. if npcall(api.nvim_win_get_var, win, name) == value then
  950. return win
  951. end
  952. end
  953. end
  954. --- Trims empty lines from input and pad top and bottom with empty lines
  955. ---
  956. ---@param contents table of lines to trim and pad
  957. ---@param opts dictionary with optional fields
  958. --- - pad_top number of lines to pad contents at top (default 0)
  959. --- - pad_bottom number of lines to pad contents at bottom (default 0)
  960. ---@return contents table of trimmed and padded lines
  961. function M._trim(contents, opts)
  962. validate {
  963. contents = { contents, 't' };
  964. opts = { opts, 't', true };
  965. }
  966. opts = opts or {}
  967. contents = M.trim_empty_lines(contents)
  968. if opts.pad_top then
  969. for _ = 1, opts.pad_top do
  970. table.insert(contents, 1, "")
  971. end
  972. end
  973. if opts.pad_bottom then
  974. for _ = 1, opts.pad_bottom do
  975. table.insert(contents, "")
  976. end
  977. end
  978. return contents
  979. end
  980. --- Generates a table mapping markdown code block lang to vim syntax,
  981. --- based on g:markdown_fenced_languages
  982. ---@return a table of lang -> syntax mappings
  983. ---@private
  984. local function get_markdown_fences()
  985. local fences = {}
  986. for _, fence in pairs(vim.g.markdown_fenced_languages or {}) do
  987. local lang, syntax = fence:match("^(.*)=(.*)$")
  988. if lang then
  989. fences[lang] = syntax
  990. end
  991. end
  992. return fences
  993. end
  994. --- Converts markdown into syntax highlighted regions by stripping the code
  995. --- blocks and converting them into highlighted code.
  996. --- This will by default insert a blank line separator after those code block
  997. --- regions to improve readability.
  998. ---
  999. --- This method configures the given buffer and returns the lines to set.
  1000. ---
  1001. --- If you want to open a popup with fancy markdown, use `open_floating_preview` instead
  1002. ---
  1003. ---@param contents table of lines to show in window
  1004. ---@param opts dictionary with optional fields
  1005. --- - height of floating window
  1006. --- - width of floating window
  1007. --- - wrap_at character to wrap at for computing height
  1008. --- - max_width maximal width of floating window
  1009. --- - max_height maximal height of floating window
  1010. --- - pad_top number of lines to pad contents at top
  1011. --- - pad_bottom number of lines to pad contents at bottom
  1012. --- - separator insert separator after code block
  1013. ---@returns width,height size of float
  1014. function M.stylize_markdown(bufnr, contents, opts)
  1015. validate {
  1016. contents = { contents, 't' };
  1017. opts = { opts, 't', true };
  1018. }
  1019. opts = opts or {}
  1020. -- table of fence types to {ft, begin, end}
  1021. -- when ft is nil, we get the ft from the regex match
  1022. local matchers = {
  1023. block = {nil, "```+([a-zA-Z0-9_]*)", "```+"},
  1024. pre = {"", "<pre>", "</pre>"},
  1025. code = {"", "<code>", "</code>"},
  1026. text = {"plaintex", "<text>", "</text>"},
  1027. }
  1028. local match_begin = function(line)
  1029. for type, pattern in pairs(matchers) do
  1030. local ret = line:match(string.format("^%%s*%s%%s*$", pattern[2]))
  1031. if ret then
  1032. return {
  1033. type = type,
  1034. ft = pattern[1] or ret
  1035. }
  1036. end
  1037. end
  1038. end
  1039. local match_end = function(line, match)
  1040. local pattern = matchers[match.type]
  1041. return line:match(string.format("^%%s*%s%%s*$", pattern[3]))
  1042. end
  1043. -- Clean up
  1044. contents = M._trim(contents, opts)
  1045. -- Insert blank line separator after code block?
  1046. local add_sep = opts.separator == nil and true or opts.separator
  1047. local stripped = {}
  1048. local highlights = {}
  1049. -- keep track of lnums that contain markdown
  1050. local markdown_lines = {}
  1051. do
  1052. local i = 1
  1053. while i <= #contents do
  1054. local line = contents[i]
  1055. local match = match_begin(line)
  1056. if match then
  1057. local start = #stripped
  1058. i = i + 1
  1059. while i <= #contents do
  1060. line = contents[i]
  1061. if match_end(line, match) then
  1062. i = i + 1
  1063. break
  1064. end
  1065. table.insert(stripped, line)
  1066. i = i + 1
  1067. end
  1068. table.insert(highlights, {
  1069. ft = match.ft;
  1070. start = start + 1;
  1071. finish = #stripped;
  1072. })
  1073. -- add a separator, but not on the last line
  1074. if add_sep and i < #contents then
  1075. table.insert(stripped, "---")
  1076. markdown_lines[#stripped] = true
  1077. end
  1078. else
  1079. -- strip any empty lines or separators prior to this separator in actual markdown
  1080. if line:match("^---+$") then
  1081. while markdown_lines[#stripped] and (stripped[#stripped]:match("^%s*$") or stripped[#stripped]:match("^---+$")) do
  1082. markdown_lines[#stripped] = false
  1083. table.remove(stripped, #stripped)
  1084. end
  1085. end
  1086. -- add the line if its not an empty line following a separator
  1087. if not (line:match("^%s*$") and markdown_lines[#stripped] and stripped[#stripped]:match("^---+$")) then
  1088. table.insert(stripped, line)
  1089. markdown_lines[#stripped] = true
  1090. end
  1091. i = i + 1
  1092. end
  1093. end
  1094. end
  1095. -- Compute size of float needed to show (wrapped) lines
  1096. opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0))
  1097. local width = M._make_floating_popup_size(stripped, opts)
  1098. local sep_line = string.rep("─", math.min(width, opts.wrap_at or width))
  1099. for l in pairs(markdown_lines) do
  1100. if stripped[l]:match("^---+$") then
  1101. stripped[l] = sep_line
  1102. end
  1103. end
  1104. vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
  1105. local idx = 1
  1106. ---@private
  1107. -- keep track of syntaxes we already included.
  1108. -- no need to include the same syntax more than once
  1109. local langs = {}
  1110. local fences = get_markdown_fences()
  1111. local function apply_syntax_to_region(ft, start, finish)
  1112. if ft == "" then
  1113. vim.cmd(string.format("syntax region markdownCode start=+\\%%%dl+ end=+\\%%%dl+ keepend extend", start, finish + 1))
  1114. return
  1115. end
  1116. ft = fences[ft] or ft
  1117. local name = ft..idx
  1118. idx = idx + 1
  1119. local lang = "@"..ft:upper()
  1120. if not langs[lang] then
  1121. -- HACK: reset current_syntax, since some syntax files like markdown won't load if it is already set
  1122. pcall(vim.api.nvim_buf_del_var, bufnr, "current_syntax")
  1123. -- TODO(ashkan): better validation before this.
  1124. if not pcall(vim.cmd, string.format("syntax include %s syntax/%s.vim", lang, ft)) then
  1125. return
  1126. end
  1127. langs[lang] = true
  1128. end
  1129. vim.cmd(string.format("syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s keepend", name, start, finish + 1, lang))
  1130. end
  1131. -- needs to run in the buffer for the regions to work
  1132. api.nvim_buf_call(bufnr, function()
  1133. -- we need to apply lsp_markdown regions speperately, since otherwise
  1134. -- markdown regions can "bleed" through the other syntax regions
  1135. -- and mess up the formatting
  1136. local last = 1
  1137. for _, h in ipairs(highlights) do
  1138. if last < h.start then
  1139. apply_syntax_to_region("lsp_markdown", last, h.start - 1)
  1140. end
  1141. apply_syntax_to_region(h.ft, h.start, h.finish)
  1142. last = h.finish + 1
  1143. end
  1144. if last <= #stripped then
  1145. apply_syntax_to_region("lsp_markdown", last, #stripped)
  1146. end
  1147. end)
  1148. return stripped
  1149. end
  1150. ---@private
  1151. --- Creates autocommands to close a preview window when events happen.
  1152. ---
  1153. ---@param events table list of events
  1154. ---@param winnr number window id of preview window
  1155. ---@param bufnrs table list of buffers where the preview window will remain visible
  1156. ---@see |autocmd-events|
  1157. local function close_preview_autocmd(events, winnr, bufnrs)
  1158. local augroup = 'preview_window_'..winnr
  1159. -- close the preview window when entered a buffer that is not
  1160. -- the floating window buffer or the buffer that spawned it
  1161. vim.cmd(string.format([[
  1162. augroup %s
  1163. autocmd!
  1164. autocmd BufEnter * lua vim.lsp.util._close_preview_window(%d, {%s})
  1165. augroup end
  1166. ]], augroup, winnr, table.concat(bufnrs, ',')))
  1167. if #events > 0 then
  1168. vim.cmd(string.format([[
  1169. augroup %s
  1170. autocmd %s <buffer> lua vim.lsp.util._close_preview_window(%d)
  1171. augroup end
  1172. ]], augroup, table.concat(events, ','), winnr))
  1173. end
  1174. end
  1175. ---@private
  1176. --- Closes the preview window
  1177. ---
  1178. ---@param winnr number window id of preview window
  1179. ---@param bufnrs table|nil optional list of ignored buffers
  1180. function M._close_preview_window(winnr, bufnrs)
  1181. vim.schedule(function()
  1182. -- exit if we are in one of ignored buffers
  1183. if bufnrs and vim.tbl_contains(bufnrs, api.nvim_get_current_buf()) then
  1184. return
  1185. end
  1186. local augroup = 'preview_window_'..winnr
  1187. vim.cmd(string.format([[
  1188. augroup %s
  1189. autocmd!
  1190. augroup end
  1191. augroup! %s
  1192. ]], augroup, augroup))
  1193. pcall(vim.api.nvim_win_close, winnr, true)
  1194. end)
  1195. end
  1196. ---@internal
  1197. --- Computes size of float needed to show contents (with optional wrapping)
  1198. ---
  1199. ---@param contents table of lines to show in window
  1200. ---@param opts dictionary with optional fields
  1201. --- - height of floating window
  1202. --- - width of floating window
  1203. --- - wrap_at character to wrap at for computing height
  1204. --- - max_width maximal width of floating window
  1205. --- - max_height maximal height of floating window
  1206. ---@returns width,height size of float
  1207. function M._make_floating_popup_size(contents, opts)
  1208. validate {
  1209. contents = { contents, 't' };
  1210. opts = { opts, 't', true };
  1211. }
  1212. opts = opts or {}
  1213. local width = opts.width
  1214. local height = opts.height
  1215. local wrap_at = opts.wrap_at
  1216. local max_width = opts.max_width
  1217. local max_height = opts.max_height
  1218. local line_widths = {}
  1219. if not width then
  1220. width = 0
  1221. for i, line in ipairs(contents) do
  1222. -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced.
  1223. line_widths[i] = vim.fn.strdisplaywidth(line)
  1224. width = math.max(line_widths[i], width)
  1225. end
  1226. end
  1227. local border_width = get_border_size(opts).width
  1228. local screen_width = api.nvim_win_get_width(0)
  1229. width = math.min(width, screen_width)
  1230. -- make sure borders are always inside the screen
  1231. if width + border_width > screen_width then
  1232. width = width - (width + border_width - screen_width)
  1233. end
  1234. if wrap_at and wrap_at > width then
  1235. wrap_at = width
  1236. end
  1237. if max_width then
  1238. width = math.min(width, max_width)
  1239. wrap_at = math.min(wrap_at or max_width, max_width)
  1240. end
  1241. if not height then
  1242. height = #contents
  1243. if wrap_at and width >= wrap_at then
  1244. height = 0
  1245. if vim.tbl_isempty(line_widths) then
  1246. for _, line in ipairs(contents) do
  1247. local line_width = vim.fn.strdisplaywidth(line)
  1248. height = height + math.ceil(line_width/wrap_at)
  1249. end
  1250. else
  1251. for i = 1, #contents do
  1252. height = height + math.max(1, math.ceil(line_widths[i]/wrap_at))
  1253. end
  1254. end
  1255. end
  1256. end
  1257. if max_height then
  1258. height = math.min(height, max_height)
  1259. end
  1260. return width, height
  1261. end
  1262. --- Shows contents in a floating window.
  1263. ---
  1264. ---@param contents table of lines to show in window
  1265. ---@param syntax string of syntax to set for opened buffer
  1266. ---@param opts table with optional fields (additional keys are passed on to |vim.api.nvim_open_win()|)
  1267. --- - height: (number) height of floating window
  1268. --- - width: (number) width of floating window
  1269. --- - wrap: (boolean, default true) wrap long lines
  1270. --- - wrap_at: (string) character to wrap at for computing height when wrap is enabled
  1271. --- - max_width: (number) maximal width of floating window
  1272. --- - max_height: (number) maximal height of floating window
  1273. --- - pad_top: (number) number of lines to pad contents at top
  1274. --- - pad_bottom: (number) number of lines to pad contents at bottom
  1275. --- - focus_id: (string) if a popup with this id is opened, then focus it
  1276. --- - close_events: (table) list of events that closes the floating window
  1277. --- - focusable: (boolean, default true) Make float focusable
  1278. --- - focus: (boolean, default true) If `true`, and if {focusable}
  1279. --- is also `true`, focus an existing floating window with the same
  1280. --- {focus_id}
  1281. ---@returns bufnr,winnr buffer and window number of the newly created floating
  1282. ---preview window
  1283. function M.open_floating_preview(contents, syntax, opts)
  1284. validate {
  1285. contents = { contents, 't' };
  1286. syntax = { syntax, 's', true };
  1287. opts = { opts, 't', true };
  1288. }
  1289. opts = opts or {}
  1290. opts.wrap = opts.wrap ~= false -- wrapping by default
  1291. opts.stylize_markdown = opts.stylize_markdown ~= false
  1292. opts.focus = opts.focus ~= false
  1293. opts.close_events = opts.close_events or {"CursorMoved", "CursorMovedI", "InsertCharPre"}
  1294. local bufnr = api.nvim_get_current_buf()
  1295. -- check if this popup is focusable and we need to focus
  1296. if opts.focus_id and opts.focusable ~= false and opts.focus then
  1297. -- Go back to previous window if we are in a focusable one
  1298. local current_winnr = api.nvim_get_current_win()
  1299. if npcall(api.nvim_win_get_var, current_winnr, opts.focus_id) then
  1300. api.nvim_command("wincmd p")
  1301. return bufnr, current_winnr
  1302. end
  1303. do
  1304. local win = find_window_by_var(opts.focus_id, bufnr)
  1305. if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then
  1306. -- focus and return the existing buf, win
  1307. api.nvim_set_current_win(win)
  1308. api.nvim_command("stopinsert")
  1309. return api.nvim_win_get_buf(win), win
  1310. end
  1311. end
  1312. end
  1313. -- check if another floating preview already exists for this buffer
  1314. -- and close it if needed
  1315. local existing_float = npcall(api.nvim_buf_get_var, bufnr, "lsp_floating_preview")
  1316. if existing_float and api.nvim_win_is_valid(existing_float) then
  1317. api.nvim_win_close(existing_float, true)
  1318. end
  1319. local floating_bufnr = api.nvim_create_buf(false, true)
  1320. local do_stylize = syntax == "markdown" and opts.stylize_markdown
  1321. -- Clean up input: trim empty lines from the end, pad
  1322. contents = M._trim(contents, opts)
  1323. if do_stylize then
  1324. -- applies the syntax and sets the lines to the buffer
  1325. contents = M.stylize_markdown(floating_bufnr, contents, opts)
  1326. else
  1327. if syntax then
  1328. api.nvim_buf_set_option(floating_bufnr, 'syntax', syntax)
  1329. end
  1330. api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
  1331. end
  1332. -- Compute size of float needed to show (wrapped) lines
  1333. if opts.wrap then
  1334. opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0)
  1335. else
  1336. opts.wrap_at = nil
  1337. end
  1338. local width, height = M._make_floating_popup_size(contents, opts)
  1339. local float_option = M.make_floating_popup_options(width, height, opts)
  1340. local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option)
  1341. if do_stylize then
  1342. api.nvim_win_set_option(floating_winnr, 'conceallevel', 2)
  1343. api.nvim_win_set_option(floating_winnr, 'concealcursor', 'n')
  1344. end
  1345. -- disable folding
  1346. api.nvim_win_set_option(floating_winnr, 'foldenable', false)
  1347. -- soft wrapping
  1348. api.nvim_win_set_option(floating_winnr, 'wrap', opts.wrap)
  1349. api.nvim_buf_set_option(floating_bufnr, 'modifiable', false)
  1350. api.nvim_buf_set_option(floating_bufnr, 'bufhidden', 'wipe')
  1351. api.nvim_buf_set_keymap(floating_bufnr, "n", "q", "<cmd>bdelete<cr>", {silent = true, noremap = true, nowait = true})
  1352. close_preview_autocmd(opts.close_events, floating_winnr, {floating_bufnr, bufnr})
  1353. -- save focus_id
  1354. if opts.focus_id then
  1355. api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr)
  1356. end
  1357. api.nvim_buf_set_var(bufnr, "lsp_floating_preview", floating_winnr)
  1358. return floating_bufnr, floating_winnr
  1359. end
  1360. do --[[ References ]]
  1361. local reference_ns = api.nvim_create_namespace("vim_lsp_references")
  1362. --- Removes document highlights from a buffer.
  1363. ---
  1364. ---@param bufnr number Buffer id
  1365. function M.buf_clear_references(bufnr)
  1366. validate { bufnr = {bufnr, 'n', true} }
  1367. api.nvim_buf_clear_namespace(bufnr, reference_ns, 0, -1)
  1368. end
  1369. --- Shows a list of document highlights for a certain buffer.
  1370. ---
  1371. ---@param bufnr number Buffer id
  1372. ---@param references table List of `DocumentHighlight` objects to highlight
  1373. ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32", or nil. Defaults to `offset_encoding` of first client of `bufnr`
  1374. ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentHighlight
  1375. function M.buf_highlight_references(bufnr, references, offset_encoding)
  1376. validate { bufnr = {bufnr, 'n', true} }
  1377. offset_encoding = offset_encoding or M._get_offset_encoding(bufnr)
  1378. for _, reference in ipairs(references) do
  1379. local start_line, start_char = reference["range"]["start"]["line"], reference["range"]["start"]["character"]
  1380. local end_line, end_char = reference["range"]["end"]["line"], reference["range"]["end"]["character"]
  1381. local start_idx = get_line_byte_from_position(bufnr, { line = start_line, character = start_char }, offset_encoding)
  1382. local end_idx = get_line_byte_from_position(bufnr, { line = start_line, character = end_char }, offset_encoding)
  1383. local document_highlight_kind = {
  1384. [protocol.DocumentHighlightKind.Text] = "LspReferenceText";
  1385. [protocol.DocumentHighlightKind.Read] = "LspReferenceRead";
  1386. [protocol.DocumentHighlightKind.Write] = "LspReferenceWrite";
  1387. }
  1388. local kind = reference["kind"] or protocol.DocumentHighlightKind.Text
  1389. highlight.range(bufnr,
  1390. reference_ns,
  1391. document_highlight_kind[kind],
  1392. { start_line, start_idx },
  1393. { end_line, end_idx })
  1394. end
  1395. end
  1396. end
  1397. local position_sort = sort_by_key(function(v)
  1398. return {v.start.line, v.start.character}
  1399. end)
  1400. --- Returns the items with the byte position calculated correctly and in sorted
  1401. --- order, for display in quickfix and location lists.
  1402. ---
  1403. --- The result can be passed to the {list} argument of |setqflist()| or
  1404. --- |setloclist()|.
  1405. ---
  1406. ---@param locations (table) list of `Location`s or `LocationLink`s
  1407. ---@returns (table) list of items
  1408. function M.locations_to_items(locations)
  1409. local items = {}
  1410. local grouped = setmetatable({}, {
  1411. __index = function(t, k)
  1412. local v = {}
  1413. rawset(t, k, v)
  1414. return v
  1415. end;
  1416. })
  1417. for _, d in ipairs(locations) do
  1418. -- locations may be Location or LocationLink
  1419. local uri = d.uri or d.targetUri
  1420. local range = d.range or d.targetSelectionRange
  1421. table.insert(grouped[uri], {start = range.start})
  1422. end
  1423. local keys = vim.tbl_keys(grouped)
  1424. table.sort(keys)
  1425. -- TODO(ashkan) I wish we could do this lazily.
  1426. for _, uri in ipairs(keys) do
  1427. local rows = grouped[uri]
  1428. table.sort(rows, position_sort)
  1429. local filename = vim.uri_to_fname(uri)
  1430. -- list of row numbers
  1431. local uri_rows = {}
  1432. for _, temp in ipairs(rows) do
  1433. local pos = temp.start
  1434. local row = pos.line
  1435. table.insert(uri_rows, row)
  1436. end
  1437. -- get all the lines for this uri
  1438. local lines = get_lines(vim.uri_to_bufnr(uri), uri_rows)
  1439. for _, temp in ipairs(rows) do
  1440. local pos = temp.start
  1441. local row = pos.line
  1442. local line = lines[row] or ""
  1443. local col = pos.character
  1444. table.insert(items, {
  1445. filename = filename,
  1446. lnum = row + 1,
  1447. col = col + 1;
  1448. text = line;
  1449. })
  1450. end
  1451. end
  1452. return items
  1453. end
  1454. --- Fills target window's location list with given list of items.
  1455. --- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
  1456. --- Defaults to current window.
  1457. ---
  1458. ---@deprecated Use |setloclist()|
  1459. ---
  1460. ---@param items (table) list of items
  1461. function M.set_loclist(items, win_id)
  1462. vim.api.nvim_echo({{'vim.lsp.util.set_loclist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {})
  1463. vim.fn.setloclist(win_id or 0, {}, ' ', {
  1464. title = 'Language Server';
  1465. items = items;
  1466. })
  1467. end
  1468. --- Fills quickfix list with given list of items.
  1469. --- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|.
  1470. ---
  1471. ---@deprecated Use |setqflist()|
  1472. ---
  1473. ---@param items (table) list of items
  1474. function M.set_qflist(items)
  1475. vim.api.nvim_echo({{'vim.lsp.util.set_qflist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {})
  1476. vim.fn.setqflist({}, ' ', {
  1477. title = 'Language Server';
  1478. items = items;
  1479. })
  1480. end
  1481. -- According to LSP spec, if the client set "symbolKind.valueSet",
  1482. -- the client must handle it properly even if it receives a value outside the specification.
  1483. -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
  1484. function M._get_symbol_kind_name(symbol_kind)
  1485. return protocol.SymbolKind[symbol_kind] or "Unknown"
  1486. end
  1487. --- Converts symbols to quickfix list items.
  1488. ---
  1489. ---@param symbols DocumentSymbol[] or SymbolInformation[]
  1490. function M.symbols_to_items(symbols, bufnr)
  1491. ---@private
  1492. local function _symbols_to_items(_symbols, _items, _bufnr)
  1493. for _, symbol in ipairs(_symbols) do
  1494. if symbol.location then -- SymbolInformation type
  1495. local range = symbol.location.range
  1496. local kind = M._get_symbol_kind_name(symbol.kind)
  1497. table.insert(_items, {
  1498. filename = vim.uri_to_fname(symbol.location.uri),
  1499. lnum = range.start.line + 1,
  1500. col = range.start.character + 1,
  1501. kind = kind,
  1502. text = '['..kind..'] '..symbol.name,
  1503. })
  1504. elseif symbol.selectionRange then -- DocumentSymbole type
  1505. local kind = M._get_symbol_kind_name(symbol.kind)
  1506. table.insert(_items, {
  1507. -- bufnr = _bufnr,
  1508. filename = vim.api.nvim_buf_get_name(_bufnr),
  1509. lnum = symbol.selectionRange.start.line + 1,
  1510. col = symbol.selectionRange.start.character + 1,
  1511. kind = kind,
  1512. text = '['..kind..'] '..symbol.name
  1513. })
  1514. if symbol.children then
  1515. for _, v in ipairs(_symbols_to_items(symbol.children, _items, _bufnr)) do
  1516. for _, s in ipairs(v) do
  1517. table.insert(_items, s)
  1518. end
  1519. end
  1520. end
  1521. end
  1522. end
  1523. return _items
  1524. end
  1525. return _symbols_to_items(symbols, {}, bufnr)
  1526. end
  1527. --- Removes empty lines from the beginning and end.
  1528. ---@param lines (table) list of lines to trim
  1529. ---@returns (table) trimmed list of lines
  1530. function M.trim_empty_lines(lines)
  1531. local start = 1
  1532. for i = 1, #lines do
  1533. if lines[i] ~= nil and #lines[i] > 0 then
  1534. start = i
  1535. break
  1536. end
  1537. end
  1538. local finish = 1
  1539. for i = #lines, 1, -1 do
  1540. if lines[i] ~= nil and #lines[i] > 0 then
  1541. finish = i
  1542. break
  1543. end
  1544. end
  1545. return vim.list_extend({}, lines, start, finish)
  1546. end
  1547. --- Accepts markdown lines and tries to reduce them to a filetype if they
  1548. --- comprise just a single code block.
  1549. ---
  1550. --- CAUTION: Modifies the input in-place!
  1551. ---
  1552. ---@param lines (table) list of lines
  1553. ---@returns (string) filetype or 'markdown' if it was unchanged.
  1554. function M.try_trim_markdown_code_blocks(lines)
  1555. local language_id = lines[1]:match("^```(.*)")
  1556. if language_id then
  1557. local has_inner_code_fence = false
  1558. for i = 2, (#lines - 1) do
  1559. local line = lines[i]
  1560. if line:sub(1,3) == '```' then
  1561. has_inner_code_fence = true
  1562. break
  1563. end
  1564. end
  1565. -- No inner code fences + starting with code fence = hooray.
  1566. if not has_inner_code_fence then
  1567. table.remove(lines, 1)
  1568. table.remove(lines)
  1569. return language_id
  1570. end
  1571. end
  1572. return 'markdown'
  1573. end
  1574. ---@private
  1575. ---@param window (optional, number): window handle or 0 for current, defaults to current
  1576. ---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window`
  1577. local function make_position_param(window, offset_encoding)
  1578. window = window or 0
  1579. local buf = vim.api.nvim_win_get_buf(window)
  1580. local row, col = unpack(api.nvim_win_get_cursor(window))
  1581. offset_encoding = offset_encoding or M._get_offset_encoding(buf)
  1582. row = row - 1
  1583. local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1]
  1584. if not line then
  1585. return { line = 0; character = 0; }
  1586. end
  1587. col = _str_utfindex_enc(line, col, offset_encoding)
  1588. return { line = row; character = col; }
  1589. end
  1590. --- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
  1591. ---
  1592. ---@param window (optional, number): window handle or 0 for current, defaults to current
  1593. ---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window`
  1594. ---@returns `TextDocumentPositionParams` object
  1595. ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
  1596. function M.make_position_params(window, offset_encoding)
  1597. window = window or 0
  1598. local buf = vim.api.nvim_win_get_buf(window)
  1599. offset_encoding = offset_encoding or M._get_offset_encoding(buf)
  1600. return {
  1601. textDocument = M.make_text_document_params(buf);
  1602. position = make_position_param(window, offset_encoding)
  1603. }
  1604. end
  1605. --- Utility function for getting the encoding of the first LSP client on the given buffer.
  1606. ---@param bufnr (number) buffer handle or 0 for current, defaults to current
  1607. ---@returns (string) encoding first client if there is one, nil otherwise
  1608. function M._get_offset_encoding(bufnr)
  1609. validate {
  1610. bufnr = {bufnr, 'n', true};
  1611. }
  1612. local offset_encoding
  1613. for _, client in pairs(vim.lsp.buf_get_clients(bufnr)) do
  1614. local this_offset_encoding = client.offset_encoding or "utf-16"
  1615. if not offset_encoding then
  1616. offset_encoding = this_offset_encoding
  1617. elseif offset_encoding ~= this_offset_encoding then
  1618. vim.notify("warning: multiple different client offset_encodings detected for buffer, this is not supported yet", vim.log.levels.WARN)
  1619. end
  1620. end
  1621. return offset_encoding
  1622. end
  1623. --- Using the current position in the current buffer, creates an object that
  1624. --- can be used as a building block for several LSP requests, such as
  1625. --- `textDocument/codeAction`, `textDocument/colorPresentation`,
  1626. --- `textDocument/rangeFormatting`.
  1627. ---
  1628. ---@param window (optional, number): window handle or 0 for current, defaults to current
  1629. ---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window`
  1630. ---@returns { textDocument = { uri = `current_file_uri` }, range = { start =
  1631. ---`current_position`, end = `current_position` } }
  1632. function M.make_range_params(window, offset_encoding)
  1633. local buf = vim.api.nvim_win_get_buf(window)
  1634. offset_encoding = offset_encoding or M._get_offset_encoding(buf)
  1635. local position = make_position_param(window, offset_encoding)
  1636. return {
  1637. textDocument = M.make_text_document_params(buf),
  1638. range = { start = position; ["end"] = position; }
  1639. }
  1640. end
  1641. --- Using the given range in the current buffer, creates an object that
  1642. --- is similar to |vim.lsp.util.make_range_params()|.
  1643. ---
  1644. ---@param start_pos ({number, number}, optional) mark-indexed position.
  1645. ---Defaults to the start of the last visual selection.
  1646. ---@param end_pos ({number, number}, optional) mark-indexed position.
  1647. ---Defaults to the end of the last visual selection.
  1648. ---@param bufnr (optional, number): buffer handle or 0 for current, defaults to current
  1649. ---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of `bufnr`
  1650. ---@returns { textDocument = { uri = `current_file_uri` }, range = { start =
  1651. ---`start_position`, end = `end_position` } }
  1652. function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding)
  1653. validate {
  1654. start_pos = {start_pos, 't', true};
  1655. end_pos = {end_pos, 't', true};
  1656. offset_encoding = {offset_encoding, 's', true};
  1657. }
  1658. bufnr = bufnr or vim.api.nvim_get_current_buf()
  1659. offset_encoding = offset_encoding or M._get_offset_encoding(bufnr)
  1660. local A = list_extend({}, start_pos or api.nvim_buf_get_mark(bufnr, '<'))
  1661. local B = list_extend({}, end_pos or api.nvim_buf_get_mark(bufnr, '>'))
  1662. -- convert to 0-index
  1663. A[1] = A[1] - 1
  1664. B[1] = B[1] - 1
  1665. -- account for offset_encoding.
  1666. if A[2] > 0 then
  1667. A = {A[1], M.character_offset(bufnr, A[1], A[2], offset_encoding)}
  1668. end
  1669. if B[2] > 0 then
  1670. B = {B[1], M.character_offset(bufnr, B[1], B[2], offset_encoding)}
  1671. end
  1672. -- we need to offset the end character position otherwise we loose the last
  1673. -- character of the selection, as LSP end position is exclusive
  1674. -- see https://microsoft.github.io/language-server-protocol/specification#range
  1675. if vim.o.selection ~= 'exclusive' then
  1676. B[2] = B[2] + 1
  1677. end
  1678. return {
  1679. textDocument = M.make_text_document_params(bufnr),
  1680. range = {
  1681. start = {line = A[1], character = A[2]},
  1682. ['end'] = {line = B[1], character = B[2]}
  1683. }
  1684. }
  1685. end
  1686. --- Creates a `TextDocumentIdentifier` object for the current buffer.
  1687. ---
  1688. ---@param bufnr (optional, number): Buffer handle, defaults to current
  1689. ---@returns `TextDocumentIdentifier`
  1690. ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier
  1691. function M.make_text_document_params(bufnr)
  1692. return { uri = vim.uri_from_bufnr(bufnr or 0) }
  1693. end
  1694. --- Create the workspace params
  1695. ---@param added
  1696. ---@param removed
  1697. function M.make_workspace_params(added, removed)
  1698. return { event = { added = added; removed = removed; } }
  1699. end
  1700. --- Returns visual width of tabstop.
  1701. ---
  1702. ---@see |softtabstop|
  1703. ---@param bufnr (optional, number): Buffer handle, defaults to current
  1704. ---@returns (number) tabstop visual width
  1705. function M.get_effective_tabstop(bufnr)
  1706. validate { bufnr = {bufnr, 'n', true} }
  1707. local bo = bufnr and vim.bo[bufnr] or vim.bo
  1708. local sts = bo.softtabstop
  1709. return (sts > 0 and sts) or (sts < 0 and bo.shiftwidth) or bo.tabstop
  1710. end
  1711. --- Creates a `DocumentFormattingParams` object for the current buffer and cursor position.
  1712. ---
  1713. ---@param options Table with valid `FormattingOptions` entries
  1714. ---@returns `DocumentFormattingParams` object
  1715. ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
  1716. function M.make_formatting_params(options)
  1717. validate { options = {options, 't', true} }
  1718. options = vim.tbl_extend('keep', options or {}, {
  1719. tabSize = M.get_effective_tabstop();
  1720. insertSpaces = vim.bo.expandtab;
  1721. })
  1722. return {
  1723. textDocument = { uri = vim.uri_from_bufnr(0) };
  1724. options = options;
  1725. }
  1726. end
  1727. --- Returns the UTF-32 and UTF-16 offsets for a position in a certain buffer.
  1728. ---
  1729. ---@param buf buffer id (0 for current)
  1730. ---@param row 0-indexed line
  1731. ---@param col 0-indexed byte offset in line
  1732. ---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of `buf`
  1733. ---@returns (number, number) `offset_encoding` index of the character in line {row} column {col} in buffer {buf}
  1734. function M.character_offset(buf, row, col, offset_encoding)
  1735. local line = get_line(buf, row)
  1736. offset_encoding = offset_encoding or M._get_offset_encoding(buf)
  1737. -- If the col is past the EOL, use the line length.
  1738. if col > #line then
  1739. return _str_utfindex_enc(line, nil, offset_encoding)
  1740. end
  1741. return _str_utfindex_enc(line, col, offset_encoding)
  1742. end
  1743. --- Helper function to return nested values in language server settings
  1744. ---
  1745. ---@param settings a table of language server settings
  1746. ---@param section a string indicating the field of the settings table
  1747. ---@returns (table or string) The value of settings accessed via section
  1748. function M.lookup_section(settings, section)
  1749. for part in vim.gsplit(section, '.', true) do
  1750. settings = settings[part]
  1751. if not settings then
  1752. return
  1753. end
  1754. end
  1755. return settings
  1756. end
  1757. M._get_line_byte_from_position = get_line_byte_from_position
  1758. M.buf_versions = {}
  1759. return M
  1760. -- vim:sw=2 ts=2 et