]> Sergey Matveev's repositories - vim-lsp.git/commitdiff
Add support for displaying symbols in the current file in a popup menu
authorYegappan Lakshmanan <yegappan@yahoo.com>
Sat, 24 Jun 2023 18:08:01 +0000 (11:08 -0700)
committerYegappan Lakshmanan <yegappan@yahoo.com>
Sat, 24 Jun 2023 18:08:01 +0000 (11:08 -0700)
README.md
autoload/lsp/lsp.vim
autoload/lsp/lspserver.vim
autoload/lsp/symbol.vim
doc/lsp.txt
plugin/lsp.vim

index 892cc75c0842b9ea8ba1f00c47dadfc95689c8d6..4657d6ce56cee81ae699c6ae931a87fb9830875d 100644 (file)
--- a/README.md
+++ b/README.md
@@ -175,6 +175,7 @@ Command|Description
 :LspDiagNext|Jump to the next diagnostic message after the current position.
 :LspDiagPrev|Jump to the previous diagnostic message before the current position.
 :LspDiagShow|Display the diagnostics messages from the language server for the current buffer in a new location list.
+:LspDocumentSymbol|Display the symbols in the current file in a popup menu and jump to the selected symbol.
 :LspFold|Fold the current file.
 :LspFormat|Format a range of lines in the current file using the language server. The **shiftwidth** and **expandtab** values set for the current buffer are used when format is applied.  The default range is the entire file.
 :LspGotoDeclaration|Go to the declaration of the keyword under cursor.
index 56c1b4724e717dbc6e82a7824463b238557deedb..7ca4a54f7c3dd205b2edd66e8c05d9b8c8e68d12 100644 (file)
@@ -45,6 +45,7 @@ def LspInitOnce()
   diag.InitOnce()
   inlayhints.InitOnce()
   signature.InitOnce()
+  symbol.InitOnce()
 
   :set ballooneval balloonevalterm
   lspInitializedOnce = true
@@ -847,7 +848,7 @@ def g:LspRequestDocSymbols()
     return
   endif
 
-  lspserver.getDocSymbols(fname)
+  lspserver.getDocSymbols(fname, true)
 enddef
 
 # open a window and display all the symbols in a file (outline)
@@ -856,6 +857,21 @@ export def Outline(cmdmods: string, winsize: number)
   g:LspRequestDocSymbols()
 enddef
 
+# show all the symbols in a file in a popup menu
+export def ShowDocSymbols()
+  var fname: string = @%
+  if fname->empty()
+    return
+  endif
+
+  var lspserver: dict<any> = buf.CurbufGetServer()
+  if lspserver->empty() || !lspserver.running || !lspserver.ready
+    return
+  endif
+
+  lspserver.getDocSymbols(fname, false)
+enddef
+
 # Format the entire file
 export def TextDocFormat(range_args: number, line1: number, line2: number)
   if !&modifiable
index d05ff37de8b38b8a174ea67749f5e44a0fcfd372..cf4dbc106f5b204fbbdd24c4c2d478848fd9a29e 100644 (file)
@@ -143,7 +143,7 @@ def ServerInitReply(lspserver: dict<any>, initResult: dict<any>): void
   # if the outline window is opened, then request the symbols for the current
   # buffer
   if bufwinid('LSP-Outline') != -1
-    lspserver.getDocSymbols(@%)
+    lspserver.getDocSymbols(@%, true)
   endif
 
   # Update the inlay hints (if enabled)
@@ -984,7 +984,7 @@ enddef
 
 # Request: "textDocument/documentSymbol"
 # Param: DocumentSymbolParams
-def GetDocSymbols(lspserver: dict<any>, fname: string): void
+def GetDocSymbols(lspserver: dict<any>, fname: string, showOutline: bool): void
   # Check whether LSP server supports getting document symbol information
   if !lspserver.isDocumentSymbolProvider
     util.ErrMsg('LSP server does not support getting list of symbols')
