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