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
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
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()
# 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
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