From 7f54c347ec9227f55a4d5672d920be343dd52e06 Mon Sep 17 00:00:00 2001 From: Yegappan Lakshmanan Date: Sun, 3 Jan 2021 12:00:49 -0800 Subject: [PATCH] Add support for highlighting current symbol in the outline window --- autoload/handlers.vim | 116 +++++--------------- autoload/lsp.vim | 233 +++++++++++++++++++++++++++++++++++++++-- autoload/lspserver.vim | 4 +- plugin/lsp.vim | 2 +- 4 files changed, 255 insertions(+), 100 deletions(-) diff --git a/autoload/handlers.vim b/autoload/handlers.vim index c5a97a5..5bc7f80 100644 --- a/autoload/handlers.vim +++ b/autoload/handlers.vim @@ -289,57 +289,29 @@ def LspSymbolKindToName(symkind: number): string return symbolMap[symkind] enddef -# jump to a symbol selected in the symbols window -def handlers#jumpToSymbol() - var lnum: number = line('.') - 1 - if w:lsp_info.data[lnum]->empty() +# process the 'textDocument/documentSymbol' reply from the LSP server +# Open a symbols window and display the symbols as a tree +def s:processDocSymbolReply(lspserver: dict, req: dict, reply: dict): void + if reply.result->empty() + WarnMsg('No symbols are found') return endif - var slnum: number = w:lsp_info.data[lnum].lnum - var scol: number = w:lsp_info.data[lnum].col - var fname: string = w:lsp_info.filename - - # If the file is already opened in a window, jump to it. Otherwise open it - # in another window - var wid: number = fname->bufwinid() - if wid == -1 - # Find a window showing a normal buffer and use it - for w in getwininfo() - if w.winid->getwinvar('&buftype') == '' - wid = w.winid - wid->win_gotoid() - break - endif - endfor - if wid == -1 - var symWinid: number = win_getid() - :rightbelow vnew - # retain the fixed symbol window width - win_execute(symWinid, 'vertical resize 20') - endif - - exe 'edit ' .. fname - else - wid->win_gotoid() - endif - [slnum, scol]->cursor() -enddef - -# display the list of document symbols from the LSP server in a window as a -# tree -def s:showSymbols(symTable: list>, uri: string) - var symbols: dict>> - var symbolType: string var fname: string - var r: dict> + var symbolTypeTable: dict>> + var symbolLineTable: list> = [] var name: string + var symbolType: string + var r: dict> + var symbolDetail: string + var symInfo: dict - if uri != '' - fname = LspUriToFile(uri) + if req.params.textDocument.uri != '' + fname = LspUriToFile(req.params.textDocument.uri) endif - for symbol in symTable + for symbol in reply.result + symbolDetail = '' if symbol->has_key('location') # interface SymbolInformation fname = LspUriToFile(symbol.location.uri) @@ -356,56 +328,20 @@ def s:showSymbols(symTable: list>, uri: string) name = symbol.name symbolType = LspSymbolKindToName(symbol.kind) r = symbol.range + if symbol->has_key('detail') + symbolDetail = symbol.detail + endif endif - if !symbols->has_key(symbolType) - symbols[symbolType] = [] + if !symbolTypeTable->has_key(symbolType) + symbolTypeTable[symbolType] = [] endif - symbols[symbolType]->add({name: name, - lnum: r.start.line + 1, col: r.start.character + 1}) + symInfo = {name: name, range: r, detail: symbolDetail} + symbolTypeTable[symbolType]->add(symInfo) + symbolLineTable->add(symInfo) endfor - - var wid: number = bufwinid('LSP-Symbols') - if wid == -1 - :20vnew LSP-Symbols - else - win_gotoid(wid) - endif - - :setlocal modifiable - :setlocal noreadonly - :silent! :%d _ - :setlocal buftype=nofile - :setlocal bufhidden=delete - :setlocal noswapfile nobuflisted - :setlocal nonumber norelativenumber fdc=0 nowrap winfixheight winfixwidth - setline(1, ['# Language Server Symbols', '# ' .. fname]) - # First two lines in the buffer display comment information - var lnumMap: list> = [{}, {}] - var text: list = [] - for [symType, syms] in items(symbols) - text->extend(['', symType]) - lnumMap->extend([{}, {}]) - for s in syms - text->add(' ' .. s.name) - lnumMap->add({lnum: s.lnum, col: s.col}) - endfor - endfor - append(line('$'), text) - w:lsp_info = {filename: fname, data: lnumMap} - :nnoremap q :quit - :nnoremap :call handlers#jumpToSymbol() - :setlocal nomodifiable -enddef - -# process the 'textDocument/documentSymbol' reply from the LSP server -# Open a symbols window and display the symbols as a tree -def s:processDocSymbolReply(lspserver: dict, req: dict, reply: dict): void - if reply.result->empty() - WarnMsg('No symbols are found') - return - endif - - s:showSymbols(reply.result, req.params.textDocument.uri) + # sort the symbols by line number + symbolLineTable->sort({a, b -> a.range.start.line - b.range.start.line}) + lsp#updateOutlineWindow(fname, symbolTypeTable, symbolLineTable) enddef # Returns the byte number of the specified line/col position. Returns a diff --git a/autoload/lsp.vim b/autoload/lsp.vim index a738bcd..83d1172 100644 --- a/autoload/lsp.vim +++ b/autoload/lsp.vim @@ -508,8 +508,223 @@ def lsp#docHighlightClear() prop_remove({'type': 'LspWriteRef', 'all': v:true}, 1, line('$')) enddef -# open a window and display all the symbols in a file -def lsp#showDocSymbols() +# jump to a symbol selected in the outline window +def s:outlineJumpToSymbol() + var lnum: number = line('.') - 1 + if w:lspSymbols.lnumTable[lnum]->empty() + return + endif + + var slnum: number = w:lspSymbols.lnumTable[lnum].lnum + var scol: number = w:lspSymbols.lnumTable[lnum].col + var fname: string = w:lspSymbols.filename + + # Highlight the selected symbol + prop_remove({type: 'LspOutlineHighlight'}) + prop_add(line('.'), 3, {type: 'LspOutlineHighlight', + length: w:lspSymbols.lnumTable[lnum].name->len()}) + + # disable the outline window refresh + skipOutlineRefresh = true + + # If the file is already opened in a window, jump to it. Otherwise open it + # in another window + var wid: number = fname->bufwinid() + if wid == -1 + # Find a window showing a normal buffer and use it + for w in getwininfo() + if w.winid->getwinvar('&buftype') == '' + wid = w.winid + wid->win_gotoid() + break + endif + endfor + if wid == -1 + var symWinid: number = win_getid() + :rightbelow vnew + # retain the fixed symbol window width + win_execute(symWinid, 'vertical resize 20') + endif + + exe 'edit ' .. fname + else + wid->win_gotoid() + endif + [slnum, scol]->cursor() + skipOutlineRefresh = false +enddef + +var skipOutlineRefresh: bool = false + +# update the symbols displayed in the outline window +def lsp#updateOutlineWindow(fname: string, + symbolTypeTable: dict>>, + symbolLineTable: list>) + var wid: number = bufwinid('LSP-Outline') + if wid == -1 + return + endif + + # stop refreshing the outline window recursively + skipOutlineRefresh = true + + var prevWinID: number = win_getid() + win_gotoid(wid) + + # if the file displayed in the outline window is same as the new file, then + # save and restore the cursor position + var symbols = getwinvar(wid, 'lspSymbols', {}) + var saveCursor: list = [] + if !symbols->empty() && symbols.filename == fname + saveCursor = getcurpos() + endif + + :setlocal modifiable + :silent! :%d _ + setline(1, ['# File Outline', '# ' .. fname]) + + # First two lines in the buffer display comment information + var lnumMap: list> = [{}, {}] + var text: list = [] + for [symType, syms] in items(symbolTypeTable) + text->extend(['', symType]) + lnumMap->extend([{}, {}]) + for s in syms + text->add(' ' .. s.name) + # remember the line number for the symbol + lnumMap->add({name: s.name, lnum: s.range.start.line + 1, + col: s.range.start.character + 1}) + s.outlineLine = lnumMap->len() + endfor + endfor + append('$', text) + w:lspSymbols = {filename: fname, lnumTable: lnumMap, + symbolsByLine: symbolLineTable} + :setlocal nomodifiable + + if !saveCursor->empty() + setpos('.', saveCursor) + endif + win_gotoid(prevWinID) + + # Highlight the current symbol + s:outlineHighlightCurrentSymbol() + + # re-enable refreshing the outline window + skipOutlineRefresh = false +enddef + +def s:outlineHighlightCurrentSymbol() + var fname: string = fnamemodify(expand('%'), ':p') + if fname == '' || &filetype == '' + return + endif + + var wid: number = bufwinid('LSP-Outline') + if wid == -1 + return + endif + + # Check whether the symbols for this file are displayed in the outline + # window + var lspSymbols = getwinvar(wid, 'lspSymbols', {}) + if lspSymbols->empty() || lspSymbols.filename != fname + return + endif + + var symbolTable: list> = lspSymbols.symbolsByLine + + # line number to locate the symbol + var lnum: number = line('.') + + # Find the symbol for the current line number (binary search) + var left: number = 0 + var right: number = symbolTable->len() - 1 + var mid: number + while left <= right + mid = (left + right) / 2 + if lnum >= (symbolTable[mid].range.start.line + 1) && + lnum <= (symbolTable[mid].range.end.line + 1) + break + endif + if lnum > (symbolTable[mid].range.start.line + 1) + left = mid + 1 + else + right = mid - 1 + endif + endwhile + + # clear the highlighting in the outline window + var bnr: number = wid->winbufnr() + prop_remove({bufnr: bnr, type: 'LspOutlineHighlight'}) + + if left > right + # symbol not found + return + endif + + # Highlight the selected symbol + prop_add(symbolTable[mid].outlineLine, 3, + {bufnr: bnr, type: 'LspOutlineHighlight', + length: symbolTable[mid].name->len()}) + + # if the line is not visible, then scroll the outline window to make the + # line visible + var wininfo = wid->getwininfo() + if symbolTable[mid].outlineLine < wininfo[0].topline + || symbolTable[mid].outlineLine > wininfo[0].botline + var cmd: string = 'call cursor(' .. + symbolTable[mid].outlineLine .. ', 1) | normal z.' + win_execute(wid, cmd) + endif +enddef + +# when the outline window is closed, do the cleanup +def s:outlineCleanup() + # Remove the outline autocommands + :silent! autocmd! LSPOutline +enddef + +# open the symbol outline window +def s:openOutlineWindow() + var wid: number = bufwinid('LSP-Outline') + if wid != -1 + return + endif + + var prevWinID: number = win_getid() + + :topleft :20vnew LSP-Outline + :setlocal modifiable + :setlocal noreadonly + :silent! :%d _ + :setlocal buftype=nofile + :setlocal bufhidden=delete + :setlocal noswapfile nobuflisted + :setlocal nonumber norelativenumber fdc=0 nowrap winfixheight winfixwidth + setline(1, ['# File Outline']) + :nnoremap q :quit + :nnoremap :call outlineJumpToSymbol() + :setlocal nomodifiable + + prop_type_add('LspOutlineHighlight', {bufnr: bufnr(), highlight: 'Search'}) + + augroup LSPOutline + au! + autocmd BufEnter * call s:requestDocSymbols() + # when the outline window is closed, do the cleanup + autocmd BufUnload LSP-Outline call s:outlineCleanup() + autocmd CursorHold * call s:outlineHighlightCurrentSymbol() + augroup END + + win_gotoid(prevWinID) +enddef + +def s:requestDocSymbols() + if skipOutlineRefresh + return + endif + var ftype = &filetype if ftype == '' return @@ -517,11 +732,9 @@ def lsp#showDocSymbols() var lspserver: dict = s:lspGetServer(ftype) if lspserver->empty() - ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not found') return endif if !lspserver.running - ErrMsg('Error: LSP server for "' .. ftype .. '" filetype is not running') return endif @@ -530,7 +743,13 @@ def lsp#showDocSymbols() return endif - lspserver.showDocSymbols(fname) + lspserver.getDocSymbols(fname) +enddef + +# open a window and display all the symbols in a file (outline) +def lsp#outline() + s:openOutlineWindow() + s:requestDocSymbols() enddef # Format the entire file @@ -699,7 +918,7 @@ def s:filterSymbols(lspserver: dict, popupID: number, key: string): bool enddef # Jump to the location of a symbol selected in the popup menu -def s:jumpToSymbol(popupID: number, result: number): void +def s:jumpToWorkspaceSymbol(popupID: number, result: number): void # clear the message displayed at the command-line echo '' @@ -750,7 +969,7 @@ def s:showSymbolMenu(lspserver: dict, query: string) fixed: 1, close: "button", filter: function('s:filterSymbols', [lspserver]), - callback: function('s:jumpToSymbol') + callback: function('s:jumpToWorkspaceSymbol') } lspserver.workspaceSymbolPopup = popup_menu([], popupAttr) lspserver.workspaceSymbolQuery = query diff --git a/autoload/lspserver.vim b/autoload/lspserver.vim index 571779a..c82f731 100644 --- a/autoload/lspserver.vim +++ b/autoload/lspserver.vim @@ -513,7 +513,7 @@ def s:docHighlight(lspserver: dict): void lspserver.sendMessage(req) enddef -def s:showDocSymbols(lspserver: dict, fname: string): void +def s:getDocSymbols(lspserver: dict, fname: string): void # Check whether LSP server supports getting document symbol information if !lspserver.caps->has_key('documentSymbolProvider') || !lspserver.caps.documentSymbolProvider @@ -758,7 +758,7 @@ export def NewLspServer(path: string, args: list): dict hover: function('s:hover', [lspserver]), showReferences: function('s:showReferences', [lspserver]), docHighlight: function('s:docHighlight', [lspserver]), - showDocSymbols: function('s:showDocSymbols', [lspserver]), + getDocSymbols: function('s:getDocSymbols', [lspserver]), textDocFormat: function('s:textDocFormat', [lspserver]), renameSymbol: function('s:renameSymbol', [lspserver]), codeAction: function('s:codeAction', [lspserver]), diff --git a/plugin/lsp.vim b/plugin/lsp.vim index f2e396f..7f68c82 100644 --- a/plugin/lsp.vim +++ b/plugin/lsp.vim @@ -29,7 +29,7 @@ command! -nargs=0 -bar LspShowDiagnostics call lsp#showDiagnostics() command! -nargs=0 -bar LspShowReferences call lsp#showReferences() command! -nargs=0 -bar LspHighlight call lsp#docHighlight() command! -nargs=0 -bar LspHighlightClear call lsp#docHighlightClear() -command! -nargs=0 -bar LspShowDocSymbols call lsp#showDocSymbols() +command! -nargs=0 -bar LspOutline call lsp#outline() command! -nargs=0 -bar -range=% LspFormat call lsp#textDocFormat(, , ) command! -nargs=0 -bar LspCalledBy call lsp#incomingCalls() command! -nargs=0 -bar LspCalling call lsp#outgoingCalls() -- 2.48.1