handlers.lua 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. local log = require('vim.lsp.log')
  2. local protocol = require('vim.lsp.protocol')
  3. local util = require('vim.lsp.util')
  4. local vim = vim
  5. local api = vim.api
  6. local M = {}
  7. -- FIXME: DOC: Expose in vimdocs
  8. ---@private
  9. --- Writes to error buffer.
  10. ---@param ... (table of strings) Will be concatenated before being written
  11. local function err_message(...)
  12. vim.notify(table.concat(vim.tbl_flatten({ ... })), vim.log.levels.ERROR)
  13. api.nvim_command('redraw')
  14. end
  15. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
  16. M['workspace/executeCommand'] = function(_, _, _, _)
  17. -- Error handling is done implicitly by wrapping all handlers; see end of this file
  18. end
  19. ---@private
  20. local function progress_handler(_, result, ctx, _)
  21. local client_id = ctx.client_id
  22. local client = vim.lsp.get_client_by_id(client_id)
  23. local client_name = client and client.name or string.format('id=%d', client_id)
  24. if not client then
  25. err_message('LSP[', client_name, '] client has shut down during progress update')
  26. return vim.NIL
  27. end
  28. local val = result.value -- unspecified yet
  29. local token = result.token -- string or number
  30. if type(val) ~= 'table' then
  31. val = { content = val }
  32. end
  33. if val.kind then
  34. if val.kind == 'begin' then
  35. client.messages.progress[token] = {
  36. title = val.title,
  37. cancellable = val.cancellable,
  38. message = val.message,
  39. percentage = val.percentage,
  40. }
  41. elseif val.kind == 'report' then
  42. client.messages.progress[token].cancellable = val.cancellable
  43. client.messages.progress[token].message = val.message
  44. client.messages.progress[token].percentage = val.percentage
  45. elseif val.kind == 'end' then
  46. if client.messages.progress[token] == nil then
  47. err_message('LSP[', client_name, '] received `end` message with no corresponding `begin`')
  48. else
  49. client.messages.progress[token].message = val.message
  50. client.messages.progress[token].done = true
  51. end
  52. end
  53. else
  54. client.messages.progress[token] = val
  55. client.messages.progress[token].done = true
  56. end
  57. api.nvim_exec_autocmds('User', { pattern = 'LspProgressUpdate', modeline = false })
  58. end
  59. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#progress
  60. M['$/progress'] = progress_handler
  61. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_workDoneProgress_create
  62. M['window/workDoneProgress/create'] = function(_, result, ctx)
  63. local client_id = ctx.client_id
  64. local client = vim.lsp.get_client_by_id(client_id)
  65. local token = result.token -- string or number
  66. local client_name = client and client.name or string.format('id=%d', client_id)
  67. if not client then
  68. err_message('LSP[', client_name, '] client has shut down while creating progress report')
  69. return vim.NIL
  70. end
  71. client.messages.progress[token] = {}
  72. return vim.NIL
  73. end
  74. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessageRequest
  75. M['window/showMessageRequest'] = function(_, result)
  76. local actions = result.actions
  77. print(result.message)
  78. local option_strings = { result.message, '\nRequest Actions:' }
  79. for i, action in ipairs(actions) do
  80. local title = action.title:gsub('\r\n', '\\r\\n')
  81. title = title:gsub('\n', '\\n')
  82. table.insert(option_strings, string.format('%d. %s', i, title))
  83. end
  84. -- window/showMessageRequest can return either MessageActionItem[] or null.
  85. local choice = vim.fn.inputlist(option_strings)
  86. if choice < 1 or choice > #actions then
  87. return vim.NIL
  88. else
  89. return actions[choice]
  90. end
  91. end
  92. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability
  93. M['client/registerCapability'] = function(_, _, ctx)
  94. local client_id = ctx.client_id
  95. local warning_tpl = 'The language server %s triggers a registerCapability '
  96. .. 'handler despite dynamicRegistration set to false. '
  97. .. 'Report upstream, this warning is harmless'
  98. local client = vim.lsp.get_client_by_id(client_id)
  99. local client_name = client and client.name or string.format('id=%d', client_id)
  100. local warning = string.format(warning_tpl, client_name)
  101. log.warn(warning)
  102. return vim.NIL
  103. end
  104. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
  105. M['workspace/applyEdit'] = function(_, workspace_edit, ctx)
  106. if not workspace_edit then
  107. return
  108. end
  109. -- TODO(ashkan) Do something more with label?
  110. local client_id = ctx.client_id
  111. local client = vim.lsp.get_client_by_id(client_id)
  112. if workspace_edit.label then
  113. print('Workspace edit', workspace_edit.label)
  114. end
  115. local status, result =
  116. pcall(util.apply_workspace_edit, workspace_edit.edit, client.offset_encoding)
  117. return {
  118. applied = status,
  119. failureReason = result,
  120. }
  121. end
  122. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_configuration
  123. M['workspace/configuration'] = function(_, result, ctx)
  124. local client_id = ctx.client_id
  125. local client = vim.lsp.get_client_by_id(client_id)
  126. if not client then
  127. err_message(
  128. 'LSP[',
  129. client_id,
  130. '] client has shut down after sending a workspace/configuration request'
  131. )
  132. return
  133. end
  134. if not result.items then
  135. return {}
  136. end
  137. local response = {}
  138. for _, item in ipairs(result.items) do
  139. if item.section then
  140. local value = util.lookup_section(client.config.settings, item.section)
  141. -- For empty sections with no explicit '' key, return settings as is
  142. if value == vim.NIL and item.section == '' then
  143. value = client.config.settings or vim.NIL
  144. end
  145. table.insert(response, value)
  146. end
  147. end
  148. return response
  149. end
  150. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_workspaceFolders
  151. M['workspace/workspaceFolders'] = function(_, _, ctx)
  152. local client_id = ctx.client_id
  153. local client = vim.lsp.get_client_by_id(client_id)
  154. if not client then
  155. err_message('LSP[id=', client_id, '] client has shut down after sending the message')
  156. return
  157. end
  158. return client.workspace_folders or vim.NIL
  159. end
  160. M['textDocument/publishDiagnostics'] = function(...)
  161. return require('vim.lsp.diagnostic').on_publish_diagnostics(...)
  162. end
  163. M['textDocument/codeLens'] = function(...)
  164. return require('vim.lsp.codelens').on_codelens(...)
  165. end
  166. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
  167. M['textDocument/references'] = function(_, result, ctx, config)
  168. if not result or vim.tbl_isempty(result) then
  169. vim.notify('No references found')
  170. else
  171. local client = vim.lsp.get_client_by_id(ctx.client_id)
  172. config = config or {}
  173. local title = 'References'
  174. local items = util.locations_to_items(result, client.offset_encoding)
  175. if config.loclist then
  176. vim.fn.setloclist(0, {}, ' ', { title = title, items = items, context = ctx })
  177. api.nvim_command('lopen')
  178. elseif config.on_list then
  179. assert(type(config.on_list) == 'function', 'on_list is not a function')
  180. config.on_list({ title = title, items = items, context = ctx })
  181. else
  182. vim.fn.setqflist({}, ' ', { title = title, items = items, context = ctx })
  183. api.nvim_command('botright copen')
  184. end
  185. end
  186. end
  187. ---@private
  188. --- Return a function that converts LSP responses to list items and opens the list
  189. ---
  190. --- The returned function has an optional {config} parameter that accepts a table
  191. --- with the following keys:
  192. ---
  193. --- loclist: (boolean) use the location list (default is to use the quickfix list)
  194. ---
  195. ---@param map_result function `((resp, bufnr) -> list)` to convert the response
  196. ---@param entity string name of the resource used in a `not found` error message
  197. ---@param title_fn function Function to call to generate list title
  198. local function response_to_list(map_result, entity, title_fn)
  199. return function(_, result, ctx, config)
  200. if not result or vim.tbl_isempty(result) then
  201. vim.notify('No ' .. entity .. ' found')
  202. else
  203. config = config or {}
  204. local title = title_fn(ctx)
  205. local items = map_result(result, ctx.bufnr)
  206. if config.loclist then
  207. vim.fn.setloclist(0, {}, ' ', { title = title, items = items, context = ctx })
  208. api.nvim_command('lopen')
  209. elseif config.on_list then
  210. assert(type(config.on_list) == 'function', 'on_list is not a function')
  211. config.on_list({ title = title, items = items, context = ctx })
  212. else
  213. vim.fn.setqflist({}, ' ', { title = title, items = items, context = ctx })
  214. api.nvim_command('botright copen')
  215. end
  216. end
  217. end
  218. end
  219. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
  220. M['textDocument/documentSymbol'] = response_to_list(
  221. util.symbols_to_items,
  222. 'document symbols',
  223. function(ctx)
  224. local fname = vim.fn.fnamemodify(vim.uri_to_fname(ctx.params.textDocument.uri), ':.')
  225. return string.format('Symbols in %s', fname)
  226. end
  227. )
  228. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol
  229. M['workspace/symbol'] = response_to_list(util.symbols_to_items, 'symbols', function(ctx)
  230. return string.format("Symbols matching '%s'", ctx.params.query)
  231. end)
  232. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename
  233. M['textDocument/rename'] = function(_, result, ctx, _)
  234. if not result then
  235. vim.notify("Language server couldn't provide rename result", vim.log.levels.INFO)
  236. return
  237. end
  238. local client = vim.lsp.get_client_by_id(ctx.client_id)
  239. util.apply_workspace_edit(result, client.offset_encoding)
  240. end
  241. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting
  242. M['textDocument/rangeFormatting'] = function(_, result, ctx, _)
  243. if not result then
  244. return
  245. end
  246. local client = vim.lsp.get_client_by_id(ctx.client_id)
  247. util.apply_text_edits(result, ctx.bufnr, client.offset_encoding)
  248. end
  249. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
  250. M['textDocument/formatting'] = function(_, result, ctx, _)
  251. if not result then
  252. return
  253. end
  254. local client = vim.lsp.get_client_by_id(ctx.client_id)
  255. util.apply_text_edits(result, ctx.bufnr, client.offset_encoding)
  256. end
  257. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
  258. M['textDocument/completion'] = function(_, result, _, _)
  259. if vim.tbl_isempty(result or {}) then
  260. return
  261. end
  262. local row, col = unpack(api.nvim_win_get_cursor(0))
  263. local line = assert(api.nvim_buf_get_lines(0, row - 1, row, false)[1])
  264. local line_to_cursor = line:sub(col + 1)
  265. local textMatch = vim.fn.match(line_to_cursor, '\\k*$')
  266. local prefix = line_to_cursor:sub(textMatch + 1)
  267. local matches = util.text_document_completion_list_to_complete_items(result, prefix)
  268. vim.fn.complete(textMatch + 1, matches)
  269. end
  270. --- |lsp-handler| for the method "textDocument/hover"
  271. --- <pre>
  272. --- vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(
  273. --- vim.lsp.handlers.hover, {
  274. --- -- Use a sharp border with `FloatBorder` highlights
  275. --- border = "single"
  276. --- }
  277. --- )
  278. --- </pre>
  279. ---@param config table Configuration table.
  280. --- - border: (default=nil)
  281. --- - Add borders to the floating window
  282. --- - See |nvim_open_win()|
  283. function M.hover(_, result, ctx, config)
  284. config = config or {}
  285. config.focus_id = ctx.method
  286. if not (result and result.contents) then
  287. vim.notify('No information available')
  288. return
  289. end
  290. local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
  291. markdown_lines = util.trim_empty_lines(markdown_lines)
  292. if vim.tbl_isempty(markdown_lines) then
  293. vim.notify('No information available')
  294. return
  295. end
  296. return util.open_floating_preview(markdown_lines, 'markdown', config)
  297. end
  298. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
  299. M['textDocument/hover'] = M.hover
  300. ---@private
  301. --- Jumps to a location. Used as a handler for multiple LSP methods.
  302. ---@param _ (not used)
  303. ---@param result (table) result of LSP method; a location or a list of locations.
  304. ---@param ctx (table) table containing the context of the request, including the method
  305. ---(`textDocument/definition` can return `Location` or `Location[]`
  306. local function location_handler(_, result, ctx, config)
  307. if result == nil or vim.tbl_isempty(result) then
  308. local _ = log.info() and log.info(ctx.method, 'No location found')
  309. return nil
  310. end
  311. local client = vim.lsp.get_client_by_id(ctx.client_id)
  312. config = config or {}
  313. -- textDocument/definition can return Location or Location[]
  314. -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
  315. if vim.tbl_islist(result) then
  316. local title = 'LSP locations'
  317. local items = util.locations_to_items(result, client.offset_encoding)
  318. if config.on_list then
  319. assert(type(config.on_list) == 'function', 'on_list is not a function')
  320. config.on_list({ title = title, items = items })
  321. else
  322. if #result == 1 then
  323. util.jump_to_location(result[1], client.offset_encoding, config.reuse_win)
  324. return
  325. end
  326. vim.fn.setqflist({}, ' ', { title = title, items = items })
  327. api.nvim_command('botright copen')
  328. end
  329. else
  330. util.jump_to_location(result, client.offset_encoding, config.reuse_win)
  331. end
  332. end
  333. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration
  334. M['textDocument/declaration'] = location_handler
  335. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
  336. M['textDocument/definition'] = location_handler
  337. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition
  338. M['textDocument/typeDefinition'] = location_handler
  339. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation
  340. M['textDocument/implementation'] = location_handler
  341. --- |lsp-handler| for the method "textDocument/signatureHelp".
  342. --- The active parameter is highlighted with |hl-LspSignatureActiveParameter|.
  343. --- <pre>
  344. --- vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.with(
  345. --- vim.lsp.handlers.signature_help, {
  346. --- -- Use a sharp border with `FloatBorder` highlights
  347. --- border = "single"
  348. --- }
  349. --- )
  350. --- </pre>
  351. ---@param config table Configuration table.
  352. --- - border: (default=nil)
  353. --- - Add borders to the floating window
  354. --- - See |nvim_open_win()|
  355. function M.signature_help(_, result, ctx, config)
  356. config = config or {}
  357. config.focus_id = ctx.method
  358. -- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp handler
  359. -- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `<silent>` to ignore
  360. if not (result and result.signatures and result.signatures[1]) then
  361. if config.silent ~= true then
  362. print('No signature help available')
  363. end
  364. return
  365. end
  366. local client = vim.lsp.get_client_by_id(ctx.client_id)
  367. local triggers =
  368. vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters')
  369. local ft = api.nvim_buf_get_option(ctx.bufnr, 'filetype')
  370. local lines, hl = util.convert_signature_help_to_markdown_lines(result, ft, triggers)
  371. lines = util.trim_empty_lines(lines)
  372. if vim.tbl_isempty(lines) then
  373. if config.silent ~= true then
  374. print('No signature help available')
  375. end
  376. return
  377. end
  378. local fbuf, fwin = util.open_floating_preview(lines, 'markdown', config)
  379. if hl then
  380. api.nvim_buf_add_highlight(fbuf, -1, 'LspSignatureActiveParameter', 0, unpack(hl))
  381. end
  382. return fbuf, fwin
  383. end
  384. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
  385. M['textDocument/signatureHelp'] = M.signature_help
  386. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight
  387. M['textDocument/documentHighlight'] = function(_, result, ctx, _)
  388. if not result then
  389. return
  390. end
  391. local client_id = ctx.client_id
  392. local client = vim.lsp.get_client_by_id(client_id)
  393. if not client then
  394. return
  395. end
  396. util.buf_highlight_references(ctx.bufnr, result, client.offset_encoding)
  397. end
  398. ---@private
  399. ---
  400. --- Displays call hierarchy in the quickfix window.
  401. ---
  402. ---@param direction `"from"` for incoming calls and `"to"` for outgoing calls
  403. ---@returns `CallHierarchyIncomingCall[]` if {direction} is `"from"`,
  404. ---@returns `CallHierarchyOutgoingCall[]` if {direction} is `"to"`,
  405. local make_call_hierarchy_handler = function(direction)
  406. return function(_, result)
  407. if not result then
  408. return
  409. end
  410. local items = {}
  411. for _, call_hierarchy_call in pairs(result) do
  412. local call_hierarchy_item = call_hierarchy_call[direction]
  413. for _, range in pairs(call_hierarchy_call.fromRanges) do
  414. table.insert(items, {
  415. filename = assert(vim.uri_to_fname(call_hierarchy_item.uri)),
  416. text = call_hierarchy_item.name,
  417. lnum = range.start.line + 1,
  418. col = range.start.character + 1,
  419. })
  420. end
  421. end
  422. vim.fn.setqflist({}, ' ', { title = 'LSP call hierarchy', items = items })
  423. api.nvim_command('botright copen')
  424. end
  425. end
  426. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_incomingCalls
  427. M['callHierarchy/incomingCalls'] = make_call_hierarchy_handler('from')
  428. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy_outgoingCalls
  429. M['callHierarchy/outgoingCalls'] = make_call_hierarchy_handler('to')
  430. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_logMessage
  431. M['window/logMessage'] = function(_, result, ctx, _)
  432. local message_type = result.type
  433. local message = result.message
  434. local client_id = ctx.client_id
  435. local client = vim.lsp.get_client_by_id(client_id)
  436. local client_name = client and client.name or string.format('id=%d', client_id)
  437. if not client then
  438. err_message('LSP[', client_name, '] client has shut down after sending ', message)
  439. end
  440. if message_type == protocol.MessageType.Error then
  441. log.error(message)
  442. elseif message_type == protocol.MessageType.Warning then
  443. log.warn(message)
  444. elseif message_type == protocol.MessageType.Info or message_type == protocol.MessageType.Log then
  445. log.info(message)
  446. else
  447. log.debug(message)
  448. end
  449. return result
  450. end
  451. --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showMessage
  452. M['window/showMessage'] = function(_, result, ctx, _)
  453. local message_type = result.type
  454. local message = result.message
  455. local client_id = ctx.client_id
  456. local client = vim.lsp.get_client_by_id(client_id)
  457. local client_name = client and client.name or string.format('id=%d', client_id)
  458. if not client then
  459. err_message('LSP[', client_name, '] client has shut down after sending ', message)
  460. end
  461. if message_type == protocol.MessageType.Error then
  462. err_message('LSP[', client_name, '] ', message)
  463. else
  464. local message_type_name = protocol.MessageType[message_type]
  465. api.nvim_out_write(string.format('LSP[%s][%s] %s\n', client_name, message_type_name, message))
  466. end
  467. return result
  468. end
  469. -- Add boilerplate error validation and logging for all of these.
  470. for k, fn in pairs(M) do
  471. M[k] = function(err, result, ctx, config)
  472. local _ = log.trace()
  473. and log.trace('default_handler', ctx.method, {
  474. err = err,
  475. result = result,
  476. ctx = vim.inspect(ctx),
  477. config = config,
  478. })
  479. if err then
  480. -- LSP spec:
  481. -- interface ResponseError:
  482. -- code: integer;
  483. -- message: string;
  484. -- data?: string | number | boolean | array | object | null;
  485. -- Per LSP, don't show ContentModified error to the user.
  486. if err.code ~= protocol.ErrorCodes.ContentModified then
  487. local client = vim.lsp.get_client_by_id(ctx.client_id)
  488. local client_name = client and client.name or string.format('client_id=%d', ctx.client_id)
  489. err_message(client_name .. ': ' .. tostring(err.code) .. ': ' .. err.message)
  490. end
  491. return
  492. end
  493. return fn(err, result, ctx, config)
  494. end
  495. end
  496. return M
  497. -- vim:sw=2 ts=2 et