3 # Functions for dealing with symbols.
4 # - LSP symbol menu and for searching symbols across the workspace.
6 # - jump to a symbol definition, declaration, type definition or
9 import './options.vim' as opt
11 import './outline.vim'
13 # Handle keys pressed when the workspace symbol popup menu is displayed
14 def FilterSymbols(lspserver: dict<any>, popupID: number, key: string): bool
15 var key_handled: bool = false
16 var update_popup: bool = false
17 var query: string = lspserver.workspaceSymbolQuery
19 if key == "\<BS>" || key == "\<C-H>"
20 # Erase one character from the filter text
26 elseif key == "\<C-U>"
27 # clear the filter text
31 elseif key == "\<C-F>"
34 || key == "\<PageDown>"
39 # scroll the popup window
40 var cmd: string = 'normal! ' .. (key == "\<C-N>" ? 'j' : key == "\<C-P>" ? 'k' : key)
41 win_execute(popupID, cmd)
43 elseif key == "\<Up>" || key == "\<Down>"
44 # Use native Vim handling for these keys
46 elseif key =~ '^\f$' || key == "\<Space>"
47 # Filter the names based on the typed key and keys typed before
54 # Update the popup with the new list of symbol names
55 popupID->popup_settext('')
57 lspserver.workspaceQuery(query)
59 []->setwinvar(popupID, 'LspSymbolTable')
61 :echo $'Symbol: {query}'
64 # Update the workspace symbol query string
65 lspserver.workspaceSymbolQuery = query
71 return popupID->popup_filter_menu(key)
74 # Jump to the location of a symbol selected in the popup menu
75 def JumpToWorkspaceSymbol(popupID: number, result: number): void
76 # clear the message displayed at the command-line
84 var symTbl: list<dict<any>> = popupID->getwinvar('LspSymbolTable', [])
89 # Save the current location in the tag stack
90 util.PushCursorToTagStack()
92 # if the selected file is already present in a window, then jump to it
93 var fname: string = symTbl[result - 1].file
94 var bufnum = fname->bufnr()
95 var winList: list<number> = bufnum->win_findbuf()
97 # Not present in any window
98 if &modified || &buftype != ''
99 # the current buffer is modified or is not a normal buffer, then open
100 # the file in a new window
101 exe $'split {symTbl[result - 1].file}'
103 exe $'confirm edit {symTbl[result - 1].file}'
107 var winID = fname->bufwinid()
109 # not present in the current tab page
115 # Set the previous cursor location mark. Instead of using setpos(), m' is
116 # used so that the current location is added to the jump list.
118 setcursorcharpos(symTbl[result - 1].pos.line + 1,
119 util.GetCharIdxWithoutCompChar(bufnum,
120 symTbl[result - 1].pos) + 1)
126 # display a list of symbols from the workspace
127 def ShowSymbolMenu(lspserver: dict<any>, query: string)
128 # Create the popup menu
129 var lnum = &lines - &cmdheight - 2 - 10
131 title: 'Workspace Symbol Search',
143 filter: function(FilterSymbols, [lspserver]),
144 callback: JumpToWorkspaceSymbol
146 lspserver.workspaceSymbolPopup = popup_menu([], popupAttr)
147 lspserver.workspaceSymbolQuery = query
148 prop_type_add('lspworkspacesymbol',
149 {bufnr: lspserver.workspaceSymbolPopup->winbufnr(),
151 :echo $'Symbol: {query}'
154 # Convert a file name to <filename> (<dirname>) format.
155 # Make sure the popup does't occupy the entire screen by reducing the width.
156 def MakeMenuName(popupWidth: number, fname: string): string
157 var filename: string = fname->fnamemodify(':t')
158 var flen: number = filename->len()
159 var dirname: string = fname->fnamemodify(':h')
161 if fname->len() > popupWidth && flen < popupWidth
162 # keep the full file name and reduce directory name length
163 # keep some characters at the beginning and end (equally).
164 # 6 spaces are used for "..." and " ()"
165 var dirsz = (popupWidth - flen - 6) / 2
166 dirname = dirname[: dirsz] .. '...' .. dirname[-dirsz : ]
168 var str: string = filename
170 str ..= $' ({dirname}/)'
175 # process the 'workspace/symbol' reply from the LSP server
176 # Result: SymbolInformation[] | null
177 export def WorkspaceSymbolPopup(lspserver: dict<any>, query: string,
178 symInfo: list<dict<any>>)
179 var symbols: list<dict<any>> = []
180 var symbolType: string
182 var r: dict<dict<number>>
185 # Create a symbol popup menu if it is not present
186 if lspserver.workspaceSymbolPopup->winbufnr() == -1
187 ShowSymbolMenu(lspserver, query)
190 for symbol in symInfo
191 if !symbol->has_key('location')
192 # ignore entries without location information
196 # interface SymbolInformation
197 fileName = util.LspUriToFile(symbol.location.uri)
198 r = symbol.location.range
199 lspserver.decodeRange(fileName->bufnr(), r)
201 symName = symbol.name
202 if symbol->has_key('containerName') && symbol.containerName != ''
203 symName = $'{symbol.containerName}::{symName}'
205 symName ..= $' [{SymbolKindToName(symbol.kind)}]'
206 symName ..= ' ' .. MakeMenuName(
207 lspserver.workspaceSymbolPopup->popup_getpos().core_width,
210 symbols->add({name: symName,
214 symbols->setwinvar(lspserver.workspaceSymbolPopup, 'LspSymbolTable')
215 lspserver.workspaceSymbolPopup->popup_settext(
216 symbols->copy()->mapnew('v:val.name'))
219 # map the LSP symbol kind number to string
220 export def SymbolKindToName(symkind: number): string
221 var symbolMap: list<string> = [
253 return symbolMap[symkind]
256 def UpdatePeekFilePopup(lspserver: dict<any>, locations: list<dict<any>>)
257 if lspserver.peekSymbolPopup->winbufnr() == -1
261 lspserver.peekSymbolFilePopup->popup_close()
263 var n = line('.', lspserver.peekSymbolPopup) - 1
264 var [uri, range] = util.LspLocationParse(locations[n])
265 var fname: string = util.LspUriToFile(uri)
267 var bnr: number = fname->bufnr()
269 bnr = fname->bufadd()
273 title: $"{fname->fnamemodify(':t')} ({fname->fnamemodify(':h')})",
278 minwidth: winwidth(0) - 38,
279 maxwidth: winwidth(0) - 38,
287 lspserver.peekSymbolFilePopup = popup_create(bnr, popupAttrs)
288 var cmds =<< trim eval END
290 [{range.start.line + 1}, 1]->cursor()
293 win_execute(lspserver.peekSymbolFilePopup, cmds)
295 lspserver.peekSymbolFilePopup->clearmatches()
296 var start_col = util.GetLineByteFromPos(bnr, range.start) + 1
297 var end_col = util.GetLineByteFromPos(bnr, range.end)
298 var pos = [[range.start.line + 1,
299 start_col, end_col - start_col + 1]]
300 matchaddpos('Search', pos, 10, -1, {window: lspserver.peekSymbolFilePopup})
303 def LocPopupFilter(lspserver: dict<any>, locations: list<dict<any>>,
304 popup_id: number, key: string): bool
305 popup_filter_menu(popup_id, key)
306 if lspserver.peekSymbolPopup->winbufnr() == -1
307 if lspserver.peekSymbolFilePopup->winbufnr() != -1
308 lspserver.peekSymbolFilePopup->popup_close()
310 lspserver.peekSymbolPopup = -1
311 lspserver.peekSymbolFilePopup = -1
313 UpdatePeekFilePopup(lspserver, locations)
318 def LocPopupCallback(lspserver: dict<any>, locations: list<dict<any>>,
319 popup_id: number, selIdx: number)
320 if lspserver.peekSymbolFilePopup->winbufnr() != -1
321 lspserver.peekSymbolFilePopup->popup_close()
323 lspserver.peekSymbolPopup = -1
325 util.PushCursorToTagStack()
326 util.JumpToLspLocation(locations[selIdx - 1], '')
330 # Display the locations in a popup menu. Display the corresponding file in
331 # an another popup window.
332 def PeekLocations(lspserver: dict<any>, locations: list<dict<any>>,
334 if lspserver.peekSymbolPopup->winbufnr() != -1
335 # If the symbol popup window is already present, close it.
336 lspserver.peekSymbolPopup->popup_close()
339 var w: number = &columns
340 var fnamelen = float2nr(w * 0.4)
342 var menuItems: list<string> = []
344 var [uri, range] = util.LspLocationParse(loc)
345 var fname: string = util.LspUriToFile(uri)
346 var bnr: number = fname->bufnr()
348 bnr = fname->bufadd()
352 var lnum = range.start.line + 1
353 var text: string = bnr->getbufline(lnum)->get(0, '')
354 menuItems->add($'{lnum}: {text}')
362 col: winwidth(0) - 34,
369 filter: function(LocPopupFilter, [lspserver, locations]),
370 callback: function(LocPopupCallback, [lspserver, locations])
372 lspserver.peekSymbolPopup = popup_menu(menuItems, popupAttrs)
373 UpdatePeekFilePopup(lspserver, locations)
376 export def ShowLocations(lspserver: dict<any>, locations: list<dict<any>>,
377 peekSymbol: bool, title: string)
379 PeekLocations(lspserver, locations, title)
383 # create a loclist the location of the locations
384 var qflist: list<dict<any>> = []
386 var [uri, range] = util.LspLocationParse(loc)
387 var fname: string = util.LspUriToFile(uri)
388 var bnr: number = fname->bufnr()
390 bnr = fname->bufadd()
393 var text: string = bnr->getbufline(range.start.line + 1)->get(0, '')->trim("\t ", 1)
394 qflist->add({filename: fname,
395 lnum: range.start.line + 1,
396 col: util.GetLineByteFromPos(bnr, range.start) + 1,
400 var save_winid = win_getid()
402 if opt.lspOptions.useQuickfixForLocations
403 setqflist([], ' ', {title: title, items: qflist})
404 var mods: string = ''
407 setloclist(0, [], ' ', {title: title, items: qflist})
408 var mods: string = ''
412 if !opt.lspOptions.keepFocusInReferences
413 save_winid->win_gotoid()
417 # Key filter callback function used for the symbol popup window.
418 # Vim doesn't close the popup window when the escape key is pressed.
419 # This is function supports that.
420 def SymbolFilterCB(lspserver: dict<any>, id: number, key: string): bool
422 lspserver.peekSymbolPopup->popup_close()
429 # Display the file specified by LSP "LocationLink" in a popup window and
430 # highlight the range in "location".
431 def PeekSymbolLocation(lspserver: dict<any>, location: dict<any>)
432 var [uri, range] = util.LspLocationParse(location)
433 var fname = util.LspUriToFile(uri)
434 var bnum = fname->bufadd()
436 # Failed to create or find a buffer
439 silent! bnum->bufload()
441 if lspserver.peekSymbolPopup->winbufnr() != -1
442 # If the symbol popup window is already present, close it.
443 lspserver.peekSymbolPopup->popup_close()
445 var CbFunc = function(SymbolFilterCB, [lspserver])
447 title: $"{fnamemodify(fname, ':t')} ({fnamemodify(fname, ':h')})",
459 lspserver.peekSymbolPopup = popup_atcursor(bnum, popupAttrs)
461 # Highlight the symbol name and center the line in the popup
462 var pwid = lspserver.peekSymbolPopup
463 var pwbuf = pwid->winbufnr()
464 var pos: list<number> = []
465 var start_col: number
467 start_col = util.GetLineByteFromPos(pwbuf, range.start) + 1
468 end_col = util.GetLineByteFromPos(pwbuf, range.end) + 1
469 pos->add(range.start.line + 1)
470 pos->extend([start_col, end_col - start_col])
471 matchaddpos('Search', [pos], 10, 101, {window: pwid})
472 var cmds =<< trim eval END
473 [{range.start.line + 1}, 1]->cursor()
476 win_execute(pwid, cmds, 'silent!')
479 # Jump to the definition, declaration or implementation of a symbol.
480 # Also, used to peek at the definition, declaration or implementation of a
482 export def GotoSymbol(lspserver: dict<any>, location: dict<any>,
483 peekSymbol: bool, cmdmods: string)
485 PeekSymbolLocation(lspserver, location)
487 # Save the current cursor location in the tag stack.
488 util.PushCursorToTagStack()
489 util.JumpToLspLocation(location, cmdmods)
493 # Process the LSP server reply message for a 'textDocument/definition' request
494 # and return a list of Dicts in a format accepted by the 'tagfunc' option.
495 export def TagFunc(lspserver: dict<any>,
496 taglocations: list<dict<any>>,
497 pat: string): list<dict<any>>
498 var retval: list<dict<any>>
500 for tagloc in taglocations
504 var [uri, range] = util.LspLocationParse(tagloc)
505 tagitem.filename = util.LspUriToFile(uri)
506 var bnr = util.LspUriToBufnr(uri)
507 var startByteIdx = util.GetLineByteFromPos(bnr, range.start)
508 tagitem.cmd = $"/\\%{range.start.line + 1}l\\%{startByteIdx + 1}c"
516 # process SymbolInformation[]
517 def ProcessSymbolInfoTable(lspserver: dict<any>,
519 symbolInfoTable: list<dict<any>>,
520 symbolTypeTable: dict<list<dict<any>>>,
521 symbolLineTable: list<dict<any>>)
523 var symbolType: string
525 var r: dict<dict<number>>
526 var symInfo: dict<any>
528 for syminfo in symbolInfoTable
529 fname = util.LspUriToFile(syminfo.location.uri)
530 symbolType = SymbolKindToName(syminfo.kind)
532 if syminfo->has_key('containerName')
533 if syminfo.containerName != ''
534 name ..= $' [{syminfo.containerName}]'
537 r = syminfo.location.range
538 lspserver.decodeRange(bnr, r)
540 if !symbolTypeTable->has_key(symbolType)
541 symbolTypeTable[symbolType] = []
543 symInfo = {name: name, range: r}
544 symbolTypeTable[symbolType]->add(symInfo)
545 symbolLineTable->add(symInfo)
549 # process DocumentSymbol[]
550 def ProcessDocSymbolTable(lspserver: dict<any>,
552 docSymbolTable: list<dict<any>>,
553 symbolTypeTable: dict<list<dict<any>>>,
554 symbolLineTable: list<dict<any>>)
555 var symbolType: string
557 var r: dict<dict<number>>
558 var symInfo: dict<any>
559 var symbolDetail: string
560 var childSymbols: dict<list<dict<any>>>
562 for syminfo in docSymbolTable
564 symbolType = SymbolKindToName(syminfo.kind)
565 r = syminfo.selectionRange
566 lspserver.decodeRange(bnr, r)
567 if syminfo->has_key('detail')
568 symbolDetail = syminfo.detail
570 if !symbolTypeTable->has_key(symbolType)
571 symbolTypeTable[symbolType] = []
574 if syminfo->has_key('children')
575 ProcessDocSymbolTable(lspserver, bnr, syminfo.children, childSymbols,
578 symInfo = {name: name, range: r, detail: symbolDetail,
579 children: childSymbols}
580 symbolTypeTable[symbolType]->add(symInfo)
581 symbolLineTable->add(symInfo)
585 # process the 'textDocument/documentSymbol' reply from the LSP server
586 # Open a symbols window and display the symbols as a tree
587 # Result: DocumentSymbol[] | SymbolInformation[] | null
588 export def DocSymbolReply(lspserver: dict<any>, docsymbol: any, fname: string)
589 var symbolTypeTable: dict<list<dict<any>>> = {}
590 var symbolLineTable: list<dict<any>> = []
591 var bnr = fname->bufnr()
593 if docsymbol->empty()
594 # No symbols defined for this file. Clear the outline window.
595 outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable)
599 if docsymbol[0]->has_key('location')
600 # SymbolInformation[]
601 ProcessSymbolInfoTable(lspserver, bnr, docsymbol, symbolTypeTable,
605 ProcessDocSymbolTable(lspserver, bnr, docsymbol, symbolTypeTable,
609 # sort the symbols by line number
610 symbolLineTable->sort((a, b) => a.range.start.line - b.range.start.line)
611 outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable)
614 # vim: tabstop=8 shiftwidth=2 softtabstop=2