@@ -995,7 +995,11 @@ def GetDocSymbols(lspserver: dict<any>, fname: string): void
   # interface TextDocumentIdentifier
   var params = {textDocument: {uri: util.LspFileToUri(fname)}}
   lspserver.rpc_a('textDocument/documentSymbol', params, (_, reply) => {
-    symbol.DocSymbolReply(lspserver, reply, fname)
+    if showOutline
+      symbol.DocSymbolOutline(lspserver, reply, fname)
+    else
+      symbol.DocSymbolPopup(lspserver, reply, fname)
+    endif
   })
 enddef
 
index b26057cd575fef7504a3ffd846fcdbbf627b0214..b4d92e2de5b485e0e1cf3083dcaa677a1eb1539a 100644 (file)
@@ -10,6 +10,22 @@ import './options.vim' as opt
 import './util.vim'
 import './outline.vim'
 
+# Initialize the highlight group and the text property type used for
+# document symbol search
+export def InitOnce()
+  # Use a high priority value to override other highlights in the line
+  hlset([{name: 'LspSymbolName', default: true, linksto: 'Search'}])
+  prop_type_add('LspSymbolNameProp', {highlight: 'LspSymbolName',
+                                      combine: false,
+                                      override: true,
+                                      priority: 201})
+  hlset([{name: 'LspSymbolRange', default: true, linksto: 'Visual'}])
+  prop_type_add('LspSymbolRangeProp', {highlight: 'LspSymbolRange',
+                                      combine: false,
+                                      override: true,
+                                      priority: 200})
+enddef
+
 # Handle keys pressed when the workspace symbol popup menu is displayed
 def FilterSymbols(lspserver: dict<any>, popupID: number, key: string): bool
   var key_handled: bool = false
@@ -104,7 +120,7 @@ def JumpToWorkspaceSymbol(cmdmods: string, popupID: number, result: number): voi
          exe $'confirm edit {symTbl[result - 1].file}'
        endif
       else
-       # If the target buffer is opened in the curent window, then don't
+       # If the target buffer is opened in the current window, then don't
        # change the window.
        if bufnr() != bnr
          # If the target buffer is opened in a window in the current tab
@@ -160,7 +176,7 @@ def ShowSymbolMenu(lspserver: dict<any>, query: string, cmdmods: string)
 enddef
 
 # Convert a file name to <filename> (<dirname>) format.
-# Make sure the popup does't occupy the entire screen by reducing the width.
+# Make sure the popup doesn't occupy the entire screen by reducing the width.
 def MakeMenuName(popupWidth: number, fname: string): string
   var filename: string = fname->fnamemodify(':t')
   var flen: number = filename->len()
@@ -590,24 +606,24 @@ enddef
 # process the 'textDocument/documentSymbol' reply from the LSP server
 # Open a symbols window and display the symbols as a tree
 # Result: DocumentSymbol[] | SymbolInformation[] | null
-export def DocSymbolReply(lspserver: dict<any>, docsymbol: any, fname: string)
+export def DocSymbolOutline(lspserver: dict<any>, docSymbol: any, fname: string)
+  var bnr = fname->bufnr()
   var symbolTypeTable: dict<list<dict<any>>> = {}
   var symbolLineTable: list<dict<any>> = []
-  var bnr = fname->bufnr()
 
-  if docsymbol->empty()
+  if docSymbol->empty()
     # No symbols defined for this file. Clear the outline window.
     outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable)
     return
   endif
 
-  if docsymbol[0]->has_key('location')
+  if docSymbol[0]->has_key('location')
     # SymbolInformation[]
