]> Sergey Matveev's repositories - vim-lsp.git/blob - autoload/lsp/symbol.vim
Add support for displaying symbols in the current file in a popup menu
[vim-lsp.git] / autoload / lsp / symbol.vim
1 vim9script
2
3 # Functions for dealing with symbols.
4 #   - LSP symbol menu and for searching symbols across the workspace.
5 #   - show locations
6 #   - jump to a symbol definition, declaration, type definition or
7 #     implementation
8
9 import './options.vim' as opt
10 import './util.vim'
11 import './outline.vim'
12
13 # Initialize the highlight group and the text property type used for
14 # document symbol search
15 export def InitOnce()
16   # Use a high priority value to override other highlights in the line
17   hlset([{name: 'LspSymbolName', default: true, linksto: 'Search'}])
18   prop_type_add('LspSymbolNameProp', {highlight: 'LspSymbolName',
19                                        combine: false,
20                                        override: true,
21                                        priority: 201})
22   hlset([{name: 'LspSymbolRange', default: true, linksto: 'Visual'}])
23   prop_type_add('LspSymbolRangeProp', {highlight: 'LspSymbolRange',
24                                        combine: false,
25                                        override: true,
26                                        priority: 200})
27 enddef
28
29 # Handle keys pressed when the workspace symbol popup menu is displayed
30 def FilterSymbols(lspserver: dict<any>, popupID: number, key: string): bool
31   var key_handled: bool = false
32   var update_popup: bool = false
33   var query: string = lspserver.workspaceSymbolQuery
34
35   if key == "\<BS>" || key == "\<C-H>"
36     # Erase one character from the filter text
37     if query->len() >= 1
38       query = query[: -2]
39       update_popup = true
40     endif
41     key_handled = true
42   elseif key == "\<C-U>"
43     # clear the filter text
44     query = ''
45     update_popup = true
46     key_handled = true
47   elseif key == "\<C-F>"
48         || key == "\<C-B>"
49         || key == "\<PageUp>"
50         || key == "\<PageDown>"
51         || key == "\<C-Home>"
52         || key == "\<C-End>"
53         || key == "\<C-N>"
54         || key == "\<C-P>"
55     # scroll the popup window
56     var cmd: string = 'normal! ' .. (key == "\<C-N>" ? 'j' : key == "\<C-P>" ? 'k' : key)
57     win_execute(popupID, cmd)
58     key_handled = true
59   elseif key == "\<Up>" || key == "\<Down>"
60     # Use native Vim handling for these keys
61     key_handled = false
62   elseif key =~ '^\f$' || key == "\<Space>"
63     # Filter the names based on the typed key and keys typed before
64     query ..= key
65     update_popup = true
66     key_handled = true
67   endif
68
69   if update_popup
70     # Update the popup with the new list of symbol names
71     popupID->popup_settext('')
72     if query != ''
73       lspserver.workspaceQuery(query, false)
74     else
75       []->setwinvar(popupID, 'LspSymbolTable')
76     endif
77     :echo $'Symbol: {query}'
78   endif
79
80   # Update the workspace symbol query string
81   lspserver.workspaceSymbolQuery = query
82
83   if key_handled
84     return true
85   endif
86
87   return popupID->popup_filter_menu(key)
88 enddef
89
90 # Jump to the location of a symbol selected in the popup menu
91 def JumpToWorkspaceSymbol(cmdmods: string, popupID: number, result: number): void
92   # clear the message displayed at the command-line
93   :echo ''
94
95   if result <= 0
96     # popup is canceled
97     return
98   endif
99
100   var symTbl: list<dict<any>> = popupID->getwinvar('LspSymbolTable', [])
101   if symTbl->empty()
102     return
103   endif
104   try
105     # Save the current location in the tag stack
106     util.PushCursorToTagStack()
107
108     # if the selected file is already present in a window, then jump to it
109     var fname: string = symTbl[result - 1].file
110     var bnr = fname->bufnr()
111     if cmdmods->empty()
112       var winList: list<number> = bnr->win_findbuf()
113       if winList->empty()
114         # Not present in any window
115         if &modified || &buftype != ''
116           # the current buffer is modified or is not a normal buffer, then
117           # open the file in a new window
118           exe $'split {symTbl[result - 1].file}'
119         else
120           exe $'confirm edit {symTbl[result - 1].file}'
121         endif
122       else
123         # If the target buffer is opened in the current window, then don't
124         # change the window.
125         if bufnr() != bnr
126           # If the target buffer is opened in a window in the current tab
127           # page, then use it.
128           var winID = fname->bufwinid()
129           if winID == -1
130             # not present in the current tab page.  Use the first window.
131             winID = winList[0]
132           endif
133           winID->win_gotoid()
134         endif
135       endif
136     else
137       exe $'{cmdmods} split {symTbl[result - 1].file}'
138     endif
139     # Set the previous cursor location mark. Instead of using setpos(), m' is
140     # used so that the current location is added to the jump list.
141     :normal m'
142     setcursorcharpos(symTbl[result - 1].pos.line + 1,
143                      util.GetCharIdxWithoutCompChar(bufnr(),
144                                                     symTbl[result - 1].pos) + 1)
145   catch
146     # ignore exceptions
147   endtry
148 enddef
149
150 # display a list of symbols from the workspace
151 def ShowSymbolMenu(lspserver: dict<any>, query: string, cmdmods: string)
152   # Create the popup menu
153   var lnum = &lines - &cmdheight - 2 - 10
154   var popupAttr = {
155       title: 'Workspace Symbol Search',
156       wrap: false,
157       pos: 'topleft',
158       line: lnum,
159       col: 2,
160       minwidth: 60,
161       minheight: 10,
162       maxheight: 10,
163       maxwidth: 60,
164       mapping: false,
165       fixed: 1,
166       close: 'button',
167       filter: function(FilterSymbols, [lspserver]),
168       callback: function('JumpToWorkspaceSymbol', [cmdmods])
169   }
170   lspserver.workspaceSymbolPopup = popup_menu([], popupAttr)
171   lspserver.workspaceSymbolQuery = query
172   prop_type_add('lspworkspacesymbol',
173                         {bufnr: lspserver.workspaceSymbolPopup->winbufnr(),
174                          highlight: 'Title'})
175   :echo $'Symbol: {query}'
176 enddef
177
178 # Convert a file name to <filename> (<dirname>) format.
179 # Make sure the popup doesn't occupy the entire screen by reducing the width.
180 def MakeMenuName(popupWidth: number, fname: string): string
181   var filename: string = fname->fnamemodify(':t')
182   var flen: number = filename->len()
183   var dirname: string = fname->fnamemodify(':h')
184
185   if fname->len() > popupWidth && flen < popupWidth
186     # keep the full file name and reduce directory name length
187     # keep some characters at the beginning and end (equally).
188     # 6 spaces are used for "..." and " ()"
189     var dirsz = (popupWidth - flen - 6) / 2
190     dirname = dirname[: dirsz] .. '...' .. dirname[-dirsz : ]
191   endif
192   var str: string = filename
193   if dirname != '.'
194     str ..= $' ({dirname}/)'
195   endif
196   return str
197 enddef
198
199 # process the 'workspace/symbol' reply from the LSP server
200 # Result: SymbolInformation[] | null
201 export def WorkspaceSymbolPopup(lspserver: dict<any>, query: string,
202                                 symInfo: list<dict<any>>, cmdmods: string)
203   var symbols: list<dict<any>> = []
204   var symbolType: string
205   var fileName: string
206   var symName: string
207
208   # Create a symbol popup menu if it is not present
209   if lspserver.workspaceSymbolPopup->winbufnr() == -1
210     ShowSymbolMenu(lspserver, query, cmdmods)
211   endif
212
213   for symbol in symInfo
214     if !symbol->has_key('location')
215       # ignore entries without location information
216       continue
217     endif
218
219     # interface SymbolInformation
220     fileName = util.LspUriToFile(symbol.location.uri)
221
222     symName = symbol.name
223     if symbol->has_key('containerName') && symbol.containerName != ''
224       symName = $'{symbol.containerName}::{symName}'
225     endif
226     symName ..= $' [{SymbolKindToName(symbol.kind)}]'
227     symName ..= ' ' .. MakeMenuName(
228                 lspserver.workspaceSymbolPopup->popup_getpos().core_width,
229                 fileName)
230
231     symbols->add({name: symName,
232                         file: fileName,
233                         pos: symbol.location.range.start})
234   endfor
235   symbols->setwinvar(lspserver.workspaceSymbolPopup, 'LspSymbolTable')
236   lspserver.workspaceSymbolPopup->popup_settext(
237                                 symbols->copy()->mapnew('v:val.name'))
238 enddef
239
240 # map the LSP symbol kind number to string
241 export def SymbolKindToName(symkind: number): string
242   var symbolMap: list<string> = [
243     '',
244     'File',
245     'Module',
246     'Namespace',
247     'Package',
248     'Class',
249     'Method',
250     'Property',
251     'Field',
252     'Constructor',
253     'Enum',
254     'Interface',
255     'Function',
256     'Variable',
257     'Constant',
258     'String',
259     'Number',
260     'Boolean',
261     'Array',
262     'Object',
263     'Key',
264     'Null',
265     'EnumMember',
266     'Struct',
267     'Event',
268     'Operator',
269     'TypeParameter'
270   ]
271   if symkind > 26
272     return ''
273   endif
274   return symbolMap[symkind]
275 enddef
276
277 def UpdatePeekFilePopup(lspserver: dict<any>, locations: list<dict<any>>)
278   if lspserver.peekSymbolPopup->winbufnr() == -1
279     return
280   endif
281
282   lspserver.peekSymbolFilePopup->popup_close()
283
284   var n = line('.', lspserver.peekSymbolPopup) - 1
285   var [uri, range] = util.LspLocationParse(locations[n])
286   var fname: string = util.LspUriToFile(uri)
287
288   var bnr: number = fname->bufnr()
289   if bnr == -1
290     bnr = fname->bufadd()
291   endif
292
293   var popupAttrs = {
294     title: $"{fname->fnamemodify(':t')} ({fname->fnamemodify(':h')})",
295     wrap: false,
296     fixed: true,
297     minheight: 10,
298     maxheight: 10,
299     minwidth: winwidth(0) - 38,
300     maxwidth: winwidth(0) - 38,
301     cursorline: true,
302     border: [],
303     mapping: false,
304     line: 'cursor+1',
305     col: 1
306   }
307
308   lspserver.peekSymbolFilePopup = popup_create(bnr, popupAttrs)
309   var cmds =<< trim eval END
310     :setlocal number
311     [{range.start.line + 1}, 1]->cursor()
312     :normal! z.
313   END
314   win_execute(lspserver.peekSymbolFilePopup, cmds)
315
316   lspserver.peekSymbolFilePopup->clearmatches()
317   var start_col = util.GetLineByteFromPos(bnr, range.start) + 1
318   var end_col = util.GetLineByteFromPos(bnr, range.end)
319   var pos = [[range.start.line + 1,
320              start_col, end_col - start_col + 1]]
321   matchaddpos('Search', pos, 10, -1, {window: lspserver.peekSymbolFilePopup})
322 enddef
323
324 def LocPopupFilter(lspserver: dict<any>, locations: list<dict<any>>,
325                    popup_id: number, key: string): bool
326   popup_filter_menu(popup_id, key)
327   if lspserver.peekSymbolPopup->winbufnr() == -1
328     if lspserver.peekSymbolFilePopup->winbufnr() != -1
329       lspserver.peekSymbolFilePopup->popup_close()
330     endif
331     lspserver.peekSymbolPopup = -1
332     lspserver.peekSymbolFilePopup = -1
333   else
334     UpdatePeekFilePopup(lspserver, locations)
335   endif
336   return true
337 enddef
338
339 def LocPopupCallback(lspserver: dict<any>, locations: list<dict<any>>,
340                      popup_id: number, selIdx: number)
341   if lspserver.peekSymbolFilePopup->winbufnr() != -1
342     lspserver.peekSymbolFilePopup->popup_close()
343   endif
344   lspserver.peekSymbolPopup = -1
345   if selIdx != -1
346     util.PushCursorToTagStack()
347     util.JumpToLspLocation(locations[selIdx - 1], '')
348   endif
349 enddef
350
351 # Display the locations in a popup menu.  Display the corresponding file in
352 # an another popup window.
353 def PeekLocations(lspserver: dict<any>, locations: list<dict<any>>,
354                   title: string)
355   if lspserver.peekSymbolPopup->winbufnr() != -1
356     # If the symbol popup window is already present, close it.
357     lspserver.peekSymbolPopup->popup_close()
358   endif
359
360   var w: number = &columns
361   var fnamelen = float2nr(w * 0.4)
362
363   var menuItems: list<string> = []
364   for loc in locations
365     var [uri, range] = util.LspLocationParse(loc)
366     var fname: string = util.LspUriToFile(uri)
367     var bnr: number = fname->bufnr()
368     if bnr == -1
369       bnr = fname->bufadd()
370     endif
371     :silent! bnr->bufload()
372
373     var lnum = range.start.line + 1
374     var text: string = bnr->getbufline(lnum)->get(0, '')
375     menuItems->add($'{lnum}: {text}')
376   endfor
377
378   var popupAttrs = {
379     title: title,
380     wrap: false,
381     pos: 'topleft',
382     line: 'cursor+1',
383     col: winwidth(0) - 34,
384     minheight: 10,
385     maxheight: 10,
386     minwidth: 30,
387     maxwidth: 30,
388     mapping: false,
389     fixed: true,
390     filter: function(LocPopupFilter, [lspserver, locations]),
391     callback: function(LocPopupCallback, [lspserver, locations])
392   }
393   lspserver.peekSymbolPopup = popup_menu(menuItems, popupAttrs)
394   UpdatePeekFilePopup(lspserver, locations)
395 enddef
396
397 export def ShowLocations(lspserver: dict<any>, locations: list<dict<any>>,
398                          peekSymbol: bool, title: string)
399   if peekSymbol
400     PeekLocations(lspserver, locations, title)
401     return
402   endif
403
404   # create a loclist the location of the locations
405   var qflist: list<dict<any>> = []
406   for loc in locations
407     var [uri, range] = util.LspLocationParse(loc)
408     var fname: string = util.LspUriToFile(uri)
409     var bnr: number = fname->bufnr()
410     if bnr == -1
411       bnr = fname->bufadd()
412     endif
413     :silent! bnr->bufload()
414     var text: string = bnr->getbufline(range.start.line + 1)->get(0, '')->trim("\t ", 1)
415     qflist->add({filename: fname,
416                         lnum: range.start.line + 1,
417                         col: util.GetLineByteFromPos(bnr, range.start) + 1,
418                         text: text})
419   endfor
420
421   var save_winid = win_getid()
422
423   if opt.lspOptions.useQuickfixForLocations
424     setqflist([], ' ', {title: title, items: qflist})
425     var mods: string = ''
426     exe $'{mods} copen'
427   else
428     setloclist(0, [], ' ', {title: title, items: qflist})
429     var mods: string = ''
430     exe $'{mods} lopen'
431   endif
432
433   if !opt.lspOptions.keepFocusInReferences
434     save_winid->win_gotoid()
435   endif
436 enddef
437
438 # Key filter callback function used for the symbol popup window.
439 # Vim doesn't close the popup window when the escape key is pressed.
440 # This is function supports that.
441 def SymbolFilterCB(lspserver: dict<any>, id: number, key: string): bool
442   if key == "\<Esc>"
443     lspserver.peekSymbolPopup->popup_close()
444     return true
445   endif
446
447   return false
448 enddef
449
450 # Display the file specified by LSP "LocationLink" in a popup window and
451 # highlight the range in "location".
452 def PeekSymbolLocation(lspserver: dict<any>, location: dict<any>)
453   var [uri, range] = util.LspLocationParse(location)
454   var fname = util.LspUriToFile(uri)
455   var bnum = fname->bufadd()
456   if bnum == 0
457     # Failed to create or find a buffer
458     return
459   endif
460   :silent! bnum->bufload()
461
462   if lspserver.peekSymbolPopup->winbufnr() != -1
463     # If the symbol popup window is already present, close it.
464     lspserver.peekSymbolPopup->popup_close()
465   endif
466   var CbFunc = function(SymbolFilterCB, [lspserver])
467   var popupAttrs = {
468     title: $"{fnamemodify(fname, ':t')} ({fnamemodify(fname, ':h')})",
469     wrap: false,
470     moved: 'any',
471     minheight: 10,
472     maxheight: 10,
473     minwidth: 10,
474     maxwidth: 60,
475     cursorline: true,
476     border: [],
477     mapping: false,
478     filter: CbFunc
479   }
480   lspserver.peekSymbolPopup = popup_atcursor(bnum, popupAttrs)
481
482   # Highlight the symbol name and center the line in the popup
483   var pwid = lspserver.peekSymbolPopup
484   var pwbuf = pwid->winbufnr()
485   var pos: list<number> = []
486   var start_col: number
487   var end_col: number
488   start_col = util.GetLineByteFromPos(pwbuf, range.start) + 1
489   end_col = util.GetLineByteFromPos(pwbuf, range.end) + 1
490   pos->add(range.start.line + 1)
491   pos->extend([start_col, end_col - start_col])
492   matchaddpos('Search', [pos], 10, 101, {window: pwid})
493   var cmds =<< trim eval END
494     [{range.start.line + 1}, 1]->cursor()
495     :normal! z.
496   END
497   win_execute(pwid, cmds, 'silent!')
498 enddef
499
500 # Jump to the definition, declaration or implementation of a symbol.
501 # Also, used to peek at the definition, declaration or implementation of a
502 # symbol.
503 export def GotoSymbol(lspserver: dict<any>, location: dict<any>,
504                       peekSymbol: bool, cmdmods: string)
505   if peekSymbol
506     PeekSymbolLocation(lspserver, location)
507   else
508     # Save the current cursor location in the tag stack.
509     util.PushCursorToTagStack()
510     util.JumpToLspLocation(location, cmdmods)
511   endif
512 enddef
513
514 # Process the LSP server reply message for a 'textDocument/definition' request
515 # and return a list of Dicts in a format accepted by the 'tagfunc' option.
516 export def TagFunc(lspserver: dict<any>,
517                         taglocations: list<dict<any>>,
518                         pat: string): list<dict<any>>
519   var retval: list<dict<any>>
520
521   for tagloc in taglocations
522     var tagitem = {}
523     tagitem.name = pat
524
525     var [uri, range] = util.LspLocationParse(tagloc)
526     tagitem.filename = util.LspUriToFile(uri)
527     var bnr = util.LspUriToBufnr(uri)
528     var startByteIdx = util.GetLineByteFromPos(bnr, range.start)
529     tagitem.cmd = $"/\\%{range.start.line + 1}l\\%{startByteIdx + 1}c"
530
531     retval->add(tagitem)
532   endfor
533
534   return retval
535 enddef
536
537 # process SymbolInformation[]
538 def ProcessSymbolInfoTable(lspserver: dict<any>,
539                            bnr: number,
540                            symbolInfoTable: list<dict<any>>,
541                            symbolTypeTable: dict<list<dict<any>>>,
542                            symbolLineTable: list<dict<any>>)
543   var fname: string
544   var symbolType: string
545   var name: string
546   var r: dict<dict<number>>
547   var symInfo: dict<any>
548
549   for syminfo in symbolInfoTable
550     fname = util.LspUriToFile(syminfo.location.uri)
551     symbolType = SymbolKindToName(syminfo.kind)
552     name = syminfo.name
553     if syminfo->has_key('containerName')
554       if syminfo.containerName != ''
555         name ..= $' [{syminfo.containerName}]'
556       endif
557     endif
558     r = syminfo.location.range
559     lspserver.decodeRange(bnr, r)
560
561     if !symbolTypeTable->has_key(symbolType)
562       symbolTypeTable[symbolType] = []
563     endif
564     symInfo = {name: name, range: r}
565     symbolTypeTable[symbolType]->add(symInfo)
566     symbolLineTable->add(symInfo)
567   endfor
568 enddef
569
570 # process DocumentSymbol[]
571 def ProcessDocSymbolTable(lspserver: dict<any>,
572                           bnr: number,
573                           docSymbolTable: list<dict<any>>,
574                           symbolTypeTable: dict<list<dict<any>>>,
575                           symbolLineTable: list<dict<any>>)
576   var symbolType: string
577   var name: string
578   var r: dict<dict<number>>
579   var symInfo: dict<any>
580   var symbolDetail: string
581   var childSymbols: dict<list<dict<any>>>
582
583   for syminfo in docSymbolTable
584     name = syminfo.name
585     symbolType = SymbolKindToName(syminfo.kind)
586     r = syminfo.selectionRange
587     lspserver.decodeRange(bnr, r)
588     if syminfo->has_key('detail')
589       symbolDetail = syminfo.detail
590     endif
591     if !symbolTypeTable->has_key(symbolType)
592       symbolTypeTable[symbolType] = []
593     endif
594     childSymbols = {}
595     if syminfo->has_key('children')
596       ProcessDocSymbolTable(lspserver, bnr, syminfo.children, childSymbols,
597                             symbolLineTable)
598     endif
599     symInfo = {name: name, range: r, detail: symbolDetail,
600                                                 children: childSymbols}
601     symbolTypeTable[symbolType]->add(symInfo)
602     symbolLineTable->add(symInfo)
603   endfor
604 enddef
605
606 # process the 'textDocument/documentSymbol' reply from the LSP server
607 # Open a symbols window and display the symbols as a tree
608 # Result: DocumentSymbol[] | SymbolInformation[] | null
609 export def DocSymbolOutline(lspserver: dict<any>, docSymbol: any, fname: string)
610   var bnr = fname->bufnr()
611   var symbolTypeTable: dict<list<dict<any>>> = {}
612   var symbolLineTable: list<dict<any>> = []
613
614   if docSymbol->empty()
615     # No symbols defined for this file. Clear the outline window.
616     outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable)
617     return
618   endif
619
620   if docSymbol[0]->has_key('location')
621     # SymbolInformation[]
622     ProcessSymbolInfoTable(lspserver, bnr, docSymbol, symbolTypeTable,
623                            symbolLineTable)
624   else
625     # DocumentSymbol[]
626     ProcessDocSymbolTable(lspserver, bnr, docSymbol, symbolTypeTable,
627                           symbolLineTable)
628   endif
629
630   # sort the symbols by line number
631   symbolLineTable->sort((a, b) => a.range.start.line - b.range.start.line)
632   outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable)
633 enddef
634
635 # Process the list of symbols (LSP interface "SymbolInformation") in
636 # "symbolInfoTable". For each symbol, create the name to display in the popup
637 # menu along with the symbol range and return the List.
638 def GetSymbolsInfoTable(lspserver: dict<any>,
639                         bnr: number,
640                         symbolInfoTable: list<dict<any>>): list<dict<any>>
641   var symbolTable: list<dict<any>> = []
642   var symbolType: string
643   var name: string
644   var containerName: string
645   var r: dict<dict<number>>
646
647   for syminfo in symbolInfoTable
648     symbolType = SymbolKindToName(syminfo.kind)
649     name = $'{symbolType} : {syminfo.name}'
650     if syminfo->has_key('containerName') && !syminfo.containerName->empty()
651       name ..= $' [{syminfo.containerName}]'
652     endif
653     r = syminfo.location.range
654     lspserver.decodeRange(bnr, r)
655
656     symbolTable->add({name: name, range: r, selectionRange: {}})
657   endfor
658
659   return symbolTable
660 enddef
661
662 # Process the list of symbols (LSP interface "DocumentSymbol") in
663 # "docSymbolTable". For each symbol, create the name to display in the popup
664 # menu along with the symbol range and return the List in "symbolTable"
665 def GetSymbolsDocSymbol(lspserver: dict<any>,
666                         bnr: number,
667                         docSymbolTable: list<dict<any>>,
668                         symbolTable: list<dict<any>>,
669                         parentName: string = '')
670   var symbolType: string
671   var name: string
672   var r: dict<dict<number>>
673   var sr: dict<dict<number>>
674   var symInfo: dict<any>
675
676   for syminfo in docSymbolTable
677     var symName = syminfo.name
678     symbolType = SymbolKindToName(syminfo.kind)->tolower()
679     sr = syminfo.selectionRange
680     lspserver.decodeRange(bnr, sr)
681     r = syminfo.range
682     lspserver.decodeRange(bnr, r)
683     name = $'{symbolType} : {symName}'
684     if parentName != ''
685       name ..= $' [{parentName}]'
686     endif
687     # TODO: Should include syminfo.detail? Will it clutter the menu?
688     symInfo = {name: name, range: r, selectionRange: sr}
689     symbolTable->add(symInfo)
690
691     if syminfo->has_key('children')
692       # Process all the child symbols
693       GetSymbolsDocSymbol(lspserver, bnr, syminfo.children, symbolTable,
694                           symName)
695     endif
696   endfor
697 enddef
698
699 # Highlight the name and the range of lines for the symbol at symTbl[symIdx]
700 def SymbolHighlight(symTbl: list<dict<any>>, symIdx: number)
701   prop_remove({type: 'LspSymbolNameProp', all: true})
702   prop_remove({type: 'LspSymbolRangeProp', all: true})
703   if symTbl->empty()
704     return
705   endif
706
707   var r = symTbl[symIdx].range
708   if r->empty()
709     return
710   endif
711   var rangeStart = r.start
712   var rangeEnd = r.end
713   var start_lnum = rangeStart.line + 1
714   var start_col = rangeStart.character + 1
715   var end_lnum = rangeEnd.line + 1
716   var end_col: number
717   var last_lnum = line('$')
718   if end_lnum > line('$')
719     end_lnum = last_lnum
720     end_col = col([last_lnum, '$'])
721   else
722     end_col = rangeEnd.character + 1
723   endif
724   prop_add(start_lnum, start_col,
725            {type: 'LspSymbolRangeProp',
726             end_lnum: end_lnum,
727             end_col: end_col})
728   cursor(start_lnum, 1)
729   :normal! z.
730
731   var sr = symTbl[symIdx].selectionRange
732   if sr->empty()
733     return
734   endif
735   rangeStart = sr.start
736   rangeEnd = sr.end
737   prop_add(rangeStart.line + 1, 1,
738            {type: 'LspSymbolNameProp',
739             start_col: rangeStart.character + 1,
740             end_lnum: rangeEnd.line + 1,
741             end_col: rangeEnd.character + 1})
742 enddef
743
744 # Callback invoked when an item is selected in the symbol popup menu
745 #   "symTbl" - list of symbols
746 #   "symInputPopup" - Symbol search input popup window ID
747 #   "save_curpos" - Cursor position before invoking the symbol search.  If the
748 #                   symbol search is canceled, restore the cursor to this
749 #                   position.
750 def SymbolMenuItemSelected(symPopupMenu: number,
751                            result: number)
752   var symTblFiltered = symPopupMenu->getwinvar('symbolTableFiltered', [])
753   var symInputPopup = symPopupMenu->getwinvar('symbolInputPopup', 0)
754   var save_curpos = symPopupMenu->getwinvar('saveCurPos', [])
755
756   # Restore the cursor to the location where the command was invoked
757   setpos('.', save_curpos)
758
759   if result > 0
760     # A symbol is selected in the popup menu
761
762     # Set the previous cursor location mark. Instead of using setpos(), m' is
763     # used so that the current location is added to the jump list.
764     :normal m'
765
766     # Jump to the selected symbol location
767     var r = symTblFiltered[result - 1].selectionRange
768     setcursorcharpos(r.start.line + 1,
769       util.GetCharIdxWithoutCompChar(bufnr(), r.start) + 1)
770   endif
771   symInputPopup->popup_close()
772   prop_remove({type: 'LspSymbolNameProp', all: true})
773   prop_remove({type: 'LspSymbolRangeProp', all: true})
774 enddef
775
776 # Key filter function for the symbol popup menu.
777 def SymbolMenuFilterKey(symPopupMenu: number,
778                         key: string): bool
779   var keyHandled = false
780   var updateInputPopup = false
781   var inputText = symPopupMenu->getwinvar('inputText', '')
782   var symInputPopup = symPopupMenu->getwinvar('symbolInputPopup', 0)
783
784   if key == "\<BS>" || key == "\<C-H>"
785     # Erase a character in the input popup
786     if inputText->len() >= 1
787       inputText = inputText[: -2]
788       keyHandled = true
789       updateInputPopup = true
790     endif
791   elseif key == "\<C-U>"
792     # Erase all the characters in the input popup
793     inputText = ''
794     keyHandled = true
795     updateInputPopup = true
796   elseif key == "\<C-F>"
797       || key == "\<C-B>"
798       || key == "\<PageUp>"
799       || key == "\<PageDown>"
800       || key == "\<C-Home>"
801       || key == "\<C-End>"
802       || key == "\<C-N>"
803       || key == "\<C-P>"
804     # scroll the symbol popup window
805     var cmd: string = 'normal! ' .. (key == "\<C-N>" ? 'j' :
806                                      key == "\<C-P>" ? 'k' : key)
807     win_execute(symPopupMenu, cmd)
808     keyHandled = true
809   elseif key =~ '^\k$'
810     # A keyword character is typed.  Add to the input text and update the
811     # popup
812     inputText ..= key
813     keyHandled = true
814     updateInputPopup = true
815   endif
816
817   var symTblFiltered: list<dict<any>> = []
818   symTblFiltered = symPopupMenu->getwinvar('symbolTableFiltered', [])
819
820   if updateInputPopup
821     # Update the input popup with the new text and update the symbol popup
822     # window with the matching symbol names.
823     symInputPopup->popup_settext(inputText)
824
825     var symbolTable = symPopupMenu->getwinvar('symbolTable')
826     symTblFiltered = symbolTable->deepcopy()
827     var symbolMatchPos: list<list<number>> = []
828
829     # Get the list of symbols fuzzy matching the entered text
830     if inputText != ''
831       var t = symTblFiltered->matchfuzzypos(inputText, {key: 'name'})
832       symTblFiltered = t[0]
833       symbolMatchPos = t[1]
834     endif
835
836     var popupText: list<dict<any>>
837     var text: list<dict<any>>
838     if !symbolMatchPos->empty()
839       # Generate a list of symbol names and the corresponding text properties
840       # to highlight the matching characters.
841       popupText = symTblFiltered->mapnew((idx, val): dict<any> => ({
842         text: val.name,
843         props: symbolMatchPos[idx]->mapnew((_, w: number): dict<any> => ({
844           col: w + 1,
845           length: 1,
846           type: 'LspSymbolMatch'}
847         ))}
848       ))
849     else
850       popupText = symTblFiltered->mapnew((idx, val): dict<string> => {
851         return {text: val.name}
852       })
853     endif
854     symPopupMenu->popup_settext(popupText)
855
856     # Select the first symbol and highlight the corresponding text range
857     win_execute(symPopupMenu, 'cursor(1, 1)')
858     SymbolHighlight(symTblFiltered, 0)
859   endif
860
861   # Save the filtered symbol table and the search text in popup window
862   # variables
863   setwinvar(symPopupMenu, 'inputText', inputText)
864   setwinvar(symPopupMenu, 'symbolTableFiltered', symTblFiltered)
865
866   if !keyHandled
867     # Use the default handler for the key
868     symPopupMenu->popup_filter_menu(key)
869   endif
870
871   # Highlight the name and range of the selected symbol
872   var lnum = line('.', symPopupMenu) - 1
873   if lnum >= 0
874     SymbolHighlight(symTblFiltered, lnum)
875   endif
876
877   return true
878 enddef
879
880 # Display the symbols popup menu
881 def SymbolPopupMenu(symbolTable: list<dict<any>>)
882   var curLine = line('.')
883   var curSymIdx = 0
884
885   # Get the names of all the symbols.  Also get the index of the symbol under
886   # the cursor.
887   var symNames = symbolTable->mapnew((idx, val): string => {
888     var r = val.range
889     if !r->empty() && curSymIdx == 0
890       if curLine >= r.start.line + 1 && curLine <= r.end.line + 1
891         curSymIdx = idx
892       endif
893     endif
894     return val.name
895   })
896
897   var symInputPopupAttr = {
898     title: 'Select Symbol',
899     wrap: false,
900     pos: 'topleft',
901     line: &lines - 14,
902     col: 10,
903     minwidth: 60,
904     minheight: 1,
905     maxheight: 1,
906     maxwidth: 60,
907     fixed: 1,
908     close: 'button',
909     border: []
910   }
911   var symInputPopup = popup_create('', symInputPopupAttr)
912
913   var symNamesPopupattr = {
914     wrap: false,
915     pos: 'topleft',
916     line: &lines - 11,
917     col: 10,
918     minwidth: 60,
919     minheight: 10,
920     maxheight: 10,
921     maxwidth: 60,
922     fixed: 1,
923     border: [0, 0, 0, 0],
924     callback: SymbolMenuItemSelected,
925     filter: SymbolMenuFilterKey,
926   }
927   var symPopupMenu = popup_menu(symNames, symNamesPopupattr)
928
929   # Save the state in the popup menu window variables
930   setwinvar(symPopupMenu, 'symbolTable', symbolTable)
931   setwinvar(symPopupMenu, 'symbolTableFiltered', symbolTable->deepcopy())
932   setwinvar(symPopupMenu, 'symbolInputPopup', symInputPopup)
933   setwinvar(symPopupMenu, 'saveCurPos', getcurpos())
934   prop_type_add('LspSymbolMatch', {bufnr: symPopupMenu->winbufnr(),
935                                    highlight: 'Title',
936                                    override: true})
937
938   # Start with the symbol under the cursor
939   var cmds =<< trim eval END
940     [{curSymIdx + 1}, 1]->cursor()
941     :normal! z.
942   END
943   win_execute(symPopupMenu, cmds, 'silent!')
944
945   # Highlight the name and range of the first symbol
946   SymbolHighlight(symbolTable, curSymIdx)
947 enddef
948
949 # process the 'textDocument/documentSymbol' reply from the LSP server
950 # Result: DocumentSymbol[] | SymbolInformation[] | null
951 # Display the symbols in a popup window and jump to the selected symbol
952 export def DocSymbolPopup(lspserver: dict<any>, docSymbol: any, fname: string)
953   var symList: list<dict<any>> = []
954
955   if docSymbol->empty()
956     return
957   endif
958
959   var bnr = fname->bufnr()
960
961   if docSymbol[0]->has_key('location')
962     # SymbolInformation[]
963     symList = GetSymbolsInfoTable(lspserver, bnr, docSymbol)
964   else
965     # DocumentSymbol[]
966     GetSymbolsDocSymbol(lspserver, bnr, docSymbol, symList)
967   endif
968
969   :redraw!
970   SymbolPopupMenu(symList)
971 enddef
972
973 # vim: tabstop=8 shiftwidth=2 softtabstop=2