]> Sergey Matveev's repositories - vim-lsp.git/blob - autoload/lsp/symbol.vim
Update diags location list when the diags for the buffer changes. Update comments
[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 # 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
18
19   if key == "\<BS>" || key == "\<C-H>"
20     # Erase one character from the filter text
21     if query->len() >= 1
22       query = query[: -2]
23       update_popup = true
24     endif
25     key_handled = true
26   elseif key == "\<C-U>"
27     # clear the filter text
28     query = ''
29     update_popup = true
30     key_handled = true
31   elseif key == "\<C-F>"
32         || key == "\<C-B>"
33         || key == "\<PageUp>"
34         || key == "\<PageDown>"
35         || key == "\<C-Home>"
36         || key == "\<C-End>"
37         || key == "\<C-N>"
38         || key == "\<C-P>"
39     # scroll the popup window
40     var cmd: string = 'normal! ' .. (key == "\<C-N>" ? 'j' : key == "\<C-P>" ? 'k' : key)
41     win_execute(popupID, cmd)
42     key_handled = true
43   elseif key == "\<Up>" || key == "\<Down>"
44     # Use native Vim handling for these keys
45     key_handled = false
46   elseif key =~ '^\f$' || key == "\<Space>"
47     # Filter the names based on the typed key and keys typed before
48     query ..= key
49     update_popup = true
50     key_handled = true
51   endif
52
53   if update_popup
54     # Update the popup with the new list of symbol names
55     popupID->popup_settext('')
56     if query != ''
57       lspserver.workspaceQuery(query)
58     else
59       []->setwinvar(popupID, 'LspSymbolTable')
60     endif
61     :echo $'Symbol: {query}'
62   endif
63
64   # Update the workspace symbol query string
65   lspserver.workspaceSymbolQuery = query
66
67   if key_handled
68     return true
69   endif
70
71   return popupID->popup_filter_menu(key)
72 enddef
73
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
77   :echo ''
78
79   if result <= 0
80     # popup is canceled
81     return
82   endif
83
84   var symTbl: list<dict<any>> = popupID->getwinvar('LspSymbolTable', [])
85   if symTbl->empty()
86     return
87   endif
88   try
89     # Save the current location in the tag stack
90     util.PushCursorToTagStack()
91
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()
96     if winList->empty()
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}'
102       else
103         exe $'confirm edit {symTbl[result - 1].file}'
104       endif
105     else
106       if bufnr() != bufnum
107         var winID = fname->bufwinid()
108         if winID == -1
109           # not present in the current tab page
110           winID = winList[0]
111         endif
112         winID->win_gotoid()
113       endif
114     endif
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.
117     :normal m'
118     setcursorcharpos(symTbl[result - 1].pos.line + 1,
119                      util.GetCharIdxWithoutCompChar(bufnum,
120                                                     symTbl[result - 1].pos) + 1)
121   catch
122     # ignore exceptions
123   endtry
124 enddef
125
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
130   var popupAttr = {
131       title: 'Workspace Symbol Search',
132       wrap: false,
133       pos: 'topleft',
134       line: lnum,
135       col: 2,
136       minwidth: 60,
137       minheight: 10,
138       maxheight: 10,
139       maxwidth: 60,
140       mapping: false,
141       fixed: 1,
142       close: 'button',
143       filter: function(FilterSymbols, [lspserver]),
144       callback: JumpToWorkspaceSymbol
145   }
146   lspserver.workspaceSymbolPopup = popup_menu([], popupAttr)
147   lspserver.workspaceSymbolQuery = query
148   prop_type_add('lspworkspacesymbol',
149                         {bufnr: lspserver.workspaceSymbolPopup->winbufnr(),
150                          highlight: 'Title'})
151   :echo $'Symbol: {query}'
152 enddef
153
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')
160
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 : ]
167   endif
168   var str: string = filename
169   if dirname != '.'
170     str ..= $' ({dirname}/)'
171   endif
172   return str
173 enddef
174
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
181   var fileName: string
182   var r: dict<dict<number>>
183   var symName: string
184
185   # Create a symbol popup menu if it is not present
186   if lspserver.workspaceSymbolPopup->winbufnr() == -1
187     ShowSymbolMenu(lspserver, query)
188   endif
189
190   for symbol in symInfo
191     if !symbol->has_key('location')
192       # ignore entries without location information
193       continue
194     endif
195
196     # interface SymbolInformation
197     fileName = util.LspUriToFile(symbol.location.uri)
198     r = symbol.location.range
199     lspserver.decodeRange(fileName->bufnr(), r)
200
201     symName = symbol.name
202     if symbol->has_key('containerName') && symbol.containerName != ''
203       symName = $'{symbol.containerName}::{symName}'
204     endif
205     symName ..= $' [{SymbolKindToName(symbol.kind)}]'
206     symName ..= ' ' .. MakeMenuName(
207                 lspserver.workspaceSymbolPopup->popup_getpos().core_width,
208                 fileName)
209
210     symbols->add({name: symName,
211                         file: fileName,
212                         pos: r.start})
213   endfor
214   symbols->setwinvar(lspserver.workspaceSymbolPopup, 'LspSymbolTable')
215   lspserver.workspaceSymbolPopup->popup_settext(
216                                 symbols->copy()->mapnew('v:val.name'))
217 enddef
218
219 # map the LSP symbol kind number to string
220 export def SymbolKindToName(symkind: number): string
221   var symbolMap: list<string> = [
222     '',
223     'File',
224     'Module',
225     'Namespace',
226     'Package',
227     'Class',
228     'Method',
229     'Property',
230     'Field',
231     'Constructor',
232     'Enum',
233     'Interface',
234     'Function',
235     'Variable',
236     'Constant',
237     'String',
238     'Number',
239     'Boolean',
240     'Array',
241     'Object',
242     'Key',
243     'Null',
244     'EnumMember',
245     'Struct',
246     'Event',
247     'Operator',
248     'TypeParameter'
249   ]
250   if symkind > 26
251     return ''
252   endif
253   return symbolMap[symkind]
254 enddef
255
256 def UpdatePeekFilePopup(lspserver: dict<any>, locations: list<dict<any>>)
257   if lspserver.peekSymbolPopup->winbufnr() == -1
258     return
259   endif
260
261   lspserver.peekSymbolFilePopup->popup_close()
262
263   var n = line('.', lspserver.peekSymbolPopup) - 1
264   var [uri, range] = util.LspLocationParse(locations[n])
265   var fname: string = util.LspUriToFile(uri)
266
267   var bnr: number = fname->bufnr()
268   if bnr == -1
269     bnr = fname->bufadd()
270   endif
271
272   var popupAttrs = {
273     title: $"{fname->fnamemodify(':t')} ({fname->fnamemodify(':h')})",
274     wrap: false,
275     fixed: true,
276     minheight: 10,
277     maxheight: 10,
278     minwidth: winwidth(0) - 38,
279     maxwidth: winwidth(0) - 38,
280     cursorline: true,
281     border: [],
282     mapping: false,
283     line: 'cursor+1',
284     col: 1
285   }
286
287   lspserver.peekSymbolFilePopup = popup_create(bnr, popupAttrs)
288   var cmds =<< trim eval END
289     :setlocal number
290     [{range.start.line + 1}, 1]->cursor()
291     :normal! z.
292   END
293   win_execute(lspserver.peekSymbolFilePopup, cmds)
294
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})
301 enddef
302
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()
309     endif
310     lspserver.peekSymbolPopup = -1
311     lspserver.peekSymbolFilePopup = -1
312   else
313     UpdatePeekFilePopup(lspserver, locations)
314   endif
315   return true
316 enddef
317
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()
322   endif
323   lspserver.peekSymbolPopup = -1
324   if selIdx != -1
325     util.PushCursorToTagStack()
326     util.JumpToLspLocation(locations[selIdx - 1], '')
327   endif
328 enddef
329
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>>,
333                   title: string)
334   if lspserver.peekSymbolPopup->winbufnr() != -1
335     # If the symbol popup window is already present, close it.
336     lspserver.peekSymbolPopup->popup_close()
337   endif
338
339   var w: number = &columns
340   var fnamelen = float2nr(w * 0.4)
341
342   var menuItems: list<string> = []
343   for loc in locations
344     var [uri, range] = util.LspLocationParse(loc)
345     var fname: string = util.LspUriToFile(uri)
346     var bnr: number = fname->bufnr()
347     if bnr == -1
348       bnr = fname->bufadd()
349     endif
350     bnr->bufload()
351
352     var lnum = range.start.line + 1
353     var text: string = bnr->getbufline(lnum)->get(0, '')
354     menuItems->add($'{lnum}: {text}')
355   endfor
356
357   var popupAttrs = {
358     title: title,
359     wrap: false,
360     pos: 'topleft',
361     line: 'cursor+1',
362     col: winwidth(0) - 34,
363     minheight: 10,
364     maxheight: 10,
365     minwidth: 30,
366     maxwidth: 30,
367     mapping: false,
368     fixed: true,
369     filter: function(LocPopupFilter, [lspserver, locations]),
370     callback: function(LocPopupCallback, [lspserver, locations])
371   }
372   lspserver.peekSymbolPopup = popup_menu(menuItems, popupAttrs)
373   UpdatePeekFilePopup(lspserver, locations)
374 enddef
375
376 export def ShowLocations(lspserver: dict<any>, locations: list<dict<any>>,
377                          peekSymbol: bool, title: string)
378   if peekSymbol
379     PeekLocations(lspserver, locations, title)
380     return
381   endif
382
383   # create a loclist the location of the locations
384   var qflist: list<dict<any>> = []
385   for loc in locations
386     var [uri, range] = util.LspLocationParse(loc)
387     var fname: string = util.LspUriToFile(uri)
388     var bnr: number = fname->bufnr()
389     if bnr == -1
390       bnr = fname->bufadd()
391     endif
392     bnr->bufload()
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,
397                         text: text})
398   endfor
399
400   var save_winid = win_getid()
401
402   if opt.lspOptions.useQuickfixForLocations
403     setqflist([], ' ', {title: title, items: qflist})
404     var mods: string = ''
405     exe $'{mods} copen'
406   else
407     setloclist(0, [], ' ', {title: title, items: qflist})
408     var mods: string = ''
409     exe $'{mods} lopen'
410   endif
411
412   if !opt.lspOptions.keepFocusInReferences
413     save_winid->win_gotoid()
414   endif
415 enddef
416
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
421   if key == "\<Esc>"
422     lspserver.peekSymbolPopup->popup_close()
423     return true
424   endif
425
426   return false
427 enddef
428
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()
435   if bnum == 0
436     # Failed to create or find a buffer
437     return
438   endif
439   silent! bnum->bufload()
440
441   if lspserver.peekSymbolPopup->winbufnr() != -1
442     # If the symbol popup window is already present, close it.
443     lspserver.peekSymbolPopup->popup_close()
444   endif
445   var CbFunc = function(SymbolFilterCB, [lspserver])
446   var popupAttrs = {
447     title: $"{fnamemodify(fname, ':t')} ({fnamemodify(fname, ':h')})",
448     wrap: false,
449     moved: 'any',
450     minheight: 10,
451     maxheight: 10,
452     minwidth: 10,
453     maxwidth: 60,
454     cursorline: true,
455     border: [],
456     mapping: false,
457     filter: CbFunc
458   }
459   lspserver.peekSymbolPopup = popup_atcursor(bnum, popupAttrs)
460
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
466   var end_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()
474     :normal! z.
475   END
476   win_execute(pwid, cmds, 'silent!')
477 enddef
478
479 # Jump to the definition, declaration or implementation of a symbol.
480 # Also, used to peek at the definition, declaration or implementation of a
481 # symbol.
482 export def GotoSymbol(lspserver: dict<any>, location: dict<any>,
483                       peekSymbol: bool, cmdmods: string)
484   if peekSymbol
485     PeekSymbolLocation(lspserver, location)
486   else
487     # Save the current cursor location in the tag stack.
488     util.PushCursorToTagStack()
489     util.JumpToLspLocation(location, cmdmods)
490   endif
491 enddef
492
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>>
499
500   for tagloc in taglocations
501     var tagitem = {}
502     tagitem.name = pat
503
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"
509
510     retval->add(tagitem)
511   endfor
512
513   return retval
514 enddef
515
516 # process SymbolInformation[]
517 def ProcessSymbolInfoTable(lspserver: dict<any>,
518                            bnr: number,
519                            symbolInfoTable: list<dict<any>>,
520                            symbolTypeTable: dict<list<dict<any>>>,
521                            symbolLineTable: list<dict<any>>)
522   var fname: string
523   var symbolType: string
524   var name: string
525   var r: dict<dict<number>>
526   var symInfo: dict<any>
527
528   for syminfo in symbolInfoTable
529     fname = util.LspUriToFile(syminfo.location.uri)
530     symbolType = SymbolKindToName(syminfo.kind)
531     name = syminfo.name
532     if syminfo->has_key('containerName')
533       if syminfo.containerName != ''
534         name ..= $' [{syminfo.containerName}]'
535       endif
536     endif
537     r = syminfo.location.range
538     lspserver.decodeRange(bnr, r)
539
540     if !symbolTypeTable->has_key(symbolType)
541       symbolTypeTable[symbolType] = []
542     endif
543     symInfo = {name: name, range: r}
544     symbolTypeTable[symbolType]->add(symInfo)
545     symbolLineTable->add(symInfo)
546   endfor
547 enddef
548
549 # process DocumentSymbol[]
550 def ProcessDocSymbolTable(lspserver: dict<any>,
551                           bnr: number,
552                           docSymbolTable: list<dict<any>>,
553                           symbolTypeTable: dict<list<dict<any>>>,
554                           symbolLineTable: list<dict<any>>)
555   var symbolType: string
556   var name: string
557   var r: dict<dict<number>>
558   var symInfo: dict<any>
559   var symbolDetail: string
560   var childSymbols: dict<list<dict<any>>>
561
562   for syminfo in docSymbolTable
563     name = syminfo.name
564     symbolType = SymbolKindToName(syminfo.kind)
565     r = syminfo.selectionRange
566     lspserver.decodeRange(bnr, r)
567     if syminfo->has_key('detail')
568       symbolDetail = syminfo.detail
569     endif
570     if !symbolTypeTable->has_key(symbolType)
571       symbolTypeTable[symbolType] = []
572     endif
573     childSymbols = {}
574     if syminfo->has_key('children')
575       ProcessDocSymbolTable(lspserver, bnr, syminfo.children, childSymbols,
576                             symbolLineTable)
577     endif
578     symInfo = {name: name, range: r, detail: symbolDetail,
579                                                 children: childSymbols}
580     symbolTypeTable[symbolType]->add(symInfo)
581     symbolLineTable->add(symInfo)
582   endfor
583 enddef
584
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()
592
593   if docsymbol->empty()
594     # No symbols defined for this file. Clear the outline window.
595     outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable)
596     return
597   endif
598
599   if docsymbol[0]->has_key('location')
600     # SymbolInformation[]
601     ProcessSymbolInfoTable(lspserver, bnr, docsymbol, symbolTypeTable,
602                            symbolLineTable)
603   else
604     # DocumentSymbol[]
605     ProcessDocSymbolTable(lspserver, bnr, docsymbol, symbolTypeTable,
606                           symbolLineTable)
607   endif
608
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)
612 enddef
613
614 # vim: tabstop=8 shiftwidth=2 softtabstop=2