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, false)
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(cmdmods: string, 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 bnr = fname->bufnr()
96 var winList: list<number> = bnr->win_findbuf()
98 # Not present in any window
99 if &modified || &buftype != ''
100 # the current buffer is modified or is not a normal buffer, then
101 # open the file in a new window
102 exe $'split {symTbl[result - 1].file}'
104 exe $'confirm edit {symTbl[result - 1].file}'
107 # If the target buffer is opened in the curent window, then don't
110 # If the target buffer is opened in a window in the current tab
112 var winID = fname->bufwinid()
114 # not present in the current tab page. Use the first window.
121 exe $'{cmdmods} split {symTbl[result - 1].file}'
123 # Set the previous cursor location mark. Instead of using setpos(), m' is
124 # used so that the current location is added to the jump list.
126 setcursorcharpos(symTbl[result - 1].pos.line + 1,
127 util.GetCharIdxWithoutCompChar(bufnr(),
128 symTbl[result - 1].pos) + 1)
134 # display a list of symbols from the workspace
135 def ShowSymbolMenu(lspserver: dict<any>, query: string, cmdmods: string)
136 # Create the popup menu
137 var lnum = &lines - &cmdheight - 2 - 10
139 title: 'Workspace Symbol Search',
151 filter: function(FilterSymbols, [lspserver]),
152 callback: function('JumpToWorkspaceSymbol', [cmdmods])
154 lspserver.workspaceSymbolPopup = popup_menu([], popupAttr)
155 lspserver.workspaceSymbolQuery = query
156 prop_type_add('lspworkspacesymbol',
157 {bufnr: lspserver.workspaceSymbolPopup->winbufnr(),
159 :echo $'Symbol: {query}'
162 # Convert a file name to <filename> (<dirname>) format.
163 # Make sure the popup does't occupy the entire screen by reducing the width.
164 def MakeMenuName(popupWidth: number, fname: string): string
165 var filename: string = fname->fnamemodify(':t')
166 var flen: number = filename->len()
167 var dirname: string = fname->fnamemodify(':h')
169 if fname->len() > popupWidth && flen < popupWidth
170 # keep the full file name and reduce directory name length
171 # keep some characters at the beginning and end (equally).
172 # 6 spaces are used for "..." and " ()"
173 var dirsz = (popupWidth - flen - 6) / 2
174 dirname = dirname[: dirsz] .. '...' .. dirname[-dirsz : ]
176 var str: string = filename
178 str ..= $' ({dirname}/)'
183 # process the 'workspace/symbol' reply from the LSP server
184 # Result: SymbolInformation[] | null
185 export def WorkspaceSymbolPopup(lspserver: dict<any>, query: string,
186 symInfo: list<dict<any>>, cmdmods: string)
187 var symbols: list<dict<any>> = []
188 var symbolType: string
192 # Create a symbol popup menu if it is not present
193 if lspserver.workspaceSymbolPopup->winbufnr() == -1
194 ShowSymbolMenu(lspserver, query, cmdmods)
197 for symbol in symInfo
198 if !symbol->has_key('location')
199 # ignore entries without location information
203 # interface SymbolInformation
204 fileName = util.LspUriToFile(symbol.location.uri)
206 symName = symbol.name
207 if symbol->has_key('containerName') && symbol.containerName != ''
208 symName = $'{symbol.containerName}::{symName}'
210 symName ..= $' [{SymbolKindToName(symbol.kind)}]'
211 symName ..= ' ' .. MakeMenuName(
212 lspserver.workspaceSymbolPopup->popup_getpos().core_width,
215 symbols->add({name: symName,
217 pos: symbol.location.range.start})
219 symbols->setwinvar(lspserver.workspaceSymbolPopup, 'LspSymbolTable')
220 lspserver.workspaceSymbolPopup->popup_settext(
221 symbols->copy()->mapnew('v:val.name'))
224 # map the LSP symbol kind number to string
225 export def SymbolKindToName(symkind: number): string
226 var symbolMap: list<string> = [
258 return symbolMap[symkind]
261 def UpdatePeekFilePopup(lspserver: dict<any>, locations: list<dict<any>>)
262 if lspserver.peekSymbolPopup->winbufnr() == -1
266 lspserver.peekSymbolFilePopup->popup_close()
268 var n = line('.', lspserver.peekSymbolPopup) - 1
269 var [uri, range] = util.LspLocationParse(locations[n])
270 var fname: string = util.LspUriToFile(uri)
272 var bnr: number = fname->bufnr()
274 bnr = fname->bufadd()
278 title: $"{fname->fnamemodify(':t')} ({fname->fnamemodify(':h')})",
283 minwidth: winwidth(0) - 38,
284 maxwidth: winwidth(0) - 38,
292 lspserver.peekSymbolFilePopup = popup_create(bnr, popupAttrs)
293 var cmds =<< trim eval END
295 [{range.start.line + 1}, 1]->cursor()
298 win_execute(lspserver.peekSymbolFilePopup, cmds)
300 lspserver.peekSymbolFilePopup->clearmatches()
301 var start_col = util.GetLineByteFromPos(bnr, range.start) + 1
302 var end_col = util.GetLineByteFromPos(bnr, range.end)
303 var pos = [[range.start.line + 1,
304 start_col, end_col - start_col + 1]]
305 matchaddpos('Search', pos, 10, -1, {window: lspserver.peekSymbolFilePopup})
308 def LocPopupFilter(lspserver: dict<any>, locations: list<dict<any>>,
309 popup_id: number, key: string): bool
310 popup_filter_menu(popup_id, key)
311 if lspserver.peekSymbolPopup->winbufnr() == -1
312 if lspserver.peekSymbolFilePopup->winbufnr() != -1
313 lspserver.peekSymbolFilePopup->popup_close()
315 lspserver.peekSymbolPopup = -1
316 lspserver.peekSymbolFilePopup = -1
318 UpdatePeekFilePopup(lspserver, locations)
323 def LocPopupCallback(lspserver: dict<any>, locations: list<dict<any>>,
324 popup_id: number, selIdx: number)
325 if lspserver.peekSymbolFilePopup->winbufnr() != -1
326 lspserver.peekSymbolFilePopup->popup_close()
328 lspserver.peekSymbolPopup = -1
330 util.PushCursorToTagStack()
331 util.JumpToLspLocation(locations[selIdx - 1], '')
335 # Display the locations in a popup menu. Display the corresponding file in
336 # an another popup window.
337 def PeekLocations(lspserver: dict<any>, locations: list<dict<any>>,
339 if lspserver.peekSymbolPopup->winbufnr() != -1
340 # If the symbol popup window is already present, close it.
341 lspserver.peekSymbolPopup->popup_close()
344 var w: number = &columns
345 var fnamelen = float2nr(w * 0.4)
347 var menuItems: list<string> = []
349 var [uri, range] = util.LspLocationParse(loc)
350 var fname: string = util.LspUriToFile(uri)
351 var bnr: number = fname->bufnr()
353 bnr = fname->bufadd()
355 :silent! bnr->bufload()
357 var lnum = range.start.line + 1
358 var text: string = bnr->getbufline(lnum)->get(0, '')
359 menuItems->add($'{lnum}: {text}')
367 col: winwidth(0) - 34,
374 filter: function(LocPopupFilter, [lspserver, locations]),
375 callback: function(LocPopupCallback, [lspserver, locations])
377 lspserver.peekSymbolPopup = popup_menu(menuItems, popupAttrs)
378 UpdatePeekFilePopup(lspserver, locations)
381 export def ShowLocations(lspserver: dict<any>, locations: list<dict<any>>,
382 peekSymbol: bool, title: string)
384 PeekLocations(lspserver, locations, title)
388 # create a loclist the location of the locations
389 var qflist: list<dict<any>> = []
391 var [uri, range] = util.LspLocationParse(loc)
392 var fname: string = util.LspUriToFile(uri)
393 var bnr: number = fname->bufnr()
395 bnr = fname->bufadd()
397 :silent! bnr->bufload()
398 var text: string = bnr->getbufline(range.start.line + 1)->get(0, '')->trim("\t ", 1)
399 qflist->add({filename: fname,
400 lnum: range.start.line + 1,
401 col: util.GetLineByteFromPos(bnr, range.start) + 1,
405 var save_winid = win_getid()
407 if opt.lspOptions.useQuickfixForLocations
408 setqflist([], ' ', {title: title, items: qflist})
409 var mods: string = ''
412 setloclist(0, [], ' ', {title: title, items: qflist})
413 var mods: string = ''
417 if !opt.lspOptions.keepFocusInReferences
418 save_winid->win_gotoid()
422 # Key filter callback function used for the symbol popup window.
423 # Vim doesn't close the popup window when the escape key is pressed.
424 # This is function supports that.
425 def SymbolFilterCB(lspserver: dict<any>, id: number, key: string): bool
427 lspserver.peekSymbolPopup->popup_close()
434 # Display the file specified by LSP "LocationLink" in a popup window and
435 # highlight the range in "location".
436 def PeekSymbolLocation(lspserver: dict<any>, location: dict<any>)
437 var [uri, range] = util.LspLocationParse(location)
438 var fname = util.LspUriToFile(uri)
439 var bnum = fname->bufadd()
441 # Failed to create or find a buffer
444 :silent! bnum->bufload()
446 if lspserver.peekSymbolPopup->winbufnr() != -1
447 # If the symbol popup window is already present, close it.
448 lspserver.peekSymbolPopup->popup_close()
450 var CbFunc = function(SymbolFilterCB, [lspserver])
452 title: $"{fnamemodify(fname, ':t')} ({fnamemodify(fname, ':h')})",
464 lspserver.peekSymbolPopup = popup_atcursor(bnum, popupAttrs)
466 # Highlight the symbol name and center the line in the popup
467 var pwid = lspserver.peekSymbolPopup
468 var pwbuf = pwid->winbufnr()
469 var pos: list<number> = []
470 var start_col: number
472 start_col = util.GetLineByteFromPos(pwbuf, range.start) + 1
473 end_col = util.GetLineByteFromPos(pwbuf, range.end) + 1
474 pos->add(range.start.line + 1)
475 pos->extend([start_col, end_col - start_col])
476 matchaddpos('Search', [pos], 10, 101, {window: pwid})
477 var cmds =<< trim eval END
478 [{range.start.line + 1}, 1]->cursor()
481 win_execute(pwid, cmds, 'silent!')
484 # Jump to the definition, declaration or implementation of a symbol.
485 # Also, used to peek at the definition, declaration or implementation of a
487 export def GotoSymbol(lspserver: dict<any>, location: dict<any>,
488 peekSymbol: bool, cmdmods: string)
490 PeekSymbolLocation(lspserver, location)
492 # Save the current cursor location in the tag stack.
493 util.PushCursorToTagStack()
494 util.JumpToLspLocation(location, cmdmods)
498 # Process the LSP server reply message for a 'textDocument/definition' request
499 # and return a list of Dicts in a format accepted by the 'tagfunc' option.
500 export def TagFunc(lspserver: dict<any>,
501 taglocations: list<dict<any>>,
502 pat: string): list<dict<any>>
503 var retval: list<dict<any>>
505 for tagloc in taglocations
509 var [uri, range] = util.LspLocationParse(tagloc)
510 tagitem.filename = util.LspUriToFile(uri)
511 var bnr = util.LspUriToBufnr(uri)
512 var startByteIdx = util.GetLineByteFromPos(bnr, range.start)
513 tagitem.cmd = $"/\\%{range.start.line + 1}l\\%{startByteIdx + 1}c"
521 # process SymbolInformation[]
522 def ProcessSymbolInfoTable(lspserver: dict<any>,
524 symbolInfoTable: list<dict<any>>,
525 symbolTypeTable: dict<list<dict<any>>>,
526 symbolLineTable: list<dict<any>>)
528 var symbolType: string
530 var r: dict<dict<number>>
531 var symInfo: dict<any>
533 for syminfo in symbolInfoTable
534 fname = util.LspUriToFile(syminfo.location.uri)
535 symbolType = SymbolKindToName(syminfo.kind)
537 if syminfo->has_key('containerName')
538 if syminfo.containerName != ''
539 name ..= $' [{syminfo.containerName}]'
542 r = syminfo.location.range
543 lspserver.decodeRange(bnr, r)
545 if !symbolTypeTable->has_key(symbolType)
546 symbolTypeTable[symbolType] = []
548 symInfo = {name: name, range: r}
549 symbolTypeTable[symbolType]->add(symInfo)
550 symbolLineTable->add(symInfo)
554 # process DocumentSymbol[]
555 def ProcessDocSymbolTable(lspserver: dict<any>,
557 docSymbolTable: list<dict<any>>,
558 symbolTypeTable: dict<list<dict<any>>>,
559 symbolLineTable: list<dict<any>>)
560 var symbolType: string
562 var r: dict<dict<number>>
563 var symInfo: dict<any>
564 var symbolDetail: string
565 var childSymbols: dict<list<dict<any>>>
567 for syminfo in docSymbolTable
569 symbolType = SymbolKindToName(syminfo.kind)
570 r = syminfo.selectionRange
571 lspserver.decodeRange(bnr, r)
572 if syminfo->has_key('detail')
573 symbolDetail = syminfo.detail
575 if !symbolTypeTable->has_key(symbolType)
576 symbolTypeTable[symbolType] = []
579 if syminfo->has_key('children')
580 ProcessDocSymbolTable(lspserver, bnr, syminfo.children, childSymbols,
583 symInfo = {name: name, range: r, detail: symbolDetail,
584 children: childSymbols}
585 symbolTypeTable[symbolType]->add(symInfo)
586 symbolLineTable->add(symInfo)
590 # process the 'textDocument/documentSymbol' reply from the LSP server
591 # Open a symbols window and display the symbols as a tree
592 # Result: DocumentSymbol[] | SymbolInformation[] | null
593 export def DocSymbolReply(lspserver: dict<any>, docsymbol: any, fname: string)
594 var symbolTypeTable: dict<list<dict<any>>> = {}
595 var symbolLineTable: list<dict<any>> = []
596 var bnr = fname->bufnr()
598 if docsymbol->empty()
599 # No symbols defined for this file. Clear the outline window.
600 outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable)
604 if docsymbol[0]->has_key('location')
605 # SymbolInformation[]
606 ProcessSymbolInfoTable(lspserver, bnr, docsymbol, symbolTypeTable,
610 ProcessDocSymbolTable(lspserver, bnr, docsymbol, symbolTypeTable,
614 # sort the symbols by line number
615 symbolLineTable->sort((a, b) => a.range.start.line - b.range.start.line)
616 outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable)
619 # vim: tabstop=8 shiftwidth=2 softtabstop=2