]> Sergey Matveev's repositories - vim-lsp.git/blob - autoload/lsp/symbol.vim
Silence the messages when loading new buffers
[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, false)
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(cmdmods: string, 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 bnr = fname->bufnr()
95     if cmdmods->empty()
96       var winList: list<number> = bnr->win_findbuf()
97       if winList->empty()
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}'
103         else
104           exe $'confirm edit {symTbl[result - 1].file}'
105         endif
106       else
107         # If the target buffer is opened in the curent window, then don't
108         # change the window.
109         if bufnr() != bnr
110           # If the target buffer is opened in a window in the current tab
111           # page, then use it.
112           var winID = fname->bufwinid()
113           if winID == -1
114             # not present in the current tab page.  Use the first window.
115             winID = winList[0]
116           endif
117           winID->win_gotoid()
118         endif
119       endif
120     else
121       exe $'{cmdmods} split {symTbl[result - 1].file}'
122     endif
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.
125     :normal m'
126     setcursorcharpos(symTbl[result - 1].pos.line + 1,
127                      util.GetCharIdxWithoutCompChar(bufnr(),
128                                                     symTbl[result - 1].pos) + 1)
129   catch
130     # ignore exceptions
131   endtry
132 enddef
133
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
138   var popupAttr = {
139       title: 'Workspace Symbol Search',
140       wrap: false,
141       pos: 'topleft',
142       line: lnum,
143       col: 2,
144       minwidth: 60,
145       minheight: 10,
146       maxheight: 10,
147       maxwidth: 60,
148       mapping: false,
149       fixed: 1,
150       close: 'button',
151       filter: function(FilterSymbols, [lspserver]),
152       callback: function('JumpToWorkspaceSymbol', [cmdmods])
153   }
154   lspserver.workspaceSymbolPopup = popup_menu([], popupAttr)
155   lspserver.workspaceSymbolQuery = query
156   prop_type_add('lspworkspacesymbol',
157                         {bufnr: lspserver.workspaceSymbolPopup->winbufnr(),
158                          highlight: 'Title'})
159   :echo $'Symbol: {query}'
160 enddef
161
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')
168
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 : ]
175   endif
176   var str: string = filename
177   if dirname != '.'
178     str ..= $' ({dirname}/)'
179   endif
180   return str
181 enddef
182
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
189   var fileName: string
190   var symName: string
191
192   # Create a symbol popup menu if it is not present
193   if lspserver.workspaceSymbolPopup->winbufnr() == -1
194     ShowSymbolMenu(lspserver, query, cmdmods)
195   endif
196
197   for symbol in symInfo
198     if !symbol->has_key('location')
199       # ignore entries without location information
200       continue
201     endif
202
203     # interface SymbolInformation
204     fileName = util.LspUriToFile(symbol.location.uri)
205
206     symName = symbol.name
207     if symbol->has_key('containerName') && symbol.containerName != ''
208       symName = $'{symbol.containerName}::{symName}'
209     endif
210     symName ..= $' [{SymbolKindToName(symbol.kind)}]'
211     symName ..= ' ' .. MakeMenuName(
212                 lspserver.workspaceSymbolPopup->popup_getpos().core_width,
213                 fileName)
214
215     symbols->add({name: symName,
216                         file: fileName,
217                         pos: symbol.location.range.start})
218   endfor
219   symbols->setwinvar(lspserver.workspaceSymbolPopup, 'LspSymbolTable')
220   lspserver.workspaceSymbolPopup->popup_settext(
221                                 symbols->copy()->mapnew('v:val.name'))
222 enddef
223
224 # map the LSP symbol kind number to string
225 export def SymbolKindToName(symkind: number): string
226   var symbolMap: list<string> = [
227     '',
228     'File',
229     'Module',
230     'Namespace',
231     'Package',
232     'Class',
233     'Method',
234     'Property',
235     'Field',
236     'Constructor',
237     'Enum',
238     'Interface',
239     'Function',
240     'Variable',
241     'Constant',
242     'String',
243     'Number',
244     'Boolean',
245     'Array',
246     'Object',
247     'Key',
248     'Null',
249     'EnumMember',
250     'Struct',
251     'Event',
252     'Operator',
253     'TypeParameter'
254   ]
255   if symkind > 26
256     return ''
257   endif
258   return symbolMap[symkind]
259 enddef
260
261 def UpdatePeekFilePopup(lspserver: dict<any>, locations: list<dict<any>>)
262   if lspserver.peekSymbolPopup->winbufnr() == -1
263     return
264   endif
265
266   lspserver.peekSymbolFilePopup->popup_close()
267
268   var n = line('.', lspserver.peekSymbolPopup) - 1
269   var [uri, range] = util.LspLocationParse(locations[n])
270   var fname: string = util.LspUriToFile(uri)
271
272   var bnr: number = fname->bufnr()
273   if bnr == -1
274     bnr = fname->bufadd()
275   endif
276
277   var popupAttrs = {
278     title: $"{fname->fnamemodify(':t')} ({fname->fnamemodify(':h')})",
279     wrap: false,
280     fixed: true,
281     minheight: 10,
282     maxheight: 10,
283     minwidth: winwidth(0) - 38,
284     maxwidth: winwidth(0) - 38,
285     cursorline: true,
286     border: [],
287     mapping: false,
288     line: 'cursor+1',
289     col: 1
290   }
291
292   lspserver.peekSymbolFilePopup = popup_create(bnr, popupAttrs)
293   var cmds =<< trim eval END
294     :setlocal number
295     [{range.start.line + 1}, 1]->cursor()
296     :normal! z.
297   END
298   win_execute(lspserver.peekSymbolFilePopup, cmds)
299
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})
306 enddef
307
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()
314     endif
315     lspserver.peekSymbolPopup = -1
316     lspserver.peekSymbolFilePopup = -1
317   else
318     UpdatePeekFilePopup(lspserver, locations)
319   endif
320   return true
321 enddef
322
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()
327   endif
328   lspserver.peekSymbolPopup = -1
329   if selIdx != -1
330     util.PushCursorToTagStack()
331     util.JumpToLspLocation(locations[selIdx - 1], '')
332   endif
333 enddef
334
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>>,
338                   title: string)
339   if lspserver.peekSymbolPopup->winbufnr() != -1
340     # If the symbol popup window is already present, close it.
341     lspserver.peekSymbolPopup->popup_close()
342   endif
343
344   var w: number = &columns
345   var fnamelen = float2nr(w * 0.4)
346
347   var menuItems: list<string> = []
348   for loc in locations
349     var [uri, range] = util.LspLocationParse(loc)
350     var fname: string = util.LspUriToFile(uri)
351     var bnr: number = fname->bufnr()
352     if bnr == -1
353       bnr = fname->bufadd()
354     endif
355     :silent! bnr->bufload()
356
357     var lnum = range.start.line + 1
358     var text: string = bnr->getbufline(lnum)->get(0, '')
359     menuItems->add($'{lnum}: {text}')
360   endfor
361
362   var popupAttrs = {
363     title: title,
364     wrap: false,
365     pos: 'topleft',
366     line: 'cursor+1',
367     col: winwidth(0) - 34,
368     minheight: 10,
369     maxheight: 10,
370     minwidth: 30,
371     maxwidth: 30,
372     mapping: false,
373     fixed: true,
374     filter: function(LocPopupFilter, [lspserver, locations]),
375     callback: function(LocPopupCallback, [lspserver, locations])
376   }
377   lspserver.peekSymbolPopup = popup_menu(menuItems, popupAttrs)
378   UpdatePeekFilePopup(lspserver, locations)
379 enddef
380
381 export def ShowLocations(lspserver: dict<any>, locations: list<dict<any>>,
382                          peekSymbol: bool, title: string)
383   if peekSymbol
384     PeekLocations(lspserver, locations, title)
385     return
386   endif
387
388   # create a loclist the location of the locations
389   var qflist: list<dict<any>> = []
390   for loc in locations
391     var [uri, range] = util.LspLocationParse(loc)
392     var fname: string = util.LspUriToFile(uri)
393     var bnr: number = fname->bufnr()
394     if bnr == -1
395       bnr = fname->bufadd()
396     endif
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,
402                         text: text})
403   endfor
404
405   var save_winid = win_getid()
406
407   if opt.lspOptions.useQuickfixForLocations
408     setqflist([], ' ', {title: title, items: qflist})
409     var mods: string = ''
410     exe $'{mods} copen'
411   else
412     setloclist(0, [], ' ', {title: title, items: qflist})
413     var mods: string = ''
414     exe $'{mods} lopen'
415   endif
416
417   if !opt.lspOptions.keepFocusInReferences
418     save_winid->win_gotoid()
419   endif
420 enddef
421
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
426   if key == "\<Esc>"
427     lspserver.peekSymbolPopup->popup_close()
428     return true
429   endif
430
431   return false
432 enddef
433
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()
440   if bnum == 0
441     # Failed to create or find a buffer
442     return
443   endif
444   :silent! bnum->bufload()
445
446   if lspserver.peekSymbolPopup->winbufnr() != -1
447     # If the symbol popup window is already present, close it.
448     lspserver.peekSymbolPopup->popup_close()
449   endif
450   var CbFunc = function(SymbolFilterCB, [lspserver])
451   var popupAttrs = {
452     title: $"{fnamemodify(fname, ':t')} ({fnamemodify(fname, ':h')})",
453     wrap: false,
454     moved: 'any',
455     minheight: 10,
456     maxheight: 10,
457     minwidth: 10,
458     maxwidth: 60,
459     cursorline: true,
460     border: [],
461     mapping: false,
462     filter: CbFunc
463   }
464   lspserver.peekSymbolPopup = popup_atcursor(bnum, popupAttrs)
465
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
471   var end_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()
479     :normal! z.
480   END
481   win_execute(pwid, cmds, 'silent!')
482 enddef
483
484 # Jump to the definition, declaration or implementation of a symbol.
485 # Also, used to peek at the definition, declaration or implementation of a
486 # symbol.
487 export def GotoSymbol(lspserver: dict<any>, location: dict<any>,
488                       peekSymbol: bool, cmdmods: string)
489   if peekSymbol
490     PeekSymbolLocation(lspserver, location)
491   else
492     # Save the current cursor location in the tag stack.
493     util.PushCursorToTagStack()
494     util.JumpToLspLocation(location, cmdmods)
495   endif
496 enddef
497
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>>
504
505   for tagloc in taglocations
506     var tagitem = {}
507     tagitem.name = pat
508
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"
514
515     retval->add(tagitem)
516   endfor
517
518   return retval
519 enddef
520
521 # process SymbolInformation[]
522 def ProcessSymbolInfoTable(lspserver: dict<any>,
523                            bnr: number,
524                            symbolInfoTable: list<dict<any>>,
525                            symbolTypeTable: dict<list<dict<any>>>,
526                            symbolLineTable: list<dict<any>>)
527   var fname: string
528   var symbolType: string
529   var name: string
530   var r: dict<dict<number>>
531   var symInfo: dict<any>
532
533   for syminfo in symbolInfoTable
534     fname = util.LspUriToFile(syminfo.location.uri)
535     symbolType = SymbolKindToName(syminfo.kind)
536     name = syminfo.name
537     if syminfo->has_key('containerName')
538       if syminfo.containerName != ''
539         name ..= $' [{syminfo.containerName}]'
540       endif
541     endif
542     r = syminfo.location.range
543     lspserver.decodeRange(bnr, r)
544
545     if !symbolTypeTable->has_key(symbolType)
546       symbolTypeTable[symbolType] = []
547     endif
548     symInfo = {name: name, range: r}
549     symbolTypeTable[symbolType]->add(symInfo)
550     symbolLineTable->add(symInfo)
551   endfor
552 enddef
553
554 # process DocumentSymbol[]
555 def ProcessDocSymbolTable(lspserver: dict<any>,
556                           bnr: number,
557                           docSymbolTable: list<dict<any>>,
558                           symbolTypeTable: dict<list<dict<any>>>,
559                           symbolLineTable: list<dict<any>>)
560   var symbolType: string
561   var name: string
562   var r: dict<dict<number>>
563   var symInfo: dict<any>
564   var symbolDetail: string
565   var childSymbols: dict<list<dict<any>>>
566
567   for syminfo in docSymbolTable
568     name = syminfo.name
569     symbolType = SymbolKindToName(syminfo.kind)
570     r = syminfo.selectionRange
571     lspserver.decodeRange(bnr, r)
572     if syminfo->has_key('detail')
573       symbolDetail = syminfo.detail
574     endif
575     if !symbolTypeTable->has_key(symbolType)
576       symbolTypeTable[symbolType] = []
577     endif
578     childSymbols = {}
579     if syminfo->has_key('children')
580       ProcessDocSymbolTable(lspserver, bnr, syminfo.children, childSymbols,
581                             symbolLineTable)
582     endif
583     symInfo = {name: name, range: r, detail: symbolDetail,
584                                                 children: childSymbols}
585     symbolTypeTable[symbolType]->add(symInfo)
586     symbolLineTable->add(symInfo)
587   endfor
588 enddef
589
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()
597
598   if docsymbol->empty()
599     # No symbols defined for this file. Clear the outline window.
600     outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable)
601     return
602   endif
603
604   if docsymbol[0]->has_key('location')
605     # SymbolInformation[]
606     ProcessSymbolInfoTable(lspserver, bnr, docsymbol, symbolTypeTable,
607                            symbolLineTable)
608   else
609     # DocumentSymbol[]
610     ProcessDocSymbolTable(lspserver, bnr, docsymbol, symbolTypeTable,
611                           symbolLineTable)
612   endif
613
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)
617 enddef
618
619 # vim: tabstop=8 shiftwidth=2 softtabstop=2