-    ProcessSymbolInfoTable(lspserver, bnr, docsymbol, symbolTypeTable,
+    ProcessSymbolInfoTable(lspserver, bnr, docSymbol, symbolTypeTable,
                           symbolLineTable)
   else
     # DocumentSymbol[]
-    ProcessDocSymbolTable(lspserver, bnr, docsymbol, symbolTypeTable,
+    ProcessDocSymbolTable(lspserver, bnr, docSymbol, symbolTypeTable,
                          symbolLineTable)
   endif
 
@@ -616,4 +632,342 @@ export def DocSymbolReply(lspserver: dict<any>, docsymbol: any, fname: string)
   outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable)
 enddef
 
+# Process the list of symbols (LSP interface "SymbolInformation") in
+# "symbolInfoTable". For each symbol, create the name to display in the popup
+# menu along with the symbol range and return the List.
+def GetSymbolsInfoTable(lspserver: dict<any>,
+                       bnr: number,
+                       symbolInfoTable: list<dict<any>>): list<dict<any>>
+  var symbolTable: list<dict<any>> = []
+  var symbolType: string
+  var name: string
+  var containerName: string
+  var r: dict<dict<number>>
+
+  for syminfo in symbolInfoTable
+    symbolType = SymbolKindToName(syminfo.kind)
+    name = $'{symbolType} : {syminfo.name}'
+    if syminfo->has_key('containerName') && !syminfo.containerName->empty()
+      name ..= $' [{syminfo.containerName}]'
+    endif
+    r = syminfo.location.range
+    lspserver.decodeRange(bnr, r)
+
+    symbolTable->add({name: name, range: r, selectionRange: {}})
+  endfor
+
+  return symbolTable
+enddef
+
+# Process the list of symbols (LSP interface "DocumentSymbol") in
+# "docSymbolTable". For each symbol, create the name to display in the popup
+# menu along with the symbol range and return the List in "symbolTable"
+def GetSymbolsDocSymbol(lspserver: dict<any>,
+                       bnr: number,
+                       docSymbolTable: list<dict<any>>,
+                       symbolTable: list<dict<any>>,
+                       parentName: string = '')
+  var symbolType: string
+  var name: string
+  var r: dict<dict<number>>
+  var sr: dict<dict<number>>
+  var symInfo: dict<any>
+
+  for syminfo in docSymbolTable
+    var symName = syminfo.name
+    symbolType = SymbolKindToName(syminfo.kind)->tolower()
+    sr = syminfo.selectionRange
+    lspserver.decodeRange(bnr, sr)
+    r = syminfo.range
+    lspserver.decodeRange(bnr, r)
+    name = $'{symbolType} : {symName}'
+    if parentName != ''
+      name ..= $' [{parentName}]'
+    endif
+    # TODO: Should include syminfo.detail? Will it clutter the menu?
+    symInfo = {name: name, range: r, selectionRange: sr}
+    symbolTable->add(symInfo)
+
+    if syminfo->has_key('children')
+      # Process all the child symbols
+      GetSymbolsDocSymbol(lspserver, bnr, syminfo.children, symbolTable,
+                         symName)
+    endif
+  endfor
+enddef
+
+# Highlight the name and the range of lines for the symbol at symTbl[symIdx]
+def SymbolHighlight(symTbl: list<dict<any>>, symIdx: number)
+  prop_remove({type: 'LspSymbolNameProp', all: true})
+  prop_remove({type: 'LspSymbolRangeProp', all: true})
+  if symTbl->empty()
+    return
+  endif
+
+  var r = symTbl[symIdx].range
+  if r->empty()
+    return
+  endif
+  var rangeStart = r.start
+  var rangeEnd = r.end
+  var start_lnum = rangeStart.line + 1
+  var start_col = rangeStart.character + 1
+  var end_lnum = rangeEnd.line + 1
+  var end_col: number
+  var last_lnum = line('$')
+  if end_lnum > line('$')
+    end_lnum = last_lnum
+    end_col = col([last_lnum, '$'])
+  else
+    end_col = rangeEnd.character + 1
+  endif
+  prop_add(start_lnum, start_col,
+          {type: 'LspSymbolRangeProp',
+           end_lnum: end_lnum,
+           end_col: end_col})
+  cursor(start_lnum, 1)
+  :normal! z.
+
+  var sr = symTbl[symIdx].selectionRange
+  if sr->empty()
+    return
+  endif
+  rangeStart = sr.start
+  rangeEnd = sr.end
+  prop_add(rangeStart.line + 1, 1,
+          {type: 'LspSymbolNameProp',
+           start_col: rangeStart.character + 1,
+           end_lnum: rangeEnd.line + 1,
+           end_col: rangeEnd.character + 1})
+enddef
+
+# Callback invoked when an item is selected in the symbol popup menu
+#   "symTbl" - list of symbols
+#   "symInputPopup" - Symbol search input popup window ID
+#   "save_curpos" - Cursor position before invoking the symbol search.  If the
+#                  symbol search is canceled, restore the cursor to this
+#                  position.
+def SymbolMenuItemSelected(symPopupMenu: number,
+                          result: number)
+  var symTblFiltered = symPopupMenu->getwinvar('symbolTableFiltered', [])
+  var symInputPopup = symPopupMenu->getwinvar('symbolInputPopup', 0)
+  var save_curpos = symPopupMenu->getwinvar('saveCurPos', [])
+
+  # Restore the cursor to the location where the command was invoked
+  setpos('.', save_curpos)
+
+  if result > 0
+    # A symbol is selected in the popup menu
+
+    # Set the previous cursor location mark. Instead of using setpos(), m' is
+    # used so that the current location is added to the jump list.
+    :normal m'
+
+    # Jump to the selected symbol location
+    var r = symTblFiltered[result - 1].selectionRange
+    setcursorcharpos(r.start.line + 1,
+      util.GetCharIdxWithoutCompChar(bufnr(), r.start) + 1)
+  endif
+  symInputPopup->popup_close()
+  prop_remove({type: 'LspSymbolNameProp', all: true})
+  prop_remove({type: 'LspSymbolRangeProp', all: true})
+enddef
+
+# Key filter function for the symbol popup menu.
+def SymbolMenuFilterKey(symPopupMenu: number,
+                       key: string): bool
+  var keyHandled = false
+  var updateInputPopup = false
+  var inputText = symPopupMenu->getwinvar('inputText', '')
+  var symInputPopup = symPopupMenu->getwinvar('symbolInputPopup', 0)
+
+  if key == "\<BS>" || key == "\<C-H>"
+    # Erase a character in the input popup
+    if inputText->len() >= 1
+      inputText = inputText[: -2]
+      keyHandled = true
+      updateInputPopup = true
+    endif
+  elseif key == "\<C-U>"
+    # Erase all the characters in the input popup
+    inputText = ''
+    keyHandled = true
+    updateInputPopup = true
+  elseif key == "\<C-F>"
+      || key == "\<C-B>"
+      || key == "\<PageUp>"
+      || key == "\<PageDown>"
+      || key == "\<C-Home>"
+      || key == "\<C-End>"
+      || key == "\<C-N>"
+      || key == "\<C-P>"
+    # scroll the symbol popup window
+    var cmd: string = 'normal! ' .. (key == "\<C-N>" ? 'j' :
+                                    key == "\<C-P>" ? 'k' : key)
+    win_execute(symPopupMenu, cmd)
+    keyHandled = true
+  elseif key =~ '^\k$'
+    # A keyword character is typed.  Add to the input text and update the
+    # popup
+    inputText ..= key
+    keyHandled = true
+    updateInputPopup = true
+  endif
+
+  var symTblFiltered: list<dict<any>> = []
+  symTblFiltered = symPopupMenu->getwinvar('symbolTableFiltered', [])
+
+  if updateInputPopup
+    # Update the input popup with the new text and update the symbol popup
+    # window with the matching symbol names.
+    symInputPopup->popup_settext(inputText)
+
+    var symbolTable = symPopupMenu->getwinvar('symbolTable')
+    symTblFiltered = symbolTable->deepcopy()
+    var symbolMatchPos: list<list<number>> = []
+
+    # Get the list of symbols fuzzy matching the entered text
+    if inputText != ''
+      var t = symTblFiltered->matchfuzzypos(inputText, {key: 'name'})
+      symTblFiltered = t[0]
+      symbolMatchPos = t[1]
+    endif
+
+    var popupText: list<dict<any>>
+    var text: list<dict<any>>
+    if !symbolMatchPos->empty()
+      # Generate a list of symbol names and the corresponding text properties
+      # to highlight the matching characters.
+      popupText = symTblFiltered->mapnew((idx, val): dict<any> => ({
+       text: val.name,
+       props: symbolMatchPos[idx]->mapnew((_, w: number): dict<any> => ({
+         col: w + 1,
+         length: 1,
+         type: 'LspSymbolMatch'}
+        ))}
+      ))
+    else
+      popupText = symTblFiltered->mapnew((idx, val): dict<string> => {
+       return {text: val.name}
+      })
+    endif
+    symPopupMenu->popup_settext(popupText)
+
+    # Select the first symbol and highlight the corresponding text range
+    win_execute(symPopupMenu, 'cursor(1, 1)')
+    SymbolHighlight(symTblFiltered, 0)
+  endif
+
+  # Save the filtered symbol table and the search text in popup window
+  # variables
+  setwinvar(symPopupMenu, 'inputText', inputText)
+  setwinvar(symPopupMenu, 'symbolTableFiltered', symTblFiltered)
+
+  if !keyHandled
+    # Use the default handler for the key
+    symPopupMenu->popup_filter_menu(key)
+  endif
+
+  # Highlight the name and range of the selected symbol
+  var lnum = line('.', symPopupMenu) - 1
+  if lnum >= 0
+    SymbolHighlight(symTblFiltered, lnum)
+  endif
+
+  return true
+enddef
+
+# Display the symbols popup menu
+def SymbolPopupMenu(symbolTable: list<dict<any>>)
+  var curLine = line('.')
+  var curSymIdx = 0
+
+  # Get the names of all the symbols.  Also get the index of the symbol under
+  # the cursor.
+  var symNames = symbolTable->mapnew((idx, val): string => {
+    var r = val.range
+    if !r->empty() && curSymIdx == 0
+      if curLine >= r.start.line + 1 && curLine <= r.end.line + 1
+       curSymIdx = idx
+      endif
+    endif
+    return val.name
+  })
+
+  var symInputPopupAttr = {
+    title: 'Select Symbol',
+    wrap: false,
+    pos: 'topleft',
+    line: &lines - 14,
+    col: 10,
+    minwidth: 60,
+    minheight: 1,
+    maxheight: 1,
+    maxwidth: 60,
+    fixed: 1,
+    close: 'button',
+    border: []
+  }
+  var symInputPopup = popup_create('', symInputPopupAttr)
+
+  var symNamesPopupattr = {
+    wrap: false,
+    pos: 'topleft',
+    line: &lines - 11,
+    col: 10,
+    minwidth: 60,
+    minheight: 10,
+    maxheight: 10,
+    maxwidth: 60,
+    fixed: 1,
+    border: [0, 0, 0, 0],
+    callback: SymbolMenuItemSelected,
+    filter: SymbolMenuFilterKey,
+  }
+  var symPopupMenu = popup_menu(symNames, symNamesPopupattr)
+
+  # Save the state in the popup menu window variables
+  setwinvar(symPopupMenu, 'symbolTable', symbolTable)
+  setwinvar(symPopupMenu, 'symbolTableFiltered', symbolTable->deepcopy())
+  setwinvar(symPopupMenu, 'symbolInputPopup', symInputPopup)
+  setwinvar(symPopupMenu, 'saveCurPos', getcurpos())
+  prop_type_add('LspSymbolMatch', {bufnr: symPopupMenu->winbufnr(),
+                                  highlight: 'Title',
+                                  override: true})
+
+  # Start with the symbol under the cursor
+  var cmds =<< trim eval END
+    [{curSymIdx + 1}, 1]->cursor()
+    :normal! z.
+  END
+  win_execute(symPopupMenu, cmds, 'silent!')
+
+  # Highlight the name and range of the first symbol
+  SymbolHighlight(symbolTable, curSymIdx)
+enddef
+
+# process the 'textDocument/documentSymbol' reply from the LSP server
+# Result: DocumentSymbol[] | SymbolInformation[] | null
+# Display the symbols in a popup window and jump to the selected symbol
+export def DocSymbolPopup(lspserver: dict<any>, docSymbol: any, fname: string)
+  var symList: list<dict<any>> = []
+
+  if docSymbol->empty()
+    return
+  endif
+
+  var bnr = fname->bufnr()
+
+  if docSymbol[0]->has_key('location')
+    # SymbolInformation[]
+    symList = GetSymbolsInfoTable(lspserver, bnr, docSymbol)
+  else
+    # DocumentSymbol[]
+    GetSymbolsDocSymbol(lspserver, bnr, docSymbol, symList)
+  endif
+
+  :redraw!
+  SymbolPopupMenu(symList)
+enddef
+
 # vim: tabstop=8 shiftwidth=2 softtabstop=2
index 40a962201230b6817ddf8329737ea79c53cfcf91..910dcd1fc3690d1f7ac4100808bca1ded490b85e 100644 (file)
@@ -91,6 +91,8 @@ The following commands are provided:
                        current buffer before the current current position.
 :LspDiagShow           Display the diagnostics messages from the language
                        server for the current buffer in a location list.
+:LspDocumentSymbol     Display the symbols in the current file in a popup
+                       menu and jump to the location of a selected symbol.
 :LspFold               Fold the current file
 :LspFormat             Format a range of lines in the current file using the
                        language server.  The default range is the entire
@@ -692,6 +694,27 @@ can map these commands to keys and make it easier to invoke them.
                        can use the Vim location list commands to browse the
                        list.
 
+                                               *:LspDocumentSymbol*
+:LspDocumentSymbol     Display the symbols in the current file in a popup
+                       menu.  When a symbol is selected in the popup menu by
+                       pressing <Enter> or <Space>, jump to the location of
+                       the symbol.
+
+                       The <Up>, <Down>, <C-F>, <C-B>, <PageUp>, <PageDown>,
+                       <C-Home>, <C-End>, <C-N>, <C-P> keys can be used to
+                       scroll the popup menu.  The <Esc> or <Ctrl-C> keys can
+                       be used to cancel the popup menu.
+
+                       If one or more keyword characters are typed, then only
+                       the symbols containing the keyword characters are
+                       displayed in the popup menu.  Fuzzy searching is used
+                       to get the list of matching symbols.  The <BS> key can
+                       be used to erase the last typed character.  The <C-U>
+                       key can be used to erase all the characters.
+
+                       When scrolling through the symbols in the popup menu,
+                       the corresponding range of lines is highlighted.
+
                                                *:LspFold*
 :LspFold               Create folds for the current buffer.
 
@@ -1363,6 +1386,13 @@ override them.
 *LspSigActiveParameter*                Used to highlight the active signature
                                parameter.  By default, linked to the "LineNr"
                                highlight group.
+*LspSymbolName*                        Used to highlight the symbol name when using
+                               the |:LspDocumentSymbol| command.  By default,
+                               linked to the "Search" highlight group.
+*LspSymbolRange*               Used to highlight the range of lines
+                               containing a symbol when using the
+                               |:LspDocumentSymbol| command.  By default,
+                               linked to the "Visual" highlight group.
 
 For example, to override the highlight used for diagnostics virtual text, you
 can use the following: >
index f38f97ab029e24d2eefae28160bfc69f9ae92700..ce9c7040f04df1d3d67f00ea0df34cdc9dc6a51f 100644 (file)
@@ -67,6 +67,7 @@ command! -nargs=0 -bar -count=1 LspDiagNext lsp.JumpToDiag('next', <count>)
 command! -nargs=0 -bar -count=1 LspDiagPrev lsp.JumpToDiag('prev', <count>)
 command! -nargs=0 -bar LspDiagShow lsp.ShowDiagnostics()
 command! -nargs=0 -bar LspDiagHere lsp.JumpToDiag('here')
+command! -nargs=0 -bar LspDocumentSymbol lsp.ShowDocSymbols()
 command! -nargs=0 -bar LspFold lsp.FoldDocument()
 command! -nargs=0 -bar -range=% LspFormat lsp.TextDocFormat(<range>, <line1>, <line2>)
 command! -nargs=0 -bar -count LspGotoDeclaration lsp.GotoDeclaration(v:false, <q-mods>, <count>)
@@ -113,6 +114,7 @@ if has('gui_running')
   anoremenu <silent> L&sp.Show\ Detail :LspHover<CR>
   anoremenu <silent> L&sp.Outline :LspOutline<CR>
 
+  anoremenu <silent> L&sp.Goto\ Symbol :LspDocumentSymbol<CR>
   anoremenu <silent> L&sp.Symbol\ Search :LspSymbolSearch<CR>
   anoremenu <silent> L&sp.Outgoing\ Calls :LspOutgoingCalls<CR>
   anoremenu <silent> L&sp.Incoming\ Calls :LspIncomingCalls<CR>