1local vim = vim
2local validate = vim.validate
3local vfn = vim.fn
4local util = require 'vim.lsp.util'
5
6local M = {}
7
8---@private
9--- Returns nil if {status} is false or nil, otherwise returns the rest of the
10--- arguments.
11local function ok_or_nil(status, ...)
12  if not status then return end
13  return ...
14end
15
16---@private
17--- Swallows errors.
18---
19---@param fn Function to run
20---@param ... Function arguments
21---@returns Result of `fn(...)` if there are no errors, otherwise nil.
22--- Returns nil if errors occur during {fn}, otherwise returns
23local function npcall(fn, ...)
24  return ok_or_nil(pcall(fn, ...))
25end
26
27---@private
28--- Sends an async request to all active clients attached to the current
29--- buffer.
30---
31---@param method (string) LSP method name
32---@param params (optional, table) Parameters to send to the server
33---@param handler (optional, functionnil) See |lsp-handler|. Follows |lsp-handler-resolution|
34--
35---@returns 2-tuple:
36---  - Map of client-id:request-id pairs for all successful requests.
37---  - Function which can be used to cancel all the requests. You could instead
38---    iterate all clients and call their `cancel_request()` methods.
39---
40---@see |vim.lsp.buf_request()|
41local function request(method, params, handler)
42  validate {
43    method = {method, 's'};
44    handler = {handler, 'f', true};
45  }
46  return vim.lsp.buf_request(0, method, params, handler)
47end
48
49--- Checks whether the language servers attached to the current buffer are
50--- ready.
51---
52---@returns `true` if server responds.
53function M.server_ready()
54  return not not vim.lsp.buf_notify(0, "window/progress", {})
55end
56
57--- Displays hover information about the symbol under the cursor in a floating
58--- window. Calling the function twice will jump into the floating window.
59function M.hover()
60  local params = util.make_position_params()
61  request('textDocument/hover', params)
62end
63
64--- Jumps to the declaration of the symbol under the cursor.
65---@note Many servers do not implement this method. Generally, see |vim.lsp.buf.definition()| instead.
66---
67function M.declaration()
68  local params = util.make_position_params()
69  request('textDocument/declaration', params)
70end
71
72--- Jumps to the definition of the symbol under the cursor.
73---
74function M.definition()
75  local params = util.make_position_params()
76  request('textDocument/definition', params)
77end
78
79--- Jumps to the definition of the type of the symbol under the cursor.
80---
81function M.type_definition()
82  local params = util.make_position_params()
83  request('textDocument/typeDefinition', params)
84end
85
86--- Lists all the implementations for the symbol under the cursor in the
87--- quickfix window.
88function M.implementation()
89  local params = util.make_position_params()
90  request('textDocument/implementation', params)
91end
92
93--- Displays signature information about the symbol under the cursor in a
94--- floating window.
95function M.signature_help()
96  local params = util.make_position_params()
97  request('textDocument/signatureHelp', params)
98end
99
100--- Retrieves the completion items at the current cursor position. Can only be
101--- called in Insert mode.
102---
103---@param context (context support not yet implemented) Additional information
104--- about the context in which a completion was triggered (how it was triggered,
105--- and by which trigger character, if applicable)
106---
107---@see |vim.lsp.protocol.constants.CompletionTriggerKind|
108function M.completion(context)
109  local params = util.make_position_params()
110  params.context = context
111  return request('textDocument/completion', params)
112end
113
114---@private
115--- If there is more than one client that supports the given method,
116--- asks the user to select one.
117--
118---@returns The client that the user selected or nil
119local function select_client(method, on_choice)
120  validate {
121    on_choice = { on_choice, 'function', false },
122  }
123  local clients = vim.tbl_values(vim.lsp.buf_get_clients())
124  clients = vim.tbl_filter(function(client)
125    return client.supports_method(method)
126  end, clients)
127  -- better UX when choices are always in the same order (between restarts)
128  table.sort(clients, function(a, b)
129    return a.name < b.name
130  end)
131
132  if #clients > 1 then
133    vim.ui.select(clients, {
134      prompt = 'Select a language server:',
135      format_item = function(client)
136        return client.name
137      end,
138    }, on_choice)
139  elseif #clients < 1 then
140    on_choice(nil)
141  else
142    on_choice(clients[1])
143  end
144end
145
146--- Formats the current buffer.
147---
148---@param options (optional, table) Can be used to specify FormattingOptions.
149--- Some unspecified options will be automatically derived from the current
150--- Neovim options.
151--
152---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting
153function M.formatting(options)
154  local params = util.make_formatting_params(options)
155  local bufnr = vim.api.nvim_get_current_buf()
156  select_client('textDocument/formatting', function(client)
157    if client == nil then
158      return
159    end
160
161    return client.request('textDocument/formatting', params, nil, bufnr)
162  end)
163end
164
165--- Performs |vim.lsp.buf.formatting()| synchronously.
166---
167--- Useful for running on save, to make sure buffer is formatted prior to being
168--- saved. {timeout_ms} is passed on to |vim.lsp.buf_request_sync()|. Example:
169---
170--- <pre>
171--- autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync()
172--- </pre>
173---
174---@param options Table with valid `FormattingOptions` entries
175---@param timeout_ms (number) Request timeout
176---@see |vim.lsp.buf.formatting_seq_sync|
177function M.formatting_sync(options, timeout_ms)
178  local params = util.make_formatting_params(options)
179  local bufnr = vim.api.nvim_get_current_buf()
180  select_client('textDocument/formatting', function(client)
181    if client == nil then
182      return
183    end
184
185    local result, err = client.request_sync('textDocument/formatting', params, timeout_ms, bufnr)
186    if result and result.result then
187      util.apply_text_edits(result.result, bufnr)
188    elseif err then
189      vim.notify('vim.lsp.buf.formatting_sync: ' .. err, vim.log.levels.WARN)
190    end
191  end)
192end
193
194--- Formats the current buffer by sequentially requesting formatting from attached clients.
195---
196--- Useful when multiple clients with formatting capability are attached.
197---
198--- Since it's synchronous, can be used for running on save, to make sure buffer is formatted
199--- prior to being saved. {timeout_ms} is passed on to the |vim.lsp.client| `request_sync` method.
200--- Example:
201--- <pre>
202--- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_seq_sync()]]
203--- </pre>
204---
205---@param options (optional, table) `FormattingOptions` entries
206---@param timeout_ms (optional, number) Request timeout
207---@param order (optional, table) List of client names. Formatting is requested from clients
208---in the following order: first all clients that are not in the `order` list, then
209---the remaining clients in the order as they occur in the `order` list.
210function M.formatting_seq_sync(options, timeout_ms, order)
211  local clients = vim.tbl_values(vim.lsp.buf_get_clients());
212  local bufnr = vim.api.nvim_get_current_buf()
213
214  -- sort the clients according to `order`
215  for _, client_name in pairs(order or {}) do
216    -- if the client exists, move to the end of the list
217    for i, client in pairs(clients) do
218      if client.name == client_name then
219        table.insert(clients, table.remove(clients, i))
220        break
221      end
222    end
223  end
224
225  -- loop through the clients and make synchronous formatting requests
226  for _, client in pairs(clients) do
227    if client.resolved_capabilities.document_formatting then
228      local params = util.make_formatting_params(options)
229      local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf())
230      if result and result.result then
231        util.apply_text_edits(result.result, bufnr)
232      elseif err then
233        vim.notify(string.format("vim.lsp.buf.formatting_seq_sync: (%s) %s", client.name, err), vim.log.levels.WARN)
234      end
235    end
236  end
237end
238
239--- Formats a given range.
240---
241---@param options Table with valid `FormattingOptions` entries.
242---@param start_pos ({number, number}, optional) mark-indexed position.
243---Defaults to the start of the last visual selection.
244---@param end_pos ({number, number}, optional) mark-indexed position.
245---Defaults to the end of the last visual selection.
246function M.range_formatting(options, start_pos, end_pos)
247  local params = util.make_given_range_params(start_pos, end_pos)
248  params.options = util.make_formatting_params(options).options
249  select_client('textDocument/rangeFormatting', function(client)
250    if client == nil then
251      return
252    end
253
254    return client.request('textDocument/rangeFormatting', params)
255  end)
256end
257
258--- Renames all references to the symbol under the cursor.
259---
260---@param new_name (string) If not provided, the user will be prompted for a new
261---name using |vim.ui.input()|.
262function M.rename(new_name)
263  local opts = {
264    prompt = "New Name: "
265  }
266
267  ---@private
268  local function on_confirm(input)
269    if not (input and #input > 0) then return end
270    local params = util.make_position_params()
271    params.newName = input
272    request('textDocument/rename', params)
273  end
274
275  ---@private
276  local function prepare_rename(err, result)
277    if err == nil and result == nil then
278      vim.notify('nothing to rename', vim.log.levels.INFO)
279      return
280    end
281    if result and result.placeholder then
282      opts.default = result.placeholder
283      if not new_name then npcall(vim.ui.input, opts, on_confirm) end
284    elseif result and result.start and result['end'] and
285      result.start.line == result['end'].line then
286      local line = vfn.getline(result.start.line+1)
287      local start_char = result.start.character+1
288      local end_char = result['end'].character
289      opts.default = string.sub(line, start_char, end_char)
290      if not new_name then npcall(vim.ui.input, opts, on_confirm) end
291    else
292      -- fallback to guessing symbol using <cword>
293      --
294      -- this can happen if the language server does not support prepareRename,
295      -- returns an unexpected response, or requests for "default behavior"
296      --
297      -- see https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareRename
298      opts.default = vfn.expand('<cword>')
299      if not new_name then npcall(vim.ui.input, opts, on_confirm) end
300    end
301    if new_name then on_confirm(new_name) end
302  end
303  request('textDocument/prepareRename', util.make_position_params(), prepare_rename)
304end
305
306--- Lists all the references to the symbol under the cursor in the quickfix window.
307---
308---@param context (table) Context for the request
309---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references
310function M.references(context)
311  validate { context = { context, 't', true } }
312  local params = util.make_position_params()
313  params.context = context or {
314    includeDeclaration = true;
315  }
316  request('textDocument/references', params)
317end
318
319--- Lists all symbols in the current buffer in the quickfix window.
320---
321function M.document_symbol()
322  local params = { textDocument = util.make_text_document_params() }
323  request('textDocument/documentSymbol', params)
324end
325
326---@private
327local function pick_call_hierarchy_item(call_hierarchy_items)
328  if not call_hierarchy_items then return end
329  if #call_hierarchy_items == 1 then
330    return call_hierarchy_items[1]
331  end
332  local items = {}
333  for i, item in pairs(call_hierarchy_items) do
334    local entry = item.detail or item.name
335    table.insert(items, string.format("%d. %s", i, entry))
336  end
337  local choice = vim.fn.inputlist(items)
338  if choice < 1 or choice > #items then
339    return
340  end
341  return choice
342end
343
344---@private
345local function call_hierarchy(method)
346  local params = util.make_position_params()
347  request('textDocument/prepareCallHierarchy', params, function(err, result, ctx)
348    if err then
349      vim.notify(err.message, vim.log.levels.WARN)
350      return
351    end
352    local call_hierarchy_item = pick_call_hierarchy_item(result)
353    local client = vim.lsp.get_client_by_id(ctx.client_id)
354    if client then
355      client.request(method, { item = call_hierarchy_item }, nil, ctx.bufnr)
356    else
357      vim.notify(string.format(
358        'Client with id=%d disappeared during call hierarchy request', ctx.client_id),
359        vim.log.levels.WARN
360      )
361    end
362  end)
363end
364
365--- Lists all the call sites of the symbol under the cursor in the
366--- |quickfix| window. If the symbol can resolve to multiple
367--- items, the user can pick one in the |inputlist|.
368function M.incoming_calls()
369  call_hierarchy('callHierarchy/incomingCalls')
370end
371
372--- Lists all the items that are called by the symbol under the
373--- cursor in the |quickfix| window. If the symbol can resolve to
374--- multiple items, the user can pick one in the |inputlist|.
375function M.outgoing_calls()
376  call_hierarchy('callHierarchy/outgoingCalls')
377end
378
379--- List workspace folders.
380---
381function M.list_workspace_folders()
382  local workspace_folders = {}
383  for _, client in pairs(vim.lsp.buf_get_clients()) do
384    for _, folder in pairs(client.workspace_folders or {}) do
385      table.insert(workspace_folders, folder.name)
386    end
387  end
388  return workspace_folders
389end
390
391--- Add the folder at path to the workspace folders. If {path} is
392--- not provided, the user will be prompted for a path using |input()|.
393function M.add_workspace_folder(workspace_folder)
394  workspace_folder = workspace_folder or npcall(vfn.input, "Workspace Folder: ", vfn.expand('%:p:h'), 'dir')
395  vim.api.nvim_command("redraw")
396  if not (workspace_folder and #workspace_folder > 0) then return end
397  if vim.fn.isdirectory(workspace_folder) == 0 then
398    print(workspace_folder, " is not a valid directory")
399    return
400  end
401  local params = util.make_workspace_params({{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}}, {{}})
402  for _, client in pairs(vim.lsp.buf_get_clients()) do
403    local found = false
404    for _, folder in pairs(client.workspace_folders or {}) do
405      if folder.name == workspace_folder then
406        found = true
407        print(workspace_folder, "is already part of this workspace")
408        break
409      end
410    end
411    if not found then
412      vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params)
413      if not client.workspace_folders then
414        client.workspace_folders = {}
415      end
416      table.insert(client.workspace_folders, params.event.added[1])
417    end
418  end
419end
420
421--- Remove the folder at path from the workspace folders. If
422--- {path} is not provided, the user will be prompted for
423--- a path using |input()|.
424function M.remove_workspace_folder(workspace_folder)
425  workspace_folder = workspace_folder or npcall(vfn.input, "Workspace Folder: ", vfn.expand('%:p:h'))
426  vim.api.nvim_command("redraw")
427  if not (workspace_folder and #workspace_folder > 0) then return end
428  local params = util.make_workspace_params({{}}, {{uri = vim.uri_from_fname(workspace_folder); name = workspace_folder}})
429  for _, client in pairs(vim.lsp.buf_get_clients()) do
430    for idx, folder in pairs(client.workspace_folders) do
431      if folder.name == workspace_folder then
432        vim.lsp.buf_notify(0, 'workspace/didChangeWorkspaceFolders', params)
433        client.workspace_folders[idx] = nil
434        return
435      end
436    end
437  end
438  print(workspace_folder,  "is not currently part of the workspace")
439end
440
441--- Lists all symbols in the current workspace in the quickfix window.
442---
443--- The list is filtered against {query}; if the argument is omitted from the
444--- call, the user is prompted to enter a string on the command line. An empty
445--- string means no filtering is done.
446---
447---@param query (string, optional)
448function M.workspace_symbol(query)
449  query = query or npcall(vfn.input, "Query: ")
450  local params = {query = query}
451  request('workspace/symbol', params)
452end
453
454--- Send request to the server to resolve document highlights for the current
455--- text document position. This request can be triggered by a  key mapping or
456--- by events such as `CursorHold`, eg:
457---
458--- <pre>
459--- autocmd CursorHold  <buffer> lua vim.lsp.buf.document_highlight()
460--- autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight()
461--- autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()
462--- </pre>
463---
464--- Note: Usage of |vim.lsp.buf.document_highlight()| requires the following highlight groups
465---       to be defined or you won't be able to see the actual highlights.
466---         |LspReferenceText|
467---         |LspReferenceRead|
468---         |LspReferenceWrite|
469function M.document_highlight()
470  local params = util.make_position_params()
471  request('textDocument/documentHighlight', params)
472end
473
474--- Removes document highlights from current buffer.
475---
476function M.clear_references()
477  util.buf_clear_references()
478end
479
480
481---@private
482--
483--- This is not public because the main extension point is
484--- vim.ui.select which can be overridden independently.
485---
486--- Can't call/use vim.lsp.handlers['textDocument/codeAction'] because it expects
487--- `(err, CodeAction[] | Command[], ctx)`, but we want to aggregate the results
488--- from multiple clients to have 1 single UI prompt for the user, yet we still
489--- need to be able to link a `CodeAction|Command` to the right client for
490--- `codeAction/resolve`
491local function on_code_action_results(results, ctx)
492  local action_tuples = {}
493  for client_id, result in pairs(results) do
494    for _, action in pairs(result.result or {}) do
495      table.insert(action_tuples, { client_id, action })
496    end
497  end
498  if #action_tuples == 0 then
499    vim.notify('No code actions available', vim.log.levels.INFO)
500    return
501  end
502
503  ---@private
504  local function apply_action(action, client)
505    if action.edit then
506      util.apply_workspace_edit(action.edit)
507    end
508    if action.command then
509      local command = type(action.command) == 'table' and action.command or action
510      local fn = client.commands[command.command] or vim.lsp.commands[command.command]
511      if fn then
512        local enriched_ctx = vim.deepcopy(ctx)
513        enriched_ctx.client_id = client.id
514        fn(command, enriched_ctx)
515      else
516        M.execute_command(command)
517      end
518    end
519  end
520
521  ---@private
522  local function on_user_choice(action_tuple)
523    if not action_tuple then
524      return
525    end
526    -- textDocument/codeAction can return either Command[] or CodeAction[]
527    --
528    -- CodeAction
529    --  ...
530    --  edit?: WorkspaceEdit    -- <- must be applied before command
531    --  command?: Command
532    --
533    -- Command:
534    --  title: string
535    --  command: string
536    --  arguments?: any[]
537    --
538    local client = vim.lsp.get_client_by_id(action_tuple[1])
539    local action = action_tuple[2]
540    if not action.edit
541        and client
542        and type(client.resolved_capabilities.code_action) == 'table'
543        and client.resolved_capabilities.code_action.resolveProvider then
544
545      client.request('codeAction/resolve', action, function(err, resolved_action)
546        if err then
547          vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR)
548          return
549        end
550        apply_action(resolved_action, client)
551      end)
552    else
553      apply_action(action, client)
554    end
555  end
556
557  vim.ui.select(action_tuples, {
558    prompt = 'Code actions:',
559    kind = 'codeaction',
560    format_item = function(action_tuple)
561      local title = action_tuple[2].title:gsub('\r\n', '\\r\\n')
562      return title:gsub('\n', '\\n')
563    end,
564  }, on_user_choice)
565end
566
567
568--- Requests code actions from all clients and calls the handler exactly once
569--- with all aggregated results
570---@private
571local function code_action_request(params)
572  local bufnr = vim.api.nvim_get_current_buf()
573  local method = 'textDocument/codeAction'
574  vim.lsp.buf_request_all(bufnr, method, params, function(results)
575    on_code_action_results(results, { bufnr = bufnr, method = method, params = params })
576  end)
577end
578
579--- Selects a code action available at the current
580--- cursor position.
581---
582---@param context table|nil `CodeActionContext` of the LSP specification:
583---               - diagnostics: (table|nil)
584---                             LSP `Diagnostic[]`. Inferred from the current
585---                             position if not provided.
586---               - only: (string|nil)
587---                      LSP `CodeActionKind` used to filter the code actions.
588---                      Most language servers support values like `refactor`
589---                      or `quickfix`.
590---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction
591function M.code_action(context)
592  validate { context = { context, 't', true } }
593  context = context or {}
594  if not context.diagnostics then
595    context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics()
596  end
597  local params = util.make_range_params()
598  params.context = context
599  code_action_request(params)
600end
601
602--- Performs |vim.lsp.buf.code_action()| for a given range.
603---
604---
605---@param context table|nil `CodeActionContext` of the LSP specification:
606---               - diagnostics: (table|nil)
607---                             LSP `Diagnostic[]`. Inferred from the current
608---                             position if not provided.
609---               - only: (string|nil)
610---                      LSP `CodeActionKind` used to filter the code actions.
611---                      Most language servers support values like `refactor`
612---                      or `quickfix`.
613---@param start_pos ({number, number}, optional) mark-indexed position.
614---Defaults to the start of the last visual selection.
615---@param end_pos ({number, number}, optional) mark-indexed position.
616---Defaults to the end of the last visual selection.
617function M.range_code_action(context, start_pos, end_pos)
618  validate { context = { context, 't', true } }
619  context = context or {}
620  if not context.diagnostics then
621    context.diagnostics = vim.lsp.diagnostic.get_line_diagnostics()
622  end
623  local params = util.make_given_range_params(start_pos, end_pos)
624  params.context = context
625  code_action_request(params)
626end
627
628--- Executes an LSP server command.
629---
630---@param command A valid `ExecuteCommandParams` object
631---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand
632function M.execute_command(command)
633  validate {
634    command = { command.command, 's' },
635    arguments = { command.arguments, 't', true }
636  }
637  request('workspace/executeCommand', command)
638end
639
640return M
641-- vim:sw=2 ts=2 et
642