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