aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md54
-rw-r--r--init.lua404
2 files changed, 297 insertions, 161 deletions
diff --git a/README.md b/README.md
index f9e49d2..0ee4c45 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@ vis-lspc currently supports:
* `textDocument/completion`
* `textDocument/declaration`
* `textDocument/definition`
+* `textDocument/references`
* `textDocument/typeDefinition`
* `textDocument/implementation`
@@ -32,11 +33,26 @@ Currently only clangd is available and somewhat tested.
There should be a mapping between vis syntax/lexer names and LSP [languageId](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentItem).
For example the syntax for C code in vis is called 'ansi_c' and in LSP 'c'.
+## Requirements
+
+* vis must be compiled with the Lua [communicate API](https://github.com/martanne/vis/pull/675).
+* Optional: the json implementation of your choice
+ * must be usable by calling `require('json')`
+ * must provide `json.encode, json.decode`
+
+## Installation
+
+1. Clone this repository into your vis plugins directory
+2. Load the plugin in your `visrc.lua` with `require('plugins/vis-lspc')`
+
+
## Usage
Note that till `textDocument/didChange` is implemented vis-lspc is hardly usable.
But if you are brave there are some default key bindings:
+### Default Bindings
+
Normal mode:
<F2> - start a language server for win.syntax
<F3> - open win.file with a running language server
@@ -45,17 +61,39 @@ But if you are brave there are some default key bindings:
Normal and Insert mode:
<C- > - get completions
-## Requirements
-* vis must be compiled with the Lua [communicate API](https://github.com/martanne/vis/pull/675).
-* Optional: the json implementation of your choice
- * must be usable by calling `require('json')`
- * must provide `json.encode, json.decode`
+### Available commands
-## Installation
+ # language-server management:
+ lspc-start-server [syntax] - start a language server for syntax or win.syntax
+ lspc-stop-server [syntax] - stop the language server for syntax or win.syntax
-1. Clone this repository into your vis plugins directory
-2. Load the plugin in your `visrc.lua` with `require('plugins/vis-lspc')`
+ # file registration:
+ lspc-open - register the file in the current window
+ lspc-close - unregister the file in the current window
+
+ # navigation commands (they all operate on the symbol under the main cursor):
+ lspc-completion - syntax completion
+ lspc-references [e | vsplit | hsplit] - select and open a reference
+ lspc-declaration [e | vsplit | hsplit] - select and open a declaration
+ lspc-definition [e | vsplit | hsplit] - open the definition
+ lspc-typeDeclaration [e | vsplit | hsplit] - select and open a type declaration
+ lspc-implementation [e | vsplit | hsplit] - I actually have no idea what this does
+
+ lspc-back - navigate back in the goto history
+
+### Available configuration fields
+
+The module table returned by `require('plugins/vis-lspc')` can be use to configure
+some aspects of vis-lspc.
+
+Available fields are:
+
+* `name = 'vis-lspc'` - the name vis-lspc introduces itself to a language server
+* `logging = false` - enable logging only useful for debugging vis-lspc
+* `log_file = 'vis-lspc.log'` - file vis-lspc writes all log messages to
+* `autostart = true` - try to start a language server in WIN_OPEN
+* `menu_cmd = 'fzf' or 'vis-menu'` - program to prompt for user choices
## License
diff --git a/init.lua b/init.lua
index bce126e..e350cff 100644
--- a/init.lua
+++ b/init.lua
@@ -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)