-
-
Notifications
You must be signed in to change notification settings - Fork 865
/
Copy path__lsp.lua
555 lines (489 loc) · 16.7 KB
/
__lsp.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
local channel = require("plenary.async.control").channel
local actions = require "telescope.actions"
local sorters = require "telescope.sorters"
local conf = require("telescope.config").values
local finders = require "telescope.finders"
local make_entry = require "telescope.make_entry"
local pickers = require "telescope.pickers"
local utils = require "telescope.utils"
local lsp = {}
local function call_hierarchy(opts, method, title, direction, item)
vim.lsp.buf_request(opts.bufnr, method, { item = item }, function(err, result)
if err then
vim.api.nvim_err_writeln("Error handling " .. title .. ": " .. err.message)
return
end
if not result or vim.tbl_isempty(result) then
return
end
local locations = {}
for _, ch_call in pairs(result) do
local ch_item = ch_call[direction]
for _, rng in pairs(ch_call.fromRanges) do
table.insert(locations, {
filename = vim.uri_to_fname(ch_item.uri),
text = ch_item.name,
lnum = rng.start.line + 1,
col = rng.start.character + 1,
})
end
end
pickers
.new(opts, {
prompt_title = title,
finder = finders.new_table {
results = locations,
entry_maker = opts.entry_maker or make_entry.gen_from_quickfix(opts),
},
previewer = conf.qflist_previewer(opts),
sorter = conf.generic_sorter(opts),
push_cursor_on_edit = true,
push_tagstack_on_edit = true,
})
:find()
end)
end
local function pick_call_hierarchy_item(call_hierarchy_items)
if not call_hierarchy_items or vim.tbl_isempty(call_hierarchy_items) then
return
end
if #call_hierarchy_items == 1 then
return call_hierarchy_items[1]
end
local items = {}
for i, item in pairs(call_hierarchy_items) do
local entry = item.detail or item.name
table.insert(items, string.format("%d. %s", i, entry))
end
local choice = vim.fn.inputlist(items)
if choice < 1 or choice > #items then
return
end
return call_hierarchy_items[choice]
end
---@param win number? Window handler
---@param extra table? Extra fields in params
---@return table|(fun(client: vim.lsp.Client): table) parmas to send to the server
local function client_position_params(win, extra)
win = win or vim.api.nvim_get_current_win()
if vim.fn.has "nvim-0.11" == 0 then
local params = vim.lsp.util.make_position_params(win)
if extra then
params = vim.tbl_extend("force", params, extra)
end
return params
end
return function(client)
local params = vim.lsp.util.make_position_params(win, client.offset_encoding)
if extra then
params = vim.tbl_extend("force", params, extra)
end
return params
end
end
local function calls(opts, direction)
local params = client_position_params()
vim.lsp.buf_request(opts.bufnr, "textDocument/prepareCallHierarchy", params, function(err, result)
if err then
vim.api.nvim_err_writeln("Error when preparing call hierarchy: " .. err)
return
end
local call_hierarchy_item = pick_call_hierarchy_item(result)
if not call_hierarchy_item then
return
end
if direction == "from" then
call_hierarchy(opts, "callHierarchy/incomingCalls", "LSP Incoming Calls", direction, call_hierarchy_item)
else
call_hierarchy(opts, "callHierarchy/outgoingCalls", "LSP Outgoing Calls", direction, call_hierarchy_item)
end
end)
end
lsp.incoming_calls = function(opts)
calls(opts, "from")
end
lsp.outgoing_calls = function(opts)
calls(opts, "to")
end
--- convert `item` type back to something we can pass to `vim.lsp.util.jump_to_location`
--- stopgap for pre-nvim 0.10 - after which we can simply use the `user_data`
--- field on the items in `vim.lsp.util.locations_to_items`
---@param item vim.quickfix.entry
---@param offset_encoding string|nil utf-8|utf-16|utf-32
---@return lsp.Location
local function item_to_location(item, offset_encoding)
local line = math.max(0, item.lnum - 1)
local character = math.max(0, utils.str_byteindex(item.text, item.col, offset_encoding or "utf-16") - 1)
local uri
if utils.is_uri(item.filename) then
uri = item.filename
else
uri = vim.uri_from_fname(item.filename)
end
return {
uri = uri,
range = {
start = {
line = line,
character = character,
},
["end"] = {
line = line,
character = character,
},
},
}
end
---@alias telescope.lsp.list_or_jump_action
---| "textDocument/references"
---| "textDocument/definition"
---| "textDocument/typeDefinition"
---| "textDocument/implementation"
---@param action telescope.lsp.list_or_jump_action
---@param items vim.quickfix.entry[]
---@param opts table
---@return vim.quickfix.entry[]
local apply_action_handler = function(action, items, opts)
if action == "textDocument/references" and not opts.include_current_line then
local lnum = vim.api.nvim_win_get_cursor(opts.winnr)[1]
items = vim.tbl_filter(function(v)
return not (v.filename == opts.curr_filepath and v.lnum == lnum)
end, items)
end
return items
end
---@param items vim.quickfix.entry[]
---@param opts table
---@return vim.quickfix.entry[]
local function filter_file_ignore_patters(items, opts)
local file_ignore_patterns = vim.F.if_nil(opts.file_ignore_patterns, conf.file_ignore_patterns)
file_ignore_patterns = file_ignore_patterns or {}
if vim.tbl_isempty(file_ignore_patterns) then
return items
end
return vim.tbl_filter(function(item)
for _, patt in ipairs(file_ignore_patterns) do
if string.match(item.filename, patt) then
return false
end
end
return true
end, items)
end
---@param action telescope.lsp.list_or_jump_action
---@param title string prompt title
---@param funname string: name of the calling function
---@param params lsp.TextDocumentPositionParams|(fun(client: vim.lsp.Client, bufnr: integer): table?)
---@param opts table
local function list_or_jump(action, title, funname, params, opts)
opts.reuse_win = vim.F.if_nil(opts.reuse_win, false)
opts.curr_filepath = vim.api.nvim_buf_get_name(opts.bufnr)
vim.lsp.buf_request_all(opts.bufnr, action, params, function(results_per_client)
local items = {}
local first_encoding
local errors = {}
for client_id, result_or_error in pairs(results_per_client) do
local error, result = result_or_error.error, result_or_error.result
if error then
errors[client_id] = error
else
if result ~= nil then
local locations = {}
if not utils.islist(result) then
vim.list_extend(locations, { result })
else
vim.list_extend(locations, result)
end
local offset_encoding = vim.lsp.get_client_by_id(client_id).offset_encoding
if not vim.tbl_isempty(result) then
first_encoding = offset_encoding
end
vim.list_extend(items, vim.lsp.util.locations_to_items(locations, offset_encoding))
end
end
end
for _, error in pairs(errors) do
vim.api.nvim_err_writeln("Error when executing " .. action .. " : " .. error.message)
end
items = apply_action_handler(action, items, opts)
items = filter_file_ignore_patters(items, opts)
if vim.tbl_isempty(items) then
utils.notify(funname, {
msg = string.format("No %s found", title),
level = "INFO",
})
return
end
if #items == 1 and opts.jump_type ~= "never" then
local item = items[1]
if opts.curr_filepath ~= item.filename then
local cmd
if opts.jump_type == "tab" then
cmd = "tabedit"
elseif opts.jump_type == "split" then
cmd = "new"
elseif opts.jump_type == "vsplit" then
cmd = "vnew"
elseif opts.jump_type == "tab drop" then
cmd = "tab drop"
end
if cmd then
vim.cmd(string.format("%s %s", cmd, item.filename))
end
end
local location = item_to_location(item, first_encoding)
vim.lsp.util.show_document(location, first_encoding, { reuse_win = opts.reuse_win })
else
pickers
.new(opts, {
prompt_title = title,
finder = finders.new_table {
results = items,
entry_maker = opts.entry_maker or make_entry.gen_from_quickfix(opts),
},
previewer = conf.qflist_previewer(opts),
sorter = conf.generic_sorter(opts),
push_cursor_on_edit = true,
push_tagstack_on_edit = true,
})
:find()
end
end)
end
lsp.references = function(opts)
opts.include_current_line = vim.F.if_nil(opts.include_current_line, false)
local params = client_position_params(opts.winnr, {
context = { includeDeclaration = vim.F.if_nil(opts.include_declaration, true) },
})
return list_or_jump("textDocument/references", "LSP References", "builtin.lsp_references", params, opts)
end
lsp.definitions = function(opts)
local params = client_position_params(opts.winnr)
return list_or_jump("textDocument/definition", "LSP Definitions", "builtin.lsp_definitions", params, opts)
end
lsp.type_definitions = function(opts)
local params = client_position_params(opts.winnr)
return list_or_jump(
"textDocument/typeDefinition",
"LSP Type Definitions",
"builtin.lsp_type_definitions",
params,
opts
)
end
lsp.implementations = function(opts)
local params = client_position_params(opts.winnr)
return list_or_jump("textDocument/implementation", "LSP Implementations", "builtin.lsp_implementations", params, opts)
end
local symbols_sorter = function(symbols)
if vim.tbl_isempty(symbols) then
return symbols
end
local current_buf = vim.api.nvim_get_current_buf()
-- sort adequately for workspace symbols
local filename_to_bufnr = {}
for _, symbol in ipairs(symbols) do
if filename_to_bufnr[symbol.filename] == nil then
filename_to_bufnr[symbol.filename] = vim.uri_to_bufnr(vim.uri_from_fname(symbol.filename))
end
symbol.bufnr = filename_to_bufnr[symbol.filename]
end
table.sort(symbols, function(a, b)
if a.bufnr == b.bufnr then
return a.lnum < b.lnum
end
if a.bufnr == current_buf then
return true
end
if b.bufnr == current_buf then
return false
end
return a.bufnr < b.bufnr
end)
return symbols
end
lsp.document_symbols = function(opts)
local params = client_position_params(opts.winnr)
vim.lsp.buf_request(opts.bufnr, "textDocument/documentSymbol", params, function(err, result, ctx, _)
if err then
vim.api.nvim_err_writeln("Error when finding document symbols: " .. err.message)
return
end
if not result or vim.tbl_isempty(result) then
utils.notify("builtin.lsp_document_symbols", {
msg = "No results from textDocument/documentSymbol",
level = "INFO",
})
return
end
local locations
if vim.fn.has "nvim-0.11" == 1 then
local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
locations = vim.lsp.util.symbols_to_items(result or {}, opts.bufnr, client.offset_encoding) or {}
else
locations = vim.lsp.util.symbols_to_items(result or {}, opts.bufnr) or {}
end
locations = utils.filter_symbols(locations, opts, symbols_sorter)
if vim.tbl_isempty(locations) then
-- error message already printed in `utils.filter_symbols`
return
end
if vim.tbl_isempty(locations) then
utils.notify("builtin.lsp_document_symbols", {
msg = "No document_symbol locations found",
level = "INFO",
})
return
end
opts.path_display = { "hidden" }
pickers
.new(opts, {
prompt_title = "LSP Document Symbols",
finder = finders.new_table {
results = locations,
entry_maker = opts.entry_maker or make_entry.gen_from_lsp_symbols(opts),
},
previewer = conf.qflist_previewer(opts),
sorter = conf.prefilter_sorter {
tag = "symbol_type",
sorter = conf.generic_sorter(opts),
},
push_cursor_on_edit = true,
push_tagstack_on_edit = true,
})
:find()
end)
end
lsp.workspace_symbols = function(opts)
local params = { query = opts.query or "" }
vim.lsp.buf_request(opts.bufnr, "workspace/symbol", params, function(err, server_result, ctx, _)
if err then
vim.api.nvim_err_writeln("Error when finding workspace symbols: " .. err.message)
return
end
local locations
if vim.fn.has "nvim-0.11" == 1 then
local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
locations = vim.lsp.util.symbols_to_items(server_result or {}, opts.bufnr, client.offset_encoding) or {}
else
locations = vim.lsp.util.symbols_to_items(server_result or {}, opts.bufnr) or {}
end
locations = utils.filter_symbols(locations, opts, symbols_sorter)
if vim.tbl_isempty(locations) then
-- error message already printed in `utils.filter_symbols`
return
end
if vim.tbl_isempty(locations) then
utils.notify("builtin.lsp_workspace_symbols", {
msg = "No results from workspace/symbol. Maybe try a different query: "
.. "'Telescope lsp_workspace_symbols query=example'",
level = "INFO",
})
return
end
opts.ignore_filename = vim.F.if_nil(opts.ignore_filename, false)
pickers
.new(opts, {
prompt_title = "LSP Workspace Symbols",
finder = finders.new_table {
results = locations,
entry_maker = opts.entry_maker or make_entry.gen_from_lsp_symbols(opts),
},
previewer = conf.qflist_previewer(opts),
sorter = conf.prefilter_sorter {
tag = "symbol_type",
sorter = conf.generic_sorter(opts),
},
})
:find()
end)
end
local function get_workspace_symbols_requester(bufnr, opts)
local cancel = function() end
return function(prompt)
local tx, rx = channel.oneshot()
cancel()
cancel = vim.lsp.buf_request_all(bufnr, "workspace/symbol", { query = prompt }, tx)
local results = rx() ---@type table<integer, {error: lsp.ResponseError?, result: lsp.WorkspaceSymbol?}>
local locations = {} ---@type vim.quickfix.entry[]
for client_id, client_res in pairs(results) do
if client_res.error then
vim.api.nvim_err_writeln("Error when executing workspace/symbol : " .. client_res.error.message)
elseif client_res.result ~= nil then
if vim.fn.has "nvim-0.11" == 1 then
local client = assert(vim.lsp.get_client_by_id(client_id))
vim.list_extend(locations, vim.lsp.util.symbols_to_items(client_res.result, bufnr, client.offset_encoding))
else
vim.list_extend(locations, vim.lsp.util.symbols_to_items(client_res.result, bufnr))
end
end
end
if not vim.tbl_isempty(locations) then
locations = utils.filter_symbols(locations, opts, symbols_sorter) or {}
end
return locations
end
end
lsp.dynamic_workspace_symbols = function(opts)
pickers
.new(opts, {
prompt_title = "LSP Dynamic Workspace Symbols",
finder = finders.new_dynamic {
entry_maker = opts.entry_maker or make_entry.gen_from_lsp_symbols(opts),
fn = get_workspace_symbols_requester(opts.bufnr, opts),
},
previewer = conf.qflist_previewer(opts),
sorter = sorters.highlighter_only(opts),
attach_mappings = function(_, map)
map("i", "<c-space>", actions.to_fuzzy_refine)
return true
end,
})
:find()
end
local function check_capabilities(method, bufnr)
--TODO(clason): remove when dropping support for Nvim 0.9
local get_clients = vim.fn.has "nvim-0.10" == 1 and vim.lsp.get_clients or vim.lsp.get_active_clients
local clients = get_clients { bufnr = bufnr }
for _, client in pairs(clients) do
if client.supports_method(method, { bufnr = bufnr }) then
return true
end
end
if #clients == 0 then
utils.notify("builtin.lsp_*", {
msg = "no client attached",
level = "INFO",
})
else
utils.notify("builtin.lsp_*", {
msg = "server does not support " .. method,
level = "INFO",
})
end
return false
end
local feature_map = {
["document_symbols"] = "textDocument/documentSymbol",
["references"] = "textDocument/references",
["definitions"] = "textDocument/definition",
["type_definitions"] = "textDocument/typeDefinition",
["implementations"] = "textDocument/implementation",
["workspace_symbols"] = "workspace/symbol",
["incoming_calls"] = "callHierarchy/incomingCalls",
["outgoing_calls"] = "callHierarchy/outgoingCalls",
}
local function apply_checks(mod)
for k, v in pairs(mod) do
mod[k] = function(opts)
opts = opts or {}
local method = feature_map[k]
if method and not check_capabilities(method, opts.bufnr) then
return
end
v(opts)
end
end
return mod
end
return apply_checks(lsp)