diff options
Diffstat (limited to 'init.lua')
| -rw-r--r-- | init.lua | 404 |
1 files changed, 251 insertions, 153 deletions
@@ -3,7 +3,7 @@ -- We require vis compiled with the communicate patch if not vis.communicate then vis:info('Error: language server support requires vis communicate patch') - return nil + return {} end local function load_fallback_json() @@ -61,13 +61,22 @@ lspc = { name = 'vis-lspc', version = '0.0.1', -- write log messages to lspc.log_file - logging = true, + logging = false, log_file = 'vis-lspc.log', log = init_logging, -- automatically start a language server when a new window is opened autostart = true, + -- program used to let the user make choices + -- The available choices are pass to <menu_cmd> on stdin separated by '\n' + menu_cmd = 'vis-menu', } +-- check if fzf is available and use fzf instead of vis-menu per default +if os.execute('type fzf >/dev/null 2>/dev/null') then + lspc.menu_cmd = 'fzf' +end + +-- clangd language server configuration local clangd = {name = 'clangd', cmd = 'clangd'} -- map of known language servers per syntax @@ -78,7 +87,7 @@ lspc.ls_map = {cpp = clangd, ansi_c = clangd} -- A document positions is defined by a file path and a (line, column) tuple -- We use 1-based indices like vis and lua. --- Note line and col numbers must be transformed into 0-based inices when communicating +-- Note line and col numbers must be transformed into 0-based indices when communicating -- with a language server local function path_to_uri(path) @@ -139,46 +148,41 @@ local function vis_get_doc_pos(win) } end +-- open a doc_pos using the vis command <cmd> +local function vis_open_doc_pos(doc_pos, cmd) + assert(cmd) + vis:command(string.format('%s \'%s\'', cmd, doc_pos.file)) + vis.win.selection:to(doc_pos.line, doc_pos.col) +end + -- Support jumping between document positions +-- Stack of edited document positions +local doc_pos_history = {} + -- Open a document position in the active window -local vis_push_doc_pos --- Restore the last text position in the active window -local vis_pop_doc_pos -do - -- stack of edited document positions - local doc_pos_stack = {} +local function vis_push_doc_pos(win) + local old_doc_pos = vis_get_doc_pos(win) + table.insert(doc_pos_history, old_doc_pos) +end - local function vis_goto_doc_pos(doc_pos) - vis:command('e ' .. doc_pos.file) - vis.win.selection:to(doc_pos.line, doc_pos.col) +-- open a new doc_pos remembering the old if it is replaced +local function vis_open_new_doc_pos(doc_pos, cmd) + if cmd == 'e' then + vis_push_doc_pos(vis.win, doc_pos) end - vis_push_doc_pos = function(doc_pos) - local old_doc_pos = {} - old_doc_pos.file = vis.win.file.path - old_doc_pos.line = vis.win.selection.line - old_doc_pos.col = vis.win.selection.col + vis_open_doc_pos(doc_pos, cmd) +end - table.insert(doc_pos_stack, old_doc_pos) - vis_goto_doc_pos(doc_pos) +local function vis_pop_doc_pos() + local last_doc_pos = table.remove(doc_pos_history) + if not last_doc_pos then + return 'Document history is empty' end - vis_pop_doc_pos = function() - local last_doc_pos = table.remove(doc_pos_stack) - if not last_doc_pos then - return - end - vis_goto_doc_pos(last_doc_pos) - - -- reopen possibly closed file - vis:command('lspc-open') - end + vis_open_doc_pos(last_doc_pos, 'e') end -vis:command_register('lspc-back', function() - vis_pop_doc_pos() -end) - -- apply a textEdit received from the language server local function vis_apply_textEdit(win, file, textEdit) assert(win.file == file) @@ -203,6 +207,59 @@ local function vis_apply_textEdit(win, file, textEdit) win:draw() end +-- concatenate all numeric values in choices and pass it on stdin to lspc.menu_cmd +local function lspc_select(choices) + local menu_input = '' + local i = 0 + for _, c in ipairs(choices) do + i = i + 1 + menu_input = menu_input .. c .. '\n' + end + + -- select the only possible choice + if i < 2 then + return choices[1] + end + + local cmd = 'printf "' .. menu_input .. '" | ' .. lspc.menu_cmd + lspc.log('collect choice using: ' .. cmd) + local menu = io.popen(cmd) + local output = menu:read('*a') + local _, _, status = menu:close() + + local choice = nil + if status == 0 then + -- trim newline from selection + if output:sub(-1) == '\n' then + choice = output:sub(1, -2) + else + choice = output + end + end + + vis:redraw() + return choice +end + +local function lspc_select_location(locations) + local choices = {} + for _, location in ipairs(locations) do + local path = uri_to_path(location.uri) + local position = lsp_pos_to_vis(location.range.start) + local choice = path .. ':' .. position.line .. ':' .. position.col + table.insert(choices, choice) + choices[choice] = location + end + + -- select a location + local choice = lspc_select(choices) + if not choice then + return nil + end + + return choices[choice] +end + -- send a rpc message to a language server local function ls_rpc(ls, req) req.jsonrpc = '2.0' @@ -235,7 +292,7 @@ local function ls_send_did_change(ls, file) end -- send a rpc method call to a language server -local function ls_call_method(ls, method, params, win) +local function ls_call_method(ls, method, params, win, ctx) local id = ls.id ls.id = ls.id + 1 @@ -246,6 +303,12 @@ local function ls_call_method(ls, method, params, win) -- remember the current window to apply the effects of a -- method call in the original window ls.inflight[id].win = win + + -- remember the user provided ctx value + -- ctx can be used to remember arbitrary data from method invocation till + -- method response handling + -- The goto-location methods remember in ctx how to open the location + ls.inflight[id].ctx = ctx end -- call textDocument/<method> and send a didChange notification upfront @@ -253,22 +316,24 @@ end -- This is not ideal since we are sending more data than needed and -- the server has less time to parse the new file content and do its work -- resulting in longer stalls after method invocation. -local function ls_call_text_document_method(ls, method, params, win) +local function ls_call_text_document_method(ls, method, params, win, ctx) ls_send_did_change(ls, win.file) - ls_call_method(ls, 'textDocument/' .. method, params, win) + ls_call_method(ls, 'textDocument/' .. method, params, win, ctx) end -local function lspc_handle_goto_method_response(method, result) +local function lspc_handle_goto_method_response(req, result, win) if not result or next(result) == nil then - vis:info('LSPC Warning: ' .. method .. ' found no results') + vis:info('LSPC Warning: ' .. req.method .. ' found no results') return end local location -- result is no plain Location -> it must be Location[] if not result.uri then - -- Use the first result. TODO: smarter result selection - location = result[1] + location = lspc_select_location(result, win) + if not location then + return + end else location = result end @@ -282,7 +347,7 @@ local function lspc_handle_goto_method_response(method, result) }, } - vis_push_doc_pos(lsp_doc_pos_to_vis(lsp_doc_pos)) + vis_open_new_doc_pos(lsp_doc_pos_to_vis(lsp_doc_pos), req.ctx) end local function lspc_handle_completion_method_response(win, result) @@ -296,30 +361,21 @@ local function lspc_handle_completion_method_response(win, result) completions = result.items end - local vis_menu_input = '' + local choices = {} for _, completion in ipairs(completions) do - vis_menu_input = vis_menu_input .. completion.label .. '\n' + table.insert(choices, completion.label) + choices[completion.label] = completion end -- select a completion - local completion = nil - local cmd = 'printf "' .. vis_menu_input .. '" | vis-menu' - local status, selection = vis:pipe(win.file, {start = 0, finish = 0}, cmd) - if status == 0 then - -- trim newline from selection - selection = selection:sub(1, -2) - - for _, item in ipairs(completions) do - if selection == item.label then - completion = item - break - end - end + local choice = lspc_select(choices) + if not choice then + return + end - assert(completion) + local completion = choices[choice] - vis_apply_textEdit(win, win.file, completion.textEdit) - end + vis_apply_textEdit(win, win.file, completion.textEdit) end -- method response dispatcher @@ -333,9 +389,10 @@ local function ls_handle_method_response(ls, method_response) if method == 'textDocument/definition' or method == 'textDocument/declaration' or method == 'textDocument/typeDefinition' or - method == 'textDocument/implementation' then + method == 'textDocument/implementation' or + method == 'textDocument/references' then -- LuaFormatter on - lspc_handle_goto_method_response(method, result) + lspc_handle_goto_method_response(req, result, win) elseif method == 'initialize' then ls.initialized = true @@ -424,37 +481,37 @@ local function ls_recv_data(ls, data) ls.partial_response.len = 0 end --- check if a language server is running and register file as open +-- check if a language server is running and initialized local function lspc_get_usable_ls(syntax) local ls = lspc.running[syntax] if not ls then - vis:info('Error: No language server running for ' .. syntax) - return nil + return nil, 'Error: No language server running for ' .. syntax end if not ls.initialized then - vis:info('Error: Language server not initialized yet. Please try again') - return nil + return nil, 'Error: Language server not initialized yet. Please try again' end return ls end local function lspc_close(ls, file) + if not ls.open_files[file.path] then + return 'Error: ' .. file.path .. ' not open' + end ls_send_notification(ls, 'textDocument/didClose', {textDocument = {uri = path_to_uri(file.path)}}) ls.open_files[file.path] = nil end -vis:command_register('lspc-close', function(_, _, win) - local ls = lspc_get_usable_ls(win.syntax) - - lspc_close(ls, win.file) -end) - -- register a file as open with a language server and setup a close and save event handlers -- A file must be opened before any textDocument functions can be used with it. local function lspc_open(ls, win, file) + -- already opened + if ls.open_files[file.path] then + return 'Error: ' .. file.path .. ' already open' + end + local params = { textDocument = { uri = 'file://' .. file.path, @@ -467,8 +524,6 @@ local function lspc_open(ls, win, file) ls.open_files[file.path] = {file = file, version = 0} ls_send_notification(ls, 'textDocument/didOpen', params) - -- TODO: implement textDocument/didChange - vis.events.subscribe(vis.events.FILE_CLOSE, function(file) for _, ls in pairs(lspc.running) do if ls.open_files[file.path] then @@ -491,46 +546,21 @@ local function lspc_open(ls, win, file) end -vis:command_register('lspc-open', function(_, _, win) - local ls = lspc_get_usable_ls(win.syntax) - - if ls.open_files[win.file.path] then - return - end - - lspc_open(ls, win, win.file) -end) - -- Initiate the shutdown of a language server -- Sending the exit notification and closing the file handle are done in -- the shutdown response handler. local function ls_shutdown_server(ls) - ls_call_method(ls, 'shutdown', nil) + ls_call_method(ls, 'shutdown') end -vis:command_register('lspc-shutdown-server', function(argv, _, win) - local ls = lspc_get_usable_ls(argv[1] or win.syntax) - if not ls then - return - end - - ls_shutdown_server(ls) -end) - -local function ls_start_server(syntax, quiet) +local function ls_start_server(syntax) if lspc.running[syntax] then - if not quiet then - vis:info('Error: Already a language server running for ' .. syntax) - end - return + return nil, 'Error: Already a language server running for ' .. syntax end local ls_conf = lspc.ls_map[syntax] if not ls_conf then - if not quiet then - vis:info('Error: No language server available for ' .. syntax) - end - return + return nil, 'Error: No language server available for ' .. syntax end local ls = { @@ -559,7 +589,7 @@ local function ls_start_server(syntax, quiet) vis:info('language server received signal: ' .. msg) end - ls.running[syntax] = nil + lspc.running[syntax] = nil return end @@ -583,76 +613,144 @@ local function ls_start_server(syntax, quiet) return ls end -vis:command_register('lspc-start-server', function(argv, _, win) - ls_start_server(argv[1] or win.syntax) +-- generic stub implementation for all textDocument methods taking +-- a textDocumentPositionParams parameter +local function lspc_method_doc_pos(ls, method, win, argv) + -- check if the language server has a provider for this method + if not ls.capabilities[method .. 'Provider'] then + return 'LPSC Error: language server ' .. ls.name .. ' does not provide ' .. + method + end + + if not ls.open_files[win.file.path] then + lspc_open(ls, win, win.file) + end + + local params = vis_doc_pos_to_lsp(vis_get_doc_pos(win)) + + ls_call_text_document_method(ls, method, params, win, argv) +end + +local lspc_goto_location_methods = { + declaration = function(ls, win, open_cmd) + return lspc_method_doc_pos(ls, 'declaration', win, open_cmd) + end, + definition = function(ls, win, open_cmd) + return lspc_method_doc_pos(ls, 'definition', win, open_cmd) + end, + typeDefinition = function(ls, win, open_cmd) + return lspc_method_doc_pos(ls, 'typeDefinition', win, open_cmd) + end, + implementation = function(ls, win, open_cmd) + return lspc_method_doc_pos(ls, 'implementation', win, open_cmd) + end, + references = function(ls, win, open_cmd) + return lspc_method_doc_pos(ls, 'references', win, open_cmd) + end, +} + +-- vis-lspc commands + +vis:command_register('lspc-back', function() + local err = vis_pop_doc_pos() + if err then + vis:info(err) + end end) -vis.events.subscribe(vis.events.WIN_OPEN, function(win) - if not lspc.autostart then +for name, func in pairs(lspc_goto_location_methods) do + vis:command_register('lspc-' .. name, function(argv, _, win) + local ls, err = lspc_get_usable_ls(win.syntax) + if err then + vis:info(err) + return + end + + -- vis cmd how to open the new location + -- 'e' (default): in same window + -- 'vsplit': in a vertical split window + -- 'hsplit': in a horizontal split window + local open_cmd = argv[1] or 'e' + err = func(ls, win, open_cmd) + if err then + vis:info(err) + end + end) +end + +vis:command_register('lspc-completion', function(_, _, win) + local ls, err = lspc_get_usable_ls(win.syntax) + if err then + vis:info(err) return end - ls_start_server(win.syntax, true) + err = lspc_method_doc_pos(ls, 'completion', win) + if err then + vis:info(err) + end end) -local function lspc_method_textDocumentPositionParams(ls, method, win) - -- check if the language server has a provider for this method - if not ls.capabilities[method .. 'Provider'] then - vis:info( - 'LPSC Error: language server ' .. ls.name .. ' does not provide ' .. - method) - return +vis:command_register('lspc-start-server', function(argv, _, win) + local _, err = ls_start_server(argv[1] or win.syntax) + if err then + vis:info(err) end +end) - if not ls.open_files[win.file.path] then - lspc_open(ls, win, win.file) +vis:command_register('lspc-shutdown-server', function(argv, _, win) + local ls, err = lspc_get_usable_ls(argv[1] or win.syntax) + if err then + vis:info(err) + vis:info('Error: no language server running') + return end - local params = vis_doc_pos_to_lsp(vis_get_doc_pos(win)) + ls_shutdown_server(ls) +end) - ls_call_text_document_method(ls, method, params, win) -end +vis:command_register('lspc-close', function(_, _, win) + local ls, err = lspc_get_usable_ls(win.syntax) + if err then + vis:info(err) + return + end -local function lspc_declaration(ls, win) - lspc_method_textDocumentPositionParams(ls, 'declaration', win) -end + lspc_close(ls, win.file) +end) -local function lspc_definition(ls, win) - lspc_method_textDocumentPositionParams(ls, 'definition', win) -end +vis:command_register('lspc-open', function(_, _, win) + local ls, err = lspc_get_usable_ls(win.syntax) + if err then + vis:info(err) + return + end -local function lspc_typeDefinition(ls, win) - lspc_method_textDocumentPositionParams(ls, 'typeDefinition', win) -end + lspc_open(ls, win, win.file) +end) -local function lspc_implementation(ls, win) - lspc_method_textDocumentPositionParams(ls, 'implementation', win) -end +-- vis-lspc event hooks -local function lspc_completion(ls, win) - lspc_method_textDocumentPositionParams(ls, 'completion', win) -end +vis.events.subscribe(vis.events.WIN_OPEN, function(win) + if lspc.autostart then + ls_start_server(win.syntax, true) + end +end) -local lspc_methods = { - declaration = lspc_declaration, - definition = lspc_definition, - typeDefinition = lspc_typeDefinition, - implementation = lspc_implementation, - completion = lspc_completion, -} +vis.events.subscribe(vis.events.FILE_OPEN, function(file) + local win = vis.win + -- the only window we can access is not our window + if not win or win.file ~= file then + return + end -for name, func in pairs(lspc_methods) do - vis:command_register('lspc-' .. name, function(_, _, win, selection) - local ls = lspc_get_usable_ls(win.syntax, win.file) - if not ls then - return - end + local ls = lspc_get_usable_ls(win.syntax) - func(ls, win, selection) - end) -end + lspc_open(ls, win, file) +end) + +-- vis-lspc default bindings --- default bindings vis:map(vis.modes.NORMAL, '<F2>', function() vis:command('lspc-start-server') end) |
