From 0cf35c2b24d5ea6a9eb1a9f1a0dcdbe27bd1cc34 Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Tue, 29 Jul 2025 13:36:54 +0200 Subject: support navigating a file by its symbols using the message window --- init.lua | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/init.lua b/init.lua index 29a4ed2..6e81f14 100644 --- a/init.lua +++ b/init.lua @@ -723,6 +723,14 @@ function LanguageServer:call_text_document_method(method, params, win, ctx) self:request_diagnostics(win) end +--- Call the textDocument/symbol method of the server if provided +function LanguageServer:request_symbols(win, ctx) + if self.capabilities['documentSymbolProvider'] then + local params = {textDocument = {uri = path_to_uri(win.file.path)}} + self:call_text_document_method('documentSymbol', params, win, ctx) + end +end + local function lspc_handle_goto_method_response(req, result) if not result or type(result) ~= 'table' or next(result) == nil then lspc:warn(req.method .. ' found no results') @@ -960,6 +968,12 @@ local function lspc_handle_initialize_response(ls, result) vis.events.emit(lspc.events.LS_INITIALIZED, ls) end +local function lspc_handle_symbol_method_response(result, ctx) + if ctx.callback and type(ctx.callback) == 'function' then + ctx.callback(result, ctx) + end +end + --- Dispatch method response from the server -- @param method_response the response send from the server -- @param req the request causing this response @@ -1009,6 +1023,9 @@ function LanguageServer:handle_method_response(method_response, req) elseif method == 'textDocument/diagnostic' then lspc_handle_diagnostic_method_response(self, result, req.ctx) + elseif method == 'textDocument/documentSymbol' then + lspc_handle_symbol_method_response(result, req.ctx) + elseif method == 'shutdown' then self:send_notification('exit') self.fd:close() @@ -1522,6 +1539,107 @@ local function lspc_show_diagnostic(win, line) end end +local lsp_symbol_kinds = { + 'File', + 'Module', + 'Namespace', + 'Package', + 'Class', + 'Method', + 'Property', + 'Field', + 'Constructor', + 'Enum', + 'Interface', + 'Function', + 'Variable', + 'Constant', + 'String', + 'Number', + 'Boolean', + 'Array', + 'Object', + 'Key', + 'Null', + 'EnumMember', + 'Struct', + 'Event', + 'Operator', + 'TypeParameter', +} + +local function recursive_bfs_symbols_flatten(symbols, indent) + local ranges = {} + local str = '' + for _, s in ipairs(symbols) do + local range = s.selectionRange or s.location.range + table.insert(ranges, range) + str = str .. + string.format('%s%s %s %d:%d\n', indent, lsp_symbol_kinds[s.kind], s.name, + range.start.line + 1, range.start.character + 1) + -- Remember DocumentSymbol.selectionRange or SymbolInformation.location.range + if s.children then + local cstr, cranges = recursive_bfs_symbols_flatten(s.children, indent .. ' ') + str = str .. cstr + for _, v in ipairs(cranges) do + table.insert(ranges, v) + end + end + end + return str, ranges +end + +local function lspc_navwin_from_symbols(symbols, ctx) + local symbols_outline, ranges = recursive_bfs_symbols_flatten(symbols, '') + + vis.ui.layout = vis.ui.layouts.VERTICAL + vis:message('') + local navwin = vis.win + + -- Prepare message window to be our symbol navigation window. + navwin.file = 'symbols' + local navfile = navwin.file + assert(navwin and navwin.file) + assert(navfile:delete(0, navfile.size)) + assert(navfile:insert(0, symbols_outline)) + + -- Create event handlers to connect the position in the navigation window to + -- the original window. + local function nav_win_win_highlight_handler() + if vis.win ~= navwin then + return + end + local line = navwin.selection.line + if line > #ranges then + return + end + local sel = lsp_pos_to_vis_sel(ranges[navwin.selection.line].start) + vis_open_new_doc_pos({ + file = ctx.win.file.path, + line = sel.line, + col = sel.col, + }, 'e', ctx.win) + vis.win = navwin + end + vis.events.subscribe(vis.events.WIN_HIGHLIGHT, nav_win_win_highlight_handler) + + -- Remove the event handlers. + local nav_win_close_handler + nav_win_close_handler = function(win) + if win ~= navwin or win ~= ctx.win then + return + end + vis.events.unsubscribe(vis.events.UI_DRAW, nav_win_win_highlight_handler) + vis.events.unsubscribe(vis.events.WIN_CLOSE, nav_win_close_handler) + end + vis.events.subscribe(vis.events.WIN_CLOSE, nav_win_close_handler) +end + +local function lspc_nav_symbols(ls, win) + local ctx = {win = win, callback = lspc_navwin_from_symbols} + ls:request_symbols(win, ctx) +end + -- vis-lspc commands vis:command_register('lspc-back', function() @@ -1748,6 +1866,19 @@ vis:command_register('lspc-show-diagnostics', function(argv, _, win) end end) +vis:command_register('lspc-navigate-symbols', function(argv, _, win) + local ls, err = lspc_get_usable_ls(win, argv[1]) + if err then + lspc:err(err) + return + end + assert(ls) + if not ls:is_file_opened(win.file.path) then + lspc_open(ls, win, win.file) + end + lspc_nav_symbols(ls, win) +end, 'navigate the file by its symbols') + -- vis-lspc event hooks vis.events.subscribe(vis.events.WIN_OPEN, function(win) if lspc.autostart and win.syntax then -- cgit v1.2.3