vis-config
lua scripts to configure vis editor
git clone https://9o.is/git/vis-config.git
init.lua
(54709B)
1 -- Copyright (c) 2021-2024 Florian Fischer. All rights reserved.
2 --
3 -- This file is part of vis-lspc.
4 --
5 -- vis-lspc is free software: you can redistribute it and/or modify it under the
6 -- terms of the GNU General Public License as published by the Free Software
7 -- Foundation, either version 3 of the License, or (at your option) any later
8 -- version.
9 --
10 -- vis-lspc is distributed in the hope that it will be useful, but WITHOUT ANY
11 -- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 -- FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13 --
14 -- You should have received a copy of the GNU General Public License along with
15 -- vis-lspc found in the LICENSE file. If not, see <https://www.gnu.org/licenses/>.
16 --
17 -- We require vis compiled with the communicate patch
18 if not vis.communicate then
19 vis:info('LSPC Error: language server support requires vis communicate patch')
20 return {}
21 end
22
23 local source_str = debug.getinfo(1, 'S').source:sub(2)
24 local source_path = source_str:match('(.*/)')
25
26 -- state of our language server client
27 local lspc = dofile(source_path .. 'lspc.lua')
28
29 -- initialise the logging system
30 lspc.logger = dofile(source_path .. 'log.lua').lazyNew('lspc', lspc)
31
32 local parser = dofile(source_path .. 'parser.lua')
33
34 -- initialise the util module
35 local util = dofile(source_path .. 'util.lua').init(lspc)
36
37 -- load a suitable json module
38 lspc.json = dofile(source_path .. 'json.lua')
39
40 local jsonrpc = {}
41 jsonrpc.error_codes = {
42 -- json rpc errors
43 ParseError = -32700,
44 InvalidRequest = -32600,
45 MethodNotFound = -32601,
46 InvalidParams = -32602,
47 InternalError = -32603,
48
49 ServerNotInitialized = -32002,
50 UnknownErrorCode = -32001,
51
52 -- lsp errors
53 ContentModified = -32801,
54 RequestCancelled = -32800,
55 }
56
57 -- get vis's pid to pass it to the language servers
58 local vis_pid
59 do
60 local vis_proc_file = io.open('/proc/self/stat', 'r')
61 if vis_proc_file then
62 vis_pid = vis_proc_file:read('*n')
63 vis_proc_file:close()
64
65 else -- fallback if /proc/self/stat
66 local out = util.capture_cmd('sh -c "echo $PPID"')
67 vis_pid = tonumber(out)
68 end
69 end
70 assert(vis_pid)
71
72 -- mapping function between vis lexer names and LSP languageIds
73 local function syntax_to_languageId(syntax)
74 -- LuaFormatter off
75 local map = {
76 ansi_c = 'c',
77 javascript = 'jsx',
78 typescript = 'tsx',
79 }
80 -- LuaFormatter on
81
82 return map[syntax] or syntax
83 end
84
85 -- map of known language servers per syntax
86 lspc.ls_map = dofile(source_path .. 'supported-servers.lua')
87
88 -- return the name of the language server for this syntax
89 local function get_ls_name_for_syntax(syntax)
90 local ls_def = lspc.ls_map[syntax]
91 if not ls_def then
92 return nil, 'No language server available for ' .. syntax
93 end
94 return ls_def.name
95 end
96
97 -- Document position code
98 -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
99
100 -- We use the following position/location/file related types in vis-lspc:
101 -- pos - like in vis a 0-based byte offset into the file.
102 -- path - posix path used by vis
103 -- uri - file uri used by LSP
104
105 -- lsp_position - 0-based tuple (line, character)
106 -- lsp_document_position - aka. LSP TextDocumentPosition, tuple of (uri, lsp_position)
107
108 -- vis_selection - 1-based tuple (line, cul) (character)
109 -- Can be used with with Selection:to
110 -- vis_document_position - 1-based tuple of (path, line, cul)
111
112 -- vis_range - tuple of 0-based byte offsets (finish, start)
113 -- lsp_range - aka. Range, tuple of two lsp_positions (start, end)
114
115 -- There exist helper function to convert from one type into another
116 -- aswell as helper to retrieve the current primary selection from a vis.window
117
118 local function path_to_uri(path)
119 return 'file://' .. path
120 end
121
122 -- uri decode logic taken from
123 -- https://stackoverflow.com/questions/20405985/lua-decodeuri-luvit
124 local uri_decode_table = {}
125 for i = 0, 255 do
126 uri_decode_table[string.format('%02x', i)] = string.char(i)
127 uri_decode_table[string.format('%02X', i)] = string.char(i)
128 end
129
130 local function decode_uri(s)
131 return (s:gsub('%%(%x%x)', uri_decode_table))
132 end
133
134 local function uri_to_path(uri)
135 return decode_uri(uri:gsub('file://', ''))
136 end
137
138 -- get the vis_selection from current primary selection
139 local function get_selection(win)
140 return {line = win.selection.line, col = win.selection.col}
141 end
142
143 -- convert lsp_position to vis_selection
144 local function lsp_pos_to_vis_sel(pos)
145 return {line = pos.line + 1, col = pos.character + 1}
146 end
147
148 -- convert vis_selection to lsp_position
149 local function vis_sel_to_lsp_pos(pos)
150 return {line = pos.line - 1, character = pos.col - 1}
151 end
152
153 -- convert our vis_document_position to lsp_document_position aka. TextDocumentPosition
154 local function vis_doc_pos_to_lsp(doc_pos)
155 return {
156 textDocument = {uri = path_to_uri(doc_pos.file)},
157 position = vis_sel_to_lsp_pos({line = doc_pos.line, col = doc_pos.col}),
158 }
159 end
160
161 -- convert lsp_document_position to vis_document_position
162 local function lsp_doc_pos_to_vis(doc_pos)
163 local pos = lsp_pos_to_vis_sel(doc_pos.position)
164 return {
165 file = uri_to_path(doc_pos.textDocument.uri),
166 line = pos.line,
167 col = pos.col,
168 }
169 end
170
171 -- get document position of the main curser
172 local function vis_get_doc_pos(win)
173 return {
174 file = win.file.path,
175 line = win.selection.line,
176 col = win.selection.col,
177 }
178 end
179
180 --- Convert a lsp_range to a vis_range
181 -- @param file the file in which the range lies
182 -- @param lsp_range the LSP range that should be converted
183 -- @return the according vis range
184 local function lsp_range_to_vis_range(file, lsp_range)
185 local start = lsp_pos_to_vis_sel(lsp_range.start)
186 local finish = lsp_pos_to_vis_sel(lsp_range['end'])
187
188 local positions = util.vis_sorted_selections_to_pos(file, {start, finish})
189 local start_pos = positions[1]
190 local finish_pos = positions[2]
191
192 return {start = start_pos, finish = finish_pos}
193 end
194
195 --- Check if a lsp_position lies before another.
196 -- @param p1 first lsp_position to compare
197 -- @param p2 second lsp_position to compare
198 -- @return true if p1 lies before p2
199 local function lsp_pos_before(p1, p2)
200 return p1.line < p2.line or (p1.line == p2.line and p1.character < p2.character)
201 end
202
203 --- Check if a lsp_range starts before another.
204 -- The ranges may overlap since only their start positions are compared.
205 -- @param r1 first lsp_range to compare
206 -- @param r2 second lsp_range to compare
207 -- @return true if r1 is starts before r2
208 local function lsp_range_starts_before(r1, r2)
209 return lsp_pos_before(r1.start, r2.start)
210 end
211
212 -- concatenate all numeric values in choices and pass it on stdin to lspc.menu_cmd
213 local function lspc_select(choices)
214 local menu_input = ''
215 local i = 0
216 for _, c in ipairs(choices) do
217 i = i + 1
218 menu_input = menu_input .. c .. '\n'
219 end
220
221 -- select the only possible choice
222 if i < 2 then
223 return choices[1]
224 end
225
226 local fullscreen = lspc.menu_cmd == 'fzf'
227 local status, output = util.vis_pipe(menu_input, lspc.menu_cmd, fullscreen)
228
229 local choice = nil
230 if status == 0 then
231 -- trim newline from selection
232 if output:sub(-1) == '\n' then
233 choice = output:sub(1, -2)
234 else
235 choice = output
236 end
237 end
238
239 vis:redraw()
240 return choice
241 end
242
243 local function lspc_select_location(locations)
244 -- Collect all paths with a list of their locations so we
245 -- can sort the locations before calling file_line_iterator_to_n
246 local collected = {}
247
248 for _, location in ipairs(locations) do
249 local path = uri_to_path(location.uri or location.targetUri)
250 local range = location.range or location.targetSelectionRange
251 local position = lsp_pos_to_vis_sel(range.start)
252
253 if collected[path] == nil then
254 table.insert(collected, path)
255 collected[path] = {}
256 end
257
258 table.insert(collected[path], {
259 ['location'] = location,
260 ['position'] = position,
261 })
262 end
263
264 local choices = {}
265 local cwd_components = util.capture_cmd('pwd')
266 -- Strip trailing newline
267 cwd_components = util.split_path_into_components(cwd_components:sub(1, #cwd_components - 1))
268
269 for _, path in ipairs(collected) do
270 -- Sort positions
271 table.sort(collected[path], function(a, b)
272 return a['position'].line < b['position'].line
273 end)
274
275 local rel_path = util.get_relative_path(cwd_components, path)
276 -- Use the already open file if present to get accurate line content for references
277 local line_iter
278 if lspc.open_files[path] ~= nil then
279 line_iter = function(n)
280 if n == -1 then
281 return nil
282 end
283
284 return lspc.open_files[path].file.lines[n]
285 end
286 else
287 line_iter = util.file_line_iterator_to_n(path)
288 end
289
290 for _, val in ipairs(collected[path]) do
291 local position = val['position']
292 local location = val['location']
293
294 local choice = rel_path .. ':' .. position.line .. ':' .. position.col .. ':' ..
295 line_iter(position.line)
296 table.insert(choices, choice)
297 choices[choice] = location
298 end
299
300 -- close the iterator
301 line_iter(-1)
302 end
303
304 -- select a location
305 local choice = lspc_select(choices)
306 if not choice then
307 return nil
308 end
309
310 return choices[choice]
311 end
312
313 -- get a user confirmation
314 -- return true if user selected yes, false otherwise
315 local function lspc_confirm(prompt)
316 local choices = 'no\nyes'
317
318 local cmd = lspc.confirm_cmd
319
320 if prompt then
321 cmd = cmd .. ' -p \'' .. prompt .. '\''
322 end
323
324 lspc:log('get confirmation using: ' .. cmd)
325
326 local choice = nil
327 local status, output = util.vis_pipe(choices, cmd)
328 if status == 0 then
329 -- trim newline from selection
330 if output:sub(-1) == '\n' then
331 choice = output:sub(1, -2)
332 else
333 choice = output
334 end
335 end
336
337 vis:redraw()
338 return choice == 'yes'
339 end
340
341 local function vis_open_file(file, cmd)
342 vis:command(('%s %s'):format(cmd, file:gsub('[\\\t "\']', '\\%1'):gsub('\n', '\\n')))
343 end
344
345 -- open a doc_pos using the vis command <cmd>
346 local function vis_open_doc_pos(doc_pos, cmd, win)
347 if win and win ~= vis.win then
348 vis.win = win
349 end
350 assert(cmd)
351 if vis.win.file.path ~= doc_pos.file then
352 if vis.win.file.modified and cmd == 'e' then
353 if lspc_confirm('Save currently open file:') then
354 vis:command('w')
355 else
356 vis:info('Not opening new file, current file has unsaved changes')
357 return
358 end
359 end
360 vis_open_file(doc_pos.file, cmd)
361 if doc_pos.line then
362 vis.win.selection:to(doc_pos.line, doc_pos.col or 0)
363 end
364 vis:command('lspc-open')
365 else
366 vis.win.selection:to(doc_pos.line, doc_pos.col)
367 end
368 end
369
370 -- Support jumping between document positions
371 -- Stack of edited document positions
372 local doc_pos_history = {}
373
374 local function vis_push_doc_pos(win)
375 local old_doc_pos = vis_get_doc_pos(win)
376 table.insert(doc_pos_history, old_doc_pos)
377 end
378
379 -- open a new doc_pos remembering the old if it is replaced
380 local function vis_open_new_doc_pos(doc_pos, cmd, win)
381 win = win or vis.win
382 if cmd == 'e' then
383 vis_push_doc_pos(win)
384 end
385
386 vis_open_doc_pos(doc_pos, cmd, win)
387 end
388
389 lspc.open_file = function(win, path, line, col, cmd)
390 vis_open_new_doc_pos({file = path, line = line, col = col}, cmd or 'e', win)
391 end
392
393 local function vis_pop_doc_pos(win)
394 local last_doc_pos = table.remove(doc_pos_history)
395 if not last_doc_pos then
396 return 'Document history is empty'
397 end
398
399 vis_open_doc_pos(last_doc_pos, 'e', win)
400 end
401
402 -- apply a textEdit received from the language server
403 local function vis_apply_textEdit(win, file, textEdit)
404 assert(win.file == file)
405
406 local range = lsp_range_to_vis_range(file, textEdit.range)
407
408 file:delete(range)
409 file:insert(range.start, textEdit.newText)
410
411 win.selection.anchored = false
412 win.selection.pos = range.start + string.len(textEdit.newText)
413
414 win:draw()
415 end
416
417 -- apply a list of textEdits received from the language server
418 local function vis_apply_textEdits(win, file, textEdits)
419 assert(win.file == file)
420
421 local edits = {}
422 for _, textEdit in ipairs(textEdits) do
423 local range = lsp_range_to_vis_range(file, textEdit.range)
424 table.insert(edits, {
425 mark = file:mark_set(range.start),
426 len = range.finish - range.start,
427 newText = textEdit.newText,
428 })
429 end
430 for _, edit in ipairs(edits) do
431 local pos = file:mark_get(edit.mark)
432 file:delete(pos, edit.len)
433 file:insert(pos, edit.newText)
434 end
435 win:draw()
436 end
437
438 --- Close the message window
439 -- TODO: close the dedicated lspc message window
440 local function lspc_close_message_win()
441 if lspc.show_message == 'message' then
442 vis:message('')
443 vis:command('q')
444 end
445 end
446
447 --- Present a message to the user
448 -- TODO: use a dedicated lspc message window
449 local function lspc_show_message(msg, hdr, syntax)
450 local current_win = vis.win
451
452 if lspc.show_message == 'message' then
453 local to_show = (hdr or '') .. msg .. '\n'
454 vis:message(to_show)
455 vis.win.selection = vis.win.file.size - #to_show
456 vis:feedkeys('zt')
457
458 elseif lspc.show_message == 'open' then
459 vis:command('open')
460 if syntax then
461 vis:command('set syntax ' .. syntax)
462 end
463
464 vis.win.file:insert(0, msg)
465 vis.win.selection.pos = 0
466 else
467 lspc:err('invalid message configuration "' .. lspc.show_message .. '".')
468 end
469
470 -- reset the focus to the current window
471 vis.win = current_win
472 end
473
474 -- apply a WorkspaceEdit received from the language server
475 local function vis_apply_workspaceEdit(_, _, workspaceEdit)
476 local file_edits = workspaceEdit.changes
477 assert(file_edits or workspaceEdit.documentChanges)
478
479 -- try to convert NOT SUPPORTED TextDocumentEdit[]
480 -- We do not announce support for versioned DocumentChanges in our
481 -- client capabilities, but some LSP servers ignore our capabilities
482 -- sending them anyway.
483 if not file_edits then
484 file_edits = {}
485 for _, edit in ipairs(workspaceEdit.documentChanges) do
486 file_edits[edit.textDocument.uri] = edit.edits
487 end
488 end
489
490 -- generate change summary
491 local summary = '--- workspace edit summary ---\n'
492 for uri, edits in pairs(file_edits) do
493 local path = uri_to_path(uri)
494 summary = summary .. path .. ':\n'
495 for i, edit in ipairs(edits) do
496 summary = summary .. '\t' .. i .. '.: ' .. lspc.json.encode(edit) .. '\n'
497 end
498 end
499
500 lspc_show_message(summary)
501 vis:redraw()
502
503 -- get user confirmation
504 local confirmation = lspc_confirm('apply changes:')
505
506 -- close summary window
507 lspc_close_message_win()
508
509 if not confirmation then
510 return
511 end
512
513 -- apply changes to open files
514 for uri, edits in pairs(file_edits) do
515 local path = uri_to_path(uri)
516
517 -- search all open windows for this uri
518 local win_with_file
519 for win in vis:windows() do
520 if win.file and win.file.path == path then
521 win_with_file = win
522 break
523 end
524 end
525
526 -- The file is not currently opened -> open it
527 local opened
528 if not win_with_file then
529 vis_open_file(path, 'o')
530 win_with_file = vis.win
531 opened = true
532 end
533
534 -- Remember the current primary cursor position
535 local old_pos = win_with_file.selection.pos
536
537 for _, edit in ipairs(edits) do
538 vis_apply_textEdit(win_with_file, win_with_file.file, edit)
539 end
540
541 -- Restore the remembered primary cursor position
542 if lspc.workspace_edit_remember_cursor then
543 win_with_file.selection.pos = old_pos
544 end
545
546 -- save changes and close the opened window
547 if opened then
548 vis:command('wq')
549 end
550 end
551 end
552
553 -- translate file line number to the relative row the line is displayed in the view of a window
554 -- returns an integer relative to the window if line is in view (starting at 1)
555 -- returns nil otherwise
556 local function file_lineno_to_viewport_lineno(win, file_lineno)
557 -- The line is not in the current viewport
558 if file_lineno < win.viewport.lines.start or file_lineno > win.viewport.lines.finish then
559 return nil
560 end
561
562 -- The line is in the viewport and there is no wrapped line
563 if win.viewport.lines.finish - win.viewport.lines.start == win.viewport.height then
564 return file_lineno - win.viewport.lines.start
565 else -- Determine the position in the viewport considering possible prior wrapped lines
566 local view_lineno = 0
567 for n = win.viewport.lines.start, file_lineno do
568 view_lineno = view_lineno + 1
569 -- Wrapped lines shift our displayed line down
570 local line_len = #win.file.lines[n]
571 if not win.options.expandtab then
572 line_len = util.visual_chars_in_line(win, win.file.lines[n], line_len)
573 end
574
575 if line_len >= win.viewport.width then
576 view_lineno = view_lineno + math.floor(line_len / win.viewport.width)
577 end
578 end
579 return view_lineno
580 end
581 end
582
583 local function lspc_highlight_server_diagnostics(win, server_diagnostics, style)
584 if not style then
585 style = lspc.diagnostic_style_id or win.STYLE_LEXER_MAX
586 end
587
588 local level_mapping = {
589 [1] = lspc.diagnostic_styles.error,
590 [2] = lspc.diagnostic_styles.warning,
591 [3] = lspc.diagnostic_styles.information,
592 [4] = lspc.diagnostic_styles.hint,
593 }
594
595 for _, diagnostic in ipairs(server_diagnostics) do
596 local diagnostic_style = level_mapping[diagnostic.severity] or level_mapping[1]
597 assert(win:style_define(style, diagnostic_style))
598
599 if lspc.highlight_diagnostics == 'range' then
600 local range = diagnostic.vis_range
601
602 -- LSP ranges use an exclusive finish
603 local finish = range.finish - 1
604
605 -- make sure to highlight only ranges which actually contain the diagnostic
606 if diagnostic.content == win.file:content(range) then
607 win:style(style, range.start, finish)
608 end
609
610 elseif lspc.highlight_diagnostics == 'line' then
611 if not win.style_pos then
612 lspc:err('Vis build does not support style_pos')
613 return
614 end
615
616 local start_line = diagnostic.range.start.line
617 local end_line = diagnostic.range['end'].line
618 for line = start_line, end_line, 1 do
619 local row = file_lineno_to_viewport_lineno(win, line)
620 if row then
621 -- Heuristic how many cells need to be styled
622 -- (at least one plus the decimal places of the line number).
623 for i = 0, #('' .. line) do
624 win:style_pos(style, i, row)
625 end
626 end
627 end
628 end
629 end
630 end
631
632 local function lspc_highlight_diagnostics(win, diagnostics, style)
633 for _, server_diagnostics in pairs(diagnostics) do
634 lspc_highlight_server_diagnostics(win, server_diagnostics, style)
635 end
636 end
637
638 --- LanguageServer class metatable
639 local LanguageServer = {}
640
641 --- send a RPC message to the language server
642 -- @param req The request to send
643 function LanguageServer:rpc(req)
644 req.jsonrpc = '2.0'
645
646 local content_part = lspc.json.encode(req)
647 local content_len = string.len(content_part)
648
649 local header_part = 'Content-Length: ' .. tostring(content_len)
650 local msg = header_part .. '\r\n\r\n' .. content_part
651 lspc:log('LSPC Sending -> ' .. self.name .. ': ' .. msg)
652
653 self.fd:write(msg)
654 self.fd:flush()
655 end
656
657 --- Send a RPC notification to the language server
658 -- @param name the name of the notification
659 -- @param params the parameters to send
660 function LanguageServer:send_notification(name, params)
661 self:rpc({method = name, params = params})
662 end
663
664 --- Send a textDocument/didChange notification to the language server
665 -- @param the vis file object which changed
666 function LanguageServer:send_did_change(file)
667 lspc:log('send didChange')
668 local new_version = assert(lspc.open_files[file.path]).version + 1
669 lspc.open_files[file.path].version = new_version
670
671 local document = {uri = path_to_uri(file.path), version = new_version}
672 local changes = {{text = file:content(0, file.size)}}
673 local params = {textDocument = document, contentChanges = changes}
674 self:send_notification('textDocument/didChange', params)
675 end
676
677 --- Send a rpc method call to the language server.
678 -- @param method name of remote procedure to call
679 -- @param params the parameter passed to the remote procedure
680 -- @param win the related vis window object
681 -- @param ctx a opaque context value stored with the request
682 function LanguageServer:call_method(method, params, win, ctx)
683 local id = self.id
684 self.id = self.id + 1
685
686 local req = {id = id, method = method, params = params}
687 self.inflight[id] = req
688
689 self:rpc(req)
690 -- remember the current window to apply the effects of a
691 -- method call in the original window
692 self.inflight[id].win = win
693
694 -- remember the user provided ctx value
695 -- ctx can be used to remember arbitrary data from method invocation till
696 -- method response handling
697 -- The goto-location methods remember in ctx how to open the location
698 self.inflight[id].ctx = ctx
699 end
700
701 --- Call textDocument/<method> of the server
702 -- We send a didChange notification upfront to make sure the server sees our
703 -- current state. This is not ideal since we are sending more data than needed
704 -- and the server has less time to parse the new file content and do its work
705 -- resulting in longer stalls after method invocation.
706 -- @param method the name of LSP textDocument method
707 -- @param params the parameters of the method call
708 -- @param win the related vis window object
709 -- @param ctx a opaque context value stored with the request
710 function LanguageServer:call_text_document_method(method, params, win, ctx)
711 self:send_did_change(win.file)
712 self:call_method('textDocument/' .. method, params, win, ctx)
713 end
714
715 local function lspc_handle_goto_method_response(req, result)
716 if not result or type(result) ~= 'table' or next(result) == nil then
717 lspc:warn(req.method .. ' found no results')
718 return
719 end
720
721 local location
722 -- result actually a list of results
723 if type(result) == 'table' then
724 location = lspc_select_location(result)
725 if not location then
726 return
727 end
728 else
729 location = result
730 end
731 assert(location)
732
733 -- location is a Location
734 local lsp_doc_pos
735 if location.uri then
736 lspc:log('Handle location: ' .. lspc.json.encode(location))
737 lsp_doc_pos = {
738 textDocument = {uri = location.uri},
739 position = {
740 line = location.range.start.line,
741 character = location.range.start.character,
742 },
743 }
744 -- location is a LocationLink
745 elseif location.targetUri then
746 lspc:log('Handle locationLink: ' .. lspc.json.encode(location))
747 lsp_doc_pos = {
748 textDocument = {uri = location.targetUri},
749 position = {
750 line = location.targetSelectionRange.start.line,
751 character = location.targetSelectionRange.start.character,
752 },
753 }
754 else
755 lspc:warn('Unknown location type: ' .. lspc.json.encode(location))
756 end
757
758 local doc_pos = lsp_doc_pos_to_vis(lsp_doc_pos)
759 vis_open_new_doc_pos(doc_pos, req.ctx, req.win)
760 end
761
762 local function lspc_handle_completion_method_response(win, result, old_pos)
763 if not result or type(result) ~= 'table' or not result.items then
764 lspc:warn('no completion available')
765 return
766 end
767
768 local completions = result
769 if result.isIncomplete ~= nil then
770 completions = result.items
771 end
772
773 local choices = {}
774 for _, completion in ipairs(completions) do
775 table.insert(choices, completion.label)
776 choices[completion.label] = completion
777 end
778
779 -- select a completion
780 local choice = lspc_select(choices)
781 if not choice then
782 return
783 end
784
785 local completion = choices[choice]
786
787 if completion.textEdit then
788 vis_apply_textEdit(win, win.file, completion.textEdit)
789 return
790 end
791
792 if completion.insertText or completion.label then
793 -- Does our current state correspont to the state when the completion method
794 -- was called.
795 -- Otherwise we don't have a good way to apply the 'insertText' completion
796 if win.selection.pos ~= old_pos then
797 lspc:warn('can not apply textInsert because the cursor position changed')
798 end
799
800 local new_word = completion.insertText or completion.label
801 local old_word_range = win.file:text_object_word(old_pos)
802 local old_word = win.file:content(old_word_range)
803
804 lspc:log(string.format('Completion old_pos=%d, old_range={start=%d, finish=%d}, old_word=%s',
805 old_pos, old_word_range.start, old_word_range.finish,
806 old_word:gsub('\n', '\\n')))
807
808 -- update old_word_range and old_word and return if old_word is a prefix of the completion
809 local function does_completion_apply_to_pos(pos)
810 old_word_range = win.file:text_object_word(pos)
811 old_word = win.file:content(old_word_range)
812
813 local is_prefix = new_word:sub(1, string.len(old_word)) == old_word
814 return is_prefix
815 end
816
817 -- search for a possible completion token which we should replace with this insertText
818 local matches = does_completion_apply_to_pos(old_pos)
819 if not matches then
820 lspc:log('Cursor looks like its not on the completion token')
821
822 -- try the common case the cursor is behind its completion token: fooba┃
823 local next_pos_candidate = old_pos - 1
824 matches = does_completion_apply_to_pos(next_pos_candidate)
825 if matches then
826 old_pos = next_pos_candidate
827 end
828 end
829
830 local completion_start
831 -- we found a completion token -> replace it
832 if matches then
833 lspc:log('replace the token: ' .. old_word .. ' we found being a prefix of the completion')
834 win.file:delete(old_word_range)
835 completion_start = old_word_range.start
836 else
837 completion_start = old_pos
838 end
839 -- apply insertText
840 win.file:insert(completion_start, new_word)
841 win.selection.pos = completion_start + string.len(new_word)
842 win:draw()
843 return
844 end
845
846 -- neither insertText nor textEdit where present
847 lspc:err('Unsupported completion')
848 end
849
850 local function lspc_handle_hover_method_response(win, result, old_pos)
851 if not result or type(result) ~= 'table' or not result.contents then
852 lspc:warn('no hover available')
853 return
854 end
855
856 local sel = util.vis_pos_to_sel(win, old_pos)
857
858 local hover_header =
859 '--- hover: ' .. (win.file.path or '') .. ': ' .. sel.line .. ', ' .. sel.col .. ' ---\n'
860 local hover_msg = ''
861 -- The most common markup kind in LSP is markdown
862 local markup_kind = 'markdown'
863
864 -- result is MarkedString[]
865 if type(result.contents) == 'table' and #result.contents > 0 then
866 lspc:log('hover returned list of length ' .. #result.contents)
867
868 for i, marked_string in ipairs(result.contents) do
869 if i == 1 then
870 hover_msg = marked_string.value or marked_string
871 else
872 hover_msg = hover_msg .. '\n---\n' .. (marked_string.value or marked_string)
873 end
874 end
875 else -- result is either MarkedString or MarkupContent
876 hover_msg = result.contents.value or result.contents
877 if result.contents.kind and result.contents.kind == 'plaintext' then
878 markup_kind = 'text'
879 end
880 end
881 lspc_show_message(hover_msg, hover_header, markup_kind)
882 end
883
884 local function lspc_handle_signature_help_method_response(win, result, call_pos)
885 if not result or type(result) ~= 'table' or not result.signatures or #result.signatures == 0 then
886 lspc:warn('no signature help available')
887 return
888 end
889
890 local signatures = result.signatures
891
892 local sel = util.vis_pos_to_sel(win, call_pos)
893 local help_header = '--- signature help: ' .. (win.file.path or '') .. ': ' .. sel.line .. ', ' ..
894 sel.col .. ' ---\n'
895
896 -- local help_msg = lspc.json.encode(result)
897 local help_msg = ''
898 for _, signature in ipairs(signatures) do
899 local sig_msg = signature.label
900 if signature.documentation then
901 local doc = signature.documentation.value or signature.documentation
902 sig_msg = sig_msg .. '\n\tdocumentation: ' .. doc
903 end
904 help_msg = help_msg .. '\n' .. sig_msg
905 end
906 -- strip first new line from the message
907 help_msg = help_msg:sub(2)
908 lspc_show_message(help_msg, help_header)
909 end
910
911 local function lspc_handle_rename_method_response(win, result)
912 -- result must always be valid because otherwise we would caught the error
913 -- in LanguageServer:handle_method_response
914 vis_apply_workspaceEdit(win, win.file, result)
915 end
916
917 local function lspc_handle_formatting_method_response(win, result)
918 -- The result of textDocument/formatting is defined as TextEdit[] | null
919 if result then
920 vis_apply_textEdits(win, win.file, result)
921 end
922 end
923
924 local function lspc_handle_initialize_response(ls, result)
925 ls.initialized = true
926 ls.capabilities = result.capabilities
927
928 local params = {}
929 setmetatable(params, {__jsontype = 'object'})
930 ls:send_notification('initialized', params)
931
932 -- According to nvim-lspconfig sendig the lsp server settings shortly after
933 -- initialization is an undocumented convention.
934 -- See https://github.com/neovim/nvim-lspconfig/blob/ed88435764d8b00442e66d39ec3d9c360e560783/CONTRIBUTING.md
935 if ls.settings then
936 ls:send_notification('workspace/didChangeConfiguration', {
937 settings = ls.settings,
938 })
939 end
940
941 vis.events.emit(lspc.events.LS_INITIALIZED, ls)
942 end
943
944 --- Dispatch method response from the server
945 -- @param method_response the response send from the server
946 -- @param req the request causing this response
947 function LanguageServer:handle_method_response(method_response, req)
948 local win = req.win
949
950 local method = req.method
951
952 local err = method_response.error
953 if err then
954 local err_msg = err.message
955 local err_code = err.code
956 lspc:err(err_msg .. ' (' .. err_code .. ') occurred during ' .. method)
957 -- Don't try to handle error responses any further
958 return
959 end
960
961 local result = method_response.result
962
963 -- LuaFormatter off
964 if method == 'textDocument/definition' or
965 method == 'textDocument/declaration' or
966 method == 'textDocument/typeDefinition' or
967 method == 'textDocument/implementation' or
968 method == 'textDocument/references' then
969 -- LuaFormatter on
970 lspc_handle_goto_method_response(req, result)
971
972 elseif method == 'initialize' then
973 lspc_handle_initialize_response(self, result)
974
975 elseif method == 'textDocument/completion' then
976 lspc_handle_completion_method_response(win, result, req.ctx)
977
978 elseif method == 'textDocument/hover' then
979 lspc_handle_hover_method_response(win, result, req.ctx)
980
981 elseif method == 'textDocument/signatureHelp' then
982 lspc_handle_signature_help_method_response(win, result, req.ctx)
983
984 elseif method == 'textDocument/rename' then
985 lspc_handle_rename_method_response(win, result)
986
987 elseif method == 'textDocument/formatting' then
988 lspc_handle_formatting_method_response(win, result)
989
990 elseif method == 'shutdown' then
991 self:send_notification('exit')
992 self.fd:close()
993
994 -- remove the ls from lspc.running
995 for ls_name, rls in pairs(lspc.running) do
996 if self == rls then
997 lspc.running[ls_name] = nil
998 break
999 end
1000 end
1001 else
1002 lspc:warn('received unknown method ' .. method)
1003 end
1004
1005 self.inflight[method_response.id] = nil
1006 end
1007
1008 local function lspc_handle_workspace_configuration_call(ls, params, response)
1009 local results = {}
1010 for _, item in ipairs(params.items) do
1011 local t = ls.settings
1012 for k in item.section:gmatch('[^.]+') do
1013 if not t then
1014 break
1015 end
1016 t = t[k]
1017 end
1018 table.insert(results, t or lspc.json.null)
1019 end
1020 response.result = results
1021 end
1022
1023 --- Handle a method call from the server
1024 -- @param method_call the received method call
1025 function LanguageServer:handle_method_call(method_call)
1026 local method = method_call.method
1027 local response = {id = method_call.id}
1028 if method == 'workspace/configuration' then
1029 lspc_handle_workspace_configuration_call(self, method_call.params, response)
1030 else
1031 lspc:log('Unknown method call ' .. method)
1032 response['error'] = {
1033 code = jsonrpc.error_codes.MethodNotFound,
1034 message = method .. ' not implemented',
1035 }
1036 end
1037 self:rpc(response)
1038 end
1039
1040 -- save the diagnostics received for a file uri
1041 local function lspc_handle_publish_diagnostics(ls, uri, diagnostics)
1042 local file_path = uri_to_path(uri)
1043 local file = lspc.open_files[file_path]
1044 if file then
1045 for _, diagnostic in ipairs(diagnostics) do
1046 -- We convert the lsp_range to a vis_range here to do it only once.
1047 -- It's an expensive operation that involves counting all newlines.
1048 diagnostic.vis_range = lsp_range_to_vis_range(file.file, diagnostic.range)
1049
1050 -- In some instances the range defined by the diagnostic starts
1051 -- and ends at the same position. Highlight the exact position.
1052 if diagnostic.vis_range.finish == diagnostic.vis_range.start then
1053 -- We fake a one char range to retrieve its content.
1054 -- In highlight_diagnostics we inconditionally decrement finish anyway.
1055 diagnostic.vis_range.finish = diagnostic.vis_range.finish + 1
1056 end
1057
1058 -- Remember the content of the diagnostic to only highlight it if the content
1059 -- did not change
1060 diagnostic.content = vis.win.file:content(diagnostic.vis_range)
1061 end
1062
1063 file.diagnostics[ls] = diagnostics
1064
1065 lspc:log('remembered ' .. #diagnostics .. ' diagnostics for ' .. file_path)
1066 else
1067 lspc:log('Diagnostics for not opened file' .. file_path)
1068 end
1069 end
1070
1071 local lsp_message_types = {'Error', 'Warning', 'Info', 'Log'}
1072 -- show a message from the server in the UI
1073 local function lspc_handle_show_message(show_message_params)
1074 if show_message_params.type > lspc.message_level then
1075 return
1076 end
1077
1078 vis:message('--- language server message ---')
1079 local level = lsp_message_types[show_message_params.type] or 'Unknown'
1080 vis:message(level .. ': ' .. show_message_params.message)
1081 end
1082
1083 --- Handle a notification received from the server
1084 -- @param notification the received notification
1085 function LanguageServer:handle_notification(notification)
1086 local method = notification.method
1087 if method == 'textDocument/publishDiagnostics' then
1088 lspc_handle_publish_diagnostics(self, notification.params.uri, notification.params.diagnostics)
1089 elseif method == 'window/showMessage' then
1090 lspc_handle_show_message(notification.params)
1091 end
1092 end
1093
1094 --- Dispatch between a method call and a message response
1095 -- Those are distinquiable because for a message response we have a req
1096 -- remembered in the inflight table
1097 -- @param method the method message received from the server
1098 function LanguageServer:handle_method(method)
1099 local req = self.inflight[method.id]
1100 if req and not method.method then
1101 self:handle_method_response(method, req)
1102 else
1103 self:handle_method_call(method)
1104 end
1105 end
1106
1107 --- Dispatch between a method call/response and a notification from the server
1108 -- @param msg the message received from the server
1109 function LanguageServer:handle_msg(msg)
1110 if msg.id then
1111 self:handle_method(msg)
1112 else
1113 self:handle_notification(msg)
1114 end
1115 end
1116
1117 -- Parse the data send by the language server
1118 -- Note the chunks received may not end with the end of a message.
1119 -- In the worst case a data chunk contains two partial messages on at the beginning
1120 -- and one at the end
1121 function LanguageServer:recv_data(data)
1122 local err = self.parser:add(data)
1123 if err then
1124 lspc:err(err)
1125 return
1126 end
1127
1128 local msgs = self.parser:get_msgs()
1129 if not msgs then
1130 return
1131 end
1132
1133 for _, msg in ipairs(msgs) do
1134 local resp = lspc.json.decode(msg)
1135 self:handle_msg(resp)
1136 end
1137 end
1138
1139 -- check if a language server is running and initialized
1140 local function lspc_get_usable_ls(win, explicit_syntax)
1141 local ls
1142 local syntax = explicit_syntax or (win and win.syntax)
1143 -- try to use the first language server managing the current file
1144 if not syntax then
1145 if win and win.file and lspc.open_files[win.file.path] and
1146 next(lspc.open_files[win.file.path].language_servers) then
1147 ls = next(lspc.open_files[win.file.path].language_servers)
1148
1149 else -- there is no language server with this file open and we have no syntax to guess
1150 return nil, 'No syntax provided and no server is running'
1151 end
1152
1153 else -- Use the syntax to guess the language server
1154 local ls_name, err = get_ls_name_for_syntax(syntax)
1155 if err then
1156 return nil, err
1157 end
1158
1159 ls = lspc.running[ls_name]
1160 if not ls then
1161 return nil, 'No language server running for ' .. syntax
1162 end
1163 end
1164
1165 if not ls.initialized then
1166 return nil, 'Language server ' .. ls.name .. ' not initialized yet. Please try again'
1167 end
1168
1169 return ls
1170 end
1171
1172 local function lspc_new_file_handle(file)
1173 return {file = file, version = 0, diagnostics = {}, language_servers = {}}
1174 end
1175
1176 --- Detect if a file is already opened by the language server
1177 -- @param file the vis file object to check
1178 -- @return true if the file is already opened by the language server
1179 function LanguageServer:is_file_opened(file)
1180 return lspc.open_files[file.path] and lspc.open_files[file.path].language_servers[self]
1181 end
1182
1183 -- close the file if associated with the language server
1184 local function lspc_close(ls, file)
1185 if not ls:is_file_opened(file) then
1186 return (file.path or '[No Name]') .. ' not open in ' .. ls.name
1187 end
1188 ls:send_notification('textDocument/didClose', {
1189 textDocument = {uri = path_to_uri(file.path)},
1190 })
1191 lspc.open_files[file.path].language_servers[ls] = nil
1192 if not next(lspc.open_files[file.path].language_servers) then
1193 lspc.open_files[file.path] = nil
1194 end
1195 end
1196
1197 -- register a file as open with a language server and setup close and save event handlers
1198 -- A file must be opened before any textDocument functions can be used with it.
1199 local function lspc_open(ls, win, file)
1200 -- already opened
1201 if ls:is_file_opened(file) then
1202 return file.path .. ' already open in ' .. ls.name
1203 end
1204
1205 local lspc_file_handle = lspc.open_files[file.path] or lspc_new_file_handle(file)
1206 lspc_file_handle.language_servers[ls] = true
1207 lspc.open_files[file.path] = lspc_file_handle
1208
1209 local params = {
1210 textDocument = {
1211 uri = 'file://' .. file.path,
1212 languageId = syntax_to_languageId(win.syntax),
1213 version = 0,
1214 text = file:content(0, file.size),
1215 },
1216 }
1217
1218 ls:send_notification('textDocument/didOpen', params)
1219
1220 vis.events.emit(lspc.events.LS_DID_OPEN, ls, file)
1221 end
1222
1223 --- Initiate the shutdown of the language server
1224 -- Sending the exit notification and closing the file handle are done in
1225 -- the shutdown response handler.
1226 function LanguageServer:shutdown()
1227 self:call_method('shutdown')
1228 end
1229
1230 --- Find the root project URI for a specific file
1231 -- @param ls the language server
1232 -- @param file_path the path to the file to find the root project of
1233 -- @return the URI of the root project or nil if none was found
1234 local function find_root_uri(ls, file_path)
1235 local globs = ''
1236
1237 local roots = ls.roots
1238 if roots then
1239 for _, glob in ipairs(roots) do
1240 globs = globs .. glob .. '\n'
1241 end
1242 end
1243
1244 if lspc.universal_root_globs then
1245 for _, glob in ipairs(lspc.universal_root_globs) do
1246 globs = globs .. glob .. '\n'
1247 end
1248 end
1249
1250 local root_path = util.find_upwards(globs, file_path)
1251 if not root_path and lspc.fallback_dirname_as_root then
1252 root_path = util.dirname(file_path)
1253 end
1254 return root_path and path_to_uri(root_path)
1255 end
1256
1257 local function ls_start(ls, init_options)
1258 ls.fd = vis:communicate(ls.name, 'exec ' .. ls.cmd)
1259
1260 -- register the response handler
1261 vis.events.subscribe(vis.events.PROCESS_RESPONSE, function(name, event, code, msg)
1262 if name ~= ls.name then
1263 return
1264 end
1265
1266 if event == 'EXIT' or event == 'SIGNAL' then
1267 if event == 'EXIT' then
1268 vis:info('language server exited with: ' .. code)
1269 else
1270 vis:info('language server received signal: ' .. code)
1271 end
1272
1273 lspc.running[ls.name] = nil
1274 return
1275 end
1276
1277 lspc:log(ls.name .. ' response(' .. event .. '): ' .. msg)
1278 if event == 'STDERR' then
1279 return
1280 end
1281
1282 ls:recv_data(msg)
1283 end)
1284
1285 local params = {
1286 processId = vis_pid,
1287 clientInfo = {name = lspc.name, version = lspc.version},
1288 rootUri = (vis.win.file and find_root_uri(ls, vis.win.file.path)) or lspc.json.null,
1289 capabilities = lspc.client_capabilites,
1290 }
1291
1292 if init_options then
1293 params.initializationOptions = init_options
1294 end
1295
1296 ls:call_method('initialize', params)
1297 end
1298
1299 function LanguageServer.new(ls_conf)
1300 local ls = {
1301 name = ls_conf.name,
1302 cmd = ls_conf.cmd,
1303 settings = ls_conf.settings,
1304 roots = ls_conf.roots,
1305 formatting_options = ls_conf.formatting_options,
1306 initialized = false,
1307 id = 0,
1308 inflight = {},
1309 parser = parser.new(),
1310 capabilities = {},
1311 }
1312 setmetatable(ls, {__index = LanguageServer})
1313
1314 return ls
1315 end
1316
1317 local function lspc_start_server(syntax)
1318 local ls_conf = lspc.ls_map[syntax]
1319 if not ls_conf then
1320 return nil, 'No language server available for ' .. syntax
1321 end
1322
1323 local exe = ls_conf.cmd:gmatch('%S+')()
1324 if not os.execute('type ' .. exe .. '>/dev/null 2>/dev/null') then
1325 -- remove the configured language server
1326 lspc.ls_map[syntax] = nil
1327 local msg = string.format('Language server for %s configured but %s not found', syntax, exe)
1328 -- the warning will be visual if the language server was automatically startet
1329 -- if the user tried to start teh server manually they will see msg as error
1330 lspc:warn(msg)
1331 return nil, msg
1332 end
1333
1334 if lspc.running[ls_conf.name] then
1335 return nil, 'Already a language server running for ' .. syntax
1336 end
1337
1338 local ls = LanguageServer.new(ls_conf)
1339 lspc.running[ls_conf.name] = ls
1340 ls_start(ls, ls_conf.init_options)
1341
1342 return ls
1343 end
1344
1345 -- generic stub implementation for all textDocument methods taking
1346 -- a textDocumentPositionParams parameter
1347 local function lspc_method_doc_pos(ls, method, win, argv, additional_params)
1348 -- check if the language server has a provider for this method
1349 if not ls.capabilities[method .. 'Provider'] then
1350 return 'language server ' .. ls.name .. ' does not provide ' .. method
1351 end
1352
1353 if not ls:is_file_opened(win.file) then
1354 lspc_open(ls, win, win.file)
1355 end
1356
1357 local params = vis_doc_pos_to_lsp(vis_get_doc_pos(win))
1358 if additional_params then
1359 for k, v in pairs(additional_params) do
1360 params[k] = v
1361 end
1362 end
1363
1364 ls:call_text_document_method(method, params, win, argv)
1365 end
1366
1367 local lspc_goto_location_methods = {
1368 declaration = function(ls, win, open_cmd)
1369 return lspc_method_doc_pos(ls, 'declaration', win, open_cmd)
1370 end,
1371 definition = function(ls, win, open_cmd)
1372 return lspc_method_doc_pos(ls, 'definition', win, open_cmd)
1373 end,
1374 typeDefinition = function(ls, win, open_cmd)
1375 return lspc_method_doc_pos(ls, 'typeDefinition', win, open_cmd)
1376 end,
1377 implementation = function(ls, win, open_cmd)
1378 return lspc_method_doc_pos(ls, 'implementation', win, open_cmd)
1379 end,
1380 references = function(ls, win, open_cmd)
1381 return lspc_method_doc_pos(ls, 'references', win, open_cmd,
1382 {context = {includeDeclaration = false}})
1383 end,
1384 }
1385
1386 local function has_diagnostics(file)
1387 if not file or not file.diagnostics then
1388 return false
1389 end
1390
1391 -- detect if at least one server has published diagnostics
1392 for _, d in pairs(file.diagnostics) do
1393 if #d then
1394 return true
1395 end
1396 end
1397
1398 return false
1399 end
1400
1401 local function lspc_goto_next_diagnostic(win, reverse)
1402 if not lspc.open_files[win.file.path] then
1403 vis:command('lspc-open')
1404 end
1405
1406 local open_file = lspc.open_files[win.file.path]
1407
1408 if not has_diagnostics(open_file) then
1409 return (win.file.path or 'window') .. ' has no available diagnostics'
1410 end
1411
1412 -- merge diagnostics
1413 -- TODO: come up with more efficient algorithm
1414 local diagnostics = {}
1415 for _, server_diagnostics in pairs(open_file.diagnostics) do
1416 for _, diagnostic in ipairs(server_diagnostics) do
1417 table.insert(diagnostics, diagnostic)
1418 end
1419 end
1420 -- sort the merged diagnostics
1421 table.sort(diagnostics, function(d1, d2)
1422 return lsp_range_starts_before(d1.range, d2.range)
1423 end)
1424
1425 local sel = get_selection(win)
1426
1427 local previous_diagnostic
1428 for _, diagnostic in ipairs(diagnostics) do
1429 local start = lsp_pos_to_vis_sel(diagnostic.range.start)
1430 local fin = lsp_pos_to_vis_sel(diagnostic.range['end'])
1431
1432 -- reverse
1433 if reverse and
1434 (start.line > sel.line or
1435 (start.line == sel.line and (start.col >= sel.col or sel.col <= fin.col))) then
1436
1437 -- wrap around
1438 if not previous_diagnostic then
1439 previous_diagnostic = lsp_pos_to_vis_sel(diagnostics[#diagnostics].range.start)
1440 end
1441
1442 win.selection:to(previous_diagnostic.line, previous_diagnostic.col)
1443 return
1444 end
1445
1446 -- forward
1447 if start.line > sel.line or (start.line == sel.line and start.col > sel.col) then
1448 win.selection:to(start.line, start.col)
1449 return
1450 end
1451
1452 previous_diagnostic = start
1453 end
1454
1455 -- wrap around
1456 if #diagnostics > 0 then
1457 local first = lsp_pos_to_vis_sel(diagnostics[1].range.start)
1458 win.selection:to(first.line, first.col)
1459 end
1460 end
1461
1462 local function lspc_show_diagnostic(win, line)
1463 if not lspc.open_files[win.file.path] then
1464 vis:command('lspc-open')
1465 end
1466 local file = lspc.open_files[win.file.path]
1467
1468 if not has_diagnostics(file) then
1469 return win.file.path .. ' has no diagnostics available'
1470 end
1471 local diagnostics = file.diagnostics
1472
1473 line = line or get_selection(win).line
1474 lspc:log('Show diagnostics for ' .. line)
1475 local diagnostics_to_show = {}
1476 for ls, server_diagnostics in pairs(diagnostics) do
1477 for _, diagnostic in ipairs(server_diagnostics) do
1478 local start = lsp_pos_to_vis_sel(diagnostic.range.start)
1479 if start.line == line then
1480 diagnostic.start = start
1481 diagnostic.server = ls.name
1482 table.insert(diagnostics_to_show, diagnostic)
1483 end
1484 end
1485 end
1486
1487 local diagnostics_fmt = '%s: %d:%d %s:%s\n'
1488 local diagnostics_msg = ''
1489 for _, diagnostic in ipairs(diagnostics_to_show) do
1490 diagnostics_msg = diagnostics_msg ..
1491 string.format(diagnostics_fmt, diagnostic.server, diagnostic.start.line,
1492 diagnostic.start.col, diagnostic.code or 'diagnostic',
1493 diagnostic.message)
1494 end
1495
1496 if diagnostics_msg ~= '' then
1497 lspc_show_message(diagnostics_msg)
1498 else
1499 lspc:warn('No diagnostics available for line: ' .. line)
1500 end
1501 end
1502
1503 -- vis-lspc commands
1504
1505 vis:command_register('lspc-back', function()
1506 local err = vis_pop_doc_pos()
1507 if err then
1508 lspc:err(err)
1509 end
1510 end)
1511
1512 for name, func in pairs(lspc_goto_location_methods) do
1513 vis:command_register('lspc-' .. name, function(argv, _, win)
1514 local ls, err = lspc_get_usable_ls(win, argv[1])
1515 if err then
1516 lspc:err(err)
1517 return
1518 end
1519 assert(ls)
1520
1521 -- vis cmd how to open the new location
1522 -- 'e' (default): in same window
1523 -- 'vsplit': in a vertical split window
1524 -- 'hsplit': in a horizontal split window
1525 local open_cmd = argv[1] or 'e'
1526 err = func(ls, win, open_cmd)
1527 if err then
1528 lspc:err(err)
1529 end
1530 end)
1531 end
1532
1533 vis:command_register('lspc-hover', function(argv, _, win)
1534 local ls, err = lspc_get_usable_ls(win, argv[1])
1535 if err then
1536 lspc:err(err)
1537 return
1538 end
1539 assert(ls)
1540
1541 -- remember the position where hover was called
1542 err = lspc_method_doc_pos(ls, 'hover', win, win.selection.pos)
1543 if err then
1544 lspc:err(err)
1545 end
1546 end)
1547
1548 vis:command_register('lspc-signature-help', function(argv, _, win)
1549 local ls, err = lspc_get_usable_ls(win, argv[1])
1550 if err then
1551 lspc:err(err)
1552 return
1553 end
1554 assert(ls)
1555
1556 -- remember the position where signatureHelp was called
1557 err = lspc_method_doc_pos(ls, 'signatureHelp', win, win.selection.pos)
1558 if err then
1559 lspc:err(err)
1560 end
1561 end)
1562
1563 vis:command_register('lspc-rename', function(argv, _, win)
1564 local new_name = argv[1]
1565 if not new_name then
1566 lspc:err('lspc-rename usage: <new name> [syntax]')
1567 return
1568 end
1569
1570 local ls, err = lspc_get_usable_ls(win, argv[2])
1571 if err then
1572 lspc:err(err)
1573 return
1574 end
1575 assert(ls)
1576
1577 -- check if the language server has a provider for this method
1578 if not ls.capabilities['renameProvider'] then
1579 lspc:err('language server ' .. ls.name .. ' does not provide rename')
1580 return
1581 end
1582
1583 if not ls:is_file_opened(win.file.path) then
1584 lspc_open(ls, win, win.file)
1585 end
1586
1587 local params = vis_doc_pos_to_lsp(vis_get_doc_pos(win))
1588 params.newName = new_name
1589
1590 ls:call_text_document_method('rename', params, win)
1591 end)
1592
1593 vis:command_register('lspc-format', function(argv, _, win)
1594 local ls, err = lspc_get_usable_ls(win, argv[1])
1595 if err then
1596 lspc:err(err)
1597 return
1598 end
1599 assert(ls)
1600
1601 -- check if the language server has a provider for this method
1602 if not ls.capabilities['documentFormattingProvider'] then
1603 lspc:err('language server ' .. ls.name .. ' does not provide formatting')
1604 return
1605 end
1606
1607 if not lspc.open_files[win.file.path] then
1608 lspc_open(ls, win, win.file)
1609 end
1610
1611 local params = {
1612 textDocument = {uri = path_to_uri(win.file.path)},
1613 options = ls.formatting_options,
1614 }
1615 if params.options == nil then
1616 params.options = {
1617 tabSize = win.options.tabwidth,
1618 insertSpaces = win.options.expandtab,
1619 }
1620 end
1621
1622 ls:call_text_document_method('formatting', params, win)
1623 end)
1624
1625 vis:command_register('lspc-completion', function(argv, _, win)
1626 local ls, err = lspc_get_usable_ls(win, argv[1])
1627 if err then
1628 lspc:err(err)
1629 return
1630 end
1631 assert(ls)
1632
1633 -- remember the position where completions were requested
1634 -- to apply insertText completions
1635 err = lspc_method_doc_pos(ls, 'completion', win, win.selection.pos)
1636 if err then
1637 lspc:err(err)
1638 end
1639 end)
1640
1641 vis:command_register('lspc-start-server', function(argv, _, win)
1642 local syntax = argv[1] or win.syntax
1643 if not syntax then
1644 lspc:err('no language specified')
1645 end
1646
1647 local _, err = lspc_start_server(syntax)
1648 if err then
1649 lspc:err(err)
1650 end
1651 end)
1652
1653 vis:command_register('lspc-shutdown-server', function(argv, _, win)
1654 local ls, err = lspc_get_usable_ls(win, argv[1])
1655 if err then
1656 lspc:err('no language server running: ' .. err)
1657 return
1658 end
1659 assert(ls)
1660
1661 ls:shutdown()
1662 end)
1663
1664 vis:command_register('lspc-close', function(argv, _, win)
1665 local ls, err = lspc_get_usable_ls(win, argv[1])
1666 if err then
1667 lspc:err(err)
1668 return
1669 end
1670 assert(ls)
1671
1672 lspc_close(ls, win.file)
1673 end)
1674
1675 vis:command_register('lspc-open', function(argv, _, win)
1676 local ls, err = lspc_get_usable_ls(win, argv[1])
1677 if err then
1678 lspc:err(err)
1679 return
1680 end
1681 assert(ls)
1682
1683 lspc_open(ls, win, win.file)
1684 end)
1685
1686 local function _lspc_next_diagnostic(win, reverse)
1687 local err = lspc_goto_next_diagnostic(win, reverse)
1688 if err then
1689 lspc:err(err)
1690 end
1691 end
1692
1693 vis:command_register('lspc-next-diagnostic', function(_, _, win)
1694 _lspc_next_diagnostic(win, false)
1695 end)
1696
1697 vis:command_register('lspc-prev-diagnostic', function(_, _, win)
1698 _lspc_next_diagnostic(win, true)
1699 end)
1700
1701 vis:command_register('lspc-show-diagnostics', function(argv, _, win)
1702 local err = lspc_show_diagnostic(win, argv[1])
1703 if err then
1704 lspc:err(err)
1705 end
1706 end)
1707
1708 -- vis-lspc event hooks
1709 vis.events.subscribe(vis.events.WIN_OPEN, function(win)
1710 if lspc.autostart and win.syntax then
1711 lspc_start_server(win.syntax)
1712 end
1713 end)
1714
1715 local function highlight_event()
1716 local win = vis.win
1717 if not win or not win.file then
1718 return
1719 end
1720
1721 local ls = lspc_get_usable_ls(win)
1722 if not ls then
1723 return
1724 end
1725
1726 local open_file = lspc.open_files[win.file.path]
1727 if open_file and open_file.diagnostics and lspc.highlight_diagnostics then
1728 lspc_highlight_diagnostics(win, open_file.diagnostics)
1729 end
1730 end
1731
1732 vis.events.subscribe(vis.events.WIN_HIGHLIGHT, highlight_event)
1733 vis.events.subscribe(vis.events.UI_DRAW, highlight_event)
1734
1735 vis.events.subscribe(vis.events.FILE_OPEN, function(file)
1736 local win = vis.win
1737 -- the only window we can access is not our window
1738 if not win or win.file ~= file then
1739 return
1740 end
1741
1742 local ls = lspc_get_usable_ls(win)
1743 if not ls then
1744 return
1745 end
1746
1747 lspc_open(ls, win, file)
1748 end)
1749
1750 vis.events.subscribe(vis.events.FILE_SAVE_POST, function(file, path)
1751 if not vis.win or vis.win.file ~= file then
1752 return
1753 end
1754
1755 local file_handle = lspc.open_files[path]
1756 if not file_handle then
1757 return
1758 end
1759
1760 for ls in pairs(file_handle.language_servers) do
1761 ls:send_did_change(file)
1762 -- the server is interested in didSave notifications
1763 if ls.capabilities.textDocumentSync and type(ls.capabilities.textDocumentSync) == 'table' and
1764 ls.capabilities.textDocumentSync.save then
1765 local did_save_params = {textDocument = {uri = path_to_uri(file.path)}}
1766 ls:send_notification('textDocument/didSave', did_save_params)
1767 end
1768 end
1769 end)
1770
1771 vis.events.subscribe(vis.events.FILE_CLOSE, function(closed_file)
1772 local file_handle = lspc.open_files[closed_file.path]
1773 if not file_handle then
1774 return
1775 end
1776
1777 for ls in pairs(file_handle.language_servers) do
1778 lspc_close(ls, closed_file)
1779 end
1780 end)
1781
1782 vis.events.subscribe(lspc.events.LS_INITIALIZED, function(ls)
1783 if vis.win and vis.win.file and lspc_get_usable_ls(vis.win) == ls then
1784 lspc_open(ls, vis.win, vis.win.file)
1785 end
1786 end)
1787
1788 vis.events.subscribe(vis.events.QUIT, function()
1789 for _, ls in pairs(lspc.running) do
1790 -- attempt to gracefully shutdown the language server
1791 ls:shutdown()
1792 -- close the fd handle to terminate the subprocess because
1793 -- a potential method response from the server will not be read after the
1794 -- QUIT event
1795 ls.fd:close()
1796 end
1797 end)
1798
1799 vis:option_register('lspc-highlight-diagnostics', 'string', function(value)
1800 lspc.highlight_diagnostics = value
1801 return true
1802 end, 'How should lspc highlight available diagnostics')
1803
1804 vis:option_register('lspc-menu-cmd', 'string', function(value)
1805 lspc.menu_cmd = value
1806 return true
1807 end, 'External tool vis-lspc uses to present choices in a menu')
1808
1809 vis:option_register('lspc-confirm-cmd', 'string', function(value)
1810 lspc.confirm_cmd = value
1811 return true
1812 end, 'External tool vis-lspc uses to ask the user for confirmation')
1813
1814 vis:option_register('lspc-message-level', 'number', function(value)
1815 lspc.message_level = value
1816 return true
1817 end, 'Message level to show in UI (for server messages)')
1818
1819 vis:option_register('lspc-diagnostic-style-error', 'string', function(value)
1820 lspc.diagnostic_styles.error = value
1821 end, 'Style for diagnostic errors')
1822
1823 vis:option_register('lspc-diagnostic-style-warning', 'string', function(value)
1824 lspc.diagnostic_styles.warning = value
1825 end, 'Style for diagnostic warnings')
1826
1827 vis:option_register('lspc-diagnostic-style-information', 'string', function(value)
1828 lspc.diagnostic_styles.information = value
1829 end, 'Style for diagnostic information')
1830
1831 vis:option_register('lspc-diagnostic-style-hint', 'string', function(value)
1832 lspc.diagnostic_styles.hint = value
1833 end, 'Style for diagnostic hints')
1834
1835 dofile(source_path .. 'bindings.lua')
1836 return lspc