diff options
author | Marc Coquand <marc@mccd.space> | 2024-07-01 21:26:30 -0500 |
---|---|---|
committer | Marc Coquand <marc@mccd.space> | 2024-07-01 21:26:30 -0500 |
commit | 0a20357d4585da91d92252972f3eb7b715ff64ab (patch) | |
tree | c1e228d72b6331e89f72d99a1ba4ba1807d60381 /vim/pack/downloads/opt/lsp/autoload | |
download | bsd-0a20357d4585da91d92252972f3eb7b715ff64ab.tar.gz bsd-0a20357d4585da91d92252972f3eb7b715ff64ab.tar.bz2 bsd-0a20357d4585da91d92252972f3eb7b715ff64ab.zip |
initial commit
Diffstat (limited to 'vim/pack/downloads/opt/lsp/autoload')
24 files changed, 10526 insertions, 0 deletions
diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/buffer.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/buffer.vim new file mode 100644 index 0000000..5a07654 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/buffer.vim @@ -0,0 +1,194 @@ +vim9script + +# Functions for managing the per-buffer LSP server information + +import './util.vim' + +# A buffer can have one or more attached language servers. The +# "bufnrToServers" Dict contains the list of language servers attached to a +# buffer. The buffer number is the key for the "bufnrToServers" Dict. The +# value is the List of attached language servers. +var bufnrToServers: dict<list<dict<any>>> = {} + +# Add "lspserver" to "bufnrToServers" map for buffer "bnr". +export def BufLspServerSet(bnr: number, lspserver: dict<any>) + if !bufnrToServers->has_key(bnr) + bufnrToServers[bnr] = [] + endif + + bufnrToServers[bnr]->add(lspserver) +enddef + +# Remove "lspserver" from "bufnrToServers" map for buffer "bnr". +export def BufLspServerRemove(bnr: number, lspserver: dict<any>) + if !bufnrToServers->has_key(bnr) + return + endif + + var servers: list<dict<any>> = bufnrToServers[bnr] + servers = servers->filter((key, srv) => srv.id != lspserver.id) + + if servers->empty() + bufnrToServers->remove(bnr) + else + bufnrToServers[bnr] = servers + endif +enddef + +var SupportedCheckFns = { + callHierarchy: (lspserver) => lspserver.isCallHierarchyProvider, + codeAction: (lspserver) => lspserver.isCodeActionProvider, + codeLens: (lspserver) => lspserver.isCodeLensProvider, + completion: (lspserver) => lspserver.isCompletionProvider, + declaration: (lspserver) => lspserver.isDeclarationProvider, + definition: (lspserver) => lspserver.isDefinitionProvider, + documentFormatting: (lspserver) => lspserver.isDocumentFormattingProvider, + documentHighlight: (lspserver) => lspserver.isDocumentHighlightProvider, + documentSymbol: (lspserver) => lspserver.isDocumentSymbolProvider, + foldingRange: (lspserver) => lspserver.isFoldingRangeProvider, + hover: (lspserver) => lspserver.isHoverProvider, + implementation: (lspserver) => lspserver.isImplementationProvider, + inlayHint: (lspserver) => lspserver.isInlayHintProvider || + lspserver.isClangdInlayHintsProvider, + references: (lspserver) => lspserver.isReferencesProvider, + rename: (lspserver) => lspserver.isRenameProvider, + selectionRange: (lspserver) => lspserver.isSelectionRangeProvider, + semanticTokens: (lspserver) => lspserver.isSemanticTokensProvider, + signatureHelp: (lspserver) => lspserver.isSignatureHelpProvider, + typeDefinition: (lspserver) => lspserver.isTypeDefinitionProvider, + typeHierarchy: (lspserver) => lspserver.isTypeHierarchyProvider, + workspaceSymbol: (lspserver) => lspserver.isWorkspaceSymbolProvider +} + +# Returns the LSP server for the buffer "bnr". If "feature" is specified, +# then returns the LSP server that provides the "feature". +# Returns an empty dict if the server is not found. +export def BufLspServerGet(bnr: number, feature: string = null_string): dict<any> + if !bufnrToServers->has_key(bnr) + return {} + endif + + if bufnrToServers[bnr]->empty() + return {} + endif + + if feature == null_string + return bufnrToServers[bnr][0] + endif + + if !SupportedCheckFns->has_key(feature) + # If this happns it is a programming error, and should be fixed in the + # source code + :throw $'Error: ''{feature}'' is not a valid feature' + endif + + var SupportedCheckFn = SupportedCheckFns[feature] + + var possibleLSPs: list<dict<any>> = [] + + for lspserver in bufnrToServers[bnr] + if !lspserver.ready || !SupportedCheckFn(lspserver) + continue + endif + + possibleLSPs->add(lspserver) + endfor + + if possibleLSPs->empty() + return {} + endif + + # LSP server is configured to be a provider for "feature" + for lspserver in possibleLSPs + var has_feature: bool = lspserver.features->get(feature, false) + if has_feature + return lspserver + endif + endfor + + # Return the first LSP server that supports "feature" and doesn't have it + # disabled + for lspserver in possibleLSPs + if lspserver.featureEnabled(feature) + return lspserver + endif + endfor + + return {} +enddef + +# Returns the LSP server for the buffer "bnr" and with ID "id". Returns an empty +# dict if the server is not found. +export def BufLspServerGetById(bnr: number, id: number): dict<any> + if !bufnrToServers->has_key(bnr) + return {} + endif + + for lspserver in bufnrToServers[bnr] + if lspserver.id == id + return lspserver + endif + endfor + + return {} +enddef + +# Returns the LSP servers for the buffer "bnr". Returns an empty list if the +# servers are not found. +export def BufLspServersGet(bnr: number): list<dict<any>> + if !bufnrToServers->has_key(bnr) + return [] + endif + + return bufnrToServers[bnr] +enddef + +# Returns the LSP server for the current buffer with the optionally "feature". +# Returns an empty dict if the server is not found. +export def CurbufGetServer(feature: string = null_string): dict<any> + return BufLspServerGet(bufnr(), feature) +enddef + +# Returns the LSP servers for the current buffer. Returns an empty list if the +# servers are not found. +export def CurbufGetServers(): list<dict<any>> + return BufLspServersGet(bufnr()) +enddef + +export def BufHasLspServer(bnr: number): bool + var lspserver = BufLspServerGet(bnr) + + return !lspserver->empty() +enddef + +# Returns the LSP server for the current buffer with the optinally "feature" if +# it is running and is ready. +# Returns an empty dict if the server is not found or is not ready. +export def CurbufGetServerChecked(feature: string = null_string): dict<any> + var fname: string = @% + if fname->empty() || &filetype->empty() + return {} + endif + + var lspserver: dict<any> = CurbufGetServer(feature) + if lspserver->empty() + if feature == null_string + util.ErrMsg($'Language server for "{&filetype}" file type is not found') + else + util.ErrMsg($'Language server for "{&filetype}" file type supporting "{feature}" feature is not found') + endif + return {} + endif + if !lspserver.running + util.ErrMsg($'Language server for "{&filetype}" file type is not running') + return {} + endif + if !lspserver.ready + util.ErrMsg($'Language server for "{&filetype}" file type is not ready') + return {} + endif + + return lspserver +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/callhierarchy.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/callhierarchy.vim new file mode 100644 index 0000000..aca6a55 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/callhierarchy.vim @@ -0,0 +1,194 @@ +vim9script + +# Functions for dealing with call hierarchy (incoming/outgoing calls) + +import './util.vim' +import './buffer.vim' as buf + +# Jump to the location of the symbol under the cursor in the call hierarchy +# tree window. +def CallHierarchyItemJump() + var item: dict<any> = w:LspCallHierItemMap[line('.')].item + util.JumpToLspLocation(item, '') +enddef + +# Refresh the call hierarchy tree for the symbol at index "idx". +def CallHierarchyTreeItemRefresh(idx: number) + var treeItem: dict<any> = w:LspCallHierItemMap[idx] + + if treeItem.open + # Already retrieved the children for this item + return + endif + + if !treeItem->has_key('children') + # First time retrieving the children for the item at index "idx" + var lspserver = buf.BufLspServerGet(w:LspBufnr, 'callHierarchy') + if lspserver->empty() || !lspserver.running + return + endif + + var reply: any + if w:LspCallHierIncoming + reply = lspserver.getIncomingCalls(treeItem.item) + else + reply = lspserver.getOutgoingCalls(treeItem.item) + endif + + treeItem.children = [] + if !reply->empty() + for item in reply + treeItem.children->add({item: w:LspCallHierIncoming ? item.from : + item.to, open: false}) + endfor + endif + endif + + # Clear and redisplay the tree in the window + treeItem.open = true + var save_cursor = getcurpos() + CallHierarchyTreeRefresh() + setpos('.', save_cursor) +enddef + +# Open the call hierarchy tree item under the cursor +def CallHierarchyTreeItemOpen() + CallHierarchyTreeItemRefresh(line('.')) +enddef + +# Refresh the entire call hierarchy tree +def CallHierarchyTreeRefreshCmd() + w:LspCallHierItemMap[2].open = false + w:LspCallHierItemMap[2]->remove('children') + CallHierarchyTreeItemRefresh(2) +enddef + +# Display the incoming call hierarchy tree +def CallHierarchyTreeIncomingCmd() + w:LspCallHierItemMap[2].open = false + w:LspCallHierItemMap[2]->remove('children') + w:LspCallHierIncoming = true + CallHierarchyTreeItemRefresh(2) +enddef + +# Display the outgoing call hierarchy tree +def CallHierarchyTreeOutgoingCmd() + w:LspCallHierItemMap[2].open = false + w:LspCallHierItemMap[2]->remove('children') + w:LspCallHierIncoming = false + CallHierarchyTreeItemRefresh(2) +enddef + +# Close the call hierarchy tree item under the cursor +def CallHierarchyTreeItemClose() + var treeItem: dict<any> = w:LspCallHierItemMap[line('.')] + treeItem.open = false + var save_cursor = getcurpos() + CallHierarchyTreeRefresh() + setpos('.', save_cursor) +enddef + +# Recursively add the call hierarchy items to w:LspCallHierItemMap +def CallHierarchyTreeItemShow(incoming: bool, treeItem: dict<any>, pfx: string) + var item = treeItem.item + var treePfx: string + if treeItem.open && treeItem->has_key('children') + treePfx = has('gui_running') ? '▼' : '-' + else + treePfx = has('gui_running') ? '▶' : '+' + endif + var fname = util.LspUriToFile(item.uri) + var s = $'{pfx}{treePfx} {item.name} ({fname->fnamemodify(":t")} [{fname->fnamemodify(":h")}])' + append('$', s) + w:LspCallHierItemMap->add(treeItem) + if treeItem.open && treeItem->has_key('children') + for child in treeItem.children + CallHierarchyTreeItemShow(incoming, child, $'{pfx} ') + endfor + endif +enddef + +def CallHierarchyTreeRefresh() + :setlocal modifiable + :silent! :%d _ + + setline(1, $'# {w:LspCallHierIncoming ? "Incoming calls to" : "Outgoing calls from"} "{w:LspCallHierarchyTree.item.name}"') + w:LspCallHierItemMap = [{}, {}] + CallHierarchyTreeItemShow(w:LspCallHierIncoming, w:LspCallHierarchyTree, '') + :setlocal nomodifiable +enddef + +def CallHierarchyTreeShow(incoming: bool, prepareItem: dict<any>, + items: list<dict<any>>) + var save_bufnr = bufnr() + var wid = bufwinid('LSP-CallHierarchy') + if wid != -1 + wid->win_gotoid() + else + :new LSP-CallHierarchy + :setlocal buftype=nofile + :setlocal bufhidden=wipe + :setlocal noswapfile + :setlocal nonumber nornu + :setlocal fdc=0 signcolumn=no + + :nnoremap <buffer> <CR> <ScriptCmd>CallHierarchyItemJump()<CR> + :nnoremap <buffer> - <ScriptCmd>CallHierarchyTreeItemOpen()<CR> + :nnoremap <buffer> + <ScriptCmd>CallHierarchyTreeItemClose()<CR> + :command -buffer LspCallHierarchyRefresh CallHierarchyTreeRefreshCmd() + :command -buffer LspCallHierarchyIncoming CallHierarchyTreeIncomingCmd() + :command -buffer LspCallHierarchyOutgoing CallHierarchyTreeOutgoingCmd() + + :syntax match Comment '^#.*$' + :syntax match Directory '(.*)$' + endif + + w:LspBufnr = save_bufnr + w:LspCallHierIncoming = incoming + w:LspCallHierarchyTree = {} + w:LspCallHierarchyTree.item = prepareItem + w:LspCallHierarchyTree.open = true + w:LspCallHierarchyTree.children = [] + for item in items + w:LspCallHierarchyTree.children->add({item: incoming ? item.from : item.to, open: false}) + endfor + + CallHierarchyTreeRefresh() + + :setlocal nomodified + :setlocal nomodifiable +enddef + +export def IncomingCalls(lspserver: dict<any>) + var prepareReply = lspserver.prepareCallHierarchy() + if prepareReply->empty() + util.WarnMsg('No incoming calls') + return + endif + + var reply = lspserver.getIncomingCalls(prepareReply) + if reply->empty() + util.WarnMsg('No incoming calls') + return + endif + + CallHierarchyTreeShow(true, prepareReply, reply) +enddef + +export def OutgoingCalls(lspserver: dict<any>) + var prepareReply = lspserver.prepareCallHierarchy() + if prepareReply->empty() + util.WarnMsg('No outgoing calls') + return + endif + + var reply = lspserver.getOutgoingCalls(prepareReply) + if reply->empty() + util.WarnMsg('No outgoing calls') + return + endif + + CallHierarchyTreeShow(false, prepareReply, reply) +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/capabilities.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/capabilities.vim new file mode 100644 index 0000000..46f5973 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/capabilities.vim @@ -0,0 +1,501 @@ +vim9script + +# Functions for managing the LSP server and client capabilities + +import './options.vim' as opt + +# Process the server capabilities +# interface ServerCapabilities +export def ProcessServerCaps(lspserver: dict<any>, caps: dict<any>) + var serverEncoding = 'utf-16' + if lspserver.caps->has_key('positionEncoding') + serverEncoding = lspserver.caps.positionEncoding + elseif lspserver.caps->has_key('~additionalInitResult_offsetEncoding') + serverEncoding = lspserver.caps['~additionalInitResult_offsetEncoding'] + endif + + if lspserver.forceOffsetEncoding != '' + serverEncoding = lspserver.forceOffsetEncoding + endif + + # one of 'utf-8', 'utf-16' or 'utf-32' + if serverEncoding == 'utf-8' + lspserver.posEncoding = 8 + elseif serverEncoding == 'utf-16' + lspserver.posEncoding = 16 + else + lspserver.posEncoding = 32 + endif + + if has('patch-9.0.1629') && lspserver.posEncoding != 32 + lspserver.needOffsetEncoding = true + else + lspserver.needOffsetEncoding = false + endif + + # completionProvider + if lspserver.caps->has_key('completionProvider') + lspserver.isCompletionProvider = true + if lspserver.caps.completionProvider->has_key('resolveProvider') + lspserver.isCompletionResolveProvider = + lspserver.caps.completionProvider.resolveProvider + else + lspserver.isCompletionResolveProvider = false + endif + else + lspserver.isCompletionProvider = false + lspserver.isCompletionResolveProvider = false + endif + + # definitionProvider + if lspserver.caps->has_key('definitionProvider') + if lspserver.caps.definitionProvider->type() == v:t_bool + lspserver.isDefinitionProvider = lspserver.caps.definitionProvider + else + lspserver.isDefinitionProvider = true + endif + else + lspserver.isDefinitionProvider = false + endif + + # declarationProvider + if lspserver.caps->has_key('declarationProvider') + if lspserver.caps.declarationProvider->type() == v:t_bool + lspserver.isDeclarationProvider = lspserver.caps.declarationProvider + else + lspserver.isDeclarationProvider = true + endif + else + lspserver.isDeclarationProvider = false + endif + + # typeDefinitionProvider + if lspserver.caps->has_key('typeDefinitionProvider') + if lspserver.caps.typeDefinitionProvider->type() == v:t_bool + lspserver.isTypeDefinitionProvider = lspserver.caps.typeDefinitionProvider + else + lspserver.isTypeDefinitionProvider = true + endif + else + lspserver.isTypeDefinitionProvider = false + endif + + # implementationProvider + if lspserver.caps->has_key('implementationProvider') + if lspserver.caps.implementationProvider->type() == v:t_bool + lspserver.isImplementationProvider = lspserver.caps.implementationProvider + else + lspserver.isImplementationProvider = true + endif + else + lspserver.isImplementationProvider = false + endif + + # signatureHelpProvider + if lspserver.caps->has_key('signatureHelpProvider') + lspserver.isSignatureHelpProvider = true + else + lspserver.isSignatureHelpProvider = false + endif + + # hoverProvider + if lspserver.caps->has_key('hoverProvider') + if lspserver.caps.hoverProvider->type() == v:t_bool + lspserver.isHoverProvider = lspserver.caps.hoverProvider + else + lspserver.isHoverProvider = true + endif + else + lspserver.isHoverProvider = false + endif + + # referencesProvider + if lspserver.caps->has_key('referencesProvider') + if lspserver.caps.referencesProvider->type() == v:t_bool + lspserver.isReferencesProvider = lspserver.caps.referencesProvider + else + lspserver.isReferencesProvider = true + endif + else + lspserver.isReferencesProvider = false + endif + + # documentHighlightProvider + if lspserver.caps->has_key('documentHighlightProvider') + if lspserver.caps.documentHighlightProvider->type() == v:t_bool + lspserver.isDocumentHighlightProvider = + lspserver.caps.documentHighlightProvider + else + lspserver.isDocumentHighlightProvider = true + endif + else + lspserver.isDocumentHighlightProvider = false + endif + + # documentSymbolProvider + if lspserver.caps->has_key('documentSymbolProvider') + if lspserver.caps.documentSymbolProvider->type() == v:t_bool + lspserver.isDocumentSymbolProvider = + lspserver.caps.documentSymbolProvider + else + lspserver.isDocumentSymbolProvider = true + endif + else + lspserver.isDocumentSymbolProvider = false + endif + + # documentFormattingProvider + if lspserver.caps->has_key('documentFormattingProvider') + if lspserver.caps.documentFormattingProvider->type() == v:t_bool + lspserver.isDocumentFormattingProvider = + lspserver.caps.documentFormattingProvider + else + lspserver.isDocumentFormattingProvider = true + endif + else + lspserver.isDocumentFormattingProvider = false + endif + + # callHierarchyProvider + if lspserver.caps->has_key('callHierarchyProvider') + if lspserver.caps.callHierarchyProvider->type() == v:t_bool + lspserver.isCallHierarchyProvider = + lspserver.caps.callHierarchyProvider + else + lspserver.isCallHierarchyProvider = true + endif + else + lspserver.isCallHierarchyProvider = false + endif + + # semanticTokensProvider + if lspserver.caps->has_key('semanticTokensProvider') + lspserver.isSemanticTokensProvider = true + lspserver.semanticTokensLegend = + lspserver.caps.semanticTokensProvider.legend + lspserver.semanticTokensRange = + lspserver.caps.semanticTokensProvider->get('range', false) + if lspserver.caps.semanticTokensProvider->has_key('full') + if lspserver.caps.semanticTokensProvider.full->type() == v:t_bool + lspserver.semanticTokensFull = + lspserver.caps.semanticTokensProvider.full + lspserver.semanticTokensDelta = false + else + lspserver.semanticTokensFull = true + if lspserver.caps.semanticTokensProvider.full->has_key('delta') + lspserver.semanticTokensDelta = + lspserver.caps.semanticTokensProvider.full.delta + else + lspserver.semanticTokensDelta = false + endif + endif + else + lspserver.semanticTokensFull = false + lspserver.semanticTokensDelta = false + endif + else + lspserver.isSemanticTokensProvider = false + endif + + # typeHierarchyProvider + if lspserver.caps->has_key('typeHierarchyProvider') + lspserver.isTypeHierarchyProvider = true + else + lspserver.isTypeHierarchyProvider = false + endif + + # renameProvider + if lspserver.caps->has_key('renameProvider') + if lspserver.caps.renameProvider->type() == v:t_bool + lspserver.isRenameProvider = lspserver.caps.renameProvider + else + lspserver.isRenameProvider = true + endif + else + lspserver.isRenameProvider = false + endif + + # codeActionProvider + if lspserver.caps->has_key('codeActionProvider') + if lspserver.caps.codeActionProvider->type() == v:t_bool + lspserver.isCodeActionProvider = lspserver.caps.codeActionProvider + else + lspserver.isCodeActionProvider = true + endif + else + lspserver.isCodeActionProvider = false + endif + + # codeLensProvider + if lspserver.caps->has_key('codeLensProvider') + lspserver.isCodeLensProvider = true + if lspserver.caps.codeLensProvider->has_key('resolveProvider') + lspserver.isCodeLensResolveProvider = true + else + lspserver.isCodeLensResolveProvider = false + endif + else + lspserver.isCodeLensProvider = false + endif + + # workspaceSymbolProvider + if lspserver.caps->has_key('workspaceSymbolProvider') + if lspserver.caps.workspaceSymbolProvider->type() == v:t_bool + lspserver.isWorkspaceSymbolProvider = + lspserver.caps.workspaceSymbolProvider + else + lspserver.isWorkspaceSymbolProvider = true + endif + else + lspserver.isWorkspaceSymbolProvider = false + endif + + # selectionRangeProvider + if lspserver.caps->has_key('selectionRangeProvider') + if lspserver.caps.selectionRangeProvider->type() == v:t_bool + lspserver.isSelectionRangeProvider = + lspserver.caps.selectionRangeProvider + else + lspserver.isSelectionRangeProvider = true + endif + else + lspserver.isSelectionRangeProvider = false + endif + + # foldingRangeProvider + if lspserver.caps->has_key('foldingRangeProvider') + if lspserver.caps.foldingRangeProvider->type() == v:t_bool + lspserver.isFoldingRangeProvider = lspserver.caps.foldingRangeProvider + else + lspserver.isFoldingRangeProvider = true + endif + else + lspserver.isFoldingRangeProvider = false + endif + + # inlayHintProvider + if lspserver.caps->has_key('inlayHintProvider') + if lspserver.caps.inlayHintProvider->type() == v:t_bool + lspserver.isInlayHintProvider = lspserver.caps.inlayHintProvider + else + lspserver.isInlayHintProvider = true + endif + else + lspserver.isInlayHintProvider = false + endif + + # clangdInlayHintsProvider + if lspserver.caps->has_key('clangdInlayHintsProvider') + lspserver.isClangdInlayHintsProvider = + lspserver.caps.clangdInlayHintsProvider + else + lspserver.isClangdInlayHintsProvider = false + endif + + # textDocumentSync capabilities + lspserver.supportsDidSave = false + # Default to TextDocumentSyncKind.None + lspserver.textDocumentSync = 0 + if lspserver.caps->has_key('textDocumentSync') + if lspserver.caps.textDocumentSync->type() == v:t_bool + || lspserver.caps.textDocumentSync->type() == v:t_number + lspserver.supportsDidSave = lspserver.caps.textDocumentSync + lspserver.textDocumentSync = lspserver.caps.textDocumentSync + elseif lspserver.caps.textDocumentSync->type() == v:t_dict + # "save" + if lspserver.caps.textDocumentSync->has_key('save') + if lspserver.caps.textDocumentSync.save->type() == v:t_bool + || lspserver.caps.textDocumentSync.save->type() == v:t_number + lspserver.supportsDidSave = lspserver.caps.textDocumentSync.save + elseif lspserver.caps.textDocumentSync.save->type() == v:t_dict + lspserver.supportsDidSave = true + endif + endif + # "change" + if lspserver.caps.textDocumentSync->has_key('change') + lspserver.textDocumentSync = lspserver.caps.textDocumentSync.change + endif + endif + endif +enddef + +# Return all the LSP client capabilities +export def GetClientCaps(): dict<any> + # client capabilities (ClientCapabilities) + var clientCaps: dict<any> = { + general: { + # Currently we always send character count as position offset, + # which meanas only utf-32 is supported. + # Adding utf-16 simply for good mesure, as I'm scared some servers will + # give up if they don't support utf-32 only. + positionEncodings: ['utf-32', 'utf-16'] + }, + textDocument: { + callHierarchy: { + dynamicRegistration: false + }, + codeAction: { + dynamicRegistration: false, + codeActionLiteralSupport: { + codeActionKind: { + valueSet: ['', 'quickfix', 'refactor', 'refactor.extract', + 'refactor.inline', 'refactor.rewrite', 'source', + 'source.organizeImports'] + } + }, + isPreferredSupport: true, + disabledSupport: true + }, + codeLens: { + dynamicRegistration: false + }, + completion: { + dynamicRegistration: false, + completionItem: { + documentationFormat: ['markdown', 'plaintext'], + resolveSupport: { + properties: ['detail', 'documentation'] + }, + snippetSupport: opt.lspOptions.snippetSupport, + insertReplaceSupport: false + }, + completionItemKind: { + valueSet: range(1, 25) + } + }, + declaration: { + dynamicRegistration: false, + linkSupport: true + }, + definition: { + dynamicRegistration: false, + linkSupport: true + }, + documentHighlight: { + dynamicRegistration: false + }, + documentSymbol: { + dynamicRegistration: false, + symbolKind: { + valueSet: range(1, 25) + }, + hierarchicalDocumentSymbolSupport: true, + labelSupport: false + }, + foldingRange: { + dynamicRegistration: false, + rangeLimit: 5000, + lineFoldingOnly: true, + foldingRangeKind: { + valueSet: ['comment', 'imports', 'region'] + }, + foldingRange: { + collapsedText: true + } + }, + formatting: { + dynamicRegistration: false + }, + hover: { + dynamicRegistration: false, + contentFormat: ['markdown', 'plaintext'] + }, + implementation: { + dynamicRegistration: false, + linkSupport: true + }, + inlayHint: { + dynamicRegistration: false + }, + publishDiagnostics: { + relatedInformation: false, + versionSupport: true, + codeDescriptionSupport: true, + dataSupport: true + }, + rangeFormatting: { + dynamicRegistration: false + }, + references: { + dynamicRegistration: false + }, + rename: { + dynamicRegistration: false, + prepareSupport: false, + }, + selectionRange: { + dynamicRegistration: false, + }, + signatureHelp: { + dynamicRegistration: false, + signatureInformation: { + documentationFormat: ['markdown', 'plaintext'], + activeParameterSupport: true + } + }, + semanticTokens: { + dynamicRegistration: false, + requests: { + range: false, + full: { + delta: true + } + }, + tokenTypes: [ + 'type', 'class', 'enum', 'interface', 'struct', 'typeParameter', + 'parameter', 'variable', 'property', 'enumMember', 'event', + 'function', 'method', 'macro', 'keyword', 'modifier', 'comment', + 'string', 'number', 'regexp', 'operator' + ], + tokenModifiers: [ + 'declaration', 'definition', 'readonly', 'static', 'deprecated', + 'abstract', 'async', 'modification', 'documentation', + 'defaultLibrary' + ], + formats: ['relative'], + overlappingTokenSupport: false, + multilineTokenSupport: false, + serverCancelSupport: false, + augmentsSyntaxTokens: true + }, + synchronization: { + dynamicRegistration: false, + didSave: true, + willSave: false, + WillSaveWaitUntil: false + }, + typeDefinition: { + dynamicRegistration: false, + linkSupport: true + }, + typeHierarchy: { + dynamicRegistration: false, + } + }, + window: {}, + workspace: { + workspaceFolders: true, + applyEdit: true, + workspaceEdit: { + resourceOperations: ['rename', 'create', 'delete'] + }, + configuration: true, + symbol: { + dynamicRegistration: false + } + }, + # This is the way clangd expects to be informated about supported encodings: + # https://clangd.llvm.org/extensions#utf-8-offsets + offsetEncoding: ['utf-32', 'utf-16'] + } + + # Vim patch 1629 is needed to properly encode/decode UTF-16 offsets + if has('patch-9.0.1629') + clientCaps.general.positionEncodings = ['utf-32', 'utf-16', 'utf-8'] + clientCaps.offsetEncoding = ['utf-32', 'utf-16', 'utf-8'] + endif + + return clientCaps +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/codeaction.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/codeaction.vim new file mode 100644 index 0000000..fb2a91a --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/codeaction.vim @@ -0,0 +1,135 @@ +vim9script + +# Functions related to handling LSP code actions to fix diagnostics. + +import './util.vim' +import './textedit.vim' +import './options.vim' as opt + +var CommandHandlers: dict<func> + +export def RegisterCmdHandler(cmd: string, Handler: func) + CommandHandlers[cmd] = Handler +enddef + +export def DoCommand(lspserver: dict<any>, cmd: dict<any>) + if cmd->has_key('command') && CommandHandlers->has_key(cmd.command) + var CmdHandler: func = CommandHandlers[cmd.command] + try + call CmdHandler(cmd) + catch + util.ErrMsg($'"{cmd.command}" handler raised exception {v:exception}') + endtry + else + lspserver.executeCommand(cmd) + endif +enddef + +# Apply the code action selected by the user. +export def HandleCodeAction(lspserver: dict<any>, selAction: dict<any>) + # textDocument/codeAction can return either Command[] or CodeAction[]. + # If it is a CodeAction, it can have either an edit, a command or both. + # Edits should be executed first. + # Both Command and CodeAction interfaces has "command" member + # so we should check "command" type - for Command it will be "string" + if selAction->has_key('edit') + || (selAction->has_key('command') && selAction.command->type() == v:t_dict) + # selAction is a CodeAction instance, apply edit and command + if selAction->has_key('edit') + # apply edit first + textedit.ApplyWorkspaceEdit(selAction.edit) + endif + if selAction->has_key('command') + DoCommand(lspserver, selAction.command) + endif + else + # selAction is a Command instance, apply it directly + DoCommand(lspserver, selAction) + endif +enddef + +# Process the list of code actions returned by the LSP server, ask the user to +# choose one action from the list and then apply it. +# If "query" is a number, then apply the corresponding action in the list. +# If "query" is a regular expression starting with "/", then apply the action +# matching the search string in the list. +# If "query" is a regular string, then apply the action matching the string. +# If "query" is an empty string, then if the "usePopupInCodeAction" option is +# configured by the user, then display the list of items in a popup menu. +# Otherwise display the items in an input list and prompt the user to select +# an action. +export def ApplyCodeAction(lspserver: dict<any>, actionlist: list<dict<any>>, query: string): void + var actions = actionlist + + if opt.lspOptions.hideDisabledCodeActions + actions = actions->filter((ix, act) => !act->has_key('disabled')) + endif + + if actions->empty() + # no action can be performed + util.WarnMsg('No code action is available') + return + endif + + var text: list<string> = [] + var act: dict<any> + for i in actions->len()->range() + act = actions[i] + var t: string = act.title->substitute('\r\n', '\\r\\n', 'g') + t = t->substitute('\n', '\\n', 'g') + text->add(printf(" %d. %s ", i + 1, t)) + endfor + + var choice: number + + var query_ = query->trim() + if query_ =~ '^\d\+$' # digit + choice = query_->str2nr() + elseif query_ =~ '^/' # regex + choice = 1 + util.Indexof(actions, (i, a) => a.title =~ query_[1 : ]) + elseif query_ != '' # literal string + choice = 1 + util.Indexof(actions, (i, a) => a.title[0 : query_->len() - 1] == query_) + elseif opt.lspOptions.usePopupInCodeAction + # Use a popup menu to show the code action + popup_create(text, { + pos: 'botleft', + line: 'cursor-1', + col: 'cursor', + zindex: 1000, + cursorline: 1, + mapping: 0, + wrap: 0, + title: 'Code action', + callback: (_, result) => { + # Invalid item selected or closed the popup + if result <= 0 || result > text->len() + return + endif + + # Do the code action + HandleCodeAction(lspserver, actions[result - 1]) + }, + filter: (winid, key) => { + if key == 'h' || key == 'l' + winid->popup_close(-1) + elseif key->str2nr() > 0 + # assume less than 10 entries are present + winid->popup_close(key->str2nr()) + else + return popup_filter_menu(winid, key) + endif + return 1 + }, + }) + else + choice = inputlist(['Code action:'] + text) + endif + + if choice < 1 || choice > text->len() + return + endif + + HandleCodeAction(lspserver, actions[choice - 1]) +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/codelens.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/codelens.vim new file mode 100644 index 0000000..082edf7 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/codelens.vim @@ -0,0 +1,32 @@ +vim9script + +import './codeaction.vim' + +# Functions related to handling LSP code lens + +export def ProcessCodeLens(lspserver: dict<any>, bnr: number, codeLensItems: list<dict<any>>) + var text: list<string> = [] + for i in codeLensItems->len()->range() + var item = codeLensItems[i] + if !item->has_key('command') + # resolve the code lens + item = lspserver.resolveCodeLens(bnr, item) + if item->empty() + continue + endif + codeLensItems[i] = item + endif + text->add(printf("%d. %s\t| L%s:%s", i + 1, item.command.title, + item.range.start.line + 1, + getline(item.range.start.line + 1))) + endfor + + var choice = inputlist(['Code Lens:'] + text) + if choice < 1 || choice > codeLensItems->len() + return + endif + + codeaction.DoCommand(lspserver, codeLensItems[choice - 1].command) +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/completion.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/completion.vim new file mode 100644 index 0000000..8ce5899 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/completion.vim @@ -0,0 +1,708 @@ +vim9script + +# LSP completion related functions + +import './util.vim' +import './buffer.vim' as buf +import './options.vim' as opt +import './textedit.vim' +import './snippet.vim' +import './codeaction.vim' + +# per-filetype omni-completion enabled/disabled table +var ftypeOmniCtrlMap: dict<bool> = {} + +var defaultKinds: dict<string> = { + 'Text': 't', + 'Method': 'm', + 'Function': 'f', + 'Constructor': 'C', + 'Field': 'F', + 'Variable': 'v', + 'Class': 'c', + 'Interface': 'i', + 'Module': 'M', + 'Property': 'p', + 'Unit': 'u', + 'Value': 'V', + 'Enum': 'e', + 'Keyword': 'k', + 'Snippet': 'S', + 'Color': 'C', + 'File': 'f', + 'Reference': 'r', + 'Folder': 'F', + 'EnumMember': 'E', + 'Constant': 'd', + 'Struct': 's', + 'Event': 'E', + 'Operator': 'o', + 'TypeParameter': 'T', + 'Buffer': 'B', +} + +# Returns true if omni-completion is enabled for filetype "ftype". +# Otherwise, returns false. +def LspOmniComplEnabled(ftype: string): bool + return ftypeOmniCtrlMap->get(ftype, false) +enddef + +# Enables or disables omni-completion for filetype "fype" +export def OmniComplSet(ftype: string, enabled: bool) + ftypeOmniCtrlMap->extend({[ftype]: enabled}) +enddef + +# Map LSP complete item kind to a character +def LspCompleteItemKindChar(kind: number): string + var kindMap: list<string> = [ + '', + 'Text', + 'Method', + 'Function', + 'Constructor', + 'Field', + 'Variable', + 'Class', + 'Interface', + 'Module', + 'Property', + 'Unit', + 'Value', + 'Enum', + 'Keyword', + 'Snippet', + 'Color', + 'File', + 'Reference', + 'Folder', + 'EnumMember', + 'Constant', + 'Struct', + 'Event', + 'Operator', + 'TypeParameter', + 'Buffer' + ] + + if kind > 26 + return '' + endif + + var kindName = kindMap[kind] + var kindValue = defaultKinds[kindName] + + var lspOpts = opt.lspOptions + if lspOpts.customCompletionKinds && + lspOpts.completionKinds->has_key(kindName) + kindValue = lspOpts.completionKinds[kindName] + endif + + return kindValue +enddef + +# Remove all the snippet placeholders from "str" and return the value. +# Based on a similar function in the vim-lsp plugin. +def MakeValidWord(str_arg: string): string + var str = str_arg->substitute('\$[0-9]\+\|\${\%(\\.\|[^}]\)\+}', '', 'g') + str = str->substitute('\\\(.\)', '\1', 'g') + var valid = str->matchstr('^[^"'' (<{\[\t\r\n]\+') + if valid->empty() + return str + endif + if valid =~ ':$' + return valid[: -2] + endif + return valid +enddef + +# add completion from current buf +def CompletionFromBuffer(items: list<dict<any>>) + var words = {} + var start = reltime() + var timeout = opt.lspOptions.bufferCompletionTimeout + var linenr = 1 + for line in getline(1, '$') + for word in line->split('\W\+') + if !words->has_key(word) && word->len() > 1 + words[word] = 1 + items->add({ + label: word, + data: { + entryNames: [word], + }, + kind: 26, + documentation: "", + }) + endif + endfor + # Check every 200 lines if timeout is exceeded + if timeout > 0 && linenr % 200 == 0 && + start->reltime()->reltimefloat() * 1000 > timeout + break + endif + linenr += 1 + endfor +enddef + +# process the 'textDocument/completion' reply from the LSP server +# Result: CompletionItem[] | CompletionList | null +export def CompletionReply(lspserver: dict<any>, cItems: any) + lspserver.completeItemsIsIncomplete = false + if cItems->empty() + if lspserver.omniCompletePending + lspserver.completeItems = [] + lspserver.omniCompletePending = false + endif + return + endif + + var items: list<dict<any>> + if cItems->type() == v:t_list + items = cItems + else + items = cItems.items + lspserver.completeItemsIsIncomplete = cItems->get('isIncomplete', false) + endif + + var lspOpts = opt.lspOptions + + # Get the keyword prefix before the current cursor column. + var chcol = charcol('.') + var starttext = chcol == 1 ? '' : getline('.')[ : chcol - 2] + var [prefix, start_idx, end_idx] = starttext->matchstrpos('\k*$') + if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_ICASE + prefix = prefix->tolower() + endif + + var start_col = start_idx + 1 + + if lspOpts.ultisnipsSupport + snippet.CompletionUltiSnips(prefix, items) + elseif lspOpts.vsnipSupport + snippet.CompletionVsnip(items) + endif + + if lspOpts.useBufferCompletion + CompletionFromBuffer(items) + endif + + var completeItems: list<dict<any>> = [] + var itemsUsed: list<string> = [] + for item in items + var d: dict<any> = {} + + # TODO: Add proper support for item.textEdit.newText and + # item.textEdit.range. Keep in mind that item.textEdit.range can start + # way before the typed keyword. + if item->has_key('textEdit') && + lspOpts.completionMatcherValue != opt.COMPLETIONMATCHER_FUZZY + var start_charcol: number + if !prefix->empty() + start_charcol = charidx(starttext, start_idx) + 1 + else + start_charcol = chcol + endif + var textEdit = item.textEdit + var textEditRange: dict<any> = {} + if textEdit->has_key('range') + textEditRange = textEdit.range + elseif textEdit->has_key('insert') + textEditRange = textEdit.insert + endif + var textEditStartCol = + util.GetCharIdxWithoutCompChar(bufnr(), textEditRange.start) + if textEditStartCol != start_charcol + var offset = start_charcol - textEditStartCol - 1 + d.word = textEdit.newText[offset : ] + else + d.word = textEdit.newText + endif + elseif item->has_key('insertText') + d.word = item.insertText + else + d.word = item.label + endif + + if item->get('insertTextFormat', 1) == 2 + # snippet completion. Needs a snippet plugin to expand the snippet. + # Remove all the snippet placeholders + d.word = MakeValidWord(d.word) + elseif !lspserver.completeItemsIsIncomplete || lspOpts.useBufferCompletion + # Filter items only when "isIncomplete" is set (otherwise server would + # have done the filtering) or when buffer completion is enabled + + # plain text completion + if !prefix->empty() + # If the completion item text doesn't start with the current (case + # ignored) keyword prefix, skip it. + var filterText: string = item->get('filterText', d.word) + if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_ICASE + if filterText->tolower()->stridx(prefix) != 0 + continue + endif + # If the completion item text doesn't fuzzy match with the current + # keyword prefix, skip it. + elseif lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_FUZZY + if matchfuzzy([filterText], prefix)->empty() + continue + endif + # If the completion item text doesn't start with the current keyword + # prefix, skip it. + else + if filterText->stridx(prefix) != 0 + continue + endif + endif + endif + endif + + d.abbr = item.label + d.dup = 1 + + if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_ICASE + d.icase = 1 + endif + + if item->has_key('kind') && item.kind != null + # namespace CompletionItemKind + # map LSP kind to complete-item-kind + d.kind = LspCompleteItemKindChar(item.kind) + endif + + if lspserver.completionLazyDoc + d.info = 'Lazy doc' + else + if item->has_key('detail') && !item.detail->empty() + # Solve a issue where if a server send a detail field + # with a "\n", on the menu will be everything joined with + # a "^@" separating it. (example: clangd) + d.menu = item.detail->split("\n")[0] + endif + if item->has_key('documentation') + var itemDoc = item.documentation + if itemDoc->type() == v:t_string && !itemDoc->empty() + d.info = itemDoc + elseif itemDoc->type() == v:t_dict + && itemDoc.value->type() == v:t_string + d.info = itemDoc.value + endif + endif + endif + + # Score is used for sorting. + d.score = item->get('sortText') + if d.score->empty() + d.score = item->get('label', '') + endif + + # Dont include duplicate items + if lspOpts.filterCompletionDuplicates + var key = d->get('word', '') .. + d->get('info', '') .. + d->get('kind', '') .. + d->get('score', '') .. + d->get('abbr', '') .. + d->get('dup', '') + if index(itemsUsed, key) != -1 + continue + endif + add(itemsUsed, key) + endif + + d.user_data = item + completeItems->add(d) + endfor + + if lspOpts.completionMatcherValue != opt.COMPLETIONMATCHER_FUZZY + # Lexographical sort (case-insensitive). + completeItems->sort((a, b) => + a.score == b.score ? 0 : a.score >? b.score ? 1 : -1) + endif + + if lspOpts.autoComplete && !lspserver.omniCompletePending + if completeItems->empty() + # no matches + return + endif + + var m = mode() + if m != 'i' && m != 'R' && m != 'Rv' + # If not in insert or replace mode, then don't start the completion + return + endif + + if completeItems->len() == 1 + && getline('.')->matchstr($'\C{completeItems[0].word}\>') != '' + # only one complete match. No need to show the completion popup + return + endif + + completeItems->complete(start_col) + else + lspserver.completeItems = completeItems + lspserver.omniCompletePending = false + endif +enddef + +# Check if completion item is selected +def CheckCompletionItemSel(label: string): bool + var cInfo = complete_info() + if cInfo->empty() || !cInfo.pum_visible || cInfo.selected == -1 + return false + endif + var selItem = cInfo.items->get(cInfo.selected, {}) + if selItem->empty() + || selItem->type() != v:t_dict + || selItem.user_data->type() != v:t_dict + || selItem.user_data.label != label + return false + endif + return true +enddef + +# Process the completion documentation +def ShowCompletionDocumentation(cItem: any) + if cItem->empty() || cItem->type() != v:t_dict + return + endif + + # check if completion item is still selected + if !CheckCompletionItemSel(cItem.label) + return + endif + + var infoText: list<string> + var infoKind: string + + if cItem->has_key('detail') && !cItem.detail->empty() + # Solve a issue where if a server send the detail field with "\n", + # on the completion popup, everything will be joined with "^@" + # (example: typescript-language-server) + infoText->extend(cItem.detail->split("\n")) + endif + + if cItem->has_key('documentation') + if !infoText->empty() + infoText->extend(['- - -']) + endif + var cItemDoc = cItem.documentation + if cItemDoc->type() == v:t_dict + # MarkupContent + if cItemDoc.kind == 'plaintext' + infoText->extend(cItemDoc.value->split("\n")) + infoKind = 'text' + elseif cItemDoc.kind == 'markdown' + infoText->extend(cItemDoc.value->split("\n")) + infoKind = 'lspgfm' + else + util.ErrMsg($'Unsupported documentation type ({cItemDoc.kind})') + return + endif + elseif cItemDoc->type() == v:t_string + infoText->extend(cItemDoc->split("\n")) + else + util.ErrMsg($'Unsupported documentation ({cItemDoc->string()})') + return + endif + endif + + if infoText->empty() + return + endif + + # check if completion item is changed in meantime + if !CheckCompletionItemSel(cItem.label) + return + endif + + # autoComplete or &omnifunc with &completeopt =~ 'popup' + var id = popup_findinfo() + if id > 0 + var bufnr = id->winbufnr() + id->popup_settext(infoText) + infoKind->setbufvar(bufnr, '&ft') + id->popup_show() + else + # &omnifunc with &completeopt =~ 'preview' + try + :wincmd P + :setlocal modifiable + bufnr()->deletebufline(1, '$') + infoText->append(0) + [1, 1]->cursor() + exe $'setlocal ft={infoKind}' + :wincmd p + catch /E441/ # No preview window + endtry + endif +enddef + +# process the 'completionItem/resolve' reply from the LSP server +# Result: CompletionItem +export def CompletionResolveReply(lspserver: dict<any>, cItem: any) + ShowCompletionDocumentation(cItem) +enddef + +# Return trigger kind and trigger char. If completion trigger is not a keyword +# and not one of the triggerCharacters, return -1 for triggerKind. +def GetTriggerAttributes(lspserver: dict<any>): list<any> + var triggerKind: number = 1 + var triggerChar: string = '' + + # Trigger kind is 1 for keyword and 2 for trigger char initiated completion. + var line: string = getline('.') + var cur_col = charcol('.') + if line[cur_col - 2] !~ '\k' + var trigChars = lspserver.completionTriggerChars + var trigidx = trigChars->index(line[cur_col - 2]) + if trigidx == -1 + triggerKind = -1 + else + triggerKind = 2 + triggerChar = trigChars[trigidx] + endif + endif + return [triggerKind, triggerChar] +enddef + + +# omni complete handler +def g:LspOmniFunc(findstart: number, base: string): any + var lspserver: dict<any> = buf.CurbufGetServerChecked('completion') + if lspserver->empty() + return -2 + endif + + if findstart + + var [triggerKind, triggerChar] = GetTriggerAttributes(lspserver) + if triggerKind < 0 + # previous character is not a keyword character or a trigger character, + # so cancel omni completion. + return -2 + endif + + # first send all the changes in the current buffer to the LSP server + listener_flush() + + lspserver.omniCompletePending = true + lspserver.completeItems = [] + + # initiate a request to LSP server to get list of completions + lspserver.getCompletion(triggerKind, triggerChar) + + # locate the start of the word + var line = getline('.')->strpart(0, col('.') - 1) + var keyword = line->matchstr('\k\+$') + lspserver.omniCompleteKeyword = keyword + return line->len() - keyword->len() + else + # Wait for the list of matches from the LSP server + var count: number = 0 + while lspserver.omniCompletePending && count < 1000 + if complete_check() + return v:none + endif + sleep 2m + count += 1 + endwhile + + if lspserver.omniCompletePending + return v:none + endif + + var res: list<dict<any>> = lspserver.completeItems + var prefix = lspserver.omniCompleteKeyword + + # Don't attempt to filter on the items, when "isIncomplete" is set + if prefix->empty() || lspserver.completeItemsIsIncomplete + return res + endif + + var lspOpts = opt.lspOptions + if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_FUZZY + return res->matchfuzzy(prefix, { key: 'word' }) + endif + + if lspOpts.completionMatcherValue == opt.COMPLETIONMATCHER_ICASE + return res->filter((i, v) => + v.word->tolower()->stridx(prefix->tolower()) == 0) + endif + + return res->filter((i, v) => v.word->stridx(prefix) == 0) + endif +enddef + +# For plugins that implement async completion this function indicates if +# omnifunc is waiting for LSP response. +def g:LspOmniCompletePending(): bool + var lspserver: dict<any> = buf.CurbufGetServerChecked('completion') + return !lspserver->empty() && lspserver.omniCompletePending +enddef + +# Insert mode completion handler. Used when 24x7 completion is enabled +# (default). +def LspComplete() + var lspserver: dict<any> = buf.CurbufGetServer('completion') + if lspserver->empty() || !lspserver.running || !lspserver.ready + return + endif + + var [triggerKind, triggerChar] = GetTriggerAttributes(lspserver) + if triggerKind < 0 + return + endif + + # first send all the changes in the current buffer to the LSP server + listener_flush() + + # initiate a request to LSP server to get list of completions + lspserver.getCompletion(triggerKind, triggerChar) +enddef + +# Lazy complete documentation handler +def LspResolve() + var lspserver: dict<any> = buf.CurbufGetServerChecked('completion') + if lspserver->empty() + return + endif + + var item = v:event.completed_item + if item->has_key('user_data') && !item.user_data->empty() + if item.user_data->type() == v:t_dict && !item.user_data->has_key('documentation') + lspserver.resolveCompletion(item.user_data) + else + ShowCompletionDocumentation(item.user_data) + endif + endif +enddef + +# If the completion popup documentation window displays "markdown" content, +# then set the 'filetype' to "lspgfm". +def LspSetPopupFileType() + var item = v:event.completed_item + var cItem = item->get('user_data', {}) + if cItem->empty() + return + endif + + if cItem->type() != v:t_dict || !cItem->has_key('documentation') + \ || cItem.documentation->type() != v:t_dict + \ || cItem.documentation.kind != 'markdown' + return + endif + + var id = popup_findinfo() + if id > 0 + var bnum = id->winbufnr() + setbufvar(bnum, '&ft', 'lspgfm') + endif +enddef + +# complete done handler (LSP server-initiated actions after completion) +def LspCompleteDone(bnr: number) + var lspserver: dict<any> = buf.BufLspServerGet(bnr, 'completion') + if lspserver->empty() + return + endif + + if v:completed_item->type() != v:t_dict + return + endif + + var completionData: any = v:completed_item->get('user_data', '') + if completionData->type() != v:t_dict + || !opt.lspOptions.completionTextEdit + return + endif + + if !completionData->has_key('additionalTextEdits') + # Some language servers (e.g. typescript) delay the computation of the + # additional text edits. So try to resolve the completion item now to get + # the text edits. + completionData = lspserver.resolveCompletion(completionData, true) + endif + if !completionData->get('additionalTextEdits', {})->empty() + textedit.ApplyTextEdits(bnr, completionData.additionalTextEdits) + endif + + if completionData->has_key('command') + # Some language servers (e.g. haskell-language-server) want to apply + # additional commands after completion. + codeaction.DoCommand(lspserver, completionData.command) + endif + +enddef + +# Initialize buffer-local completion options and autocmds +export def BufferInit(lspserver: dict<any>, bnr: number, ftype: string) + if !lspserver.isCompletionProvider + # no support for completion + return + endif + + if !opt.lspOptions.autoComplete && !LspOmniComplEnabled(ftype) && !opt.lspOptions.omniComplete + # LSP auto/omni completion support is not enabled for this buffer + return + endif + + # buffer-local autocmds for completion + var acmds: list<dict<any>> = [] + + # set options for insert mode completion + if opt.lspOptions.autoComplete + if lspserver.completionLazyDoc + setbufvar(bnr, '&completeopt', 'menuone,popuphidden,noinsert,noselect') + else + setbufvar(bnr, '&completeopt', 'menuone,popup,noinsert,noselect') + endif + setbufvar(bnr, '&completepopup', + 'width:80,highlight:Pmenu,align:item,border:off') + # <Enter> in insert mode stops completion and inserts a <Enter> + if !opt.lspOptions.noNewlineInCompletion + :inoremap <expr> <buffer> <CR> pumvisible() ? "\<C-Y>\<CR>" : "\<CR>" + endif + + # Trigger 24x7 insert mode completion when text is changed + acmds->add({bufnr: bnr, + event: 'TextChangedI', + group: 'LSPBufferAutocmds', + cmd: 'LspComplete()'}) + endif + + if LspOmniComplEnabled(ftype) + setbufvar(bnr, '&omnifunc', 'g:LspOmniFunc') + endif + + if lspserver.completionLazyDoc + # resolve additional documentation for a selected item + acmds->add({bufnr: bnr, + event: 'CompleteChanged', + group: 'LSPBufferAutocmds', + cmd: 'LspResolve()'}) + endif + + acmds->add({bufnr: bnr, + event: 'CompleteChanged', + group: 'LSPBufferAutocmds', + cmd: 'LspSetPopupFileType()'}) + + # Execute LSP server initiated text edits after completion + acmds->add({bufnr: bnr, + event: 'CompleteDone', + group: 'LSPBufferAutocmds', + cmd: $'LspCompleteDone({bnr})'}) + + autocmd_add(acmds) +enddef + +# Buffer "bnr" is loaded in a window. If omni-completion is enabled for this +# buffer, then set the 'omnifunc' option. +export def BufferLoadedInWin(bnr: number) + if !opt.lspOptions.autoComplete + && LspOmniComplEnabled(bnr->getbufvar('&filetype')) + setbufvar(bnr, '&omnifunc', 'g:LspOmniFunc') + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/diag.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/diag.vim new file mode 100644 index 0000000..6975edf --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/diag.vim @@ -0,0 +1,920 @@ +vim9script + +# Functions related to handling LSP diagnostics. + +import './options.vim' as opt +import './buffer.vim' as buf +import './util.vim' + +# [bnr] = { +# serverDiagnostics: { +# lspServer1Id: [diag, diag, diag] +# lspServer2Id: [diag, diag, diag] +# }, +# serverDiagnosticsByLnum: { +# lspServer1Id: { [lnum]: [diag, diag diag] }, +# lspServer2Id: { [lnum]: [diag, diag diag] }, +# }, +# sortedDiagnostics: [lspServer1.diags, ...lspServer2.diags]->sort() +# } +var diagsMap: dict<dict<any>> = {} + +# Initialize the signs and the text property type used for diagnostics. +export def InitOnce() + # Signs and their highlight groups used for LSP diagnostics + hlset([ + {name: 'LspDiagLine', default: true, linksto: 'NONE'}, + {name: 'LspDiagSignErrorText', default: true, linksto: 'ErrorMsg'}, + {name: 'LspDiagSignWarningText', default: true, linksto: 'Search'}, + {name: 'LspDiagSignInfoText', default: true, linksto: 'Pmenu'}, + {name: 'LspDiagSignHintText', default: true, linksto: 'Question'} + ]) + sign_define([ + { + name: 'LspDiagError', + text: opt.lspOptions.diagSignErrorText, + texthl: 'LspDiagSignErrorText', + linehl: 'LspDiagLine' + }, + { + name: 'LspDiagWarning', + text: opt.lspOptions.diagSignWarningText, + texthl: 'LspDiagSignWarningText', + linehl: 'LspDiagLine' + }, + { + name: 'LspDiagInfo', + text: opt.lspOptions.diagSignInfoText, + texthl: 'LspDiagSignInfoText', + linehl: 'LspDiagLine' + }, + { + name: 'LspDiagHint', + text: opt.lspOptions.diagSignHintText, + texthl: 'LspDiagSignHintText', + linehl: 'LspDiagLine' + } + ]) + + # Diag inline highlight groups and text property types + hlset([ + {name: 'LspDiagInlineError', default: true, linksto: 'SpellBad'}, + {name: 'LspDiagInlineWarning', default: true, linksto: 'SpellCap'}, + {name: 'LspDiagInlineInfo', default: true, linksto: 'SpellRare'}, + {name: 'LspDiagInlineHint', default: true, linksto: 'SpellLocal'} + ]) + + var override = &cursorline + && &cursorlineopt =~ '\<line\>\|\<screenline\>\|\<both\>' + + prop_type_add('LspDiagInlineError', + {highlight: 'LspDiagInlineError', + priority: 10, + override: override}) + prop_type_add('LspDiagInlineWarning', + {highlight: 'LspDiagInlineWarning', + priority: 9, + override: override}) + prop_type_add('LspDiagInlineInfo', + {highlight: 'LspDiagInlineInfo', + priority: 8, + override: override}) + prop_type_add('LspDiagInlineHint', + {highlight: 'LspDiagInlineHint', + priority: 7, + override: override}) + + # Diag virtual text highlight groups and text property types + hlset([ + {name: 'LspDiagVirtualTextError', default: true, linksto: 'SpellBad'}, + {name: 'LspDiagVirtualTextWarning', default: true, linksto: 'SpellCap'}, + {name: 'LspDiagVirtualTextInfo', default: true, linksto: 'SpellRare'}, + {name: 'LspDiagVirtualTextHint', default: true, linksto: 'SpellLocal'}, + ]) + prop_type_add('LspDiagVirtualTextError', + {highlight: 'LspDiagVirtualTextError', override: true}) + prop_type_add('LspDiagVirtualTextWarning', + {highlight: 'LspDiagVirtualTextWarning', override: true}) + prop_type_add('LspDiagVirtualTextInfo', + {highlight: 'LspDiagVirtualTextInfo', override: true}) + prop_type_add('LspDiagVirtualTextHint', + {highlight: 'LspDiagVirtualTextHint', override: true}) + + autocmd_add([{group: 'LspCmds', + event: 'User', + pattern: 'LspOptionsChanged', + cmd: 'LspDiagsOptionsChanged()'}]) + + # ALE plugin support + if opt.lspOptions.aleSupport + opt.lspOptions.autoHighlightDiags = false + autocmd_add([ + { + group: 'LspAleCmds', + event: 'User', + pattern: 'ALEWantResults', + cmd: 'AleHook(g:ale_want_results_buffer)' + } + ]) + endif +enddef + +# Initialize the diagnostics features for the buffer 'bnr' +export def BufferInit(lspserver: dict<any>, bnr: number) + if opt.lspOptions.showDiagInBalloon + :set ballooneval balloonevalterm + setbufvar(bnr, '&balloonexpr', 'g:LspDiagExpr()') + endif + + var acmds: list<dict<any>> = [] + # Show diagnostics on the status line + if opt.lspOptions.showDiagOnStatusLine + acmds->add({bufnr: bnr, + event: 'CursorMoved', + group: 'LSPBufferAutocmds', + cmd: 'ShowCurrentDiagInStatusLine()'}) + endif + autocmd_add(acmds) +enddef + +# Function to sort the diagnostics in ascending order based on the line and +# character offset +def DiagsSortFunc(a: dict<any>, b: dict<any>): number + var a_start: dict<number> = a.range.start + var b_start: dict<number> = b.range.start + var linediff: number = a_start.line - b_start.line + if linediff == 0 + return a_start.character - b_start.character + endif + return linediff +enddef + +# Sort diagnostics ascending based on line and character offset +def SortDiags(diags: list<dict<any>>): list<dict<any>> + return diags->sort(DiagsSortFunc) +enddef + +# Remove the diagnostics stored for buffer "bnr" +export def DiagRemoveFile(bnr: number) + if diagsMap->has_key(bnr) + diagsMap->remove(bnr) + endif +enddef + +def DiagSevToSignName(severity: number): string + var typeMap: list<string> = ['LspDiagError', 'LspDiagWarning', + 'LspDiagInfo', 'LspDiagHint'] + if severity > 4 + return 'LspDiagHint' + endif + return typeMap[severity - 1] +enddef + +def DiagSevToInlineHLName(severity: number): string + var typeMap: list<string> = [ + 'LspDiagInlineError', + 'LspDiagInlineWarning', + 'LspDiagInlineInfo', + 'LspDiagInlineHint' + ] + if severity > 4 + return 'LspDiagInlineHint' + endif + return typeMap[severity - 1] +enddef + +def DiagSevToVirtualTextHLName(severity: number): string + var typeMap: list<string> = [ + 'LspDiagVirtualTextError', + 'LspDiagVirtualTextWarning', + 'LspDiagVirtualTextInfo', + 'LspDiagVirtualTextHint' + ] + if severity > 4 + return 'LspDiagVirtualTextHint' + endif + return typeMap[severity - 1] +enddef + +def DiagSevToSymbolText(severity: number): string + var lspOpts = opt.lspOptions + var typeMap: list<string> = [ + lspOpts.diagSignErrorText, + lspOpts.diagSignWarningText, + lspOpts.diagSignInfoText, + lspOpts.diagSignHintText + ] + if severity > 4 + return lspOpts.diagSignHintText + endif + return typeMap[severity - 1] +enddef + +# Remove signs and text properties for diagnostics in buffer +def RemoveDiagVisualsForBuffer(bnr: number, all: bool = false) + var lspOpts = opt.lspOptions + if lspOpts.showDiagWithSign || all + # Remove all the existing diagnostic signs + sign_unplace('LSPDiag', {buffer: bnr}) + endif + + if lspOpts.showDiagWithVirtualText || all + # Remove all the existing virtual text + prop_remove({type: 'LspDiagVirtualTextError', bufnr: bnr, all: true}) + prop_remove({type: 'LspDiagVirtualTextWarning', bufnr: bnr, all: true}) + prop_remove({type: 'LspDiagVirtualTextInfo', bufnr: bnr, all: true}) + prop_remove({type: 'LspDiagVirtualTextHint', bufnr: bnr, all: true}) + endif + + if lspOpts.highlightDiagInline || all + # Remove all the existing virtual text + prop_remove({type: 'LspDiagInlineError', bufnr: bnr, all: true}) + prop_remove({type: 'LspDiagInlineWarning', bufnr: bnr, all: true}) + prop_remove({type: 'LspDiagInlineInfo', bufnr: bnr, all: true}) + prop_remove({type: 'LspDiagInlineHint', bufnr: bnr, all: true}) + endif +enddef + +# Refresh the placed diagnostics in buffer "bnr" +# This inline signs, inline props, and virtual text diagnostics +export def DiagsRefresh(bnr: number, all: bool = false) + var lspOpts = opt.lspOptions + if !lspOpts.autoHighlightDiags + return + endif + + :silent! bnr->bufload() + + RemoveDiagVisualsForBuffer(bnr, all) + + if !diagsMap->has_key(bnr) || + diagsMap[bnr].sortedDiagnostics->empty() + return + endif + + # Initialize default/fallback properties for diagnostic virtual text: + var diag_align: string = 'above' + var diag_wrap: string = 'truncate' + var diag_symbol: string = '┌─' + + if lspOpts.diagVirtualTextAlign == 'below' + diag_align = 'below' + diag_wrap = 'truncate' + diag_symbol = '└─' + elseif lspOpts.diagVirtualTextAlign == 'after' + diag_align = 'after' + diag_wrap = 'wrap' + diag_symbol = 'E>' + endif + + if lspOpts.diagVirtualTextWrap != 'default' + diag_wrap = lspOpts.diagVirtualTextWrap + endif + + var signs: list<dict<any>> = [] + var diags: list<dict<any>> = diagsMap[bnr].sortedDiagnostics + var inlineHLprops: list<list<list<number>>> = [[], [], [], [], []] + for diag in diags + # TODO: prioritize most important severity if there are multiple + # diagnostics from the same line + var d_range = diag.range + var d_start = d_range.start + var d_end = d_range.end + var lnum = d_start.line + 1 + if lspOpts.showDiagWithSign + signs->add({id: 0, buffer: bnr, group: 'LSPDiag', + lnum: lnum, name: DiagSevToSignName(diag.severity), + priority: 10 - diag.severity}) + endif + + try + if lspOpts.highlightDiagInline + var propLocation: list<number> = [ + lnum, util.GetLineByteFromPos(bnr, d_start) + 1, + d_end.line + 1, util.GetLineByteFromPos(bnr, d_end) + 1 + ] + inlineHLprops[diag.severity]->add(propLocation) + endif + + if lspOpts.showDiagWithVirtualText + var padding: number + var symbol: string = diag_symbol + + if diag_align == 'after' + padding = 3 + symbol = DiagSevToSymbolText(diag.severity) + else + var charIdx = util.GetCharIdxWithoutCompChar(bnr, d_start) + padding = charIdx + if padding > 0 + padding = strdisplaywidth(getline(lnum)[ : charIdx - 1]) + endif + endif + + prop_add(lnum, 0, {bufnr: bnr, + type: DiagSevToVirtualTextHLName(diag.severity), + text: $'{symbol} {diag.message}', + text_align: diag_align, + text_wrap: diag_wrap, + text_padding_left: padding}) + endif + catch /E966\|E964/ # Invalid lnum | Invalid col + # Diagnostics arrive asynchronously and the document changed while they + # were in transit. Ignore this as new once will arrive shortly. + endtry + endfor + + if lspOpts.highlightDiagInline + for i in range(1, 4) + if !inlineHLprops[i]->empty() + try + prop_add_list({bufnr: bnr, type: DiagSevToInlineHLName(i)}, + inlineHLprops[i]) + catch /E966\|E964/ # Invalid lnum | Invalid col + endtry + endif + endfor + endif + + if lspOpts.showDiagWithSign + signs->sign_placelist() + endif +enddef + +# Sends diagnostics to Ale +def SendAleDiags(bnr: number, timerid: number) + if !diagsMap->has_key(bnr) + return + endif + + # Convert to Ale's diagnostics format (:h ale-loclist-format) + ale#other_source#ShowResults(bnr, 'lsp', + diagsMap[bnr].sortedDiagnostics->mapnew((_, v) => { + return {text: v.message, + lnum: v.range.start.line + 1, + col: util.GetLineByteFromPos(bnr, v.range.start) + 1, + end_lnum: v.range.end.line + 1, + end_col: util.GetLineByteFromPos(bnr, v.range.end) + 1, + type: "EWIH"[v.severity - 1]} + }) + ) +enddef + +# Hook called when Ale wants to retrieve new diagnostics +def AleHook(bnr: number) + ale#other_source#StartChecking(bnr, 'lsp') + timer_start(0, function('SendAleDiags', [bnr])) +enddef + +# New LSP diagnostic messages received from the server for a file. +# Update the signs placed in the buffer for this file +export def ProcessNewDiags(bnr: number) + DiagsUpdateLocList(bnr) + + var lspOpts = opt.lspOptions + if lspOpts.aleSupport + SendAleDiags(bnr, -1) + endif + + if bnr == -1 || !diagsMap->has_key(bnr) + return + endif + + var curmode: string = mode() + if curmode == 'i' || curmode == 'R' || curmode == 'Rv' + # postpone placing signs in insert mode and replace mode. These will be + # placed after the user returns to Normal mode. + setbufvar(bnr, 'LspDiagsUpdatePending', true) + return + endif + + DiagsRefresh(bnr) +enddef + +# process a diagnostic notification message from the LSP server +# Notification: textDocument/publishDiagnostics +# Param: PublishDiagnosticsParams +export def DiagNotification(lspserver: dict<any>, uri: string, diags_arg: list<dict<any>>): void + # Diagnostics are disabled for this server? + if !lspserver.featureEnabled('diagnostics') + return + endif + + var fname: string = util.LspUriToFile(uri) + var bnr: number = fname->bufnr() + if bnr == -1 + # Is this condition possible? + return + endif + + var newDiags: list<dict<any>> = diags_arg + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the diags + newDiags->map((_, dval) => { + lspserver.decodeRange(bnr, dval.range) + return dval + }) + endif + + if lspserver.processDiagHandler != null_function + newDiags = lspserver.processDiagHandler(diags_arg) + endif + + # TODO: Is the buffer (bnr) always a loaded buffer? Should we load it here? + var lastlnum: number = bnr->getbufinfo()[0].linecount + + # store the diagnostic for each line separately + var diagsByLnum: dict<list<dict<any>>> = {} + + var diagWithinRange: list<dict<any>> = [] + for diag in newDiags + var d_start = diag.range.start + if d_start.line + 1 > lastlnum + # Make sure the line number is a valid buffer line number + d_start.line = lastlnum - 1 + endif + + var lnum = d_start.line + 1 + if !diagsByLnum->has_key(lnum) + diagsByLnum[lnum] = [] + endif + diagsByLnum[lnum]->add(diag) + + diagWithinRange->add(diag) + endfor + + var serverDiags: dict<list<any>> = diagsMap->has_key(bnr) ? + diagsMap[bnr].serverDiagnostics : {} + serverDiags[lspserver.id] = diagWithinRange + + var serverDiagsByLnum: dict<dict<list<any>>> = diagsMap->has_key(bnr) ? + diagsMap[bnr].serverDiagnosticsByLnum : {} + serverDiagsByLnum[lspserver.id] = diagsByLnum + + # store the diagnostic for each line separately + var joinedServerDiags: list<dict<any>> = [] + for diags in serverDiags->values() + joinedServerDiags->extend(diags) + endfor + + var sortedDiags = SortDiags(joinedServerDiags) + + diagsMap[bnr] = { + sortedDiagnostics: sortedDiags, + serverDiagnosticsByLnum: serverDiagsByLnum, + serverDiagnostics: serverDiags + } + + ProcessNewDiags(bnr) + + # Notify user scripts that diags has been updated + if exists('#User#LspDiagsUpdated') + :doautocmd <nomodeline> User LspDiagsUpdated + endif +enddef + +# get the count of error in the current buffer +export def DiagsGetErrorCount(bnr: number): dict<number> + var diagSevCount: list<number> = [0, 0, 0, 0, 0] + if diagsMap->has_key(bnr) + var diags = diagsMap[bnr].sortedDiagnostics + for diag in diags + var severity = diag->get('severity', 0) + diagSevCount[severity] += 1 + endfor + endif + + return { + Error: diagSevCount[1], + Warn: diagSevCount[2], + Info: diagSevCount[3], + Hint: diagSevCount[4] + } +enddef + +# Map the LSP DiagnosticSeverity to a quickfix type character +def DiagSevToQfType(severity: number): string + var typeMap: list<string> = ['E', 'W', 'I', 'N'] + + if severity > 4 + return '' + endif + + return typeMap[severity - 1] +enddef + +# Update the location list window for the current window with the diagnostic +# messages. +# Returns true if diagnostics is not empty and false if it is empty. +def DiagsUpdateLocList(bnr: number, calledByCmd: bool = false): bool + var fname: string = bnr->bufname()->fnamemodify(':p') + if fname->empty() + return false + endif + + var LspQfId: number = bnr->getbufvar('LspQfId', 0) + if LspQfId == 0 && !opt.lspOptions.autoPopulateDiags && !calledByCmd + # Diags location list is not present. Create the location list only if + # the 'autoPopulateDiags' option is set or the ":LspDiag show" command is + # invoked. + return false + endif + + if LspQfId != 0 && getloclist(0, {id: LspQfId}).id != LspQfId + # Previously used location list for the diagnostics is gone + LspQfId = 0 + endif + + if !diagsMap->has_key(bnr) || + diagsMap[bnr].sortedDiagnostics->empty() + if LspQfId != 0 + setloclist(0, [], 'r', {id: LspQfId, items: []}) + endif + return false + endif + + var qflist: list<dict<any>> = [] + var text: string + + var diags = diagsMap[bnr].sortedDiagnostics + for diag in diags + var d_range = diag.range + var d_start = d_range.start + var d_end = d_range.end + text = diag.message->substitute("\n\\+", "\n", 'g') + qflist->add({filename: fname, + lnum: d_start.line + 1, + col: util.GetLineByteFromPos(bnr, d_start) + 1, + end_lnum: d_end.line + 1, + end_col: util.GetLineByteFromPos(bnr, d_end) + 1, + text: text, + type: DiagSevToQfType(diag.severity)}) + endfor + + var op: string = ' ' + var props = {title: 'Language Server Diagnostics', items: qflist} + if LspQfId != 0 + op = 'r' + props.id = LspQfId + endif + setloclist(0, [], op, props) + if LspQfId == 0 + setbufvar(bnr, 'LspQfId', getloclist(0, {id: 0}).id) + endif + + return true +enddef + +# Display the diagnostic messages from the LSP server for the current buffer +# in a location list +export def ShowAllDiags(): void + var bnr: number = bufnr() + if !DiagsUpdateLocList(bnr, true) + util.WarnMsg($'No diagnostic messages found for {@%}') + return + endif + + var save_winid = win_getid() + # make the diagnostics error list the active one and open it + var LspQfId: number = bnr->getbufvar('LspQfId', 0) + var LspQfNr: number = getloclist(0, {id: LspQfId, nr: 0}).nr + exe $':{LspQfNr} lhistory' + :lopen + if !opt.lspOptions.keepFocusInDiags + save_winid->win_gotoid() + endif +enddef + +# Display the message of "diag" in a popup window right below the position in +# the diagnostic message. +def ShowDiagInPopup(diag: dict<any>) + var d_start = diag.range.start + var dlnum = d_start.line + 1 + var ltext = dlnum->getline() + var dlcol = ltext->byteidxcomp(d_start.character) + 1 + + var lastline = line('$') + if dlnum > lastline + # The line number is outside the last line in the file. + dlnum = lastline + endif + if dlcol < 1 + # The column is outside the last character in line. + dlcol = ltext->len() + 1 + endif + var d = screenpos(0, dlnum, dlcol) + if d->empty() + # If the diag position cannot be converted to Vim lnum/col, then use + # the current cursor position + d = {row: line('.'), col: col('.')} + endif + + # Display a popup right below the diagnostics position + var msg = diag.message->split("\n") + var msglen = msg->reduce((acc, val) => max([acc, val->strcharlen()]), 0) + + var ppopts = {} + ppopts.pos = 'topleft' + ppopts.line = d.row + 1 + ppopts.moved = 'any' + + if msglen > &columns + ppopts.wrap = true + ppopts.col = 1 + else + ppopts.wrap = false + ppopts.col = d.col + endif + + popup_create(msg, ppopts) +enddef + +# Display the "diag" message in a popup or in the status message area +def DisplayDiag(diag: dict<any>) + if opt.lspOptions.showDiagInPopup + # Display the diagnostic message in a popup window. + ShowDiagInPopup(diag) + else + # Display the diagnostic message in the status message area + :echo diag.message + endif +enddef + +# Show the diagnostic message for the current line +export def ShowCurrentDiag(atPos: bool) + var bnr: number = bufnr() + var lnum: number = line('.') + var col: number = charcol('.') + var diag: dict<any> = GetDiagByPos(bnr, lnum, col, atPos) + if diag->empty() + util.WarnMsg($'No diagnostic messages found for current {atPos ? "position" : "line"}') + else + DisplayDiag(diag) + endif +enddef + +# Show the diagnostic message for the current line without linebreak +def ShowCurrentDiagInStatusLine() + var bnr: number = bufnr() + var lnum: number = line('.') + var col: number = charcol('.') + var diag: dict<any> = GetDiagByPos(bnr, lnum, col) + if !diag->empty() + # 15 is a enough length not to cause line break + var max_width = &columns - 15 + var code = '' + if diag->has_key('code') + code = $'[{diag.code}] ' + endif + var msgNoLineBreak = code .. + diag.message->substitute("\n", ' ', '')->substitute("\\n", ' ', '') + :echo msgNoLineBreak[ : max_width] + else + # clear the previous message + :echo '' + endif +enddef + +# Get the diagnostic from the LSP server for a particular line and character +# offset in a file +export def GetDiagByPos(bnr: number, lnum: number, col: number, + atPos: bool = false): dict<any> + var diags_in_line = GetDiagsByLine(bnr, lnum) + + for diag in diags_in_line + var r = diag.range + var startCharIdx = util.GetCharIdxWithoutCompChar(bnr, r.start) + var endCharIdx = util.GetCharIdxWithoutCompChar(bnr, r.end) + if atPos + if col >= startCharIdx + 1 && col < endCharIdx + 1 + return diag + endif + elseif col <= startCharIdx + 1 + return diag + endif + endfor + + # No diagnostic to the right of the position, return the last one instead + if !atPos && diags_in_line->len() > 0 + return diags_in_line[-1] + endif + + return {} +enddef + +# Get all diagnostics from the LSP server for a particular line in a file +export def GetDiagsByLine(bnr: number, lnum: number, lspserver: dict<any> = null_dict): list<dict<any>> + if !diagsMap->has_key(bnr) + return [] + endif + + var diags: list<dict<any>> = [] + + var serverDiagsByLnum = diagsMap[bnr].serverDiagnosticsByLnum + + if lspserver == null_dict + for diagsByLnum in serverDiagsByLnum->values() + if diagsByLnum->has_key(lnum) + diags->extend(diagsByLnum[lnum]) + endif + endfor + else + if !serverDiagsByLnum->has_key(lspserver.id) + return [] + endif + if serverDiagsByLnum[lspserver.id]->has_key(lnum) + diags = serverDiagsByLnum[lspserver.id][lnum] + endif + endif + + return diags->sort((a, b) => { + return a.range.start.character - b.range.start.character + }) +enddef + +# Utility function to do the actual jump +def JumpDiag(diag: dict<any>) + var startPos: dict<number> = diag.range.start + setcursorcharpos(startPos.line + 1, + util.GetCharIdxWithoutCompChar(bufnr(), startPos) + 1) + :normal! zv + if !opt.lspOptions.showDiagWithVirtualText + :redraw + DisplayDiag(diag) + endif +enddef + +# jump to the next/previous/first diagnostic message in the current buffer +export def LspDiagsJump(which: string, a_count: number = 0): void + var fname: string = expand('%:p') + if fname->empty() + return + endif + var bnr: number = bufnr() + + if !diagsMap->has_key(bnr) || + diagsMap[bnr].sortedDiagnostics->empty() + util.WarnMsg($'No diagnostic messages found for {fname}') + return + endif + + var diags = diagsMap[bnr].sortedDiagnostics + + if which == 'first' + JumpDiag(diags[0]) + return + endif + + if which == 'last' + JumpDiag(diags[-1]) + return + endif + + # Find the entry just before the current line (binary search) + var count = a_count > 1 ? a_count : 1 + var curlnum: number = line('.') + var curcol: number = charcol('.') + for diag in (which == 'next' || which == 'nextWrap' || which == 'here') ? + diags : diags->copy()->reverse() + var d_start = diag.range.start + var lnum = d_start.line + 1 + var col = util.GetCharIdxWithoutCompChar(bnr, d_start) + 1 + if ((which == 'next' || which == 'nextWrap') && (lnum > curlnum || lnum == curlnum && col > curcol)) + || ((which == 'prev' || which == 'prevWrap') && (lnum < curlnum || lnum == curlnum + && col < curcol)) + || (which == 'here' && (lnum == curlnum && col >= curcol)) + + # Skip over as many diags as "count" dictates + count = count - 1 + if count > 0 + continue + endif + + JumpDiag(diag) + return + endif + endfor + + # If [count] exceeded the remaining diags + if ((which == 'next' || which == 'nextWrap') && a_count > 1 && a_count != count) + JumpDiag(diags[-1]) + return + endif + + # If [count] exceeded the previous diags + if ((which == 'prev' || which == 'prevWrap') && a_count > 1 && a_count != count) + JumpDiag(diags[0]) + return + endif + + if which == 'nextWrap' || which == 'prevWrap' + JumpDiag(diags[which == 'nextWrap' ? 0 : -1]) + return + endif + + if which == 'here' + util.WarnMsg('No more diagnostics found on this line') + else + util.WarnMsg('No more diagnostics found') + endif +enddef + +# Return the sorted diagnostics for buffer "bnr". Default is the current +# buffer. A copy of the diagnostics is returned so that the caller can modify +# the diagnostics. +export def GetDiagsForBuf(bnr: number = bufnr()): list<dict<any>> + if !diagsMap->has_key(bnr) || + diagsMap[bnr].sortedDiagnostics->empty() + return [] + endif + + return diagsMap[bnr].sortedDiagnostics->deepcopy() +enddef + +# Return the diagnostic text from the LSP server for the current mouse line to +# display in a balloon +def g:LspDiagExpr(): any + if !opt.lspOptions.showDiagInBalloon + return '' + endif + + var diagsInfo: list<dict<any>> = + GetDiagsByLine(v:beval_bufnr, v:beval_lnum) + if diagsInfo->empty() + # No diagnostic for the current cursor location + return '' + endif + var diagFound: dict<any> = {} + for diag in diagsInfo + var r = diag.range + var startcol = util.GetLineByteFromPos(v:beval_bufnr, r.start) + 1 + var endcol = util.GetLineByteFromPos(v:beval_bufnr, r.end) + 1 + if v:beval_col >= startcol && v:beval_col < endcol + diagFound = diag + break + endif + endfor + if diagFound->empty() + # mouse is outside of the diagnostics range + return '' + endif + + # return the found diagnostic + return diagFound.message->split("\n") +enddef + +# Track the current diagnostics auto highlight enabled/disabled state. Used +# when the "autoHighlightDiags" option value is changed. +var save_autoHighlightDiags = opt.lspOptions.autoHighlightDiags +var save_highlightDiagInline = opt.lspOptions.highlightDiagInline +var save_showDiagWithSign = opt.lspOptions.showDiagWithSign +var save_showDiagWithVirtualText = opt.lspOptions.showDiagWithVirtualText + +# Enable the LSP diagnostics highlighting +export def DiagsHighlightEnable() + opt.lspOptions.autoHighlightDiags = true + save_autoHighlightDiags = true + for binfo in getbufinfo({bufloaded: true}) + if diagsMap->has_key(binfo.bufnr) + DiagsRefresh(binfo.bufnr) + endif + endfor +enddef + +# Disable the LSP diagnostics highlighting in all the buffers +export def DiagsHighlightDisable() + # turn off all diags highlight + opt.lspOptions.autoHighlightDiags = false + save_autoHighlightDiags = false + for binfo in getbufinfo() + if diagsMap->has_key(binfo.bufnr) + RemoveDiagVisualsForBuffer(binfo.bufnr) + endif + endfor +enddef + +# Some options are changed. If 'autoHighlightDiags' option is changed, then +# either enable or disable diags auto highlight. +export def LspDiagsOptionsChanged() + if save_autoHighlightDiags && !opt.lspOptions.autoHighlightDiags + DiagsHighlightDisable() + elseif !save_autoHighlightDiags && opt.lspOptions.autoHighlightDiags + DiagsHighlightEnable() + endif + + if save_highlightDiagInline != opt.lspOptions.highlightDiagInline + || save_showDiagWithSign != opt.lspOptions.showDiagWithSign + || save_showDiagWithVirtualText != opt.lspOptions.showDiagWithVirtualText + save_highlightDiagInline = opt.lspOptions.highlightDiagInline + save_showDiagWithSign = opt.lspOptions.showDiagWithSign + save_showDiagWithVirtualText = opt.lspOptions.showDiagWithVirtualText + for binfo in getbufinfo({bufloaded: true}) + if diagsMap->has_key(binfo.bufnr) + DiagsRefresh(binfo.bufnr, true) + endif + endfor + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/handlers.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/handlers.vim new file mode 100644 index 0000000..dd597ed --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/handlers.vim @@ -0,0 +1,305 @@ +vim9script + +# Handlers for messages from the LSP server +# Refer to https://microsoft.github.io/language-server-protocol/specification +# for the Language Server Protocol (LSP) specification. + +import './util.vim' +import './diag.vim' +import './textedit.vim' + +# Process various reply messages from the LSP server +export def ProcessReply(lspserver: dict<any>, req: dict<any>, reply: dict<any>): void + util.ErrMsg($'Unsupported reply received from LSP server: {reply->string()} for request: {req->string()}') +enddef + +# process a diagnostic notification message from the LSP server +# Notification: textDocument/publishDiagnostics +# Param: PublishDiagnosticsParams +def ProcessDiagNotif(lspserver: dict<any>, reply: dict<any>): void + var params = reply.params + diag.DiagNotification(lspserver, params.uri, params.diagnostics) +enddef + +# Convert LSP message type to a string +def LspMsgTypeToString(lspMsgType: number): string + var msgStrMap: list<string> = ['', 'Error', 'Warning', 'Info', 'Log'] + var mtype: string = 'Log' + if lspMsgType > 0 && lspMsgType < 5 + mtype = msgStrMap[lspMsgType] + endif + return mtype +enddef + +# process a show notification message from the LSP server +# Notification: window/showMessage +# Param: ShowMessageParams +def ProcessShowMsgNotif(lspserver: dict<any>, reply: dict<any>) + var msgType = reply.params.type + if msgType >= 4 + # ignore log messages from the LSP server (too chatty) + # TODO: Add a configuration to control the message level that will be + # displayed. Also store these messages and provide a command to display + # them. + return + endif + if msgType == 1 + util.ErrMsg($'Lsp({lspserver.name}) {reply.params.message}') + elseif msgType == 2 + util.WarnMsg($'Lsp({lspserver.name}) {reply.params.message}') + elseif msgType == 3 + util.InfoMsg($'Lsp({lspserver.name}) {reply.params.message}') + endif +enddef + +# process a log notification message from the LSP server +# Notification: window/logMessage +# Param: LogMessageParams +def ProcessLogMsgNotif(lspserver: dict<any>, reply: dict<any>) + var params = reply.params + var mtype = LspMsgTypeToString(params.type) + lspserver.addMessage(mtype, params.message) +enddef + +# process the log trace notification messages +# Notification: $/logTrace +# Param: LogTraceParams +def ProcessLogTraceNotif(lspserver: dict<any>, reply: dict<any>) + lspserver.addMessage('trace', reply.params.message) +enddef + +# process unsupported notification messages +def ProcessUnsupportedNotif(lspserver: dict<any>, reply: dict<any>) + util.WarnMsg($'Unsupported notification message received from the LSP server ({lspserver.name}), message = {reply->string()}') +enddef + +# Dict to process telemetry notification messages only once per filetype +var telemetryProcessed: dict<bool> = {} +# process unsupported notification messages only once +def ProcessUnsupportedNotifOnce(lspserver: dict<any>, reply: dict<any>) + if !telemetryProcessed->get(&ft, false) + ProcessUnsupportedNotif(lspserver, reply) + telemetryProcessed->extend({[&ft]: true}) + endif +enddef + +# process notification messages from the LSP server +export def ProcessNotif(lspserver: dict<any>, reply: dict<any>): void + var lsp_notif_handlers: dict<func> = + { + 'window/showMessage': ProcessShowMsgNotif, + 'window/logMessage': ProcessLogMsgNotif, + 'textDocument/publishDiagnostics': ProcessDiagNotif, + '$/logTrace': ProcessLogTraceNotif, + 'telemetry/event': ProcessUnsupportedNotifOnce, + } + + # Explicitly ignored notification messages (many of them are specific to a + # particular language server) + var lsp_ignored_notif_handlers: list<string> = + [ + '$/progress', + '$/status/report', + '$/status/show', + # PHP intelephense server sends the "indexingStarted" and + # "indexingEnded" notifications which is not in the LSP specification. + 'indexingStarted', + 'indexingEnded', + # Java language server sends the 'language/status' notification which is + # not in the LSP specification. + 'language/status', + # Typescript language server sends the '$/typescriptVersion' + # notification which is not in the LSP specification. + '$/typescriptVersion', + # Dart language server sends the '$/analyzerStatus' notification which + # is not in the LSP specification. + '$/analyzerStatus', + # pyright language server notifications + 'pyright/beginProgress', + 'pyright/reportProgress', + 'pyright/endProgress', + 'eslint/status', + 'taplo/didChangeSchemaAssociation', + 'sqlLanguageServer.finishSetup', + # ccls language server notifications + '$ccls/publishSkippedRanges', + '$ccls/publishSemanticHighlight', + # omnisharp language server notifications + 'o#/backgrounddiagnosticstatus', + 'o#/msbuildprojectdiagnostics', + 'o#/projectadded', + 'o#/projectchanged', + 'o#/projectconfiguration', + 'o#/projectdiagnosticstatus', + 'o#/unresolveddependencies', + '@/tailwindCSS/projectInitialized' + ] + + if lsp_notif_handlers->has_key(reply.method) + lsp_notif_handlers[reply.method](lspserver, reply) + elseif lspserver.customNotificationHandlers->has_key(reply.method) + lspserver.customNotificationHandlers[reply.method](lspserver, reply) + elseif lsp_ignored_notif_handlers->index(reply.method) == -1 + ProcessUnsupportedNotif(lspserver, reply) + endif +enddef + +# process the workspace/applyEdit LSP server request +# Request: "workspace/applyEdit" +# Param: ApplyWorkspaceEditParams +def ProcessApplyEditReq(lspserver: dict<any>, request: dict<any>) + # interface ApplyWorkspaceEditParams + if !request->has_key('params') + return + endif + var workspaceEditParams: dict<any> = request.params + if workspaceEditParams->has_key('label') + util.InfoMsg($'Workspace edit {workspaceEditParams.label}') + endif + textedit.ApplyWorkspaceEdit(workspaceEditParams.edit) + # TODO: Need to return the proper result of the edit operation + lspserver.sendResponse(request, {applied: true}, {}) +enddef + +# process the workspace/workspaceFolders LSP server request +# Request: "workspace/workspaceFolders" +# Param: none +def ProcessWorkspaceFoldersReq(lspserver: dict<any>, request: dict<any>) + if !lspserver->has_key('workspaceFolders') + lspserver.sendResponse(request, null, {}) + return + endif + if lspserver.workspaceFolders->empty() + lspserver.sendResponse(request, [], {}) + else + lspserver.sendResponse(request, + \ lspserver.workspaceFolders->copy()->map('{name: v:val->fnamemodify(":t"), uri: util.LspFileToUri(v:val)}'), + \ {}) + endif +enddef + +# process the workspace/configuration LSP server request +# Request: "workspace/configuration" +# Param: ConfigurationParams +def ProcessWorkspaceConfiguration(lspserver: dict<any>, request: dict<any>) + var items = request.params.items + var response = items->map((_, item) => lspserver.workspaceConfigGet(item)) + + # Server expect null value if no config is given + if response->type() == v:t_list && response->len() == 1 + && response[0]->type() == v:t_dict + && response[0] == null_dict + response[0] = null + endif + + lspserver.sendResponse(request, response, {}) +enddef + +# process the window/workDoneProgress/create LSP server request +# Request: "window/workDoneProgress/create" +# Param: none +def ProcessWorkDoneProgressCreate(lspserver: dict<any>, request: dict<any>) + lspserver.sendResponse(request, null, {}) +enddef + +# process the window/showMessageRequest LSP server request +# Request: "window/showMessageRequest" +# Param: ShowMessageRequestParams +def ProcessShowMessageRequest(lspserver: dict<any>, request: dict<any>) + # TODO: for now 'showMessageRequest' handled same like 'showMessage' + # regardless 'actions' + ProcessShowMsgNotif(lspserver, request) + lspserver.sendResponse(request, null, {}) +enddef + +# process the client/registerCapability LSP server request +# Request: "client/registerCapability" +# Param: RegistrationParams +def ProcessClientRegisterCap(lspserver: dict<any>, request: dict<any>) + lspserver.sendResponse(request, null, {}) +enddef + +# process the client/unregisterCapability LSP server request +# Request: "client/unregisterCapability" +# Param: UnregistrationParams +def ProcessClientUnregisterCap(lspserver: dict<any>, request: dict<any>) + lspserver.sendResponse(request, null, {}) +enddef + +# process a request message from the server +export def ProcessRequest(lspserver: dict<any>, request: dict<any>) + var lspRequestHandlers: dict<func> = + { + 'client/registerCapability': ProcessClientRegisterCap, + 'client/unregisterCapability': ProcessClientUnregisterCap, + 'window/workDoneProgress/create': ProcessWorkDoneProgressCreate, + 'window/showMessageRequest': ProcessShowMessageRequest, + 'workspace/applyEdit': ProcessApplyEditReq, + 'workspace/configuration': ProcessWorkspaceConfiguration, + 'workspace/workspaceFolders': ProcessWorkspaceFoldersReq + # TODO: Handle the following requests from the server: + # workspace/codeLens/refresh + # workspace/diagnostic/refresh + # workspace/inlayHint/refresh + # workspace/inlineValue/refresh + # workspace/semanticTokens/refresh + } + + # Explicitly ignored requests + var lspIgnoredRequestHandlers: list<string> = + [ + # Eclipse java language server sends the + # 'workspace/executeClientCommand' request (to reload bundles) which is + # not in the LSP specification. + 'workspace/executeClientCommand', + ] + + if lspRequestHandlers->has_key(request.method) + lspRequestHandlers[request.method](lspserver, request) + elseif lspserver.customRequestHandlers->has_key(request.method) + lspserver.customRequestHandlers[request.method](lspserver, request) + elseif lspIgnoredRequestHandlers->index(request.method) == -1 + util.ErrMsg($'Unsupported request message received from the LSP server ({lspserver.name}), message = {request->string()}') + endif +enddef + +# process one or more LSP server messages +export def ProcessMessages(lspserver: dict<any>): void + var idx: number + var len: number + var content: string + var msg: dict<any> + var req: dict<any> + + msg = lspserver.data + if msg->has_key('result') || msg->has_key('error') + # response message from the server + req = lspserver.requests->get(msg.id->string(), {}) + if !req->empty() + # Remove the corresponding stored request message + lspserver.requests->remove(msg.id->string()) + + if msg->has_key('result') + lspserver.processReply(req, msg) + else + # request failed + var emsg: string = msg.error.message + emsg ..= $', code = {msg.error.code}' + if msg.error->has_key('data') + emsg ..= $', data = {msg.error.data->string()}' + endif + util.ErrMsg($'request {req.method} failed ({emsg})') + endif + endif + elseif msg->has_key('id') && msg->has_key('method') + # request message from the server + lspserver.processRequest(msg) + elseif msg->has_key('method') + # notification message from the server + lspserver.processNotif(msg) + else + util.ErrMsg($'Unsupported message ({msg->string()})') + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/hover.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/hover.vim new file mode 100644 index 0000000..41e7f11 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/hover.vim @@ -0,0 +1,137 @@ +vim9script + +# Functions related to displaying hover symbol information. + +import './util.vim' +import './options.vim' as opt + +# Util used to compute the hoverText from textDocument/hover reply +def GetHoverText(lspserver: dict<any>, hoverResult: any): list<any> + if hoverResult->empty() + return ['', ''] + endif + + # MarkupContent + if hoverResult.contents->type() == v:t_dict + && hoverResult.contents->has_key('kind') + if hoverResult.contents.kind == 'plaintext' + return [hoverResult.contents.value->split("\n"), 'text'] + endif + + if hoverResult.contents.kind == 'markdown' + return [hoverResult.contents.value->split("\n"), 'lspgfm'] + endif + + lspserver.errorLog( + $'{strftime("%m/%d/%y %T")}: Unsupported hover contents kind ({hoverResult.contents.kind})' + ) + return ['', ''] + endif + + # MarkedString + if hoverResult.contents->type() == v:t_dict + && hoverResult.contents->has_key('value') + return [ + [$'``` {hoverResult.contents.language}'] + + hoverResult.contents.value->split("\n") + + ['```'], + 'lspgfm' + ] + endif + + # MarkedString + if hoverResult.contents->type() == v:t_string + return [hoverResult.contents->split("\n"), 'lspgfm'] + endif + + # interface MarkedString[] + if hoverResult.contents->type() == v:t_list + var hoverText: list<string> = [] + for e in hoverResult.contents + if !hoverText->empty() + hoverText->extend(['- - -']) + endif + + if e->type() == v:t_string + hoverText->extend(e->split("\n")) + else + hoverText->extend([$'``` {e.language}']) + hoverText->extend(e.value->split("\n")) + hoverText->extend(['```']) + endif + endfor + + return [hoverText, 'lspgfm'] + endif + + lspserver.errorLog( + $'{strftime("%m/%d/%y %T")}: Unsupported hover reply ({hoverResult})' + ) + return ['', ''] +enddef + +# Key filter function for the hover popup window. +# Only keys to scroll the popup window are supported. +def HoverWinFilterKey(hoverWin: number, key: string): bool + var keyHandled = false + + if key == "\<C-E>" + || key == "\<C-D>" + || key == "\<C-F>" + || key == "\<PageDown>" + || key == "\<C-Y>" + || key == "\<C-U>" + || key == "\<C-B>" + || key == "\<PageUp>" + || key == "\<C-Home>" + || key == "\<C-End>" + # scroll the hover popup window + win_execute(hoverWin, $'normal! {key}') + keyHandled = true + endif + + if key == "\<Esc>" + hoverWin->popup_close() + keyHandled = true + endif + + return keyHandled +enddef + +# process the 'textDocument/hover' reply from the LSP server +# Result: Hover | null +export def HoverReply(lspserver: dict<any>, hoverResult: any, cmdmods: string): void + var [hoverText, hoverKind] = GetHoverText(lspserver, hoverResult) + + # Nothing to show + if hoverText->empty() + if cmdmods !~ 'silent' + util.WarnMsg($'No documentation found for current keyword') + endif + return + endif + + if opt.lspOptions.hoverInPreview + execute $':silent! {cmdmods} pedit LspHover' + :wincmd P + :setlocal buftype=nofile + :setlocal bufhidden=delete + bufnr()->deletebufline(1, '$') + hoverText->append(0) + [1, 1]->cursor() + exe $'setlocal ft={hoverKind}' + :wincmd p + else + popup_clear() + var winid = hoverText->popup_atcursor({moved: 'any', + close: 'click', + fixed: true, + maxwidth: 80, + border: [0, 1, 0, 1], + borderchars: [' '], + filter: HoverWinFilterKey}) + win_execute(winid, $'setlocal ft={hoverKind}') + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/inlayhints.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/inlayhints.vim new file mode 100644 index 0000000..a793699 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/inlayhints.vim @@ -0,0 +1,235 @@ +vim9script + +# Functions for dealing with inlay hints + +import './util.vim' +import './buffer.vim' as buf +import './options.vim' as opt + +# Initialize the highlight group and the text property type used for +# inlay hints. +export def InitOnce() + hlset([ + {name: 'LspInlayHintsType', default: true, linksto: 'Label'}, + {name: 'LspInlayHintsParam', default: true, linksto: 'Conceal'} + ]) + prop_type_add('LspInlayHintsType', {highlight: 'LspInlayHintsType'}) + prop_type_add('LspInlayHintsParam', {highlight: 'LspInlayHintsParam'}) + + autocmd_add([{group: 'LspCmds', + event: 'User', + pattern: 'LspOptionsChanged', + cmd: 'LspInlayHintsOptionsChanged()'}]) +enddef + +# Clear all the inlay hints text properties in the current buffer +def InlayHintsClear(bnr: number) + prop_remove({type: 'LspInlayHintsType', bufnr: bnr, all: true}) + prop_remove({type: 'LspInlayHintsParam', bufnr: bnr, all: true}) +enddef + +# LSP inlay hints reply message handler +export def InlayHintsReply(lspserver: dict<any>, bnr: number, inlayHints: any) + if inlayHints->empty() + return + endif + + InlayHintsClear(bnr) + + if mode() != 'n' + # Update inlay hints only in normal mode + return + endif + + for hint in inlayHints + var label = '' + if hint.label->type() == v:t_list + label = hint.label->copy()->map((_, v) => v.value)->join('') + else + label = hint.label + endif + + # add a space before or after the label + var padLeft: bool = hint->get('paddingLeft', false) + var padRight: bool = hint->get('paddingRight', false) + if padLeft + label = $' {label}' + endif + if padRight + label = $'{label} ' + endif + + var kind = hint->has_key('kind') ? hint.kind->string() : '1' + try + lspserver.decodePosition(bnr, hint.position) + var byteIdx = util.GetLineByteFromPos(bnr, hint.position) + if kind == "'type'" || kind == '1' + prop_add(hint.position.line + 1, byteIdx + 1, + {type: 'LspInlayHintsType', text: label, bufnr: bnr}) + elseif kind == "'parameter'" || kind == '2' + prop_add(hint.position.line + 1, byteIdx + 1, + {type: 'LspInlayHintsParam', text: label, bufnr: bnr}) + endif + catch /E966\|E964/ # Invalid lnum | Invalid col + # Inlay hints replies arrive asynchronously and the document might have + # been modified in the mean time. As the reply is stale, ignore invalid + # line number and column number errors. + endtry + endfor +enddef + +# Timer callback to display the inlay hints. +def InlayHintsTimerCb(lspserver: dict<any>, bnr: number, timerid: number) + lspserver.inlayHintsShow(bnr) + setbufvar(bnr, 'LspInlayHintsNeedsUpdate', false) +enddef + +# Update all the inlay hints. A timer is used to throttle the updates. +def LspInlayHintsUpdate(bnr: number) + if !bnr->getbufvar('LspInlayHintsNeedsUpdate', true) + return + endif + + var timerid = bnr->getbufvar('LspInlayHintsTimer', -1) + if timerid != -1 + timerid->timer_stop() + setbufvar(bnr, 'LspInlayHintsTimer', -1) + endif + + var lspserver: dict<any> = buf.BufLspServerGet(bnr, 'inlayHint') + if lspserver->empty() + return + endif + + if get(g:, 'LSPTest') + # When running tests, update the inlay hints immediately + InlayHintsTimerCb(lspserver, bnr, -1) + else + timerid = timer_start(300, function('InlayHintsTimerCb', [lspserver, bnr])) + setbufvar(bnr, 'LspInlayHintsTimer', timerid) + endif +enddef + +# Text is modified. Need to update the inlay hints. +def LspInlayHintsChanged(bnr: number) + setbufvar(bnr, 'LspInlayHintsNeedsUpdate', true) +enddef + +# Trigger an update of the inlay hints in the current buffer. +export def LspInlayHintsUpdateNow(bnr: number) + setbufvar(bnr, 'LspInlayHintsNeedsUpdate', true) + LspInlayHintsUpdate(bnr) +enddef + +# Stop updating the inlay hints. +def LspInlayHintsUpdateStop(bnr: number) + var timerid = bnr->getbufvar('LspInlayHintsTimer', -1) + if timerid != -1 + timerid->timer_stop() + setbufvar(bnr, 'LspInlayHintsTimer', -1) + endif +enddef + +# Do buffer-local initialization for displaying inlay hints +export def BufferInit(lspserver: dict<any>, bnr: number) + if !lspserver.isInlayHintProvider && !lspserver.isClangdInlayHintsProvider + # no support for inlay hints + return + endif + + # Inlays hints are disabled + if !opt.lspOptions.showInlayHints + || !lspserver.featureEnabled('inlayHint') + return + endif + + var acmds: list<dict<any>> = [] + + # Update the inlay hints (if needed) when the cursor is not moved for some + # time. + acmds->add({bufnr: bnr, + event: ['CursorHold'], + group: 'LspInlayHints', + cmd: $'LspInlayHintsUpdate({bnr})'}) + # After the text in the current buffer is modified, the inlay hints need to + # be updated. + acmds->add({bufnr: bnr, + event: ['TextChanged'], + group: 'LspInlayHints', + cmd: $'LspInlayHintsChanged({bnr})'}) + # Editing a file should trigger an inlay hint update. + acmds->add({bufnr: bnr, + event: ['BufReadPost'], + group: 'LspInlayHints', + cmd: $'LspInlayHintsUpdateNow({bnr})'}) + # Inlay hints need not be updated if a buffer is no longer active. + acmds->add({bufnr: bnr, + event: ['BufLeave'], + group: 'LspInlayHints', + cmd: $'LspInlayHintsUpdateStop({bnr})'}) + + # Inlay hints maybe a bit delayed if it was a sync init lsp server. + if lspserver.syncInit + acmds->add({bufnr: bnr, + event: ['User'], + group: 'LspAttached', + cmd: $'LspInlayHintsUpdateNow({bnr})'}) + endif + + autocmd_add(acmds) +enddef + +# Track the current inlay hints enabled/disabled state. Used when the +# "showInlayHints" option value is changed. +var save_showInlayHints = opt.lspOptions.showInlayHints + +# Enable inlay hints. For all the buffers with an attached language server +# that supports inlay hints, refresh the inlay hints. +export def InlayHintsEnable() + opt.lspOptions.showInlayHints = true + for binfo in getbufinfo() + var lspservers: list<dict<any>> = buf.BufLspServersGet(binfo.bufnr) + if lspservers->empty() + continue + endif + for lspserver in lspservers + if !lspserver.ready + || !lspserver.featureEnabled('inlayHint') + || (!lspserver.isInlayHintProvider && + !lspserver.isClangdInlayHintsProvider) + continue + endif + BufferInit(lspserver, binfo.bufnr) + LspInlayHintsUpdateNow(binfo.bufnr) + endfor + endfor + save_showInlayHints = true +enddef + +# Disable inlay hints for the current Vim session. Clear the inlay hints in +# all the buffers. +export def InlayHintsDisable() + opt.lspOptions.showInlayHints = false + for binfo in getbufinfo() + var lspserver: dict<any> = buf.BufLspServerGet(binfo.bufnr, 'inlayHint') + if lspserver->empty() + continue + endif + LspInlayHintsUpdateStop(binfo.bufnr) + :silent! autocmd_delete([{bufnr: binfo.bufnr, group: 'LspInlayHints'}]) + InlayHintsClear(binfo.bufnr) + endfor + save_showInlayHints = false +enddef + +# Some options are changed. If 'showInlayHints' option is changed, then +# either enable or disable inlay hints. +export def LspInlayHintsOptionsChanged() + if save_showInlayHints && !opt.lspOptions.showInlayHints + InlayHintsDisable() + elseif !save_showInlayHints && opt.lspOptions.showInlayHints + InlayHintsEnable() + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/lsp.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/lsp.vim new file mode 100644 index 0000000..507eb00 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/lsp.vim @@ -0,0 +1,1304 @@ +vim9script + +# Vim9 LSP client +# +# The functions called by plugin/lsp.vim are in this file. + +# Needs Vim 9.0 and higher +if v:version < 900 + finish +endif + +import './options.vim' as opt +import './lspserver.vim' as lserver +import './util.vim' +import './buffer.vim' as buf +import './completion.vim' +import './textedit.vim' +import './diag.vim' +import './symbol.vim' +import './outline.vim' +import './signature.vim' +import './codeaction.vim' +import './inlayhints.vim' +import './semantichighlight.vim' + +# LSP server information +var LSPServers: list<dict<any>> = [] + +# filetype to LSP server map +var ftypeServerMap: dict<list<dict<any>>> = {} + +var lspInitializedOnce = false + +def LspInitOnce() + hlset([ + {name: 'LspTextRef', default: true, linksto: 'Search'}, + {name: 'LspReadRef', default: true, linksto: 'DiffChange'}, + {name: 'LspWriteRef', default: true, linksto: 'DiffDelete'} + ]) + + var override = &cursorline + && &cursorlineopt =~ '\<line\>\|\<screenline\>\|\<both\>' + + prop_type_add('LspTextRef', {highlight: 'LspTextRef', override: override}) + prop_type_add('LspReadRef', {highlight: 'LspReadRef', override: override}) + prop_type_add('LspWriteRef', {highlight: 'LspWriteRef', override: override}) + + diag.InitOnce() + inlayhints.InitOnce() + signature.InitOnce() + symbol.InitOnce() + semantichighlight.InitOnce() + + lspInitializedOnce = true +enddef + +# Returns the LSP servers for the a specific filetype. Based on how well there +# score, LSP servers with the same score are being returned. +# Returns an empty list if the servers is not found. +def LspGetServers(bnr: number, ftype: string): list<dict<any>> + if !ftypeServerMap->has_key(ftype) + return [] + endif + + var bufDir = bnr->bufname()->fnamemodify(':p:h') + + return ftypeServerMap[ftype]->filter((key, lspserver) => { + # Don't run the server if no path is found + if !lspserver.runIfSearchFiles->empty() + var path = util.FindNearestRootDir(bufDir, lspserver.runIfSearchFiles) + + if path->empty() + return false + endif + endif + + # Don't run the server if a path is found + if !lspserver.runUnlessSearchFiles->empty() + var path = util.FindNearestRootDir(bufDir, lspserver.runUnlessSearchFiles) + + if !path->empty() + return false + endif + endif + + # Run the server + return true + }) +enddef + +# Add a LSP server for a filetype +def LspAddServer(ftype: string, lspsrv: dict<any>) + var lspsrvlst = ftypeServerMap->has_key(ftype) ? ftypeServerMap[ftype] : [] + lspsrvlst->add(lspsrv) + ftypeServerMap[ftype] = lspsrvlst +enddef + +# Enable/disable the logging of the language server protocol messages +def ServerDebug(arg: string) + if ['errors', 'messages', 'off', 'on']->index(arg) == -1 + util.ErrMsg($'Unsupported argument "{arg}"') + return + endif + + var lspservers: list<dict<any>> = buf.CurbufGetServers() + if lspservers->empty() + util.WarnMsg($'No Lsp servers found for "{@%}"') + return + endif + + for lspserver in lspservers + if arg == 'on' + util.ClearTraceLogs(lspserver.logfile) + util.ClearTraceLogs(lspserver.errfile) + lspserver.debug = true + elseif arg == 'off' + lspserver.debug = false + elseif arg == 'messages' + util.ServerMessagesShow(lspserver.logfile) + else + util.ServerMessagesShow(lspserver.errfile) + endif + endfor +enddef + +# Show information about all the LSP servers +export def ShowAllServers() + var lines = [] + # Add filetype to server mapping information + lines->add('Filetype Information') + lines->add('====================') + for [ftype, lspservers] in ftypeServerMap->items() + for lspserver in lspservers + lines->add($"Filetype: '{ftype}'") + lines->add($"Server Name: '{lspserver.name}'") + lines->add($"Server Path: '{lspserver.path}'") + lines->add($"Status: {lspserver.running ? 'Running' : 'Not running'}") + lines->add('') + endfor + endfor + + # Add buffer to server mapping information + lines->add('Buffer Information') + lines->add('==================') + for bnr in range(1, bufnr('$')) + var lspservers: list<dict<any>> = buf.BufLspServersGet(bnr) + if !lspservers->empty() + lines->add($"Buffer: '{bufname(bnr)}'") + for lspserver in lspservers + lines->add($"Server Path: '{lspserver.path}'") + lines->add($"Status: {lspserver.running ? 'Running' : 'Not running'}") + endfor + lines->add('') + endif + endfor + + var wid = bufwinid('Language-Servers') + if wid != -1 + wid->win_gotoid() + :setlocal modifiable + :silent! :%d _ + else + :new Language-Servers + :setlocal buftype=nofile + :setlocal bufhidden=wipe + :setlocal noswapfile + :setlocal nonumber nornu + :setlocal fdc=0 signcolumn=no + endif + setline(1, lines) + :setlocal nomodified + :setlocal nomodifiable +enddef + +# Create a new window containing the buffer "bname" or if the window is +# already present then jump to it. +def OpenScratchWindow(bname: string) + var wid = bufwinid(bname) + if wid != -1 + wid->win_gotoid() + :setlocal modifiable + :silent! :%d _ + else + exe $':new {bname}' + :setlocal buftype=nofile + :setlocal bufhidden=wipe + :setlocal noswapfile + :setlocal nonumber nornu + :setlocal fdc=0 signcolumn=no + endif +enddef + +# Show the status of the LSP server for the current buffer +def ShowServer(arg: string) + if ['status', 'capabilities', 'initializeRequest', 'messages']->index(arg) == -1 + util.ErrMsg($'Unsupported argument "{arg}"') + return + endif + + var lspservers: list<dict<any>> = buf.CurbufGetServers() + if lspservers->empty() + util.WarnMsg($'No Lsp servers found for "{@%}"') + return + endif + + var windowName: string = '' + var lines: list<string> = [] + if arg->empty() || arg == 'status' + windowName = $'LangServer-Status' + for lspserver in lspservers + if !lines->empty() + lines->extend(['', repeat('=', &columns), '']) + endif + var msg = $"LSP server '{lspserver.name}' is " + if lspserver.running + msg ..= 'running' + else + msg ..= 'not running' + endif + lines->add(msg) + endfor + elseif arg == 'capabilities' + windowName = $'LangServer-Capabilities' + for lspserver in lspservers + if !lines->empty() + lines->extend(['', repeat('=', &columns), '']) + endif + lines->extend(lspserver.getCapabilities()) + endfor + elseif arg == 'initializeRequest' + windowName = $'LangServer-InitializeRequest' + for lspserver in lspservers + if !lines->empty() + lines->extend(['', repeat('=', &columns), '']) + endif + lines->extend(lspserver.getInitializeRequest()) + endfor + elseif arg == 'messages' + windowName = $'LangServer-Messages' + for lspserver in lspservers + if !lines->empty() + lines->extend(['', repeat('=', &columns), '']) + endif + lines->extend(lspserver.getMessages()) + endfor + else + util.ErrMsg($'Unsupported argument "{arg}"') + return + endif + + if lines->len() > 1 + OpenScratchWindow(windowName) + setline(1, lines) + :setlocal nomodified + :setlocal nomodifiable + else + util.InfoMsg(lines[0]) + endif +enddef + +# Get LSP server running status for filetype "ftype" +# Return true if running, or false if not found or not running +export def ServerRunning(ftype: string): bool + if ftypeServerMap->has_key(ftype) + var lspservers = ftypeServerMap[ftype] + for lspserver in lspservers + if lspserver.running + return true + endif + endfor + endif + + return false +enddef + +# Go to a definition using "textDocument/definition" LSP request +export def GotoDefinition(peek: bool, cmdmods: string, count: number) + var lspserver: dict<any> = buf.CurbufGetServerChecked('definition') + if lspserver->empty() + return + endif + + lspserver.gotoDefinition(peek, cmdmods, count) +enddef + +# Go to a declaration using "textDocument/declaration" LSP request +export def GotoDeclaration(peek: bool, cmdmods: string, count: number) + var lspserver: dict<any> = buf.CurbufGetServerChecked('declaration') + if lspserver->empty() + return + endif + + lspserver.gotoDeclaration(peek, cmdmods, count) +enddef + +# Go to a type definition using "textDocument/typeDefinition" LSP request +export def GotoTypedef(peek: bool, cmdmods: string, count: number) + var lspserver: dict<any> = buf.CurbufGetServerChecked('typeDefinition') + if lspserver->empty() + return + endif + + lspserver.gotoTypeDef(peek, cmdmods, count) +enddef + +# Go to a implementation using "textDocument/implementation" LSP request +export def GotoImplementation(peek: bool, cmdmods: string, count: number) + var lspserver: dict<any> = buf.CurbufGetServerChecked('implementation') + if lspserver->empty() + return + endif + + lspserver.gotoImplementation(peek, cmdmods, count) +enddef + +# Switch source header using "textDocument/switchSourceHeader" LSP request +# (Clangd specifc extension) +export def SwitchSourceHeader() + var lspserver: dict<any> = buf.CurbufGetServerChecked() + if lspserver->empty() + return + endif + + lspserver.switchSourceHeader() +enddef + +# A buffer is saved. Send the "textDocument/didSave" LSP notification +def LspSavedFile(bnr: number) + var lspservers: list<dict<any>> = buf.BufLspServersGet(bnr)->filter( + (key, lspsrv) => !lspsrv->empty() && lspsrv.running + ) + + if lspservers->empty() + return + endif + + for lspserver in lspservers + lspserver.didSaveFile(bnr) + endfor +enddef + +# Called after leaving insert mode. Used to process diag messages (if any) +def LspLeftInsertMode(bnr: number) + var updatePending: bool = bnr->getbufvar('LspDiagsUpdatePending', false) + if !updatePending + return + endif + setbufvar(bnr, 'LspDiagsUpdatePending', false) + + diag.ProcessNewDiags(bnr) +enddef + +# Add buffer-local autocmds when attaching a LSP server to a buffer +def AddBufLocalAutocmds(lspserver: dict<any>, bnr: number): void + var acmds: list<dict<any>> = [] + + # file saved notification handler + acmds->add({bufnr: bnr, + event: 'BufWritePost', + group: 'LSPBufferAutocmds', + cmd: $'LspSavedFile({bnr})'}) + + # Update the diagnostics when insert mode is stopped + acmds->add({bufnr: bnr, + event: 'InsertLeave', + group: 'LSPBufferAutocmds', + cmd: $'LspLeftInsertMode({bnr})'}) + + # Auto highlight all the occurrences of the current keyword + if opt.lspOptions.autoHighlight && + lspserver.isDocumentHighlightProvider + acmds->add({bufnr: bnr, + event: 'CursorMoved', + group: 'LSPBufferAutocmds', + cmd: $'call LspDocHighlightClear({bnr}) | call LspDocHighlight({bnr}, "silent")'}) + endif + + autocmd_add(acmds) +enddef + +# The LSP server with ID "lspserverId" is ready, initialize the LSP features +# for buffer "bnr". +def BufferInit(lspserverId: number, bnr: number): void + var lspserver = buf.BufLspServerGetById(bnr, lspserverId) + if lspserver->empty() || !lspserver.running + return + endif + + var ftype: string = bnr->getbufvar('&filetype') + lspserver.textdocDidOpen(bnr, ftype) + + # add a listener to track changes to this buffer + listener_add((_bnr: number, start: number, end: number, added: number, changes: list<dict<number>>) => { + lspserver.textdocDidChange(bnr, start, end, added, changes) + }, bnr) + + AddBufLocalAutocmds(lspserver, bnr) + + diag.BufferInit(lspserver, bnr) + + var allServersReady = true + var lspservers: list<dict<any>> = buf.BufLspServersGet(bnr) + for lspsrv in lspservers + if !lspsrv.ready + allServersReady = false + break + endif + endfor + + if allServersReady + for lspsrv in lspservers + # It's only possible to initialize the features when the server + # capabilities of all the registered language servers for this file type + # are known. + var completionServer = buf.BufLspServerGet(bnr, 'completion') + if !completionServer->empty() && lspsrv.id == completionServer.id + completion.BufferInit(lspsrv, bnr, ftype) + endif + + var signatureServer = buf.BufLspServerGet(bnr, 'signatureHelp') + if !signatureServer->empty() && lspsrv.id == signatureServer.id + signature.BufferInit(lspsrv) + endif + + var inlayHintServer = buf.BufLspServerGet(bnr, 'inlayHint') + if !inlayHintServer->empty() && lspsrv.id == inlayHintServer.id + inlayhints.BufferInit(lspsrv, bnr) + endif + + var semanticServer = buf.BufLspServerGet(bnr, 'semanticTokens') + if !semanticServer->empty() && lspsrv.id == semanticServer.id + semantichighlight.BufferInit(lspserver, bnr) + endif + endfor + + if exists('#User#LspAttached') + doautocmd <nomodeline> User LspAttached + endif + endif +enddef + +# A new buffer is opened. If LSP is supported for this buffer, then add it +export def AddFile(bnr: number): void + if buf.BufHasLspServer(bnr) + # LSP server for this buffer is already initialized and running + return + endif + + # Skip remote files + if util.LspUriRemote(bnr->bufname()->fnamemodify(':p')) + return + endif + + var ftype: string = bnr->getbufvar('&filetype') + if ftype->empty() + return + endif + var lspservers: list<dict<any>> = LspGetServers(bnr, ftype) + if lspservers->empty() + return + endif + for lspserver in lspservers + if !lspserver.running + if !lspInitializedOnce + LspInitOnce() + endif + lspserver.startServer(bnr) + endif + buf.BufLspServerSet(bnr, lspserver) + + if lspserver.ready + BufferInit(lspserver.id, bnr) + else + augroup LSPBufferAutocmds + exe $'autocmd User LspServerReady_{lspserver.id} ++once BufferInit({lspserver.id}, {bnr})' + augroup END + endif + endfor +enddef + +# Notify LSP server to remove a file +export def RemoveFile(bnr: number): void + var lspservers: list<dict<any>> = buf.BufLspServersGet(bnr) + for lspserver in lspservers->copy() + if lspserver->empty() + continue + endif + if lspserver.running + lspserver.textdocDidClose(bnr) + endif + diag.DiagRemoveFile(bnr) + buf.BufLspServerRemove(bnr, lspserver) + endfor +enddef + +# Buffer 'bnr' is loaded in a window, send the latest buffer contents to the +# language servers. +export def BufferLoadedInWin(bnr: number) + var lspservers: list<dict<any>> = buf.BufLspServersGet(bnr) + if lspservers->empty() + # No language servers for this buffer + return + endif + for lspserver in lspservers + if !lspserver->empty() && lspserver.ready + lspserver.textdocDidChange(bnr, 0, 0, 0, []) + endif + endfor + # Refresh the displayed diags visuals + if opt.lspOptions.autoHighlightDiags + diag.DiagsRefresh(bnr) + endif + + completion.BufferLoadedInWin(bnr) + + # Refresh the semantic highlights + if opt.lspOptions.semanticHighlight + var semanticServer = buf.BufLspServerGet(bnr, 'semanticTokens') + if !semanticServer->empty() + semanticServer.semanticHighlightUpdate(bnr) + endif + endif +enddef + +# Stop all the LSP servers +export def StopAllServers() + for lspserver in LSPServers + if lspserver.running + lspserver.stopServer() + endif + endfor +enddef + +# Add all the buffers with 'filetype' set to "ftype" to the language server. +def AddBuffersToLsp(ftype: string) + # Add all the buffers with the same file type as the current buffer + for binfo in getbufinfo({bufloaded: 1}) + if binfo.bufnr->getbufvar('&filetype') == ftype + AddFile(binfo.bufnr) + endif + endfor +enddef + +# Restart the LSP server for the current buffer +def RestartServer() + var lspservers: list<dict<any>> = buf.CurbufGetServers() + if lspservers->empty() + util.WarnMsg($'No Lsp servers found for "{@%}"') + return + endif + + # Remove all the buffers with the same file type as the current buffer + var ftype: string = &filetype + for binfo in getbufinfo() + if binfo.bufnr->getbufvar('&filetype') == ftype + RemoveFile(binfo.bufnr) + endif + endfor + + for lspserver in lspservers + # Stop the server (if running) + if lspserver.running + lspserver.stopServer() + endif + + # Start the server again + lspserver.startServer(bufnr()) + endfor + + AddBuffersToLsp(ftype) +enddef + +# Add the LSP server for files with 'filetype' as "ftype". +def AddServerForFiltype(lspserver: dict<any>, ftype: string, omnicompl: bool) + LspAddServer(ftype, lspserver) + completion.OmniComplSet(ftype, omnicompl) + + # If a buffer of this file type is already present, then send it to the LSP + # server now. + AddBuffersToLsp(ftype) +enddef + +# Register a LSP server for one or more file types +export def AddServer(serverList: list<dict<any>>) + for server in serverList + if !server->has_key('filetype') || !server->has_key('path') + util.ErrMsg('LSP server information is missing filetype or path') + continue + endif + # Enable omni-completion by default + var omnicompl_def: bool = false + if opt.lspOptions.omniComplete == true + || (opt.lspOptions.omniComplete == null + && !opt.lspOptions.autoComplete) + omnicompl_def = true + endif + server.omnicompl = server->get('omnicompl', omnicompl_def) + + if !server.path->executable() + if !opt.lspOptions.ignoreMissingServer + util.ErrMsg($'LSP server {server.path} is not found') + endif + continue + endif + if server->has_key('args') + if server.args->type() != v:t_list + util.ErrMsg($'Arguments for LSP server {server.args} is not a List') + continue + endif + else + server.args = [] + endif + + if !server->has_key('initializationOptions') + || server.initializationOptions->type() != v:t_dict + server.initializationOptions = {} + endif + + if !server->has_key('forceOffsetEncoding') + || server.forceOffsetEncoding->type() != v:t_string + || (server.forceOffsetEncoding != 'utf-8' + && server.forceOffsetEncoding != 'utf-16' + && server.forceOffsetEncoding != 'utf-32') + server.forceOffsetEncoding = '' + endif + + if !server->has_key('customNotificationHandlers') + || server.customNotificationHandlers->type() != v:t_dict + server.customNotificationHandlers = {} + endif + + if server->has_key('processDiagHandler') + if server.processDiagHandler->type() != v:t_func + util.ErrMsg($'Setting of processDiagHandler {server.processDiagHandler} is not a Funcref nor lambda') + continue + endif + else + server.processDiagHandler = null_function + endif + + if !server->has_key('customRequestHandlers') + || server.customRequestHandlers->type() != v:t_dict + server.customRequestHandlers = {} + endif + + if !server->has_key('features') || server.features->type() != v:t_dict + server.features = {} + endif + + if server.omnicompl->type() != v:t_bool + util.ErrMsg($'Setting of omnicompl {server.omnicompl} is not a Boolean') + continue + endif + + if !server->has_key('syncInit') + server.syncInit = false + endif + + if !server->has_key('name') || server.name->type() != v:t_string + || server.name->empty() + # Use the executable name (without the extension) as the language server + # name. + server.name = server.path->fnamemodify(':t:r') + endif + + if !server->has_key('debug') || server.debug->type() != v:t_bool + server.debug = false + endif + + if !server->has_key('traceLevel') + || server->type() != v:t_string + || (server.traceLevel != 'off' && server.traceLevel != 'debug' + && server.traceLevel != 'verbose') + server.traceLevel = 'off' + endif + + if !server->has_key('workspaceConfig') + || server.workspaceConfig->type() != v:t_dict + server.workspaceConfig = {} + endif + + if !server->has_key('rootSearch') || server.rootSearch->type() != v:t_list + server.rootSearch = [] + endif + + if !server->has_key('runIfSearch') || + server.runIfSearch->type() != v:t_list + server.runIfSearch = [] + endif + + if !server->has_key('runUnlessSearch') || + server.runUnlessSearch->type() != v:t_list + server.runUnlessSearch = [] + endif + + var lspserver: dict<any> = lserver.NewLspServer(server) + + var ftypes = server.filetype + if ftypes->type() == v:t_string + AddServerForFiltype(lspserver, ftypes, server.omnicompl) + elseif ftypes->type() == v:t_list + for ftype in ftypes + AddServerForFiltype(lspserver, ftype, server.omnicompl) + endfor + else + util.ErrMsg($'Unsupported file type information "{ftypes->string()}" in LSP server registration') + continue + endif + endfor +enddef + +# The LSP server is considered ready when the server capabilities are +# received ("initialize" LSP reply message) +export def ServerReady(): bool + var fname: string = @% + if fname->empty() + return false + endif + + var lspservers: list<dict<any>> = buf.CurbufGetServers() + if lspservers->empty() + return false + endif + + for lspserver in lspservers + if !lspserver.ready + return false + endif + endfor + + return true +enddef + +# set the LSP server trace level for the current buffer +# Params: SetTraceParams +def ServerTraceSet(traceVal: string) + if ['off', 'messages', 'verbose']->index(traceVal) == -1 + util.ErrMsg($'Unsupported argument "{traceVal}"') + return + endif + + var lspservers: list<dict<any>> = buf.CurbufGetServers() + if lspservers->empty() + util.WarnMsg($'No Lsp servers found for "{@%}"') + return + endif + + for lspserver in lspservers + lspserver.setTrace(traceVal) + endfor +enddef + +# Display the diagnostic messages from the LSP server for the current buffer +# in a quickfix list +export def ShowDiagnostics(): void + diag.ShowAllDiags() +enddef + +# Show the diagnostic message for the current line +export def LspShowCurrentDiag(atPos: bool) + diag.ShowCurrentDiag(atPos) +enddef + +# get the count of diagnostics in the current buffer +export def ErrorCount(): dict<number> + var res = {Error: 0, Warn: 0, Info: 0, Hint: 0} + var fname: string = @% + if fname->empty() + return res + endif + + return diag.DiagsGetErrorCount(bufnr()) +enddef + +# jump to the next/previous/first diagnostic message in the current buffer +export def JumpToDiag(which: string, count: number = 0): void + diag.LspDiagsJump(which, count) +enddef + +# Display the hover message from the LSP server for the current cursor +# location +export def Hover(cmdmods: string) + var lspserver: dict<any> = buf.CurbufGetServerChecked('hover') + if lspserver->empty() + return + endif + + lspserver.hover(cmdmods) +enddef + +# Enable or disable inlay hints +export def InlayHints(ctl: string) + if ctl == 'enable' + inlayhints.InlayHintsEnable() + elseif ctl == 'disable' + inlayhints.InlayHintsDisable() + else + util.ErrMsg($'LspInlayHints - Unsupported argument "{ctl}"') + endif +enddef + +# Command-line completion for the ":LspInlayHints" command +export def LspInlayHintsComplete(arglead: string, cmdline: string, cursorPos: number): list<string> + var l = ['enable', 'disable'] + return filter(l, (_, val) => val =~ $'^{arglead}') +enddef + +# show symbol references +export def ShowReferences(peek: bool) + var lspserver: dict<any> = buf.CurbufGetServerChecked('references') + if lspserver->empty() + return + endif + + lspserver.showReferences(peek) +enddef + +# highlight all the places where a symbol is referenced +def g:LspDocHighlight(bnr: number = bufnr(), cmdmods: string = '') + var lspserver: dict<any> = buf.CurbufGetServerChecked('documentHighlight') + if lspserver->empty() + return + endif + + lspserver.docHighlight(bnr, cmdmods) +enddef + +# clear the symbol reference highlight +def g:LspDocHighlightClear(bnr: number = bufnr()) + var lspserver: dict<any> = buf.CurbufGetServerChecked('documentHighlight') + if lspserver->empty() + return + endif + + var propNames = ['LspTextRef', 'LspReadRef', 'LspWriteRef'] + if has('patch-9.0.0233') + prop_remove({types: propNames, bufnr: bnr, all: true}) + else + for propName in propNames + prop_remove({type: propName, bufnr: bnr, all: true}) + endfor + endif +enddef + +def g:LspRequestDocSymbols() + if outline.SkipOutlineRefresh() + return + endif + + var fname: string = @% + if fname->empty() + return + endif + + var lspserver: dict<any> = buf.CurbufGetServer('documentSymbol') + if lspserver->empty() || !lspserver.running || !lspserver.ready + return + endif + + lspserver.getDocSymbols(fname, true) +enddef + +# open a window and display all the symbols in a file (outline) +export def Outline(cmdmods: string, winsize: number) + var fname: string = @% + if fname->empty() + return + endif + + var lspserver: dict<any> = buf.CurbufGetServerChecked('documentSymbol') + if lspserver->empty() || !lspserver.running || !lspserver.ready + return + endif + + outline.OpenOutlineWindow(cmdmods, winsize) + g:LspRequestDocSymbols() +enddef + +# show all the symbols in a file in a popup menu +export def ShowDocSymbols() + var fname: string = @% + if fname->empty() + return + endif + + var lspserver: dict<any> = buf.CurbufGetServerChecked('documentSymbol') + if lspserver->empty() || !lspserver.running || !lspserver.ready + return + endif + + lspserver.getDocSymbols(fname, false) +enddef + +# Format the entire file +export def TextDocFormat(range_args: number, line1: number, line2: number) + if !&modifiable + util.ErrMsg('Current file is not a modifiable file') + return + endif + + var lspserver: dict<any> = buf.CurbufGetServerChecked('documentFormatting') + if lspserver->empty() + return + endif + + var fname: string = @% + if range_args > 0 + lspserver.textDocFormat(fname, true, line1, line2) + else + lspserver.textDocFormat(fname, false, 0, 0) + endif +enddef + +# TODO: Add support for textDocument.onTypeFormatting? +# Will this slow down Vim? + +# Display all the locations where the current symbol is called from. +# Uses LSP "callHierarchy/incomingCalls" request +export def IncomingCalls() + var lspserver: dict<any> = buf.CurbufGetServerChecked('callHierarchy') + if lspserver->empty() + return + endif + + lspserver.incomingCalls(@%) +enddef + +# Display all the symbols used by the current symbol. +# Uses LSP "callHierarchy/outgoingCalls" request +export def OutgoingCalls() + var lspserver: dict<any> = buf.CurbufGetServerChecked('callHierarchy') + if lspserver->empty() + return + endif + + lspserver.outgoingCalls(@%) +enddef + +# Display the type hierarchy for the current symbol. Direction is 0 for +# sub types and 1 for super types. +export def TypeHierarchy(direction: number) + var lspserver: dict<any> = buf.CurbufGetServerChecked('typeHierarchy') + if lspserver->empty() + return + endif + + lspserver.typeHierarchy(direction) +enddef + +# Rename a symbol +# Uses LSP "textDocument/rename" request +export def Rename(a_newName: string) + var lspserver: dict<any> = buf.CurbufGetServerChecked('rename') + if lspserver->empty() + return + endif + + var newName: string = a_newName + if newName->empty() + var sym: string = expand('<cword>') + newName = input($"Rename symbol '{sym}' to: ", sym) + if newName->empty() + return + endif + + # clear the input prompt + :echo "\r" + endif + + lspserver.renameSymbol(newName) +enddef + +# Perform a code action +# Uses LSP "textDocument/codeAction" request +export def CodeAction(line1: number, line2: number, query: string) + var lspserver: dict<any> = buf.CurbufGetServerChecked('codeAction') + if lspserver->empty() + return + endif + + var fname: string = @% + lspserver.codeAction(fname, line1, line2, query) +enddef + +# Code lens +# Uses LSP "textDocument/codeLens" request +export def CodeLens() + var lspserver: dict<any> = buf.CurbufGetServerChecked('codeLens') + if lspserver->empty() + return + endif + + lspserver.codeLens(@%) +enddef + +# Perform a workspace wide symbol lookup +# Uses LSP "workspace/symbol" request +export def SymbolSearch(queryArg: string, cmdmods: string) + var lspserver: dict<any> = buf.CurbufGetServerChecked('workspaceSymbol') + if lspserver->empty() + return + endif + + var query: string = queryArg + if query->empty() + query = input('Lookup symbol: ', expand('<cword>')) + if query->empty() + return + endif + endif + :redraw! + + lspserver.workspaceQuery(query, true, cmdmods) +enddef + +# Display the list of workspace folders +export def ListWorkspaceFolders() + var lspservers: list<dict<any>> = buf.CurbufGetServers() + for lspserver in lspservers + util.InfoMsg($'Workspace Folders: "{lspserver.name}" {lspserver.workspaceFolders->string()}') + endfor +enddef + +# Add a workspace folder. Default is to use the current folder. +export def AddWorkspaceFolder(dirArg: string) + var dirName: string = dirArg + if dirName->empty() + dirName = input('Add Workspace Folder: ', getcwd(), 'dir') + if dirName->empty() + return + endif + endif + :redraw! + if !dirName->isdirectory() + util.ErrMsg($'{dirName} is not a directory') + return + endif + + var lspservers: list<dict<any>> = buf.CurbufGetServers() + + for lspserver in lspservers + lspserver.addWorkspaceFolder(dirName) + endfor +enddef + +# Remove a workspace folder. Default is to use the current folder. +export def RemoveWorkspaceFolder(dirArg: string) + var dirName: string = dirArg + if dirName->empty() + dirName = input('Remove Workspace Folder: ', getcwd(), 'dir') + if dirName->empty() + return + endif + endif + :redraw! + if !dirName->isdirectory() + util.ErrMsg($'{dirName} is not a directory') + return + endif + + var lspservers: list<dict<any>> = buf.CurbufGetServers() + for lspserver in lspservers + lspserver.removeWorkspaceFolder(dirName) + endfor +enddef + +# expand the previous selection or start a new selection +export def SelectionExpand() + var lspserver: dict<any> = buf.CurbufGetServerChecked('selectionRange') + if lspserver->empty() + return + endif + + lspserver.selectionExpand() +enddef + +# shrink the previous selection or start a new selection +export def SelectionShrink() + var lspserver: dict<any> = buf.CurbufGetServerChecked('selectionRange') + if lspserver->empty() + return + endif + + lspserver.selectionShrink() +enddef + +# fold the entire document +export def FoldDocument() + var lspserver: dict<any> = buf.CurbufGetServerChecked('foldingRange') + if lspserver->empty() + return + endif + + if &foldmethod != 'manual' + util.ErrMsg("Only works when 'foldmethod' is 'manual'") + return + endif + + var fname: string = @% + lspserver.foldRange(fname) +enddef + +# Enable diagnostic highlighting for all the buffers +export def DiagHighlightEnable() + diag.DiagsHighlightEnable() +enddef + +# Disable diagnostic highlighting for all the buffers +export def DiagHighlightDisable() + diag.DiagsHighlightDisable() +enddef + +# Function to use with the 'tagfunc' option. +export def TagFunc(pat: string, flags: string, info: dict<any>): any + var lspserver: dict<any> = buf.CurbufGetServerChecked('definition') + if lspserver->empty() + return v:null + endif + + return lspserver.tagFunc(pat, flags, info) +enddef + +# Function to use with the 'formatexpr' option. +export def FormatExpr(): number + var lspserver: dict<any> = buf.CurbufGetServerChecked('documentFormatting') + if lspserver->empty() + return 1 + endif + + lspserver.textDocFormat(@%, true, v:lnum, v:lnum + v:count - 1) + return 0 +enddef + +export def RegisterCmdHandler(cmd: string, Handler: func) + codeaction.RegisterCmdHandler(cmd, Handler) +enddef + +# Command-line completion for the ":LspServer <cmd>" and ":LspDiag <cmd>" sub +# commands +def LspSubCmdComplete(cmds: list<string>, arglead: string, cmdline: string, cursorPos: number): list<string> + var wordBegin = cmdline->match('\s\+\zs\S', cursorPos) + if wordBegin == -1 + return cmds + endif + + # Make sure there are no additional sub-commands + var wordEnd = cmdline->stridx(' ', wordBegin) + if wordEnd == -1 + return cmds->filter((_, val) => val =~ $'^{arglead}') + endif + + return [] +enddef + +# Command-line completion for the ":LspDiag highlight" command +def LspDiagHighlightComplete(arglead: string, cmdline: string, cursorPos: number): list<string> + return LspSubCmdComplete(['enable', 'disable'], arglead, cmdline, cursorPos) +enddef + +# Command-line completion for the ":LspDiag" command +export def LspDiagComplete(arglead: string, cmdline: string, cursorPos: number): list<string> + var wordBegin = -1 + var wordEnd = -1 + var l = ['first', 'current', 'here', 'highlight', 'last', 'next', 'nextWrap', + 'prev', 'prevWrap', 'show'] + + # Skip the command name + var i = cmdline->stridx(' ', 0) + wordBegin = cmdline->match('\s\+\zs\S', i) + if wordBegin == -1 + return l + endif + + wordEnd = cmdline->stridx(' ', wordBegin) + if wordEnd == -1 + return filter(l, (_, val) => val =~ $'^{arglead}') + endif + + var cmd = cmdline->strpart(wordBegin, wordEnd - wordBegin) + if cmd == 'highlight' + return LspDiagHighlightComplete(arglead, cmdline, wordEnd) + endif + + return [] +enddef + +# ":LspDiag" command handler +export def LspDiagCmd(args: string, cmdCount: number, force: bool) + if args->stridx('highlight') == 0 + if args[9] == ' ' + var subcmd = args[10 : ]->trim() + if subcmd == 'enable' + diag.DiagsHighlightEnable() + elseif subcmd == 'disable' + diag.DiagsHighlightDisable() + else + util.ErrMsg($':LspDiag highlight - Unsupported argument "{subcmd}"') + endif + else + util.ErrMsg('Argument required for ":LspDiag highlight"') + endif + elseif args == 'first' + diag.LspDiagsJump('first', 0) + elseif args == 'current' + LspShowCurrentDiag(force) + elseif args == 'here' + diag.LspDiagsJump('here', 0) + elseif args == 'last' + diag.LspDiagsJump('last', 0) + elseif args == 'next' + diag.LspDiagsJump('next', cmdCount) + elseif args == 'nextWrap' + diag.LspDiagsJump('nextWrap', cmdCount) + elseif args == 'prev' + diag.LspDiagsJump('prev', cmdCount) + elseif args == 'prevWrap' + diag.LspDiagsJump('prevWrap', cmdCount) + elseif args == 'show' + ShowDiagnostics() + else + util.ErrMsg($':LspDiag - Unsupported argument "{args}"') + endif +enddef + +# Command-line completion for the ":LspServer debug" command +def LspServerDebugComplete(arglead: string, cmdline: string, cursorPos: number): list<string> + return LspSubCmdComplete(['errors', 'messages', 'off', 'on'], arglead, + cmdline, cursorPos) +enddef + +# Command-line completion for the ":LspServer show" command +def LspServerShowComplete(arglead: string, cmdline: string, cursorPos: number): list<string> + return LspSubCmdComplete(['capabilities', 'initializeRequest', 'messages', + 'status'], arglead, cmdline, cursorPos) +enddef + +# Command-line completion for the ":LspServer trace" command +def LspServerTraceComplete(arglead: string, cmdline: string, cursorPos: number): list<string> + return LspSubCmdComplete(['messages', 'off', 'verbose'], arglead, cmdline, + cursorPos) +enddef + +# Command-line completion for the ":LspServer" command +export def LspServerComplete(arglead: string, cmdline: string, cursorPos: number): list<string> + var wordBegin = -1 + var wordEnd = -1 + var l = ['debug', 'restart', 'show', 'trace'] + + # Skip the command name + var i = cmdline->stridx(' ', 0) + wordBegin = cmdline->match('\s\+\zs\S', i) + if wordBegin == -1 + return l + endif + + wordEnd = cmdline->stridx(' ', wordBegin) + if wordEnd == -1 + return filter(l, (_, val) => val =~ $'^{arglead}') + endif + + var cmd = cmdline->strpart(wordBegin, wordEnd - wordBegin) + if cmd == 'debug' + return LspServerDebugComplete(arglead, cmdline, wordEnd) + elseif cmd == 'restart' + elseif cmd == 'show' + return LspServerShowComplete(arglead, cmdline, wordEnd) + elseif cmd == 'trace' + return LspServerTraceComplete(arglead, cmdline, wordEnd) + endif + + return [] +enddef + +# ":LspServer" command handler +export def LspServerCmd(args: string) + if args->stridx('debug') == 0 + if args[5] == ' ' + var subcmd = args[6 : ]->trim() + ServerDebug(subcmd) + else + util.ErrMsg('Argument required for ":LspServer debug"') + endif + elseif args == 'restart' + RestartServer() + elseif args->stridx('show') == 0 + if args[4] == ' ' + var subcmd = args[5 : ]->trim() + ShowServer(subcmd) + else + util.ErrMsg('Argument required for ":LspServer show"') + endif + elseif args->stridx('trace') == 0 + if args[5] == ' ' + var subcmd = args[6 : ]->trim() + ServerTraceSet(subcmd) + else + util.ErrMsg('Argument required for ":LspServer trace"') + endif + else + util.ErrMsg($'LspServer - Unsupported argument "{args}"') + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 noexpandtab diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/lspserver.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/lspserver.vim new file mode 100644 index 0000000..bfbdd77 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/lspserver.vim @@ -0,0 +1,1982 @@ +vim9script + +# LSP server functions +# +# The functions to send request messages to the language server are in this +# file. +# +# Refer to https://microsoft.github.io/language-server-protocol/specification +# for the Language Server Protocol (LSP) specificaiton. + +import './options.vim' as opt +import './handlers.vim' +import './util.vim' +import './capabilities.vim' +import './offset.vim' +import './diag.vim' +import './selection.vim' +import './symbol.vim' +import './textedit.vim' +import './completion.vim' +import './hover.vim' +import './signature.vim' +import './codeaction.vim' +import './codelens.vim' +import './callhierarchy.vim' as callhier +import './typehierarchy.vim' as typehier +import './inlayhints.vim' +import './semantichighlight.vim' + +# LSP server standard output handler +def Output_cb(lspserver: dict<any>, chan: channel, msg: any): void + if lspserver.debug + lspserver.traceLog($'{strftime("%m/%d/%y %T")}: Received {msg->string()}') + endif + lspserver.data = msg + lspserver.processMessages() +enddef + +# LSP server error output handler +def Error_cb(lspserver: dict<any>, chan: channel, emsg: string): void + lspserver.errorLog(emsg) +enddef + +# LSP server exit callback +def Exit_cb(lspserver: dict<any>, job: job, status: number): void + util.WarnMsg($'{strftime("%m/%d/%y %T")}: LSP server ({lspserver.name}) exited with status {status}') + lspserver.running = false + lspserver.ready = false + lspserver.requests = {} +enddef + +# Start a LSP server +# +def StartServer(lspserver: dict<any>, bnr: number): number + if lspserver.running + util.WarnMsg($'LSP server "{lspserver.name}" is already running') + return 0 + endif + + var cmd = [lspserver.path] + cmd->extend(lspserver.args) + + var opts = {in_mode: 'lsp', + out_mode: 'lsp', + err_mode: 'raw', + noblock: 1, + out_cb: function(Output_cb, [lspserver]), + err_cb: function(Error_cb, [lspserver]), + exit_cb: function(Exit_cb, [lspserver])} + + lspserver.data = '' + lspserver.caps = {} + lspserver.nextID = 1 + lspserver.requests = {} + lspserver.omniCompletePending = false + lspserver.completionLazyDoc = false + lspserver.completionTriggerChars = [] + lspserver.signaturePopup = -1 + + var job = cmd->job_start(opts) + if job->job_status() == 'fail' + util.ErrMsg($'Failed to start LSP server {lspserver.path}') + return 1 + endif + + # wait a little for the LSP server to start + sleep 10m + + lspserver.job = job + lspserver.running = true + + lspserver.initServer(bnr) + + return 0 +enddef + +# process the "initialize" method reply from the LSP server +# Result: InitializeResult +def ServerInitReply(lspserver: dict<any>, initResult: dict<any>): void + if initResult->empty() + return + endif + + var caps: dict<any> = initResult.capabilities + lspserver.caps = caps + + for [key, val] in initResult->items() + if key == 'capabilities' + continue + endif + + lspserver.caps[$'~additionalInitResult_{key}'] = val + endfor + + capabilities.ProcessServerCaps(lspserver, caps) + + if caps->has_key('completionProvider') + lspserver.completionTriggerChars = + caps.completionProvider->get('triggerCharacters', []) + lspserver.completionLazyDoc = + caps.completionProvider->get('resolveProvider', false) + endif + + # send a "initialized" notification to server + lspserver.sendInitializedNotif() + # send any workspace configuration (optional) + if !lspserver.workspaceConfig->empty() + lspserver.sendWorkspaceConfig() + endif + lspserver.ready = true + if exists($'#User#LspServerReady{lspserver.name}') + exe $'doautocmd <nomodeline> User LspServerReady{lspserver.name}' + endif + # Used internally, and shouldn't be used by users + if exists($'#User#LspServerReady_{lspserver.id}') + exe $'doautocmd <nomodeline> User LspServerReady_{lspserver.id}' + endif + + # set the server debug trace level + if lspserver.traceLevel != 'off' + lspserver.setTrace(lspserver.traceLevel) + endif + + # if the outline window is opened, then request the symbols for the current + # buffer + if bufwinid('LSP-Outline') != -1 + lspserver.getDocSymbols(@%, true) + endif + + # Update the inlay hints (if enabled) + if opt.lspOptions.showInlayHints && (lspserver.isInlayHintProvider + || lspserver.isClangdInlayHintsProvider) + inlayhints.LspInlayHintsUpdateNow(bufnr()) + endif +enddef + +# Request: "initialize" +# Param: InitializeParams +def InitServer(lspserver: dict<any>, bnr: number) + # interface 'InitializeParams' + var initparams: dict<any> = {} + initparams.processId = getpid() + initparams.clientInfo = { + name: 'Vim', + version: v:versionlong->string(), + } + + # Compute the rootpath (based on the directory of the buffer) + var rootPath = '' + var rootSearchFiles = lspserver.rootSearchFiles + var bufDir = bnr->bufname()->fnamemodify(':p:h') + if !rootSearchFiles->empty() + rootPath = util.FindNearestRootDir(bufDir, rootSearchFiles) + endif + if rootPath->empty() + var cwd = getcwd() + + # bufDir is within cwd + var bufDirPrefix = bufDir[0 : cwd->strcharlen() - 1] + if &fileignorecase + ? bufDirPrefix ==? cwd + : bufDirPrefix == cwd + rootPath = cwd + else + rootPath = bufDir + endif + endif + + lspserver.workspaceFolders = [rootPath] + + var rootUri = util.LspFileToUri(rootPath) + initparams.rootPath = rootPath + initparams.rootUri = rootUri + initparams.workspaceFolders = [{ + name: rootPath->fnamemodify(':t'), + uri: rootUri + }] + + initparams.trace = 'off' + initparams.capabilities = capabilities.GetClientCaps() + if !lspserver.initializationOptions->empty() + initparams.initializationOptions = lspserver.initializationOptions + else + initparams.initializationOptions = {} + endif + + lspserver.rpcInitializeRequest = initparams + + lspserver.rpc_a('initialize', initparams, ServerInitReply) +enddef + +# Send a "initialized" notification to the language server +def SendInitializedNotif(lspserver: dict<any>) + # Notification: 'initialized' + # Params: InitializedParams + lspserver.sendNotification('initialized') +enddef + +# Request: shutdown +# Param: void +def ShutdownServer(lspserver: dict<any>): void + lspserver.rpc('shutdown', {}) +enddef + +# Send a 'exit' notification to the language server +def ExitServer(lspserver: dict<any>): void + # Notification: 'exit' + # Params: void + lspserver.sendNotification('exit') +enddef + +# Stop a LSP server +def StopServer(lspserver: dict<any>): number + if !lspserver.running + util.WarnMsg($'LSP server {lspserver.name} is not running') + return 0 + endif + + # Send the shutdown request to the server + lspserver.shutdownServer() + + # Notify the server to exit + lspserver.exitServer() + + # Wait for the server to process the exit notification and exit for a + # maximum of 2 seconds. + var maxCount: number = 1000 + while lspserver.job->job_status() == 'run' && maxCount > 0 + sleep 2m + maxCount -= 1 + endwhile + + if lspserver.job->job_status() == 'run' + lspserver.job->job_stop() + endif + lspserver.running = false + lspserver.ready = false + lspserver.requests = {} + return 0 +enddef + +# Set the language server trace level using the '$/setTrace' notification. +# Supported values for "traceVal" are "off", "messages" and "verbose". +def SetTrace(lspserver: dict<any>, traceVal: string) + # Notification: '$/setTrace' + # Params: SetTraceParams + var params = {value: traceVal} + lspserver.sendNotification('$/setTrace', params) +enddef + +# Log a debug message to the LSP server debug file +def TraceLog(lspserver: dict<any>, msg: string) + if lspserver.debug + util.TraceLog(lspserver.logfile, false, msg) + endif +enddef + +# Log an error message to the LSP server error file +def ErrorLog(lspserver: dict<any>, errmsg: string) + if lspserver.debug + util.TraceLog(lspserver.errfile, true, errmsg) + endif +enddef + +# Return the next id for a LSP server request message +def NextReqID(lspserver: dict<any>): number + var id = lspserver.nextID + lspserver.nextID = id + 1 + return id +enddef + +# create a LSP server request message +def CreateRequest(lspserver: dict<any>, method: string): dict<any> + var req = { + jsonrpc: '2.0', + id: lspserver.nextReqID(), + method: method, + params: {} + } + + # Save the request, so that the corresponding response can be processed + lspserver.requests->extend({[req.id->string()]: req}) + + return req +enddef + +# create a LSP server response message +def CreateResponse(lspserver: dict<any>, req_id: number): dict<any> + var resp = { + jsonrpc: '2.0', + id: req_id + } + return resp +enddef + +# create a LSP server notification message +def CreateNotification(lspserver: dict<any>, notif: string): dict<any> + var req = { + jsonrpc: '2.0', + method: notif, + params: {} + } + + return req +enddef + +# send a response message to the server +def SendResponse(lspserver: dict<any>, request: dict<any>, result: any, error: dict<any>) + if (request.id->type() == v:t_string + && (request.id->trim() =~ '[^[:digit:]]\+' + || request.id->trim()->empty())) + || (request.id->type() != v:t_string && request.id->type() != v:t_number) + util.ErrMsg('request.id of response to LSP server is not a correct number') + return + endif + var resp: dict<any> = lspserver.createResponse( + request.id->type() == v:t_string ? request.id->str2nr() : request.id) + if error->empty() + resp->extend({result: result}) + else + resp->extend({error: error}) + endif + lspserver.sendMessage(resp) +enddef + +# Send a request message to LSP server +def SendMessage(lspserver: dict<any>, content: dict<any>): void + var job = lspserver.job + if job->job_status() != 'run' + # LSP server has exited + return + endif + job->ch_sendexpr(content) + if content->has_key('id') + if lspserver.debug + lspserver.traceLog($'{strftime("%m/%d/%y %T")}: Sent {content->string()}') + endif + endif +enddef + +# Send a notification message to the language server +def SendNotification(lspserver: dict<any>, method: string, params: any = {}) + var notif: dict<any> = CreateNotification(lspserver, method) + notif.params->extend(params) + lspserver.sendMessage(notif) +enddef + +# Translate an LSP error code into a readable string +def LspGetErrorMessage(errcode: number): string + var errmap = { + -32001: 'UnknownErrorCode', + -32002: 'ServerNotInitialized', + -32600: 'InvalidRequest', + -32601: 'MethodNotFound', + -32602: 'InvalidParams', + -32603: 'InternalError', + -32700: 'ParseError', + -32800: 'RequestCancelled', + -32801: 'ContentModified', + -32802: 'ServerCancelled', + -32803: 'RequestFailed' + } + + return errmap->get(errcode, errcode->string()) +enddef + +# Process a LSP server response error and display an error message. +def ProcessLspServerError(method: string, responseError: dict<any>) + # request failed + var emsg: string = responseError.message + emsg ..= $', error = {LspGetErrorMessage(responseError.code)}' + if responseError->has_key('data') + emsg ..= $', data = {responseError.data->string()}' + endif + util.ErrMsg($'request {method} failed ({emsg})') +enddef + +# Send a sync RPC request message to the LSP server and return the received +# reply. In case of an error, an empty Dict is returned. +def Rpc(lspserver: dict<any>, method: string, params: any, handleError: bool = true): dict<any> + var req = { + method: method, + params: params + } + + var job = lspserver.job + if job->job_status() != 'run' + # LSP server has exited + return {} + endif + + if lspserver.debug + lspserver.traceLog($'{strftime("%m/%d/%y %T")}: Sent {req->string()}') + endif + + # Do the synchronous RPC call + var reply = job->ch_evalexpr(req) + + if lspserver.debug + lspserver.traceLog($'{strftime("%m/%d/%y %T")}: Received {reply->string()}') + endif + + if reply->has_key('result') + # successful reply + return reply + endif + + if reply->has_key('error') && handleError + # request failed + ProcessLspServerError(method, reply.error) + endif + + return {} +enddef + +# LSP server asynchronous RPC callback +def AsyncRpcCb(lspserver: dict<any>, method: string, RpcCb: func, chan: channel, reply: dict<any>) + if lspserver.debug + lspserver.traceLog($'{strftime("%m/%d/%y %T")}: Received {reply->string()}') + endif + + if reply->empty() + return + endif + + if reply->has_key('error') + # request failed + ProcessLspServerError(method, reply.error) + return + endif + + if !reply->has_key('result') + util.ErrMsg($'request {method} failed (no result)') + return + endif + + RpcCb(lspserver, reply.result) +enddef + +# Send a async RPC request message to the LSP server with a callback function. +# Returns the LSP message id. This id can be used to cancel the RPC request +# (if needed). Returns -1 on error. +def AsyncRpc(lspserver: dict<any>, method: string, params: any, Cbfunc: func): number + var req = { + method: method, + params: params + } + + var job = lspserver.job + if job->job_status() != 'run' + # LSP server has exited + return -1 + endif + + if lspserver.debug + lspserver.traceLog($'{strftime("%m/%d/%y %T")}: Sent {req->string()}') + endif + + # Do the asynchronous RPC call + var Fn = function('AsyncRpcCb', [lspserver, method, Cbfunc]) + + var reply: dict<any> + if get(g:, 'LSPTest') + # When running LSP tests, make this a synchronous RPC call + reply = Rpc(lspserver, method, params) + Fn(test_null_channel(), reply) + else + # Otherwise, make an asynchronous RPC call + reply = job->ch_sendexpr(req, {callback: Fn}) + endif + if reply->empty() + return -1 + endif + + return reply.id +enddef + +# Wait for a response message from the LSP server for the request "req" +# Waits for a maximum of 5 seconds +def WaitForResponse(lspserver: dict<any>, req: dict<any>) + var maxCount: number = 2500 + var key: string = req.id->string() + + while lspserver.requests->has_key(key) && maxCount > 0 + sleep 2m + maxCount -= 1 + endwhile +enddef + +# Returns true when the "lspserver" has "feature" enabled. +# By default, all the features of a lsp server are enabled. +def FeatureEnabled(lspserver: dict<any>, feature: string): bool + return lspserver.features->get(feature, true) +enddef + +# Retrieve the Workspace configuration asked by the server. +# Request: workspace/configuration +def WorkspaceConfigGet(lspserver: dict<any>, configItem: dict<any>): dict<any> + if lspserver.workspaceConfig->empty() + return {} + endif + if !configItem->has_key('section') || configItem.section->empty() + return lspserver.workspaceConfig + endif + var config: dict<any> = lspserver.workspaceConfig + for part in configItem.section->split('\.') + if !config->has_key(part) + return {} + endif + config = config[part] + endfor + return config +enddef + +# Update semantic highlighting for buffer "bnr" +# Request: textDocument/semanticTokens/full or +# textDocument/semanticTokens/full/delta +def SemanticHighlightUpdate(lspserver: dict<any>, bnr: number) + if !lspserver.isSemanticTokensProvider + return + endif + + # Send the pending buffer changes to the language server + bnr->listener_flush() + + var method = 'textDocument/semanticTokens/full' + var params: dict<any> = { + textDocument: { + uri: util.LspBufnrToUri(bnr) + } + } + + # Should we send a semantic tokens delta request instead of a full request? + if lspserver.semanticTokensDelta + var prevResultId: string = '' + prevResultId = bnr->getbufvar('LspSemanticResultId', '') + if prevResultId != '' + # semantic tokens delta request + params.previousResultId = prevResultId + method ..= '/delta' + endif + endif + + var reply = lspserver.rpc(method, params) + + if reply->empty() || reply.result->empty() + return + endif + + semantichighlight.UpdateTokens(lspserver, bnr, reply.result) +enddef + +# Send a "workspace/didChangeConfiguration" notification to the language +# server. +def SendWorkspaceConfig(lspserver: dict<any>) + # Params: DidChangeConfigurationParams + var params = {settings: lspserver.workspaceConfig} + lspserver.sendNotification('workspace/didChangeConfiguration', params) +enddef + +# Send a file/document opened notification to the language server. +def TextdocDidOpen(lspserver: dict<any>, bnr: number, ftype: string): void + # Notification: 'textDocument/didOpen' + # Params: DidOpenTextDocumentParams + var params = { + textDocument: { + uri: util.LspBufnrToUri(bnr), + languageId: ftype, + # Use Vim 'changedtick' as the LSP document version number + version: bnr->getbufvar('changedtick'), + text: bnr->getbufline(1, '$')->join("\n") .. "\n" + } + } + lspserver.sendNotification('textDocument/didOpen', params) +enddef + +# Send a file/document closed notification to the language server. +def TextdocDidClose(lspserver: dict<any>, bnr: number): void + # Notification: 'textDocument/didClose' + # Params: DidCloseTextDocumentParams + var params = { + textDocument: { + uri: util.LspBufnrToUri(bnr) + } + } + lspserver.sendNotification('textDocument/didClose', params) +enddef + +# Send a file/document change notification to the language server. +# Params: DidChangeTextDocumentParams +def TextdocDidChange(lspserver: dict<any>, bnr: number, start: number, + end: number, added: number, + changes: list<dict<number>>): void + # Notification: 'textDocument/didChange' + # Params: DidChangeTextDocumentParams + + # var changeset: list<dict<any>> + + ##### FIXME: Sending specific buffer changes to the LSP server doesn't + ##### work properly as the computed line range numbers is not correct. + ##### For now, send the entire buffer content to LSP server. + # # Range + # for change in changes + # var lines: string + # var start_lnum: number + # var end_lnum: number + # var start_col: number + # var end_col: number + # if change.added == 0 + # # lines changed + # start_lnum = change.lnum - 1 + # end_lnum = change.end - 1 + # lines = getbufline(bnr, change.lnum, change.end - 1)->join("\n") .. "\n" + # start_col = 0 + # end_col = 0 + # elseif change.added > 0 + # # lines added + # start_lnum = change.lnum - 1 + # end_lnum = change.lnum - 1 + # start_col = 0 + # end_col = 0 + # lines = getbufline(bnr, change.lnum, change.lnum + change.added - 1)->join("\n") .. "\n" + # else + # # lines removed + # start_lnum = change.lnum - 1 + # end_lnum = change.lnum + (-change.added) - 1 + # start_col = 0 + # end_col = 0 + # lines = '' + # endif + # var range: dict<dict<number>> = {'start': {'line': start_lnum, 'character': start_col}, 'end': {'line': end_lnum, 'character': end_col}} + # changeset->add({'range': range, 'text': lines}) + # endfor + + var params = { + textDocument: { + uri: util.LspBufnrToUri(bnr), + # Use Vim 'changedtick' as the LSP document version number + version: bnr->getbufvar('changedtick') + }, + contentChanges: [ + {text: bnr->getbufline(1, '$')->join("\n") .. "\n"} + ] + } + lspserver.sendNotification('textDocument/didChange', params) +enddef + +# Return the current cursor position as a LSP position. +# find_ident will search for a identifier in front of the cursor, just like +# CTRL-] and c_CTRL-R_CTRL-W does. +# +# LSP line and column numbers start from zero, whereas Vim line and column +# numbers start from one. The LSP column number is the character index in the +# line and not the byte index in the line. +def GetPosition(lspserver: dict<any>, find_ident: bool): dict<number> + var lnum: number = line('.') - 1 + var col: number = charcol('.') - 1 + var line = getline('.') + + if find_ident + # 1. skip to start of identifier + while line[col] != '' && line[col] !~ '\k' + col = col + 1 + endwhile + + # 2. back up to start of identifier + while col > 0 && line[col - 1] =~ '\k' + col = col - 1 + endwhile + endif + + # Compute character index counting composing characters as separate + # characters + var pos = {line: lnum, character: util.GetCharIdxWithCompChar(line, col)} + lspserver.encodePosition(bufnr(), pos) + + return pos +enddef + +# Return the current file name and current cursor position as a LSP +# TextDocumentPositionParams structure +def GetTextDocPosition(lspserver: dict<any>, find_ident: bool): dict<dict<any>> + # interface TextDocumentIdentifier + # interface Position + return {textDocument: {uri: util.LspFileToUri(@%)}, + position: lspserver.getPosition(find_ident)} +enddef + +# Get a list of completion items. +# Request: "textDocument/completion" +# Param: CompletionParams +def GetCompletion(lspserver: dict<any>, triggerKind_arg: number, triggerChar: string): void + # Check whether LSP server supports completion + if !lspserver.isCompletionProvider + util.ErrMsg('LSP server does not support completion') + return + endif + + var fname = @% + if fname->empty() + return + endif + + # interface CompletionParams + # interface TextDocumentPositionParams + var params = lspserver.getTextDocPosition(false) + # interface CompletionContext + params.context = {triggerKind: triggerKind_arg, triggerCharacter: triggerChar} + + lspserver.rpc_a('textDocument/completion', params, + completion.CompletionReply) +enddef + +# Get lazy properties for a completion item. +# Request: "completionItem/resolve" +# Param: CompletionItem +def ResolveCompletion(lspserver: dict<any>, item: dict<any>, sync: bool = false): dict<any> + # Check whether LSP server supports completion item resolve + if !lspserver.isCompletionResolveProvider + return {} + endif + + # interface CompletionItem + if sync + var reply = lspserver.rpc('completionItem/resolve', item) + if !reply->empty() && !reply.result->empty() + return reply.result + endif + else + lspserver.rpc_a('completionItem/resolve', item, + completion.CompletionResolveReply) + endif + return {} +enddef + +# Jump to or peek a symbol location. +# +# Send 'msg' to a LSP server and process the reply. 'msg' is one of the +# following: +# textDocument/definition +# textDocument/declaration +# textDocument/typeDefinition +# textDocument/implementation +# +# Process the LSP server reply and jump to the symbol location. Before +# jumping to the symbol location, save the current cursor position in the tag +# stack. +# +# If 'peekSymbol' is true, then display the symbol location in the preview +# window but don't jump to the symbol location. +# +# Result: Location | Location[] | LocationLink[] | null +def GotoSymbolLoc(lspserver: dict<any>, msg: string, peekSymbol: bool, + cmdmods: string, count: number) + var reply = lspserver.rpc(msg, lspserver.getTextDocPosition(true), false) + if reply->empty() || reply.result->empty() + var emsg: string + if msg == 'textDocument/declaration' + emsg = 'symbol declaration is not found' + elseif msg == 'textDocument/typeDefinition' + emsg = 'symbol type definition is not found' + elseif msg == 'textDocument/implementation' + emsg = 'symbol implementation is not found' + else + emsg = 'symbol definition is not found' + endif + + util.WarnMsg(emsg) + return + endif + + var result = reply.result + var location: dict<any> + if result->type() == v:t_list + if count == 0 + # When there are multiple symbol locations, and a specific one isn't + # requested with 'count', display the locations in a location list. + if result->len() > 1 + var title: string = '' + if msg == 'textDocument/declaration' + title = 'Declarations' + elseif msg == 'textDocument/typeDefinition' + title = 'Type Definitions' + elseif msg == 'textDocument/implementation' + title = 'Implementations' + else + title = 'Definitions' + endif + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the symbol locations + result->map((_, loc) => { + lspserver.decodeLocation(loc) + return loc + }) + endif + + symbol.ShowLocations(lspserver, result, peekSymbol, title) + return + endif + endif + + # Select the location requested in 'count' + var idx = count - 1 + if idx >= result->len() + idx = result->len() - 1 + endif + location = result[idx] + else + location = result + endif + lspserver.decodeLocation(location) + + symbol.GotoSymbol(lspserver, location, peekSymbol, cmdmods) +enddef + +# Request: "textDocument/definition" +# Param: DefinitionParams +def GotoDefinition(lspserver: dict<any>, peek: bool, cmdmods: string, count: number) + # Check whether LSP server supports jumping to a definition + if !lspserver.isDefinitionProvider + util.ErrMsg('Jumping to a symbol definition is not supported') + return + endif + + # interface DefinitionParams + # interface TextDocumentPositionParams + GotoSymbolLoc(lspserver, 'textDocument/definition', peek, cmdmods, count) +enddef + +# Request: "textDocument/declaration" +# Param: DeclarationParams +def GotoDeclaration(lspserver: dict<any>, peek: bool, cmdmods: string, count: number) + # Check whether LSP server supports jumping to a declaration + if !lspserver.isDeclarationProvider + util.ErrMsg('Jumping to a symbol declaration is not supported') + return + endif + + # interface DeclarationParams + # interface TextDocumentPositionParams + GotoSymbolLoc(lspserver, 'textDocument/declaration', peek, cmdmods, count) +enddef + +# Request: "textDocument/typeDefinition" +# Param: TypeDefinitionParams +def GotoTypeDef(lspserver: dict<any>, peek: bool, cmdmods: string, count: number) + # Check whether LSP server supports jumping to a type definition + if !lspserver.isTypeDefinitionProvider + util.ErrMsg('Jumping to a symbol type definition is not supported') + return + endif + + # interface TypeDefinitionParams + # interface TextDocumentPositionParams + GotoSymbolLoc(lspserver, 'textDocument/typeDefinition', peek, cmdmods, count) +enddef + +# Request: "textDocument/implementation" +# Param: ImplementationParams +def GotoImplementation(lspserver: dict<any>, peek: bool, cmdmods: string, count: number) + # Check whether LSP server supports jumping to a implementation + if !lspserver.isImplementationProvider + util.ErrMsg('Jumping to a symbol implementation is not supported') + return + endif + + # interface ImplementationParams + # interface TextDocumentPositionParams + GotoSymbolLoc(lspserver, 'textDocument/implementation', peek, cmdmods, count) +enddef + +# Request: "textDocument/switchSourceHeader" +# Param: TextDocumentIdentifier +# Clangd specific extension +def SwitchSourceHeader(lspserver: dict<any>) + var param = { + uri: util.LspFileToUri(@%) + } + var reply = lspserver.rpc('textDocument/switchSourceHeader', param) + if reply->empty() || reply.result->empty() + util.WarnMsg('Source/Header file is not found') + return + endif + + # process the 'textDocument/switchSourceHeader' reply from the LSP server + # Result: URI | null + var fname = util.LspUriToFile(reply.result) + # TODO: Add support for cmd modifiers + if (&modified && !&hidden) || &buftype != '' + # if the current buffer has unsaved changes and 'hidden' is not set, + # or if the current buffer is a special buffer, then ask to save changes + exe $'confirm edit {fname}' + else + exe $'edit {fname}' + endif +enddef + +# get symbol signature help. +# Request: "textDocument/signatureHelp" +# Param: SignatureHelpParams +def ShowSignature(lspserver: dict<any>): void + # Check whether LSP server supports signature help + if !lspserver.isSignatureHelpProvider + util.ErrMsg('LSP server does not support signature help') + return + endif + + # interface SignatureHelpParams + # interface TextDocumentPositionParams + var params = lspserver.getTextDocPosition(false) + lspserver.rpc_a('textDocument/signatureHelp', params, + signature.SignatureHelp) +enddef + +# Send a file/document saved notification to the language server +def DidSaveFile(lspserver: dict<any>, bnr: number): void + # Check whether the LSP server supports the didSave notification + if !lspserver.supportsDidSave + # LSP server doesn't support text document synchronization + return + endif + + # Notification: 'textDocument/didSave' + # Params: DidSaveTextDocumentParams + var params: dict<any> = {textDocument: {uri: util.LspBufnrToUri(bnr)}} + + if lspserver.caps.textDocumentSync->type() == v:t_dict + && lspserver.caps.textDocumentSync->has_key('save') + if lspserver.caps.textDocumentSync.save->type() == v:t_dict + && lspserver.caps.textDocumentSync.save->has_key('includeText') + && lspserver.caps.textDocumentSync.save.includeText + params.text = bnr->getbufline(1, '$')->join("\n") .. "\n" + endif + endif + + lspserver.sendNotification('textDocument/didSave', params) +enddef + +# get the hover information +# Request: "textDocument/hover" +# Param: HoverParams +def ShowHoverInfo(lspserver: dict<any>, cmdmods: string): void + # Check whether LSP server supports getting hover information. + # caps->hoverProvider can be a "boolean" or "HoverOptions" + if !lspserver.isHoverProvider + return + endif + + # interface HoverParams + # interface TextDocumentPositionParams + var params = lspserver.getTextDocPosition(false) + lspserver.rpc_a('textDocument/hover', params, (_, reply) => { + hover.HoverReply(lspserver, reply, cmdmods) + }) +enddef + +# Request: "textDocument/references" +# Param: ReferenceParams +def ShowReferences(lspserver: dict<any>, peek: bool): void + # Check whether LSP server supports getting reference information + if !lspserver.isReferencesProvider + util.ErrMsg('LSP server does not support showing references') + return + endif + + # interface ReferenceParams + # interface TextDocumentPositionParams + var param: dict<any> + param = lspserver.getTextDocPosition(true) + param.context = {includeDeclaration: true} + var reply = lspserver.rpc('textDocument/references', param) + + # Result: Location[] | null + if reply->empty() || reply.result->empty() + util.WarnMsg('No references found') + return + endif + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the reference locations + reply.result->map((_, loc) => { + lspserver.decodeLocation(loc) + return loc + }) + endif + + symbol.ShowLocations(lspserver, reply.result, peek, 'Symbol References') +enddef + +# process the 'textDocument/documentHighlight' reply from the LSP server +# Result: DocumentHighlight[] | null +def DocHighlightReply(lspserver: dict<any>, docHighlightReply: any, + bnr: number, cmdmods: string): void + if docHighlightReply->empty() + if cmdmods !~ 'silent' + util.WarnMsg($'No highlight for the current position') + endif + return + endif + + for docHL in docHighlightReply + lspserver.decodeRange(bnr, docHL.range) + var kind: number = docHL->get('kind', 1) + var propName: string + if kind == 2 + # Read-access + propName = 'LspReadRef' + elseif kind == 3 + # Write-access + propName = 'LspWriteRef' + else + # textual reference + propName = 'LspTextRef' + endif + try + var docHL_range = docHL.range + var docHL_start = docHL_range.start + var docHL_end = docHL_range.end + prop_add(docHL_start.line + 1, + util.GetLineByteFromPos(bnr, docHL_start) + 1, + {end_lnum: docHL_end.line + 1, + end_col: util.GetLineByteFromPos(bnr, docHL_end) + 1, + bufnr: bnr, + type: propName}) + catch /E966\|E964/ # Invalid lnum | Invalid col + # Highlight replies arrive asynchronously and the document might have + # been modified in the mean time. As the reply is stale, ignore invalid + # line number and column number errors. + endtry + endfor +enddef + +# Request: "textDocument/documentHighlight" +# Param: DocumentHighlightParams +def DocHighlight(lspserver: dict<any>, bnr: number, cmdmods: string): void + # Check whether LSP server supports getting highlight information + if !lspserver.isDocumentHighlightProvider + util.ErrMsg('LSP server does not support document highlight') + return + endif + + # Send the pending buffer changes to the language server + bnr->listener_flush() + + # interface DocumentHighlightParams + # interface TextDocumentPositionParams + var params = lspserver.getTextDocPosition(false) + lspserver.rpc_a('textDocument/documentHighlight', params, (_, reply) => { + DocHighlightReply(lspserver, reply, bufnr(), cmdmods) + }) +enddef + +# Request: "textDocument/documentSymbol" +# Param: DocumentSymbolParams +def GetDocSymbols(lspserver: dict<any>, fname: string, showOutline: bool): void + # Check whether LSP server supports getting document symbol information + if !lspserver.isDocumentSymbolProvider + util.ErrMsg('LSP server does not support getting list of symbols') + return + endif + + # interface DocumentSymbolParams + # interface TextDocumentIdentifier + var params = {textDocument: {uri: util.LspFileToUri(fname)}} + lspserver.rpc_a('textDocument/documentSymbol', params, (_, reply) => { + if showOutline + symbol.DocSymbolOutline(lspserver, reply, fname) + else + symbol.DocSymbolPopup(lspserver, reply, fname) + endif + }) +enddef + +# Request: "textDocument/formatting" +# Param: DocumentFormattingParams +# or +# Request: "textDocument/rangeFormatting" +# Param: DocumentRangeFormattingParams +def TextDocFormat(lspserver: dict<any>, fname: string, rangeFormat: bool, + start_lnum: number, end_lnum: number) + # Check whether LSP server supports formatting documents + if !lspserver.isDocumentFormattingProvider + util.ErrMsg('LSP server does not support formatting documents') + return + endif + + var cmd: string + if rangeFormat + cmd = 'textDocument/rangeFormatting' + else + cmd = 'textDocument/formatting' + endif + + # interface DocumentFormattingParams + # interface TextDocumentIdentifier + # interface FormattingOptions + var fmtopts: dict<any> = { + tabSize: shiftwidth(), + insertSpaces: &expandtab ? true : false, + } + var param = { + textDocument: { + uri: util.LspFileToUri(fname) + }, + options: fmtopts + } + + if rangeFormat + var r: dict<dict<number>> = { + start: {line: start_lnum - 1, character: 0}, + end: {line: end_lnum - 1, character: charcol([end_lnum, '$']) - 1}} + param.range = r + endif + + var reply = lspserver.rpc(cmd, param) + + # result: TextEdit[] | null + + if reply->empty() || reply.result->empty() + # nothing to format + return + endif + + var bnr: number = fname->bufnr() + if bnr == -1 + # file is already removed + return + endif + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the reference locations + reply.result->map((_, textEdit) => { + lspserver.decodeRange(bnr, textEdit.range) + return textEdit + }) + endif + + # interface TextEdit + # Apply each of the text edit operations + var save_cursor: list<number> = getcurpos() + textedit.ApplyTextEdits(bnr, reply.result) + save_cursor->setpos('.') +enddef + +# Request: "textDocument/prepareCallHierarchy" +def PrepareCallHierarchy(lspserver: dict<any>): dict<any> + # interface CallHierarchyPrepareParams + # interface TextDocumentPositionParams + var param: dict<any> + param = lspserver.getTextDocPosition(false) + var reply = lspserver.rpc('textDocument/prepareCallHierarchy', param) + if reply->empty() || reply.result->empty() + return {} + endif + + # Result: CallHierarchyItem[] | null + var choice: number = 1 + if reply.result->len() > 1 + var items: list<string> = ['Select a Call Hierarchy Item:'] + for i in reply.result->len()->range() + items->add(printf("%d. %s", i + 1, reply.result[i].name)) + endfor + choice = items->inputlist() + if choice < 1 || choice > items->len() + return {} + endif + endif + + return reply.result[choice - 1] +enddef + +# Request: "callHierarchy/incomingCalls" +def IncomingCalls(lspserver: dict<any>, fname: string) + # Check whether LSP server supports call hierarchy + if !lspserver.isCallHierarchyProvider + util.ErrMsg('LSP server does not support call hierarchy') + return + endif + + callhier.IncomingCalls(lspserver) +enddef + +def GetIncomingCalls(lspserver: dict<any>, item_arg: dict<any>): any + # Request: "callHierarchy/incomingCalls" + # Param: CallHierarchyIncomingCallsParams + var param = { + item: item_arg + } + var reply = lspserver.rpc('callHierarchy/incomingCalls', param) + if reply->empty() + return null + endif + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the incoming call locations + var bnr = util.LspUriToBufnr(item_arg.uri) + reply.result->map((_, hierItem) => { + lspserver.decodeRange(bnr, hierItem.from.range) + return hierItem + }) + endif + + return reply.result +enddef + +# Request: "callHierarchy/outgoingCalls" +def OutgoingCalls(lspserver: dict<any>, fname: string) + # Check whether LSP server supports call hierarchy + if !lspserver.isCallHierarchyProvider + util.ErrMsg('LSP server does not support call hierarchy') + return + endif + + callhier.OutgoingCalls(lspserver) +enddef + +def GetOutgoingCalls(lspserver: dict<any>, item_arg: dict<any>): any + # Request: "callHierarchy/outgoingCalls" + # Param: CallHierarchyOutgoingCallsParams + var param = { + item: item_arg + } + var reply = lspserver.rpc('callHierarchy/outgoingCalls', param) + if reply->empty() + return null + endif + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the outgoing call locations + var bnr = util.LspUriToBufnr(item_arg.uri) + reply.result->map((_, hierItem) => { + lspserver.decodeRange(bnr, hierItem.to.range) + return hierItem + }) + endif + + return reply.result +enddef + +# Request: "textDocument/inlayHint" +# Inlay hints. +def InlayHintsShow(lspserver: dict<any>, bnr: number) + # Check whether LSP server supports type hierarchy + if !lspserver.isInlayHintProvider && !lspserver.isClangdInlayHintsProvider + util.ErrMsg('LSP server does not support inlay hint') + return + endif + + # Send the pending buffer changes to the language server + bnr->listener_flush() + + var binfo = bnr->getbufinfo() + if binfo->empty() + return + endif + var lastlnum = binfo[0].linecount + var lastline = bnr->getbufline('$') + var lastcol = 1 + if !lastline->empty() && !lastline[0]->empty() + lastcol = lastline[0]->strchars() + endif + var param = { + textDocument: {uri: util.LspBufnrToUri(bnr)}, + range: + { + start: {line: 0, character: 0}, + end: {line: lastlnum - 1, character: lastcol - 1} + } + } + + lspserver.encodeRange(bnr, param.range) + + var msg: string + if lspserver.isClangdInlayHintsProvider + # clangd-style inlay hints + msg = 'clangd/inlayHints' + else + msg = 'textDocument/inlayHint' + endif + var reply = lspserver.rpc_a(msg, param, (_, reply) => { + inlayhints.InlayHintsReply(lspserver, bnr, reply) + }) +enddef + +def DecodeTypeHierarchy(lspserver: dict<any>, isSuper: bool, typeHier: dict<any>) + if !lspserver.needOffsetEncoding + return + endif + var bnr = util.LspUriToBufnr(typeHier.uri) + lspserver.decodeRange(bnr, typeHier.range) + lspserver.decodeRange(bnr, typeHier.selectionRange) + var subType: list<dict<any>> + if isSuper + subType = typeHier->get('parents', []) + else + subType = typeHier->get('children', []) + endif + if !subType->empty() + # Decode the position encoding in all the type hierarchy items + subType->map((_, typeHierItem) => { + DecodeTypeHierarchy(lspserver, isSuper, typeHierItem) + return typeHierItem + }) + endif +enddef + +# Request: "textDocument/typehierarchy" +# Support the clangd version of type hierarchy retrieval method. +# The method described in the LSP 3.17.0 standard is not supported as clangd +# doesn't support that method. +def TypeHierarchy(lspserver: dict<any>, direction: number) + # Check whether LSP server supports type hierarchy + if !lspserver.isTypeHierarchyProvider + util.ErrMsg('LSP server does not support type hierarchy') + return + endif + + # interface TypeHierarchy + # interface TextDocumentPositionParams + var param: dict<any> + param = lspserver.getTextDocPosition(false) + # 0: children, 1: parent, 2: both + param.direction = direction + param.resolve = 5 + var reply = lspserver.rpc('textDocument/typeHierarchy', param) + if reply->empty() || reply.result->empty() + util.WarnMsg('No type hierarchy available') + return + endif + + var isSuper = (direction == 1) + + DecodeTypeHierarchy(lspserver, isSuper, reply.result) + + typehier.ShowTypeHierarchy(lspserver, isSuper, reply.result) +enddef + +# Decode the ranges in "WorkspaceEdit" +def DecodeWorkspaceEdit(lspserver: dict<any>, workspaceEdit: dict<any>) + if !lspserver.needOffsetEncoding + return + endif + if workspaceEdit->has_key('changes') + for [uri, changes] in workspaceEdit.changes->items() + var bnr: number = util.LspUriToBufnr(uri) + if bnr <= 0 + continue + endif + # Decode the position encoding in all the text edit locations + changes->map((_, textEdit) => { + lspserver.decodeRange(bnr, textEdit.range) + return textEdit + }) + endfor + endif + + if workspaceEdit->has_key('documentChanges') + for change in workspaceEdit.documentChanges + if !change->has_key('kind') + var bnr: number = util.LspUriToBufnr(change.textDocument.uri) + if bnr <= 0 + continue + endif + # Decode the position encoding in all the text edit locations + change.edits->map((_, textEdit) => { + lspserver.decodeRange(bnr, textEdit.range) + return textEdit + }) + endif + endfor + endif +enddef + +# Request: "textDocument/rename" +# Param: RenameParams +def RenameSymbol(lspserver: dict<any>, newName: string) + # Check whether LSP server supports rename operation + if !lspserver.isRenameProvider + util.ErrMsg('LSP server does not support rename operation') + return + endif + + # interface RenameParams + # interface TextDocumentPositionParams + var param: dict<any> = {} + param = lspserver.getTextDocPosition(true) + param.newName = newName + + var reply = lspserver.rpc('textDocument/rename', param) + + # Result: WorkspaceEdit | null + if reply->empty() || reply.result->empty() + # nothing to rename + return + endif + + # result: WorkspaceEdit + DecodeWorkspaceEdit(lspserver, reply.result) + textedit.ApplyWorkspaceEdit(reply.result) +enddef + +# Decode the range in "CodeAction" +def DecodeCodeAction(lspserver: dict<any>, actionList: list<dict<any>>) + if !lspserver.needOffsetEncoding + return + endif + actionList->map((_, act) => { + if !act->has_key('disabled') && act->has_key('edit') + DecodeWorkspaceEdit(lspserver, act.edit) + endif + return act + }) +enddef + +# Request: "textDocument/codeAction" +# Param: CodeActionParams +def CodeAction(lspserver: dict<any>, fname_arg: string, line1: number, + line2: number, query: string) + # Check whether LSP server supports code action operation + if !lspserver.isCodeActionProvider + util.ErrMsg('LSP server does not support code action operation') + return + endif + + # interface CodeActionParams + var params: dict<any> = {} + var fname: string = fname_arg->fnamemodify(':p') + var bnr: number = fname_arg->bufnr() + var r: dict<dict<number>> = { + start: { + line: line1 - 1, + character: line1 == line2 ? util.GetCharIdxWithCompChar(getline('.'), charcol('.') - 1) : 0 + }, + end: { + line: line2 - 1, + character: util.GetCharIdxWithCompChar(getline(line2), charcol([line2, '$']) - 1) + } + } + lspserver.encodeRange(bnr, r) + params->extend({textDocument: {uri: util.LspFileToUri(fname)}, range: r}) + var d: list<dict<any>> = [] + for lnum in range(line1, line2) + var diagsInfo: list<dict<any>> = diag.GetDiagsByLine(bnr, lnum, lspserver)->deepcopy() + if lspserver.needOffsetEncoding + diagsInfo->map((_, di) => { + lspserver.encodeRange(bnr, di.range) + return di + }) + endif + d->extend(diagsInfo) + endfor + params->extend({context: {diagnostics: d, triggerKind: 1}}) + + var reply = lspserver.rpc('textDocument/codeAction', params) + + # Result: (Command | CodeAction)[] | null + if reply->empty() || reply.result->empty() + # no action can be performed + util.WarnMsg('No code action is available') + return + endif + + DecodeCodeAction(lspserver, reply.result) + + codeaction.ApplyCodeAction(lspserver, reply.result, query) +enddef + +# Request: "textDocument/codeLens" +# Param: CodeLensParams +def CodeLens(lspserver: dict<any>, fname: string) + # Check whether LSP server supports code lens operation + if !lspserver.isCodeLensProvider + util.ErrMsg('LSP server does not support code lens operation') + return + endif + + var params = {textDocument: {uri: util.LspFileToUri(fname)}} + var reply = lspserver.rpc('textDocument/codeLens', params) + if reply->empty() || reply.result->empty() + util.WarnMsg($'No code lens actions found for the current file') + return + endif + + var bnr = fname->bufnr() + + # Decode the position encoding in all the code lens items + if lspserver.needOffsetEncoding + reply.result->map((_, codeLensItem) => { + lspserver.decodeRange(bnr, codeLensItem.range) + return codeLensItem + }) + endif + + codelens.ProcessCodeLens(lspserver, bnr, reply.result) +enddef + +# Request: "codeLens/resolve" +# Param: CodeLens +def ResolveCodeLens(lspserver: dict<any>, bnr: number, + codeLens: dict<any>): dict<any> + if !lspserver.isCodeLensResolveProvider + return {} + endif + + if lspserver.needOffsetEncoding + lspserver.encodeRange(bnr, codeLens.range) + endif + + var reply = lspserver.rpc('codeLens/resolve', codeLens) + if reply->empty() + return {} + endif + + var codeLensItem: dict<any> = reply.result + + # Decode the position encoding in the code lens item + if lspserver.needOffsetEncoding + lspserver.decodeRange(bnr, codeLensItem.range) + endif + + return codeLensItem +enddef + +# List project-wide symbols matching query string +# Request: "workspace/symbol" +# Param: WorkspaceSymbolParams +def WorkspaceQuerySymbols(lspserver: dict<any>, query: string, firstCall: bool, cmdmods: string = '') + # Check whether the LSP server supports listing workspace symbols + if !lspserver.isWorkspaceSymbolProvider + util.ErrMsg('LSP server does not support listing workspace symbols') + return + endif + + # Param: WorkspaceSymbolParams + var param = { + query: query + } + var reply = lspserver.rpc('workspace/symbol', param) + if reply->empty() || reply.result->empty() + util.WarnMsg($'Symbol "{query}" is not found') + return + endif + + var symInfo: list<dict<any>> = reply.result + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the symbol locations + symInfo->map((_, sym) => { + if sym->has_key('location') + lspserver.decodeLocation(sym.location) + endif + return sym + }) + endif + + if firstCall && symInfo->len() == 1 + # If there is only one symbol, then jump to the symbol location + var symLoc: dict<any> = symInfo[0]->get('location', {}) + if !symLoc->empty() + symbol.GotoSymbol(lspserver, symLoc, false, cmdmods) + endif + else + symbol.WorkspaceSymbolPopup(lspserver, query, symInfo, cmdmods) + endif +enddef + +# Add a workspace folder to the language server. +def AddWorkspaceFolder(lspserver: dict<any>, dirName: string): void + if !lspserver.caps->has_key('workspace') + || !lspserver.caps.workspace->has_key('workspaceFolders') + || !lspserver.caps.workspace.workspaceFolders->has_key('supported') + || !lspserver.caps.workspace.workspaceFolders.supported + util.ErrMsg('LSP server does not support workspace folders') + return + endif + + if lspserver.workspaceFolders->index(dirName) != -1 + util.ErrMsg($'{dirName} is already part of this workspace') + return + endif + + # Notification: 'workspace/didChangeWorkspaceFolders' + # Params: DidChangeWorkspaceFoldersParams + var params = {event: {added: [dirName], removed: []}} + lspserver.sendNotification('workspace/didChangeWorkspaceFolders', params) + + lspserver.workspaceFolders->add(dirName) +enddef + +# Remove a workspace folder from the language server. +def RemoveWorkspaceFolder(lspserver: dict<any>, dirName: string): void + if !lspserver.caps->has_key('workspace') + || !lspserver.caps.workspace->has_key('workspaceFolders') + || !lspserver.caps.workspace.workspaceFolders->has_key('supported') + || !lspserver.caps.workspace.workspaceFolders.supported + util.ErrMsg('LSP server does not support workspace folders') + return + endif + + var idx: number = lspserver.workspaceFolders->index(dirName) + if idx == -1 + util.ErrMsg($'{dirName} is not currently part of this workspace') + return + endif + + # Notification: "workspace/didChangeWorkspaceFolders" + # Param: DidChangeWorkspaceFoldersParams + var params = {event: {added: [], removed: [dirName]}} + lspserver.sendNotification('workspace/didChangeWorkspaceFolders', params) + + lspserver.workspaceFolders->remove(idx) +enddef + +def DecodeSelectionRange(lspserver: dict<any>, bnr: number, selRange: dict<any>) + lspserver.decodeRange(bnr, selRange.range) + if selRange->has_key('parent') + DecodeSelectionRange(lspserver, bnr, selRange.parent) + endif +enddef + +# select the text around the current cursor location +# Request: "textDocument/selectionRange" +# Param: SelectionRangeParams +def SelectionRange(lspserver: dict<any>, fname: string) + # Check whether LSP server supports selection ranges + if !lspserver.isSelectionRangeProvider + util.ErrMsg('LSP server does not support selection ranges') + return + endif + + # clear the previous selection reply + lspserver.selection = {} + + # interface SelectionRangeParams + # interface TextDocumentIdentifier + var param = { + textDocument: { + uri: util.LspFileToUri(fname) + }, + positions: [lspserver.getPosition(false)] + } + var reply = lspserver.rpc('textDocument/selectionRange', param) + + if reply->empty() || reply.result->empty() + return + endif + + # Decode the position encoding in all the selection range items + if lspserver.needOffsetEncoding + var bnr = fname->bufnr() + reply.result->map((_, selItem) => { + DecodeSelectionRange(lspserver, bnr, selItem) + return selItem + }) + endif + + selection.SelectionStart(lspserver, reply.result) +enddef + +# Expand the previous selection or start a new one +def SelectionExpand(lspserver: dict<any>) + # Check whether LSP server supports selection ranges + if !lspserver.isSelectionRangeProvider + util.ErrMsg('LSP server does not support selection ranges') + return + endif + + selection.SelectionModify(lspserver, true) +enddef + +# Shrink the previous selection or start a new one +def SelectionShrink(lspserver: dict<any>) + # Check whether LSP server supports selection ranges + if !lspserver.isSelectionRangeProvider + util.ErrMsg('LSP server does not support selection ranges') + return + endif + + selection.SelectionModify(lspserver, false) +enddef + +# fold the entire document +# Request: "textDocument/foldingRange" +# Param: FoldingRangeParams +def FoldRange(lspserver: dict<any>, fname: string) + # Check whether LSP server supports fold ranges + if !lspserver.isFoldingRangeProvider + util.ErrMsg('LSP server does not support folding') + return + endif + + # interface FoldingRangeParams + # interface TextDocumentIdentifier + var params = {textDocument: {uri: util.LspFileToUri(fname)}} + var reply = lspserver.rpc('textDocument/foldingRange', params) + if reply->empty() || reply.result->empty() + return + endif + + # result: FoldingRange[] + var end_lnum: number + var last_lnum: number = line('$') + for foldRange in reply.result + end_lnum = foldRange.endLine + 1 + if end_lnum < foldRange.startLine + 2 + end_lnum = foldRange.startLine + 2 + endif + exe $':{foldRange.startLine + 2}, {end_lnum}fold' + # Open all the folds, otherwise the subsequently created folds are not + # correct. + :silent! foldopen! + endfor + + if &foldcolumn == 0 + :setlocal foldcolumn=2 + endif +enddef + +# process the 'workspace/executeCommand' reply from the LSP server +# Result: any | null +def WorkspaceExecuteReply(lspserver: dict<any>, execReply: any) + # Nothing to do for the reply +enddef + +# Request the LSP server to execute a command +# Request: workspace/executeCommand +# Params: ExecuteCommandParams +def ExecuteCommand(lspserver: dict<any>, cmd: dict<any>) + # Need to check for lspserver.caps.executeCommandProvider? + var params: dict<any> = {} + params.command = cmd.command + if cmd->has_key('arguments') + params.arguments = cmd.arguments + endif + + lspserver.rpc_a('workspace/executeCommand', params, WorkspaceExecuteReply) +enddef + +# Display the LSP server capabilities (received during the initialization +# stage). +def GetCapabilities(lspserver: dict<any>): list<string> + var l = [] + var heading = $"'{lspserver.path}' Language Server Capabilities" + var underlines = repeat('=', heading->len()) + l->extend([heading, underlines]) + for k in lspserver.caps->keys()->sort() + l->add($'{k}: {lspserver.caps[k]->string()}') + endfor + return l +enddef + +# Display the LSP server initialize request and result +def GetInitializeRequest(lspserver: dict<any>): list<string> + var l = [] + var heading = $"'{lspserver.path}' Language Server Initialize Request" + var underlines = repeat('=', heading->len()) + l->extend([heading, underlines]) + if lspserver->has_key('rpcInitializeRequest') + for k in lspserver.rpcInitializeRequest->keys()->sort() + l->add($'{k}: {lspserver.rpcInitializeRequest[k]->string()}') + endfor + endif + return l +enddef + +# Store a log or trace message received from the language server. +def AddMessage(lspserver: dict<any>, msgType: string, newMsg: string) + # A single message may contain multiple lines separate by newline + var msgs = newMsg->split("\n") + lspserver.messages->add($'{strftime("%m/%d/%y %T")}: [{msgType}]: {msgs[0]}') + lspserver.messages->extend(msgs[1 : ]) + # Keep only the last 500 messages to reduce the memory usage + if lspserver.messages->len() >= 600 + lspserver.messages = lspserver.messages[-500 : ] + endif +enddef + +# Display the log messages received from the LSP server (window/logMessage) +def GetMessages(lspserver: dict<any>): list<string> + if lspserver.messages->empty() + return [$'No messages received from "{lspserver.name}" server'] + endif + + var l = [] + var heading = $"'{lspserver.path}' Language Server Messages" + var underlines = repeat('=', heading->len()) + l->extend([heading, underlines]) + l->extend(lspserver.messages) + return l +enddef + +# Send a 'textDocument/definition' request to the LSP server to get the +# location where the symbol under the cursor is defined and return a list of +# Dicts in a format accepted by the 'tagfunc' option. +# Returns null if the LSP server doesn't support getting the location of a +# symbol definition or the symbol is not defined. +def TagFunc(lspserver: dict<any>, pat: string, flags: string, info: dict<any>): any + # Check whether LSP server supports getting the location of a definition + if !lspserver.isDefinitionProvider + return null + endif + + # interface DefinitionParams + # interface TextDocumentPositionParams + var reply = lspserver.rpc('textDocument/definition', + lspserver.getTextDocPosition(false)) + if reply->empty() || reply.result->empty() + return null + endif + + var taglocations: list<dict<any>> + if reply.result->type() == v:t_list + taglocations = reply.result + else + taglocations = [reply.result] + endif + + if lspserver.needOffsetEncoding + # Decode the position encoding in all the reference locations + taglocations->map((_, loc) => { + lspserver.decodeLocation(loc) + return loc + }) + endif + + return symbol.TagFunc(lspserver, taglocations, pat) +enddef + +# Returns unique ID used for identifying the various servers +var UniqueServerIdCounter = 0 +def GetUniqueServerId(): number + UniqueServerIdCounter = UniqueServerIdCounter + 1 + return UniqueServerIdCounter +enddef + +export def NewLspServer(serverParams: dict<any>): dict<any> + var lspserver: dict<any> = { + id: GetUniqueServerId(), + name: serverParams.name, + path: serverParams.path, + args: serverParams.args->deepcopy(), + running: false, + ready: false, + job: v:none, + data: '', + nextID: 1, + caps: {}, + requests: {}, + callHierarchyType: '', + completionTriggerChars: [], + customNotificationHandlers: serverParams.customNotificationHandlers->deepcopy(), + customRequestHandlers: serverParams.customRequestHandlers->deepcopy(), + debug: serverParams.debug, + features: serverParams.features->deepcopy(), + forceOffsetEncoding: serverParams.forceOffsetEncoding, + initializationOptions: serverParams.initializationOptions->deepcopy(), + messages: [], + needOffsetEncoding: false, + omniCompletePending: false, + peekSymbolFilePopup: -1, + peekSymbolPopup: -1, + processDiagHandler: serverParams.processDiagHandler, + rootSearchFiles: serverParams.rootSearch->deepcopy(), + runIfSearchFiles: serverParams.runIfSearch->deepcopy(), + runUnlessSearchFiles: serverParams.runUnlessSearch->deepcopy(), + selection: {}, + signaturePopup: -1, + syncInit: serverParams.syncInit, + traceLevel: serverParams.traceLevel, + typeHierFilePopup: -1, + typeHierPopup: -1, + workspaceConfig: serverParams.workspaceConfig->deepcopy(), + workspaceSymbolPopup: -1, + workspaceSymbolQuery: '' + } + lspserver.logfile = $'lsp-{lspserver.name}.log' + lspserver.errfile = $'lsp-{lspserver.name}.err' + + # Add the LSP server functions + lspserver->extend({ + startServer: function(StartServer, [lspserver]), + initServer: function(InitServer, [lspserver]), + stopServer: function(StopServer, [lspserver]), + shutdownServer: function(ShutdownServer, [lspserver]), + exitServer: function(ExitServer, [lspserver]), + setTrace: function(SetTrace, [lspserver]), + traceLog: function(TraceLog, [lspserver]), + errorLog: function(ErrorLog, [lspserver]), + nextReqID: function(NextReqID, [lspserver]), + createRequest: function(CreateRequest, [lspserver]), + createResponse: function(CreateResponse, [lspserver]), + sendResponse: function(SendResponse, [lspserver]), + sendMessage: function(SendMessage, [lspserver]), + sendNotification: function(SendNotification, [lspserver]), + rpc: function(Rpc, [lspserver]), + rpc_a: function(AsyncRpc, [lspserver]), + waitForResponse: function(WaitForResponse, [lspserver]), + processReply: function(handlers.ProcessReply, [lspserver]), + processNotif: function(handlers.ProcessNotif, [lspserver]), + processRequest: function(handlers.ProcessRequest, [lspserver]), + processMessages: function(handlers.ProcessMessages, [lspserver]), + encodePosition: function(offset.EncodePosition, [lspserver]), + decodePosition: function(offset.DecodePosition, [lspserver]), + encodeRange: function(offset.EncodeRange, [lspserver]), + decodeRange: function(offset.DecodeRange, [lspserver]), + encodeLocation: function(offset.EncodeLocation, [lspserver]), + decodeLocation: function(offset.DecodeLocation, [lspserver]), + getPosition: function(GetPosition, [lspserver]), + getTextDocPosition: function(GetTextDocPosition, [lspserver]), + featureEnabled: function(FeatureEnabled, [lspserver]), + textdocDidOpen: function(TextdocDidOpen, [lspserver]), + textdocDidClose: function(TextdocDidClose, [lspserver]), + textdocDidChange: function(TextdocDidChange, [lspserver]), + sendInitializedNotif: function(SendInitializedNotif, [lspserver]), + sendWorkspaceConfig: function(SendWorkspaceConfig, [lspserver]), + getCompletion: function(GetCompletion, [lspserver]), + resolveCompletion: function(ResolveCompletion, [lspserver]), + gotoDefinition: function(GotoDefinition, [lspserver]), + gotoDeclaration: function(GotoDeclaration, [lspserver]), + gotoTypeDef: function(GotoTypeDef, [lspserver]), + gotoImplementation: function(GotoImplementation, [lspserver]), + tagFunc: function(TagFunc, [lspserver]), + switchSourceHeader: function(SwitchSourceHeader, [lspserver]), + showSignature: function(ShowSignature, [lspserver]), + didSaveFile: function(DidSaveFile, [lspserver]), + hover: function(ShowHoverInfo, [lspserver]), + showReferences: function(ShowReferences, [lspserver]), + docHighlight: function(DocHighlight, [lspserver]), + getDocSymbols: function(GetDocSymbols, [lspserver]), + textDocFormat: function(TextDocFormat, [lspserver]), + prepareCallHierarchy: function(PrepareCallHierarchy, [lspserver]), + incomingCalls: function(IncomingCalls, [lspserver]), + getIncomingCalls: function(GetIncomingCalls, [lspserver]), + outgoingCalls: function(OutgoingCalls, [lspserver]), + getOutgoingCalls: function(GetOutgoingCalls, [lspserver]), + inlayHintsShow: function(InlayHintsShow, [lspserver]), + typeHierarchy: function(TypeHierarchy, [lspserver]), + renameSymbol: function(RenameSymbol, [lspserver]), + codeAction: function(CodeAction, [lspserver]), + codeLens: function(CodeLens, [lspserver]), + resolveCodeLens: function(ResolveCodeLens, [lspserver]), + workspaceQuery: function(WorkspaceQuerySymbols, [lspserver]), + addWorkspaceFolder: function(AddWorkspaceFolder, [lspserver]), + removeWorkspaceFolder: function(RemoveWorkspaceFolder, [lspserver]), + selectionRange: function(SelectionRange, [lspserver]), + selectionExpand: function(SelectionExpand, [lspserver]), + selectionShrink: function(SelectionShrink, [lspserver]), + foldRange: function(FoldRange, [lspserver]), + executeCommand: function(ExecuteCommand, [lspserver]), + workspaceConfigGet: function(WorkspaceConfigGet, [lspserver]), + semanticHighlightUpdate: function(SemanticHighlightUpdate, [lspserver]), + getCapabilities: function(GetCapabilities, [lspserver]), + getInitializeRequest: function(GetInitializeRequest, [lspserver]), + addMessage: function(AddMessage, [lspserver]), + getMessages: function(GetMessages, [lspserver]) + }) + + return lspserver +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/markdown.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/markdown.vim new file mode 100644 index 0000000..fd73cee --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/markdown.vim @@ -0,0 +1,757 @@ +vim9script + +# Markdown parser +# Refer to https://github.github.com/gfm/ +# for the GitHub Flavored Markdown specification. + +# TODO: different highlight for different heading level +# TODO: links +# TODO: pretty table + + +# Container blocks +var block_quote = '^ \{,3\}\zs> \=' +var list_marker = '[-+*]\|[0-9]\{1,9}[.)]' +var list_item = $'^\%({list_marker}\)\ze\s*$\|^ \{{,3}}\zs\%({list_marker}\) \{{1,4}}\ze\S\|^ \{{,3}}\zs\%({list_marker}\) \{{5}}\ze\s*\S' +# pattern to match list items +export var list_pattern = $'^ *\%({list_marker}\) *' + + +# Leaf blocks +var blank_line = '^\s*$' +var thematic_break = '^ \{,3\}\([-_*]\)\%(\s*\1\)\{2,\}\s*$' +var code_fence = '^ \{,3\}\(`\{3,\}\|\~\{3,\}\)\s*\(\S*\)' +var code_indent = '^ \{4\}\zs\s*\S.*' +var paragraph = '^\s*\zs\S.\{-}\s*\ze$' + +var atx_heading = '^ \{,3}\zs\(#\{1,6}\) \s*\(.\{-}\)\s*\ze\%( #\{1,}\s*\)\=$' +var setext_heading = '^ \{,3}\zs\%(=\{1,}\|-\{1,}\)\ze *$' +var setext_heading_level = {"=": 1, "-": 2} + +var table_delimiter = '^|\=\zs *:\=-\{1,}:\= *\%(| *:\=-\{1,}:\= *\)*\ze|\=$' + +var punctuation = "[!\"#$%&'()*+,-./:;<=>?@[\\\\\\\]^_`{|}~]" + +# Setting text properties +highlight LspBold term=bold cterm=bold gui=bold +highlight LspItalic term=italic cterm=italic gui=italic +highlight LspStrikeThrough term=strikethrough cterm=strikethrough gui=strikethrough +prop_type_add('LspMarkdownBold', {highlight: 'LspBold'}) +prop_type_add('LspMarkdownItalic', {highlight: 'LspItalic'}) +prop_type_add('LspMarkdownStrikeThrough', {highlight: 'LspStrikeThrough'}) +prop_type_add('LspMarkdownHeading', {highlight: 'Function'}) +prop_type_add('LspMarkdownCode', {highlight: 'PreProc'}) +prop_type_add('LspMarkdownCodeBlock', {highlight: 'PreProc'}) +prop_type_add('LspMarkdownListMarker', {highlight: 'Special'}) +prop_type_add('LspMarkdownTableHeader', {highlight: 'Label'}) +prop_type_add('LspMarkdownTableMarker', {highlight: 'Special'}) + + +def GetMarkerProp(marker: string, col: number, ...opt: list<any>): dict<any> + if marker == 'list_item' + return { + type: 'LspMarkdownListMarker', + col: col, + length: opt[0] + } + elseif marker == 'code_block' + return { + type: 'LspMarkdownCodeBlock', + col: col, + end_lnum: opt[0], + end_col: opt[1] + } + elseif marker == 'heading' + return { + type: 'LspMarkdownHeading', + col: col, + length: opt[0] + } + elseif marker == 'table_header' + return { + type: 'LspMarkdownTableHeader', + col: col, + length: opt[0] + } + elseif marker == 'table_sep' + return { + type: 'LspMarkdownTableMarker', + col: col, + length: opt[0] + } + elseif marker == 'code_span' + return { + type: 'LspMarkdownCode', + col: col, + length: opt[0] + } + elseif marker == 'emphasis' + return { + type: 'LspMarkdownItalic', + col: col, + length: opt[0] + } + elseif marker == 'strong' + return { + type: 'LspMarkdownBold', + col: col, + length: opt[0] + } + elseif marker == 'strikethrough' + return { + type: 'LspMarkdownStrikeThrough', + col: col, + length: opt[0] + } + endif + return {} +enddef + +def GetCodeSpans(text: string): list<dict<any>> + var code_spans = [] + var pos = 0 + while pos < text->len() + var backtick = text->matchstrpos('\\*`', pos) + if backtick[1] < 0 + break + endif + if backtick[0]->len() % 2 == 0 + # escaped backtick + pos = backtick[2] + continue + endif + pos = backtick[2] - 1 + var code_span = text->matchstrpos('^\(`\+\)`\@!.\{-}`\@1<!\1`\@!', pos) + if code_span[1] < 0 + break + endif + var code_text = text->matchstrpos('^\(`\+\)\%(\zs \+\ze\|\([ \n]\=\)\zs.\{-}\S.\{-}\ze\2\)`\@1<!\1`\@!', pos) + code_spans->add({ + marker: '`', + start: [code_span[1], code_text[1]], + end: [code_text[2], code_span[2]] + }) + pos = code_span[2] + endwhile + return code_spans +enddef + +def Unescape(text: string, block_marker: string = ""): string + if block_marker == '`' + # line breaks do not occur inside code spans + return text->substitute('\n', ' ', 'g') + endif + # use 2 spaces instead of \ for hard line break + var result = text->substitute('\\\@<!\(\(\\\\\)*\)\\\n', '\1 \n', 'g') + # change soft line breaks + result = result->substitute(' \@<! \=\n', ' ', 'g') + # change hard line breaks + result = result->substitute(' \{2,}\n', '\n', 'g') + # replace non-breaking spaces with spaces + result = result->substitute(' ', ' ', 'g') + return result->substitute($'\\\({punctuation}\)', '\1', 'g') +enddef + +def GetNextInlineDelimiter(text: string, start_pos: number, end_pos: number): dict<any> + var pos = start_pos + while pos < text->len() + # search the first delimiter char + var delimiter = text->matchstrpos('\\*[_*~]', pos) + if delimiter[1] < 0 || delimiter[1] > end_pos + return {} + endif + if delimiter[0]->len() % 2 == 0 + # escaped delimiter char + pos = delimiter[2] + continue + endif + pos = delimiter[2] - 1 + var delimiter_run = text->matchstrpos( + $'{delimiter[0][-1]->substitute("\\([*~]\\)", "\\\\\\1", "g")}\+', + pos) + if delimiter_run[0][0] == '~' && delimiter_run[0]->len() > 2 + pos = delimiter_run[2] + continue + endif + var add_char = '' + if pos > 0 + pos -= 1 + add_char = '.' + endif + var delim_regex = delimiter_run[0]->substitute('\([*~]\)', '\\\1', 'g') + var is_left = text->match($'^{add_char}{delim_regex}\%(\s\|$\|{punctuation}\)\@!\|^{add_char}\%(\s\|^\|{punctuation}\)\@1<={delim_regex}{punctuation}', pos) >= 0 + var is_right = text->match($'^{add_char}\%(\s\|^\|{punctuation}\)\@1<!{delim_regex}\|^{add_char}{punctuation}\@1<={delim_regex}\%(\s\|$\|{punctuation}\)', pos) >= 0 + if !is_left && ! is_right + pos = delimiter_run[2] + continue + endif + if delimiter_run[0][0] == '_' + && text->match($'^\w{delimiter_run[0]}\w', pos) >= 0 + # intraword emphasis is disallowed + pos = delimiter_run[2] + continue + endif + return { + marker: delimiter_run[0], + start: [delimiter_run[1], delimiter_run[2]], + left: is_left, + right: is_right + } + endwhile + return {} +enddef + +def GetNextInlineBlock(text: string, blocks: list<any>, rel_pos: number): dict<any> + var result = { + text: '', + props: [] + } + var cur = blocks->remove(0) + var pos = cur.start[1] + while blocks->len() > 0 && cur.end[0] >= blocks[0].start[0] + result.text ..= Unescape(text->strpart(pos, blocks[0].start[0] - pos), cur.marker[0]) + # get nested block + var part = GetNextInlineBlock(text, blocks, rel_pos + result.text->len()) + result.text ..= part.text + result.props += part.props + pos = part.end_pos + endwhile + result.text ..= Unescape(text->strpart(pos, cur.end[0] - pos), cur.marker[0]) + # add props for current inline block + var prop_type = { + '`': 'code_span', + '_': 'emphasis', + '__': 'strong', + '*': 'emphasis', + '**': 'strong', + '~': 'strikethrough', + '~~': 'strikethrough' + } + result.props->insert(GetMarkerProp(prop_type[cur.marker], + rel_pos + 1, + result.text->len())) + result->extend({'end_pos': cur.end[1]}) + return result +enddef + +def ParseInlines(text: string, rel_pos: number = 0): dict<any> + var formatted = { + text: '', + props: [] + } + var code_spans = GetCodeSpans(text) + + var pos = 0 + var seq = [] + # search all emphasis + while pos < text->len() + var code_pos: list<number> + if code_spans->len() > 0 + code_pos = [code_spans[0].start[0], code_spans[0].end[1]] + if pos >= code_pos[0] + pos = code_pos[1] + seq->add(code_spans->remove(0)) + continue + endif + else + code_pos = [text->len(), text->len()] + endif + var delimiter = GetNextInlineDelimiter(text, pos, code_pos[0]) + if delimiter->empty() + pos = code_pos[1] + continue + endif + if delimiter.right + var idx = seq->len() - 1 + while idx >= 0 + if delimiter.marker[0] != seq[idx].marker[0] + || seq[idx]->has_key('end') + idx -= 1 + continue + endif + if delimiter.left || seq[idx].right + # check the sum rule + if (delimiter.marker->len() + seq[idx].marker->len()) % 3 == 0 + && (delimiter.marker->len() % 3 > 0 + || seq[idx].marker->len() % 3 > 0) + # not valid condition + idx -= 1 + continue + endif + endif + var marker_len = min([delimiter.marker->len(), + seq[idx].marker->len(), 2]) + if seq[idx].marker->len() > marker_len + var new_delim = { + marker: delimiter.marker[0]->repeat(marker_len), + start: [seq[idx].start[1] - marker_len, seq[idx].start[1]], + left: true, + right: false + } + seq[idx].marker = seq[idx].marker[: -1 - marker_len] + seq[idx].start[1] -= marker_len + seq[idx].right = false + idx += 1 + seq->insert(new_delim, idx) + endif + seq[idx]->extend({ + end: [delimiter.start[0], + delimiter.start[0] + marker_len]}) + # close all overlapped emphasis spans not closed + for i in range(seq->len() - 1, idx + 1, -1) + if !seq[i]->has_key('end') + seq->remove(i) + endif + endfor + if delimiter.marker->len() > marker_len + delimiter.start[0] += marker_len + else + delimiter.left = false + break + endif + idx -= 1 + endwhile + endif + if delimiter.left + seq->add(delimiter) + endif + pos = delimiter.start[1] + endwhile + while code_spans->len() > 0 + seq->add(code_spans->remove(0)) + endwhile + # remove all not closed delimiters + for i in range(seq->len() - 1, 0, -1) + if !seq[i]->has_key('end') + seq->remove(i) + endif + endfor + + # compose final text + pos = 0 + while seq->len() > 0 + if pos < seq[0].start[0] + formatted.text ..= Unescape(text->strpart(pos, seq[0].start[0] - pos)) + pos = seq[0].start[0] + endif + var inline = GetNextInlineBlock(text, seq, + rel_pos + formatted.text->len()) + formatted.text ..= inline.text + formatted.props += inline.props + pos = inline.end_pos + endwhile + if pos < text->len() + formatted.text ..= Unescape(text->strpart(pos)) + endif + return formatted +enddef + +# new open container block +def CreateContainerBlock(match: list<any>, start_lnum: number): dict<any> + if match[0][0] == '>' + return { + type: 'quote_block', + lnum: start_lnum, + indent: 0 + } + else + return { + type: 'list_item', + lnum: start_lnum, + marker: $' {match[0]->matchstr("\\S\\+")} ', + indent: match[2] + } + endif +enddef + +# new open leaf block +def CreateLeafBlock(block_type: string, line: string, ...opt: list<any>): dict<any> + if block_type == 'fenced_code' + var token = line->matchlist(code_fence) + return { + type: block_type, + fence: token[1], + language: token[2], + text: [] + } + elseif block_type == 'indented_code' + return { + type: block_type, + text: [line->matchstr(code_indent)] + } + elseif block_type == 'paragraph' + return { + type: block_type, + text: [line->matchstr(paragraph)] + } + elseif block_type == 'heading' + return { + type: block_type, + level: opt[0], + text: line + } + elseif block_type == 'table' + return { + type: block_type, + header: line, + delimiter: opt[0], + text: [] + } + endif + return {} +enddef + +def NeedBlankLine(prev: string, cur: string): bool + if prev == 'hr' || cur == 'hr' + return false + elseif prev == 'heading' || cur == 'heading' + return true + elseif prev == 'paragraph' && cur == 'paragraph' + return true + elseif prev != cur + return true + endif + return false +enddef + +def SplitLine(line: dict<any>, indent: number = 0): list<dict<any>> + var lines: list<dict<any>> = [] + var tokens: list<string> = line.text->split("\n", true) + if tokens->len() == 1 + lines->add(line) + return lines + endif + var props: list<dict<any>> = line.props + for cur_text in tokens + var cur_props: list<dict<any>> = [] + var next_props: list<dict<any>> = [] + var length: number = cur_text->len() + for prop in props + if prop.col + prop.length - 1 <= length + cur_props->add(prop) + elseif prop.col > length + prop.col -= length + 1 + next_props->add(prop) + else + var cur_length: number = length - prop.col + 1 + cur_props->add({ + type: prop.type, + col: prop.col, + length: cur_length + }) + prop.col = 1 + prop.length -= cur_length + 1 + next_props->add(prop) + endif + endfor + lines->add({ + text: cur_text, + props: cur_props + }) + props = next_props + endfor + return lines +enddef + +var last_block: string = '' + +def CloseBlocks(document: dict<list<any>>, blocks: list<dict<any>>, start: number = 0): void + if start >= blocks->len() + return + endif + var line: dict<any> = { + text: '', + props: [] + } + if !document.content->empty() && NeedBlankLine(last_block, blocks[0].type) + document.content->add({text: '', props: []}) + endif + last_block = blocks[0].type + + for i in start->range() + if blocks[i]->has_key('marker') + if blocks[i].marker =~ '\S' + line.props->add(GetMarkerProp('list_item', + line.text->len() + 1, + blocks[i].marker->len())) + line.text ..= blocks[i].marker + blocks[i].marker = ' '->repeat(blocks[i].marker->len()) + else + line.text ..= blocks[i].marker + endif + endif + endfor + for block in blocks->remove(start, -1) + if block.type =~ 'quote_block\|list_item' + if block->has_key('marker') + if block.marker =~ '\S' + line.props->add(GetMarkerProp('list_item', + line.text->len() + 1, + block.marker->len())) + line.text ..= block.marker + block.marker = ' '->repeat(block.marker->len()) + else + line.text ..= block.marker + endif + endif + else + # leaf block + if block.type =~ '_code' + if block.type == 'indented_code' + while !block.text->empty() && block.text[0] !~ '\S' + block.text->remove(0) + endwhile + while !block.text->empty() && block.text[-1] !~ '\S' + block.text->remove(-1) + endwhile + endif + if !block.text->empty() + var indent = ' '->repeat(line.text->len()) + var max_len = mapnew(block.text, (_, l) => l->len())->max() + var text = block.text->remove(0) + line.text ..= text + document.content->add(line) + var startline = document.content->len() + for l in block.text + document.content->add({text: indent .. l}) + endfor + if block->has_key('language') + && !globpath(&rtp, $'syntax/{block.language}.vim')->empty() + document.syntax->add({lang: block.language, + start: $'\%{startline}l\%{indent->len() + 1}c', + end: $'\%{document.content->len()}l$'}) + else + line.props->add(GetMarkerProp('code_block', + indent->len() + 1, + document.content->len(), + indent->len() + max_len + 1)) + endif + endif + elseif block.type == 'heading' + line.props->add(GetMarkerProp('heading', + line.text->len() + 1, + block.text->len(), + block.level)) + var format = ParseInlines(block.text, line.text->len()) + line.text ..= format.text + line.props += format.props + document.content += SplitLine(line) + elseif block.type == 'table' + var indent = line.text + var head = block.header->split('\\\@1<!|') + var col1 = head->remove(0) + var format = ParseInlines(col1, line.text->len()) + line.props->add(GetMarkerProp('table_header', + line.text->len() + 1, + format.text->len())) + line.text ..= format.text + line.props += format.props + for colx in head + format = ParseInlines(colx, line.text->len() + 1) + line.props->add(GetMarkerProp('table_sep', line.text->len() + 1, 1)) + line.props->add(GetMarkerProp('table_header', + line.text->len() + 2, + format.text->len())) + line.text ..= $'|{format.text}' + line.props += format.props + endfor + document.content->add(line) + var data = { + text: indent .. block.delimiter, + props: [GetMarkerProp('table_sep', + indent->len() + 1, + block.delimiter->len())] + } + document.content->add(data) + for row in block.text + data = { + text: indent, + props: [] + } + var cell = row->split('\\\@1<!|') + col1 = cell->remove(0) + format = ParseInlines(col1, data.text->len()) + data.text ..= format.text + data.props += format.props + for colx in cell + format = ParseInlines(colx, data.text->len() + 1) + data.props->add(GetMarkerProp('table_sep', + data.text->len() + 1, + 1)) + data.text ..= $'|{format.text}' + data.props += format.props + endfor + document.content->add(data) + endfor + elseif block.type == 'paragraph' + var indent = line.text->len() + var format = ParseInlines(block.text->join("\n")->substitute('\s\+$', '', ''), indent) + line.text ..= format.text + line.props += format.props + document.content += SplitLine(line, indent) + endif + endif + endfor +enddef + +def ExpandTabs(line: string): string + var block_marker = line->matchstrpos($'^ \{{,3}}>[ \t]\+\|^[ \t]*\%({list_marker}\)\=[ \t]*') + if block_marker[0]->match('\t') < 0 + return line + endif + var begin: string = "" + for char in block_marker[0] + if char == ' ' + begin ..= ' '->repeat(4 - (begin->len() % 4)) + else + begin ..= char + endif + endfor + return begin .. line[block_marker[2] :] +enddef + +export def ParseMarkdown(data: list<string>, width: number = 80): dict<list<any>> + var document: dict<list<any>> = {content: [], syntax: []} + var open_blocks: list<dict<any>> = [] + + for l in data + var line: string = ExpandTabs(l) + var cur = 0 + + # for each open block check if current line continue it + while cur < open_blocks->len() + if open_blocks[cur].type == 'quote_block' + var marker = line->matchstrpos(block_quote) + if marker[1] == -1 + break + endif + line = line->strpart(marker[2]) + elseif open_blocks[cur].type == 'list_item' + var marker = line->matchstrpos($'^ \{{{open_blocks[cur].indent}}}') + if marker[1] == -1 + break + endif + line = line->strpart(marker[2]) + elseif open_blocks[cur].type == 'fenced_code' + if line =~ $'^ \{{,3}}{open_blocks[cur].fence}{open_blocks[cur].fence[0]}* *$' + CloseBlocks(document, open_blocks, cur) + else + open_blocks[cur].text->add(line) + endif + cur = -1 + break + elseif open_blocks[cur].type == 'indented_code' + var marker = line->matchstrpos(code_indent) + if marker[1] >= 0 + open_blocks[cur].text->add(marker[0]) + cur = -1 + endif + break + elseif open_blocks[cur].type == 'paragraph' + if line =~ setext_heading + var marker = line->matchstrpos(setext_heading) + open_blocks->add(CreateLeafBlock( + 'heading', + open_blocks->remove(cur).text->join("\n")->substitute('\s\+$', '', ''), + setext_heading_level[marker[0][0]])) + CloseBlocks(document, open_blocks, cur) + cur = -1 + elseif open_blocks[cur].text->len() == 1 + # may be a table + var marker = line->matchstr(table_delimiter) + if !marker->empty() + if open_blocks[cur].text[0]->split('\\\@1<!|')->len() == marker->split('|')->len() + open_blocks->add(CreateLeafBlock( + 'table', + open_blocks->remove(cur).text[0], + marker)) + cur = -1 + endif + endif + endif + break + endif + cur += 1 + endwhile + + if cur < 0 + # the whole line is already consumed + continue + endif + + # a thematic break close all previous blocks + if line =~ thematic_break + CloseBlocks(document, open_blocks) + if &g:encoding == 'utf-8' + document.content->add({text: "\u2500"->repeat(width)}) + else + document.content->add({text: '-'->repeat(width)}) + endif + last_block = 'hr' + continue + endif + + # check for new container blocks + while true + var block = line->matchstrpos($'{block_quote}\|{list_item}') + if block[1] < 0 + break + endif + # close unmatched blocks + CloseBlocks(document, open_blocks, cur) + # start a new block + open_blocks->add(CreateContainerBlock(block, document->len())) + cur = open_blocks->len() + line = line->strpart(block[2]) + endwhile + + # check for leaf block + if line =~ code_fence + CloseBlocks(document, open_blocks, cur) + open_blocks->add(CreateLeafBlock('fenced_code', line)) + elseif line =~ blank_line + if open_blocks->empty() + continue + endif + if open_blocks[-1].type == 'paragraph' + CloseBlocks(document, open_blocks, min([cur, open_blocks->len() - 1])) + elseif open_blocks[-1].type == 'table' + CloseBlocks(document, open_blocks, open_blocks->len() - 1) + elseif open_blocks[-1].type =~ '_code' + open_blocks[-1].text->add(line) + endif + elseif line =~ code_indent + if open_blocks->empty() + open_blocks->add(CreateLeafBlock('indented_code', line)) + elseif open_blocks[-1].type =~ '_code' + open_blocks[-1].text->add(line->matchstr(code_indent)) + elseif open_blocks[-1].type == 'paragraph' + open_blocks[-1].text->add(line->matchstr(paragraph)) + else + CloseBlocks(document, open_blocks, cur) + open_blocks->add(CreateLeafBlock('indented_code', line)) + endif + elseif line =~ atx_heading + CloseBlocks(document, open_blocks, cur) + var token = line->matchlist(atx_heading) + open_blocks->add(CreateLeafBlock('heading', token[2], token[1]->len())) + CloseBlocks(document, open_blocks, cur) + elseif !open_blocks->empty() + if open_blocks[-1].type == 'table' + open_blocks[-1].text->add(line) + elseif open_blocks[-1].type == 'paragraph' + open_blocks[-1].text->add(line->matchstr(paragraph)) + else + CloseBlocks(document, open_blocks, cur) + open_blocks->add(CreateLeafBlock('paragraph', line)) + endif + else + open_blocks->add(CreateLeafBlock('paragraph', line)) + endif + endfor + + CloseBlocks(document, open_blocks) + return document +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/offset.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/offset.vim new file mode 100644 index 0000000..699964e --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/offset.vim @@ -0,0 +1,168 @@ +vim9script + +import './util.vim' + +# Functions for encoding and decoding the LSP position offsets. Language +# servers support either UTF-8 or UTF-16 or UTF-32 position offsets. The +# character related Vim functions use the UTF-32 position offset. The +# encoding used is negotiated during the language server initialization. + +# Encode the UTF-32 character offset in the LSP position "pos" to the encoding +# negotiated with the language server. +# +# Modifies in-place the UTF-32 offset in pos.character to a UTF-8 or UTF-16 or +# UTF-32 offset. +export def EncodePosition(lspserver: dict<any>, bnr: number, pos: dict<number>) + if has('patch-9.0.1629') + if lspserver.posEncoding == 32 || bnr <= 0 + # LSP client plugin also uses utf-32 encoding + return + endif + + :silent! bnr->bufload() + var text = bnr->getbufline(pos.line + 1)->get(0, '') + if text->empty() + return + endif + + if lspserver.posEncoding == 16 + pos.character = text->utf16idx(pos.character, true, true) + else + pos.character = text->byteidxcomp(pos.character) + endif + endif +enddef + +# Decode the character offset in the LSP position "pos" using the encoding +# negotiated with the language server to a UTF-32 offset. +# +# Modifies in-place the UTF-8 or UTF-16 or UTF-32 offset in pos.character to a +# UTF-32 offset. +export def DecodePosition(lspserver: dict<any>, bnr: number, pos: dict<number>) + if has('patch-9.0.1629') + if lspserver.posEncoding == 32 || bnr <= 0 + # LSP client plugin also uses utf-32 encoding + return + endif + + :silent! bnr->bufload() + var text = bnr->getbufline(pos.line + 1)->get(0, '') + # If the line is empty then don't decode the character position. + if text->empty() + return + endif + + # If the character position is out-of-bounds, then don't decode the + # character position. + var textLen = 0 + if lspserver.posEncoding == 16 + textLen = text->strutf16len(true) + else + textLen = text->strlen() + endif + + if pos.character > textLen + return + endif + + if pos.character == textLen + pos.character = text->strchars() + else + if lspserver.posEncoding == 16 + pos.character = text->charidx(pos.character, true, true) + else + pos.character = text->charidx(pos.character, true) + endif + endif + endif +enddef + +# Encode the start and end UTF-32 character offsets in the LSP range "range" +# to the encoding negotiated with the language server. +# +# Modifies in-place the UTF-32 offset in range.start.character and +# range.end.character to a UTF-8 or UTF-16 or UTF-32 offset. +export def EncodeRange(lspserver: dict<any>, bnr: number, + range: dict<dict<number>>) + if has('patch-9.0.1629') + if lspserver.posEncoding == 32 + return + endif + + EncodePosition(lspserver, bnr, range.start) + EncodePosition(lspserver, bnr, range.end) + endif +enddef + +# Decode the start and end character offsets in the LSP range "range" to +# UTF-32 offsets. +# +# Modifies in-place the offset value in range.start.character and +# range.end.character to a UTF-32 offset. +export def DecodeRange(lspserver: dict<any>, bnr: number, + range: dict<dict<number>>) + if has('patch-9.0.1629') + if lspserver.posEncoding == 32 + return + endif + + DecodePosition(lspserver, bnr, range.start) + DecodePosition(lspserver, bnr, range.end) + endif +enddef + +# Encode the range in the LSP position "location" to the encoding negotiated +# with the language server. +# +# Modifies in-place the UTF-32 offset in location.range to a UTF-8 or UTF-16 +# or UTF-32 offset. +export def EncodeLocation(lspserver: dict<any>, location: dict<any>) + if has('patch-9.0.1629') + if lspserver.posEncoding == 32 + return + endif + + var bnr = 0 + if location->has_key('targetUri') + # LocationLink + bnr = util.LspUriToBufnr(location.targetUri) + if bnr > 0 + # We use only the "targetSelectionRange" item. The + # "originSelectionRange" and the "targetRange" items are not used. + lspserver.encodeRange(bnr, location.targetSelectionRange) + endif + else + # Location + bnr = util.LspUriToBufnr(location.uri) + if bnr > 0 + lspserver.encodeRange(bnr, location.range) + endif + endif + endif +enddef + +# Decode the range in the LSP location "location" to UTF-32. +# +# Modifies in-place the offset value in location.range to a UTF-32 offset. +export def DecodeLocation(lspserver: dict<any>, location: dict<any>) + if has('patch-9.0.1629') + if lspserver.posEncoding == 32 + return + endif + + var bnr = 0 + if location->has_key('targetUri') + # LocationLink + bnr = util.LspUriToBufnr(location.targetUri) + # We use only the "targetSelectionRange" item. The + # "originSelectionRange" and the "targetRange" items are not used. + lspserver.decodeRange(bnr, location.targetSelectionRange) + else + # Location + bnr = util.LspUriToBufnr(location.uri) + lspserver.decodeRange(bnr, location.range) + endif + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/options.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/options.vim new file mode 100644 index 0000000..50697a3 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/options.vim @@ -0,0 +1,179 @@ +vim9script + +export const COMPLETIONMATCHER_CASE = 1 +export const COMPLETIONMATCHER_ICASE = 2 +export const COMPLETIONMATCHER_FUZZY = 3 + +# LSP plugin options +# User can override these by calling the OptionsSet() function. +export var lspOptions: dict<any> = { + # Enable ale diagnostics support. + # If true, diagnostics will be sent to ale, which will be responsible for + # showing them. + aleSupport: false, + + # In insert mode, complete the current symbol automatically + # Otherwise, use omni-completion + autoComplete: true, + + # In normal mode, highlight the current symbol automatically + autoHighlight: false, + + # Automatically highlight diagnostics messages from LSP server + autoHighlightDiags: true, + + # Automatically populate the location list with new diagnostics + autoPopulateDiags: false, + + # icase | fuzzy | case match for language servers that replies with a full + # list of completion items + completionMatcher: 'case', + + # Due to a bug in the earlier versions of Vim, cannot use the + # COMPLETIONMATCHER_CASE constant here for initialization. + completionMatcherValue: 1, + + # diagnostics signs options + diagSignErrorText: 'E>', + diagSignHintText: 'H>', + diagSignInfoText: 'I>', + diagSignWarningText: 'W>', + + # In insert mode, echo the current symbol signature in the status line + # instead of showing it in a popup + echoSignature: false, + + # hide disabled code actions + hideDisabledCodeActions: false, + + # Highlight diagnostics inline + highlightDiagInline: true, + + # Show the symbol documentation in the preview window instead of in a popup + hoverInPreview: false, + + # Don't print message when a configured language server is missing. + ignoreMissingServer: false, + + # Focus on the location list window after ":LspDiag show" + keepFocusInDiags: true, + + # Focus on the location list window after LspShowReferences + keepFocusInReferences: true, + + # If true, apply the LSP server supplied text edits after a completion. + # If a snippet plugin is going to apply the text edits, then set this to + # false to avoid applying the text edits twice. + completionTextEdit: true, + + # Alignment of virtual diagnostic text, when showDiagWithVirtualText is true + # Allowed values: 'above' | 'below' | 'after' (default is 'above') + diagVirtualTextAlign: 'above', + + # Wrapping of virtual diagnostic text, when showDiagWithVirtualText is true. + # Allowed valuse: 'default' | 'truncate' | 'wrap' (default is 'default') + diagVirtualTextWrap: 'default', + + # Suppress adding a new line on completion selection with <CR> + noNewlineInCompletion: false, + + # Omni-completion support. To keep backward compatibility, this option is + # set to null by default instead of false. + omniComplete: null, + + # Open outline window on right side + outlineOnRight: false, + + # Outline window size + outlineWinSize: 20, + + # Enable semantic highlighting + semanticHighlight: false, + + # Show diagnostic text in a balloon when the mouse is over the diagnostic + showDiagInBalloon: true, + + # Make diagnostics show in a popup instead of echoing + showDiagInPopup: true, + + # Suppress diagnostic hover from appearing when the mouse is over the line + # Show a diagnostic message on a status line + showDiagOnStatusLine: false, + + # Show a diagnostic messages using signs + showDiagWithSign: true, + + # Show a diagnostic messages with virtual text + showDiagWithVirtualText: false, + + # enable inlay hints + showInlayHints: false, + + # In insert mode, show the current symbol signature automatically + showSignature: true, + + # enable snippet completion support + snippetSupport: false, + + # enable SirVer/ultisnips completion support + ultisnipsSupport: false, + + # add to autocomplition list current buffer words + useBufferCompletion: false, + + # Use a floating menu to show the code action menu instead of asking for + # input + usePopupInCodeAction: false, + + # ShowReferences in a quickfix list instead of a location list` + useQuickfixForLocations: false, + + # enable hrsh7th/vim-vsnip completion support + vsnipSupport: false, + + # Limit the time autocompletion searches for words in current buffer (in + # milliseconds) + bufferCompletionTimeout: 100, + + # Enable support for custom completion kinds + customCompletionKinds: false, + + # A dictionary with all completion kinds that you want to customize + completionKinds: {}, + + # Filter duplicate completion items + filterCompletionDuplicates: false, +} + +# set the LSP plugin options from the user provided option values +export def OptionsSet(opts: dict<any>) + lspOptions->extend(opts) + if !has('patch-9.0.0178') + lspOptions.showInlayHints = false + endif + if !has('patch-9.0.1157') + lspOptions.showDiagWithVirtualText = false + endif + + # For faster comparison, convert the 'completionMatcher' option value from a + # string to a number. + if lspOptions.completionMatcher == 'icase' + lspOptions.completionMatcherValue = COMPLETIONMATCHER_ICASE + elseif lspOptions.completionMatcher == 'fuzzy' + lspOptions.completionMatcherValue = COMPLETIONMATCHER_FUZZY + else + lspOptions.completionMatcherValue = COMPLETIONMATCHER_CASE + endif + + # Apply the changed options + if exists('#LspCmds#User#LspOptionsChanged') + :doautocmd <nomodeline> LspCmds User LspOptionsChanged + endif +enddef + +# return a copy of the LSP plugin options +export def OptionsGet(): dict<any> + return lspOptions->deepcopy() +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/outline.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/outline.vim new file mode 100644 index 0000000..b414884 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/outline.vim @@ -0,0 +1,304 @@ +vim9script + +import './util.vim' +import './options.vim' as opt + +# jump to a symbol selected in the outline window +def OutlineJumpToSymbol() + var lnum: number = line('.') - 1 + if w:lspSymbols.lnumTable[lnum]->empty() + return + endif + + var slnum: number = w:lspSymbols.lnumTable[lnum].lnum + var scol: number = w:lspSymbols.lnumTable[lnum].col + var fname: string = w:lspSymbols.filename + + # Highlight the selected symbol + prop_remove({type: 'LspOutlineHighlight'}) + var col: number = getline('.')->match('\S') + 1 + prop_add(line('.'), col, {type: 'LspOutlineHighlight', + length: w:lspSymbols.lnumTable[lnum].name->len()}) + + # disable the outline window refresh + skipRefresh = true + + # If the file is already opened in a window, jump to it. Otherwise open it + # in another window + var wid: number = fname->bufwinid() + if wid == -1 + # Find a window showing a normal buffer and use it + for w in getwininfo() + if w.winid->getwinvar('&buftype')->empty() + wid = w.winid + wid->win_gotoid() + break + endif + endfor + if wid == -1 + var symWinid: number = win_getid() + :rightbelow vnew + # retain the fixed symbol window width + win_execute(symWinid, 'vertical resize 20') + endif + + exe $'edit {fname}' + else + wid->win_gotoid() + endif + [slnum, scol]->cursor() + skipRefresh = false +enddef + +# Skip refreshing the outline window. Used to prevent recursive updates to the +# outline window +var skipRefresh: bool = false + +export def SkipOutlineRefresh(): bool + return skipRefresh +enddef + +def AddSymbolText(bnr: number, + symbolTypeTable: dict<list<dict<any>>>, + pfx: string, + text: list<string>, + lnumMap: list<dict<any>>, + children: bool) + var prefix: string = pfx .. ' ' + for [symType, symbols] in symbolTypeTable->items() + if !children + # Add an empty line for the top level symbol types. For types in the + # children symbols, don't add the empty line. + text->extend(['']) + lnumMap->extend([{}]) + endif + if children + text->extend([prefix .. symType]) + prefix ..= ' ' + else + text->extend([symType]) + endif + lnumMap->extend([{}]) + for s in symbols + text->add(prefix .. s.name) + # remember the line number for the symbol + var s_start = s.range.start + var start_col: number = util.GetLineByteFromPos(bnr, s_start) + 1 + lnumMap->add({name: s.name, lnum: s_start.line + 1, + col: start_col}) + s.outlineLine = lnumMap->len() + if s->has_key('children') && !s.children->empty() + AddSymbolText(bnr, s.children, prefix, text, lnumMap, true) + endif + endfor + endfor +enddef + +# update the symbols displayed in the outline window +export def UpdateOutlineWindow(fname: string, + symbolTypeTable: dict<list<dict<any>>>, + symbolLineTable: list<dict<any>>) + var wid: number = bufwinid('LSP-Outline') + if wid == -1 + return + endif + + # stop refreshing the outline window recursively + skipRefresh = true + + var prevWinID: number = win_getid() + wid->win_gotoid() + + # if the file displayed in the outline window is same as the new file, then + # save and restore the cursor position + var symbols = wid->getwinvar('lspSymbols', {}) + var saveCursor: list<number> = [] + if !symbols->empty() && symbols.filename == fname + saveCursor = getcurpos() + endif + + :setlocal modifiable + :silent! :%d _ + setline(1, ['# LSP Outline View', + $'# {fname->fnamemodify(":t")} ({fname->fnamemodify(":h")})']) + + # First two lines in the buffer display comment information + var lnumMap: list<dict<any>> = [{}, {}] + var text: list<string> = [] + AddSymbolText(fname->bufnr(), symbolTypeTable, '', text, lnumMap, false) + text->append('$') + w:lspSymbols = {filename: fname, lnumTable: lnumMap, + symbolsByLine: symbolLineTable} + :setlocal nomodifiable + + if !saveCursor->empty() + saveCursor->setpos('.') + endif + + prevWinID->win_gotoid() + + # Highlight the current symbol + OutlineHighlightCurrentSymbol() + + # re-enable refreshing the outline window + skipRefresh = false +enddef + +def OutlineHighlightCurrentSymbol() + var fname: string = expand('%')->fnamemodify(':p') + if fname->empty() || &filetype->empty() + return + endif + + var wid: number = bufwinid('LSP-Outline') + if wid == -1 + return + endif + + # Check whether the symbols for this file are displayed in the outline + # window + var lspSymbols = wid->getwinvar('lspSymbols', {}) + if lspSymbols->empty() || lspSymbols.filename != fname + return + endif + + var symbolTable: list<dict<any>> = lspSymbols.symbolsByLine + + # line number to locate the symbol + var lnum: number = line('.') + + # Find the symbol for the current line number (binary search) + var left: number = 0 + var right: number = symbolTable->len() - 1 + var mid: number + while left <= right + mid = (left + right) / 2 + var r = symbolTable[mid].range + if lnum >= (r.start.line + 1) && lnum <= (r.end.line + 1) + break + endif + if lnum > (r.start.line + 1) + left = mid + 1 + else + right = mid - 1 + endif + endwhile + + # clear the highlighting in the outline window + var bnr: number = wid->winbufnr() + prop_remove({bufnr: bnr, type: 'LspOutlineHighlight'}) + + if left > right + # symbol not found + return + endif + + # Highlight the selected symbol + var col: number = + bnr->getbufline(symbolTable[mid].outlineLine)->get(0, '')->match('\S') + 1 + prop_add(symbolTable[mid].outlineLine, col, + {bufnr: bnr, type: 'LspOutlineHighlight', + length: symbolTable[mid].name->len()}) + + # if the line is not visible, then scroll the outline window to make the + # line visible + var wininfo = wid->getwininfo() + if symbolTable[mid].outlineLine < wininfo[0].topline + || symbolTable[mid].outlineLine > wininfo[0].botline + var cmd: string = $'call cursor({symbolTable[mid].outlineLine}, 1) | normal z.' + win_execute(wid, cmd) + endif +enddef + +# when the outline window is closed, do the cleanup +def OutlineCleanup() + # Remove the outline autocommands + :silent! autocmd_delete([{group: 'LSPOutline'}]) + + :silent! syntax clear LSPTitle +enddef + +# open the symbol outline window +export def OpenOutlineWindow(cmdmods: string, winsize: number) + var wid: number = bufwinid('LSP-Outline') + if wid != -1 + return + endif + + var prevWinID: number = win_getid() + + var mods = cmdmods + if mods->empty() + if opt.lspOptions.outlineOnRight + mods = ':vert :botright' + else + mods = ':vert :topleft' + endif + endif + + var size = winsize + if size == 0 + size = opt.lspOptions.outlineWinSize + endif + + execute $'{mods} :{size}new LSP-Outline' + :setlocal modifiable + :setlocal noreadonly + :silent! :%d _ + :setlocal buftype=nofile + :setlocal bufhidden=delete + :setlocal noswapfile nobuflisted + :setlocal nonumber norelativenumber fdc=0 nowrap winfixheight winfixwidth + :setlocal shiftwidth=2 + :setlocal foldenable + :setlocal foldcolumn=4 + :setlocal foldlevel=4 + :setlocal foldmethod=indent + setline(1, ['# File Outline']) + :nnoremap <silent> <buffer> q :quit<CR> + :nnoremap <silent> <buffer> <CR> :call <SID>OutlineJumpToSymbol()<CR> + :setlocal nomodifiable + + # highlight all the symbol types + :syntax keyword LSPTitle File Module Namespace Package Class Method Property + :syntax keyword LSPTitle Field Constructor Enum Interface Function Variable + :syntax keyword LSPTitle Constant String Number Boolean Array Object Key Null + :syntax keyword LSPTitle EnumMember Struct Event Operator TypeParameter + + if str2nr(&t_Co) > 2 + :highlight clear LSPTitle + :highlight default link LSPTitle Title + endif + + prop_type_add('LspOutlineHighlight', {bufnr: bufnr(), highlight: 'Search', override: true}) + + try + autocmd_delete([{group: 'LSPOutline', event: '*'}]) + catch /E367:/ + endtry + var acmds: list<dict<any>> + + # Refresh or add the symbols in a buffer to the outline window + acmds->add({event: 'BufEnter', + group: 'LSPOutline', + pattern: '*', + cmd: 'call g:LspRequestDocSymbols()'}) + + # when the outline window is closed, do the cleanup + acmds->add({event: 'BufUnload', + group: 'LSPOutline', + pattern: 'LSP-Outline', + cmd: 'OutlineCleanup()'}) + + # Highlight the current symbol when the cursor is not moved for sometime + acmds->add({event: 'CursorHold', + group: 'LSPOutline', + pattern: '*', + cmd: 'OutlineHighlightCurrentSymbol()'}) + + autocmd_add(acmds) + + prevWinID->win_gotoid() +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/selection.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/selection.vim new file mode 100644 index 0000000..1ef289b --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/selection.vim @@ -0,0 +1,107 @@ +vim9script + +# Functions related to handling LSP range selection. + +import './util.vim' + +# Visually (character-wise) select the text in a range +def SelectText(bnr: number, range: dict<dict<number>>) + var rstart = range.start + var rend = range.end + var start_col: number = util.GetLineByteFromPos(bnr, rstart) + 1 + var end_col: number = util.GetLineByteFromPos(bnr, rend) + + :normal! v"_y + setcharpos("'<", [0, rstart.line + 1, start_col, 0]) + setcharpos("'>", [0, rend.line + 1, end_col, 0]) + :normal! gv +enddef + +# Process the range selection reply from LSP server and start a new selection +export def SelectionStart(lspserver: dict<any>, sel: list<dict<any>>) + if sel->empty() + return + endif + + var bnr: number = bufnr() + + # save the reply for expanding or shrinking the selected text. + lspserver.selection = {bnr: bnr, selRange: sel[0], index: 0} + + SelectText(bnr, sel[0].range) +enddef + +# Locate the range in the LSP reply at a specified level +def GetSelRangeAtLevel(selRange: dict<any>, level: number): dict<any> + var r: dict<any> = selRange + var idx: number = 0 + + while idx != level + if !r->has_key('parent') + break + endif + r = r.parent + idx += 1 + endwhile + + return r +enddef + +# Returns true if the current visual selection matches a range in the +# selection reply from LSP. +def SelectionFromLSP(range: dict<any>, startpos: list<number>, endpos: list<number>): bool + var rstart = range.start + var rend = range.end + return startpos[1] == rstart.line + 1 + && endpos[1] == rend.line + 1 + && startpos[2] == rstart.character + 1 + && endpos[2] == rend.character +enddef + +# Expand or Shrink the current selection or start a new one. +export def SelectionModify(lspserver: dict<any>, expand: bool) + var fname: string = @% + var bnr: number = bufnr() + + if mode() == 'v' && !lspserver.selection->empty() + && lspserver.selection.bnr == bnr + && !lspserver.selection->empty() + # Already in characterwise visual mode and the previous LSP selection + # reply for this buffer is available. Modify the current selection. + + var selRange: dict<any> = lspserver.selection.selRange + var startpos: list<number> = getcharpos('v') + var endpos: list<number> = getcharpos('.') + var idx: number = lspserver.selection.index + + # Locate the range in the LSP reply for the current selection + selRange = GetSelRangeAtLevel(selRange, lspserver.selection.index) + + # If the current selection is present in the LSP reply, then modify the + # selection + if SelectionFromLSP(selRange.range, startpos, endpos) + if expand + # expand the selection + if selRange->has_key('parent') + selRange = selRange.parent + lspserver.selection.index = idx + 1 + endif + else + # shrink the selection + if idx > 0 + idx -= 1 + selRange = GetSelRangeAtLevel(lspserver.selection.selRange, idx) + lspserver.selection.index = idx + endif + endif + + SelectText(bnr, selRange.range) + return + endif + endif + + # Start a new selection + lspserver.selectionRange(fname) +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/semantichighlight.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/semantichighlight.vim new file mode 100644 index 0000000..243aea9 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/semantichighlight.vim @@ -0,0 +1,264 @@ +vim9script + +# LSP semantic highlighting functions + +import './offset.vim' +import './options.vim' as opt +import './buffer.vim' as buf + +# Map token type names to higlight group/text property type names +var TokenTypeMap: dict<string> = { + 'namespace': 'LspSemanticNamespace', + 'type': 'LspSemanticType', + 'class': 'LspSemanticClass', + 'enum': 'LspSemanticEnum', + 'interface': 'LspSemanticInterface', + 'struct': 'LspSemanticStruct', + 'typeParameter': 'LspSemanticTypeParameter', + 'parameter': 'LspSemanticParameter', + 'variable': 'LspSemanticVariable', + 'property': 'LspSemanticProperty', + 'enumMember': 'LspSemanticEnumMember', + 'event': 'LspSemanticEvent', + 'function': 'LspSemanticFunction', + 'method': 'LspSemanticMethod', + 'macro': 'LspSemanticMacro', + 'keyword': 'LspSemanticKeyword', + 'modifier': 'LspSemanticModifier', + 'comment': 'LspSemanticComment', + 'string': 'LspSemanticString', + 'number': 'LspSemanticNumber', + 'regexp': 'LspSemanticRegexp', + 'operator': 'LspSemanticOperator', + 'decorator': 'LspSemanticDecorator' +} + +export def InitOnce() + # Define the default semantic token type highlight groups + hlset([ + {name: 'LspSemanticNamespace', default: true, linksto: 'Type'}, + {name: 'LspSemanticType', default: true, linksto: 'Type'}, + {name: 'LspSemanticClass', default: true, linksto: 'Type'}, + {name: 'LspSemanticEnum', default: true, linksto: 'Type'}, + {name: 'LspSemanticInterface', default: true, linksto: 'TypeDef'}, + {name: 'LspSemanticStruct', default: true, linksto: 'Type'}, + {name: 'LspSemanticTypeParameter', default: true, linksto: 'Type'}, + {name: 'LspSemanticParameter', default: true, linksto: 'Identifier'}, + {name: 'LspSemanticVariable', default: true, linksto: 'Identifier'}, + {name: 'LspSemanticProperty', default: true, linksto: 'Identifier'}, + {name: 'LspSemanticEnumMember', default: true, linksto: 'Constant'}, + {name: 'LspSemanticEvent', default: true, linksto: 'Identifier'}, + {name: 'LspSemanticFunction', default: true, linksto: 'Function'}, + {name: 'LspSemanticMethod', default: true, linksto: 'Function'}, + {name: 'LspSemanticMacro', default: true, linksto: 'Macro'}, + {name: 'LspSemanticKeyword', default: true, linksto: 'Keyword'}, + {name: 'LspSemanticModifier', default: true, linksto: 'Type'}, + {name: 'LspSemanticComment', default: true, linksto: 'Comment'}, + {name: 'LspSemanticString', default: true, linksto: 'String'}, + {name: 'LspSemanticNumber', default: true, linksto: 'Number'}, + {name: 'LspSemanticRegexp', default: true, linksto: 'String'}, + {name: 'LspSemanticOperator', default: true, linksto: 'Operator'}, + {name: 'LspSemanticDecorator', default: true, linksto: 'Macro'} + ]) + + for hlName in TokenTypeMap->values() + prop_type_add(hlName, {highlight: hlName, combine: true}) + endfor +enddef + +def ParseSemanticTokenMods(lspserverTokenMods: list<string>, tokenMods: number): string + var n = tokenMods + var tokenMod: number + var str = '' + + while n > 0 + tokenMod = float2nr(log10(and(n, invert(n - 1))) / log10(2)) + str = $'{str}{lspserverTokenMods[tokenMod]},' + n = and(n, n - 1) + endwhile + + return str +enddef + +# Apply the edit operations in a semantic tokens delta update message +# (SemanticTokensDelta) from the language server. +# +# The previous list of tokens are stored in the buffer-local +# LspSemanticTokensData variable. After applying the edits in +# semTokens.edits, the new set of tokens are returned in semTokens.data. +def ApplySemanticTokenEdits(bnr: number, semTokens: dict<any>) + if semTokens.edits->empty() + return + endif + + # Need to sort the edits and apply the last edit first. + semTokens.edits->sort((a: dict<any>, b: dict<any>) => a.start - b.start) + + # TODO: Remove this code + # var d = bnr->getbufvar('LspSemanticTokensData', []) + # for e in semTokens.edits + # var insertData = e->get('data', []) + # d = (e.start > 0 ? d[: e.start - 1] : []) + insertData + + # d[e.start + e.deleteCount :] + # endfor + # semTokens.data = d + + var oldTokens = bnr->getbufvar('LspSemanticTokensData', []) + var newTokens = [] + var idx = 0 + for e in semTokens.edits + if e.start > 0 + newTokens->extend(oldTokens[idx : e.start - 1]) + endif + newTokens->extend(e->get('data', [])) + idx = e.start + e.deleteCount + endfor + newTokens->extend(oldTokens[idx : ]) + semTokens.data = newTokens +enddef + +# Process a list of semantic tokens and return the corresponding text +# properties for highlighting. +def ProcessSemanticTokens(lspserver: dict<any>, bnr: number, tokens: list<number>): dict<list<list<number>>> + var props: dict<list<list<number>>> = {} + var tokenLine: number = 0 + var startChar: number = 0 + var length: number = 0 + var tokenType: number = 0 + var tokenMods: number = 0 + var prevTokenLine = 0 + var lnum = 1 + var charIdx = 0 + + var lspserverTokenTypes: list<string> = + lspserver.semanticTokensLegend.tokenTypes + var lspserverTokenMods: list<string> = + lspserver.semanticTokensLegend.tokenModifiers + + # Each semantic token uses 5 items in the tokens List + var i = 0 + while i < tokens->len() + tokenLine = tokens[i] + # tokenLine is relative to the previous token line number + lnum += tokenLine + if prevTokenLine != lnum + # this token is on a different line from the previous token + charIdx = 0 + prevTokenLine = lnum + endif + startChar = tokens[i + 1] + charIdx += startChar + length = tokens[i + 2] + tokenType = tokens[i + 3] + tokenMods = tokens[i + 4] + + var typeStr = lspserverTokenTypes[tokenType] + var modStr = ParseSemanticTokenMods(lspserverTokenMods, tokenMods) + + # Decode the semantic token line number, column number and length to + # UTF-32 encoding. + var r = { + start: { + line: lnum - 1, + character: charIdx + }, + end: { + line: lnum - 1, + character: charIdx + length + } + } + offset.DecodeRange(lspserver, bnr, r) + + if !props->has_key(typeStr) + props[typeStr] = [] + endif + props[typeStr]->add([ + lnum, r.start.character + 1, + lnum, r.end.character + 1 + ]) + + i += 5 + endwhile + + return props +enddef + +# Parse the semantic highlight reply from the language server and update the +# text properties +export def UpdateTokens(lspserver: dict<any>, bnr: number, semTokens: dict<any>) + + if semTokens->has_key('edits') + # Delta semantic update. Need to sort the edits and apply the last edit + # first. + ApplySemanticTokenEdits(bnr, semTokens) + endif + + # Cache the semantic tokens in a buffer-local variable, it will be used + # later for a delta update. + setbufvar(bnr, 'LspSemanticResultId', semTokens->get('resultId', '')) + if !semTokens->has_key('data') + return + endif + setbufvar(bnr, 'LspSemanticTokensData', semTokens.data) + + var props: dict<list<list<number>>> + props = ProcessSemanticTokens(lspserver, bnr, semTokens.data) + + # First clear all the previous text properties + if has('patch-9.0.0233') + prop_remove({types: TokenTypeMap->values(), bufnr: bnr, all: true}) + else + for propName in TokenTypeMap->values() + prop_remove({type: propName, bufnr: bnr, all: true}) + endfor + endif + + if props->empty() + return + endif + + # Apply the new text properties + for tokenType in TokenTypeMap->keys() + if props->has_key(tokenType) + prop_add_list({bufnr: bnr, type: TokenTypeMap[tokenType]}, + props[tokenType]) + endif + endfor +enddef + +# Update the semantic highlighting for buffer "bnr" +def LspUpdateSemanticHighlight(bnr: number) + var lspserver: dict<any> = buf.BufLspServerGet(bnr, 'semanticTokens') + if lspserver->empty() + return + endif + + lspserver.semanticHighlightUpdate(bnr) +enddef + +# Initialize the semantic highlighting for the buffer 'bnr' +export def BufferInit(lspserver: dict<any>, bnr: number) + if !opt.lspOptions.semanticHighlight || !lspserver.isSemanticTokensProvider + # no support for semantic highlighting + return + endif + + # Highlight all the semantic tokens + LspUpdateSemanticHighlight(bnr) + + # buffer-local autocmds for semantic highlighting + var acmds: list<dict<any>> = [] + + acmds->add({bufnr: bnr, + event: 'TextChanged', + group: 'LSPBufferAutocmds', + cmd: $'LspUpdateSemanticHighlight({bnr})'}) + acmds->add({bufnr: bnr, + event: 'BufUnload', + group: 'LSPBufferAutocmds', + cmd: $"b:LspSemanticTokensData = [] | b:LspSemanticResultId = ''"}) + + autocmd_add(acmds) +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/signature.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/signature.vim new file mode 100644 index 0000000..ca2bed4 --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/signature.vim @@ -0,0 +1,145 @@ +vim9script + +# Functions related to handling LSP symbol signature help. + +import './options.vim' as opt +import './util.vim' +import './buffer.vim' as buf + +# close the signature popup window +def CloseSignaturePopup(lspserver: dict<any>) + if lspserver.signaturePopup != -1 + lspserver.signaturePopup->popup_close() + endif + lspserver.signaturePopup = -1 +enddef + +def CloseCurBufSignaturePopup() + var lspserver: dict<any> = buf.CurbufGetServer('signatureHelp') + if lspserver->empty() + return + endif + + CloseSignaturePopup(lspserver) +enddef + +# Show the signature using "textDocument/signatureHelp" LSP method +# Invoked from an insert-mode mapping, so return an empty string. +def g:LspShowSignature(): string + var lspserver: dict<any> = buf.CurbufGetServerChecked('signatureHelp') + if lspserver->empty() + return '' + endif + + # first send all the changes in the current buffer to the LSP server + listener_flush() + lspserver.showSignature() + return '' +enddef + +export def InitOnce() + hlset([{name: 'LspSigActiveParameter', default: true, linksto: 'LineNr'}]) +enddef + +# Initialize the signature triggers for the current buffer +export def BufferInit(lspserver: dict<any>) + if !lspserver.isSignatureHelpProvider + || !lspserver.caps.signatureHelpProvider->has_key('triggerCharacters') + # no support for signature help + return + endif + + if !opt.lspOptions.showSignature + || !lspserver.featureEnabled('signatureHelp') + # Show signature support is disabled + return + endif + + # map characters that trigger signature help + for ch in lspserver.caps.signatureHelpProvider.triggerCharacters + exe $"inoremap <buffer> <silent> {ch} {ch}<C-R>=g:LspShowSignature()<CR>" + endfor + + # close the signature popup when leaving insert mode + autocmd_add([{bufnr: bufnr(), + event: 'InsertLeave', + cmd: 'CloseCurBufSignaturePopup()'}]) +enddef + +# process the 'textDocument/signatureHelp' reply from the LSP server and +# display the symbol signature help. +# Result: SignatureHelp | null +export def SignatureHelp(lspserver: dict<any>, sighelp: any): void + if sighelp->empty() + CloseSignaturePopup(lspserver) + return + endif + + if sighelp.signatures->len() <= 0 + CloseSignaturePopup(lspserver) + return + endif + + var sigidx: number = 0 + if sighelp->has_key('activeSignature') + sigidx = sighelp.activeSignature + endif + + var sig: dict<any> = sighelp.signatures[sigidx] + var text: string = sig.label + var hllen: number = 0 + var startcol: number = 0 + if sig->has_key('parameters') && sighelp->has_key('activeParameter') + var params: list<dict<any>> = sig.parameters + var params_len: number = params->len() + var activeParam: number = sighelp.activeParameter + if params_len > 0 && activeParam < params_len + var paramInfo: dict<any> = params[activeParam] + var label: any = paramInfo.label + if label->type() == v:t_string + # label string + var label_str: string = label + hllen = label_str->len() + startcol = text->stridx(label_str) + else + # [inclusive start offset, exclusive end offset] + var label_offset: list<number> = params[activeParam].label + var start_offset: number = label_offset[0] + var end_offset: number = label_offset[1] + + if has('patch-9.0.1629') + # Convert UTF-16 offsets + startcol = text->byteidx(start_offset, true) + var endcol: number = text->byteidx(end_offset, true) + hllen = endcol - startcol + else + startcol = start_offset + hllen = end_offset - start_offset + endif + endif + endif + endif + + if opt.lspOptions.echoSignature + :echon "\r\r" + :echon '' + :echon text->strpart(0, startcol) + :echoh LspSigActiveParameter + :echon text->strpart(startcol, hllen) + :echoh None + :echon text->strpart(startcol + hllen) + else + # Close the previous signature popup and open a new one + lspserver.signaturePopup->popup_close() + + var popupID = text->popup_atcursor({padding: [0, 1, 0, 1], moved: [col('.') - 1, 9999999]}) + var bnr: number = popupID->winbufnr() + prop_type_add('signature', {bufnr: bnr, highlight: 'LspSigActiveParameter'}) + if hllen > 0 + prop_add(1, startcol + 1, {bufnr: bnr, length: hllen, type: 'signature'}) + endif + lspserver.signaturePopup = popupID + endif +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/snippet.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/snippet.vim new file mode 100644 index 0000000..c03b49d --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/snippet.vim @@ -0,0 +1,82 @@ +vim9script + +# Snippet support + +# Integration with the UltiSnips plugin +export def CompletionUltiSnips(prefix: string, items: list<dict<any>>) + call UltiSnips#SnippetsInCurrentScope(1) + for key in matchfuzzy(g:current_ulti_dict_info->keys(), prefix) + var item = g:current_ulti_dict_info[key] + var parts = split(item.location, ':') + var txt = parts[0]->readfile()[parts[1]->str2nr() : parts[1]->str2nr() + 20] + var restxt = item.description .. "\n\n" + for line in txt + if line->empty() || line[0 : 6] == "snippet" + break + else + restxt = restxt .. line .. "\n" + endif + endfor + items->add({ + label: key, + data: { + entryNames: [key], + }, + kind: 15, + documentation: restxt, + }) + endfor +enddef + +# Integration with the vim-vsnip plugin +export def CompletionVsnip(items: list<dict<any>>) + def Pattern(abbr: string): string + var chars = escape(abbr, '\/?')->split('\zs') + var chars_pattern = '\%(\V' .. chars->join('\m\|\V') .. '\m\)' + var separator = chars[0] =~ '\a' ? '\<' : '' + return $'{separator}\V{chars[0]}\m{chars_pattern}*$' + enddef + + if charcol('.') == 1 + return + endif + var starttext = getline('.')->slice(0, charcol('.') - 1) + for item in vsnip#get_complete_items(bufnr()) + var match = starttext->matchstrpos(Pattern(item.abbr)) + if match[0] != '' + var user_data = item.user_data->json_decode() + var documentation = [] + for line in vsnip#to_string(user_data.vsnip.snippet)->split("\n") + documentation->add(line) + endfor + items->add({ + label: item.abbr, + filterText: item.word, + insertTextFormat: 2, + textEdit: { + newText: user_data.vsnip.snippet->join("\n"), + range: { + start: { + line: line('.'), + character: match[1], + }, + ['end']: { + line: line('.'), + character: match[2], + }, + }, + }, + data: { + entryNames: [item.word], + }, + kind: 15, + documentation: { + kind: 'markdown', + value: documentation->join("\n"), + }, + }) + endif + endfor +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/symbol.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/symbol.vim new file mode 100644 index 0000000..bcd547a --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/symbol.vim @@ -0,0 +1,1014 @@ +vim9script + +# Functions for dealing with symbols. +# - LSP symbol menu and for searching symbols across the workspace. +# - show locations +# - jump to a symbol definition, declaration, type definition or +# implementation + +import './options.vim' as opt +import './util.vim' +import './outline.vim' + +# Initialize the highlight group and the text property type used for +# document symbol search +export def InitOnce() + # Use a high priority value to override other highlights in the line + hlset([ + {name: 'LspSymbolName', default: true, linksto: 'Search'}, + {name: 'LspSymbolRange', default: true, linksto: 'Visual'} + ]) + prop_type_add('LspSymbolNameProp', {highlight: 'LspSymbolName', + combine: false, + override: true, + priority: 201}) + prop_type_add('LspSymbolRangeProp', {highlight: 'LspSymbolRange', + combine: false, + override: true, + priority: 200}) +enddef + +# Handle keys pressed when the workspace symbol popup menu is displayed +def FilterSymbols(lspserver: dict<any>, popupID: number, key: string): bool + var key_handled: bool = false + var update_popup: bool = false + var query: string = lspserver.workspaceSymbolQuery + + if key == "\<BS>" || key == "\<C-H>" + # Erase one character from the filter text + if query->len() >= 1 + query = query[: -2] + update_popup = true + endif + key_handled = true + elseif key == "\<C-U>" + # clear the filter text + query = '' + update_popup = true + key_handled = true + elseif key == "\<C-F>" + || key == "\<C-B>" + || key == "\<PageUp>" + || key == "\<PageDown>" + || key == "\<C-Home>" + || key == "\<C-End>" + || key == "\<C-N>" + || key == "\<C-P>" + # scroll the popup window + var cmd: string = 'normal! ' .. (key == "\<C-N>" ? 'j' : key == "\<C-P>" ? 'k' : key) + win_execute(popupID, cmd) + key_handled = true + elseif key == "\<Up>" || key == "\<Down>" + # Use native Vim handling for these keys + key_handled = false + elseif key =~ '^\f$' || key == "\<Space>" + # Filter the names based on the typed key and keys typed before + query ..= key + update_popup = true + key_handled = true + endif + + if update_popup + # Update the popup with the new list of symbol names + popupID->popup_settext('') + if query != '' + lspserver.workspaceQuery(query, false) + else + []->setwinvar(popupID, 'LspSymbolTable') + endif + :echo $'Symbol: {query}' + endif + + # Update the workspace symbol query string + lspserver.workspaceSymbolQuery = query + + if key_handled + return true + endif + + return popupID->popup_filter_menu(key) +enddef + +# Jump to the location of a symbol selected in the popup menu +def JumpToWorkspaceSymbol(cmdmods: string, popupID: number, result: number): void + # clear the message displayed at the command-line + :echo '' + + if result <= 0 + # popup is canceled + return + endif + + var symTbl: list<dict<any>> = popupID->getwinvar('LspSymbolTable', []) + if symTbl->empty() + return + endif + try + # Save the current location in the tag stack + util.PushCursorToTagStack() + + # if the selected file is already present in a window, then jump to it + var fname: string = symTbl[result - 1].file + var bnr = fname->bufnr() + if cmdmods->empty() + var winList: list<number> = bnr->win_findbuf() + if winList->empty() + # Not present in any window + if &modified || &buftype != '' + # the current buffer is modified or is not a normal buffer, then + # open the file in a new window + exe $'split {symTbl[result - 1].file}' + else + exe $'confirm edit {symTbl[result - 1].file}' + endif + else + # If the target buffer is opened in the current window, then don't + # change the window. + if bufnr() != bnr + # If the target buffer is opened in a window in the current tab + # page, then use it. + var winID = fname->bufwinid() + if winID == -1 + # not present in the current tab page. Use the first window. + winID = winList[0] + endif + winID->win_gotoid() + endif + endif + else + exe $'{cmdmods} split {symTbl[result - 1].file}' + endif + # Set the previous cursor location mark. Instead of using setpos(), m' is + # used so that the current location is added to the jump list. + :normal m' + setcursorcharpos(symTbl[result - 1].pos.line + 1, + util.GetCharIdxWithoutCompChar(bufnr(), + symTbl[result - 1].pos) + 1) + :normal! zv + catch + # ignore exceptions + endtry +enddef + +# display a list of symbols from the workspace +def ShowSymbolMenu(lspserver: dict<any>, query: string, cmdmods: string) + # Create the popup menu + var lnum = &lines - &cmdheight - 2 - 10 + var popupAttr = { + title: 'Workspace Symbol Search', + wrap: false, + pos: 'topleft', + line: lnum, + col: 2, + minwidth: 60, + minheight: 10, + maxheight: 10, + maxwidth: 60, + mapping: false, + fixed: 1, + close: 'button', + filter: function(FilterSymbols, [lspserver]), + callback: function('JumpToWorkspaceSymbol', [cmdmods]) + } + lspserver.workspaceSymbolPopup = popup_menu([], popupAttr) + lspserver.workspaceSymbolQuery = query + prop_type_add('lspworkspacesymbol', + {bufnr: lspserver.workspaceSymbolPopup->winbufnr(), + highlight: 'Title'}) + :echo $'Symbol: {query}' +enddef + +# Convert a file name to <filename> (<dirname>) format. +# Make sure the popup doesn't occupy the entire screen by reducing the width. +def MakeMenuName(popupWidth: number, fname: string): string + var filename: string = fname->fnamemodify(':t') + var flen: number = filename->len() + var dirname: string = fname->fnamemodify(':h') + + if fname->len() > popupWidth && flen < popupWidth + # keep the full file name and reduce directory name length + # keep some characters at the beginning and end (equally). + # 6 spaces are used for "..." and " ()" + var dirsz = (popupWidth - flen - 6) / 2 + dirname = dirname[: dirsz] .. '...' .. dirname[-dirsz : ] + endif + var str: string = filename + if dirname != '.' + str ..= $' ({dirname}/)' + endif + return str +enddef + +# process the 'workspace/symbol' reply from the LSP server +# Result: SymbolInformation[] | null +export def WorkspaceSymbolPopup(lspserver: dict<any>, query: string, + symInfo: list<dict<any>>, cmdmods: string) + var symbols: list<dict<any>> = [] + var symbolType: string + var fileName: string + var symName: string + + # Create a symbol popup menu if it is not present + if lspserver.workspaceSymbolPopup->winbufnr() == -1 + ShowSymbolMenu(lspserver, query, cmdmods) + endif + + for symbol in symInfo + if !symbol->has_key('location') + # ignore entries without location information + continue + endif + + # interface SymbolInformation + fileName = util.LspUriToFile(symbol.location.uri) + + symName = symbol.name + if symbol->has_key('containerName') && symbol.containerName != '' + symName = $'{symbol.containerName}::{symName}' + endif + symName ..= $' [{SymbolKindToName(symbol.kind)}]' + symName ..= ' ' .. MakeMenuName( + lspserver.workspaceSymbolPopup->popup_getpos().core_width, + fileName) + + symbols->add({name: symName, + file: fileName, + pos: symbol.location.range.start}) + endfor + symbols->setwinvar(lspserver.workspaceSymbolPopup, 'LspSymbolTable') + lspserver.workspaceSymbolPopup->popup_settext( + symbols->copy()->mapnew('v:val.name')) +enddef + +# map the LSP symbol kind number to string +export def SymbolKindToName(symkind: number): string + var symbolMap: list<string> = [ + '', + 'File', + 'Module', + 'Namespace', + 'Package', + 'Class', + 'Method', + 'Property', + 'Field', + 'Constructor', + 'Enum', + 'Interface', + 'Function', + 'Variable', + 'Constant', + 'String', + 'Number', + 'Boolean', + 'Array', + 'Object', + 'Key', + 'Null', + 'EnumMember', + 'Struct', + 'Event', + 'Operator', + 'TypeParameter' + ] + if symkind > 26 + return '' + endif + return symbolMap[symkind] +enddef + +def UpdatePeekFilePopup(lspserver: dict<any>, locations: list<dict<any>>) + if lspserver.peekSymbolPopup->winbufnr() == -1 + return + endif + + lspserver.peekSymbolFilePopup->popup_close() + + var n = line('.', lspserver.peekSymbolPopup) - 1 + var [uri, range] = util.LspLocationParse(locations[n]) + var fname: string = util.LspUriToFile(uri) + + var bnr: number = fname->bufnr() + if bnr == -1 + bnr = fname->bufadd() + endif + + var popupAttrs = { + title: $"{fname->fnamemodify(':t')} ({fname->fnamemodify(':h')})", + wrap: false, + fixed: true, + minheight: 10, + maxheight: 10, + minwidth: winwidth(0) - 38, + maxwidth: winwidth(0) - 38, + cursorline: true, + border: [], + mapping: false, + line: 'cursor+1', + col: 1 + } + + lspserver.peekSymbolFilePopup = popup_create(bnr, popupAttrs) + var rstart = range.start + var cmds =<< trim eval END + :setlocal number + [{rstart.line + 1}, 1]->cursor() + :normal! z. + END + win_execute(lspserver.peekSymbolFilePopup, cmds) + + lspserver.peekSymbolFilePopup->clearmatches() + var start_col = util.GetLineByteFromPos(bnr, rstart) + 1 + var end_col = util.GetLineByteFromPos(bnr, range.end) + var pos = [[rstart.line + 1, + start_col, end_col - start_col + 1]] + matchaddpos('Search', pos, 10, -1, {window: lspserver.peekSymbolFilePopup}) +enddef + +def LocPopupFilter(lspserver: dict<any>, locations: list<dict<any>>, + popup_id: number, key: string): bool + popup_filter_menu(popup_id, key) + if lspserver.peekSymbolPopup->winbufnr() == -1 + if lspserver.peekSymbolFilePopup->winbufnr() != -1 + lspserver.peekSymbolFilePopup->popup_close() + endif + lspserver.peekSymbolPopup = -1 + lspserver.peekSymbolFilePopup = -1 + else + UpdatePeekFilePopup(lspserver, locations) + endif + return true +enddef + +def LocPopupCallback(lspserver: dict<any>, locations: list<dict<any>>, + popup_id: number, selIdx: number) + if lspserver.peekSymbolFilePopup->winbufnr() != -1 + lspserver.peekSymbolFilePopup->popup_close() + endif + lspserver.peekSymbolPopup = -1 + if selIdx != -1 + util.PushCursorToTagStack() + util.JumpToLspLocation(locations[selIdx - 1], '') + endif +enddef + +# Display the locations in a popup menu. Display the corresponding file in +# an another popup window. +def PeekLocations(lspserver: dict<any>, locations: list<dict<any>>, + title: string) + if lspserver.peekSymbolPopup->winbufnr() != -1 + # If the symbol popup window is already present, close it. + lspserver.peekSymbolPopup->popup_close() + endif + + var w: number = &columns + var fnamelen = float2nr(w * 0.4) + + var curlnum = line('.') + var symIdx = 1 + var curSymIdx = 1 + var menuItems: list<string> = [] + for loc in locations + var [uri, range] = util.LspLocationParse(loc) + var fname: string = util.LspUriToFile(uri) + var bnr: number = fname->bufnr() + if bnr == -1 + bnr = fname->bufadd() + endif + :silent! bnr->bufload() + + var lnum = range.start.line + 1 + var text: string = bnr->getbufline(lnum)->get(0, '') + menuItems->add($'{lnum}: {text}') + + if lnum == curlnum + curSymIdx = symIdx + endif + symIdx += 1 + endfor + + var popupAttrs = { + title: title, + wrap: false, + pos: 'topleft', + line: 'cursor+1', + col: winwidth(0) - 34, + minheight: 10, + maxheight: 10, + minwidth: 30, + maxwidth: 30, + mapping: false, + fixed: true, + filter: function(LocPopupFilter, [lspserver, locations]), + callback: function(LocPopupCallback, [lspserver, locations]) + } + lspserver.peekSymbolPopup = popup_menu(menuItems, popupAttrs) + # Select the current symbol in the menu + var cmds =<< trim eval END + [{curSymIdx}, 1]->cursor() + END + win_execute(lspserver.peekSymbolPopup, cmds, 'silent!') + UpdatePeekFilePopup(lspserver, locations) +enddef + +export def ShowLocations(lspserver: dict<any>, locations: list<dict<any>>, + peekSymbol: bool, title: string) + if peekSymbol + PeekLocations(lspserver, locations, title) + return + endif + + # create a loclist the location of the locations + var qflist: list<dict<any>> = [] + for loc in locations + var [uri, range] = util.LspLocationParse(loc) + var fname: string = util.LspUriToFile(uri) + var bnr: number = fname->bufnr() + if bnr == -1 + bnr = fname->bufadd() + endif + :silent! bnr->bufload() + var rstart = range.start + var text: string = bnr->getbufline(rstart.line + 1)->get(0, '')->trim("\t ", 1) + qflist->add({filename: fname, + lnum: rstart.line + 1, + col: util.GetLineByteFromPos(bnr, rstart) + 1, + text: text}) + endfor + + var save_winid = win_getid() + + if opt.lspOptions.useQuickfixForLocations + setqflist([], ' ', {title: title, items: qflist}) + var mods: string = '' + exe $'{mods} copen' + else + setloclist(0, [], ' ', {title: title, items: qflist}) + var mods: string = '' + exe $'{mods} lopen' + endif + + if !opt.lspOptions.keepFocusInReferences + save_winid->win_gotoid() + endif +enddef + +# Key filter callback function used for the symbol popup window. +# Vim doesn't close the popup window when the escape key is pressed. +# This is function supports that. +def SymbolFilterCB(lspserver: dict<any>, id: number, key: string): bool + if key == "\<Esc>" + lspserver.peekSymbolPopup->popup_close() + return true + endif + + return false +enddef + +# Display the file specified by LSP "LocationLink" in a popup window and +# highlight the range in "location". +def PeekSymbolLocation(lspserver: dict<any>, location: dict<any>) + var [uri, range] = util.LspLocationParse(location) + var fname = util.LspUriToFile(uri) + var bnum = fname->bufadd() + if bnum == 0 + # Failed to create or find a buffer + return + endif + :silent! bnum->bufload() + + if lspserver.peekSymbolPopup->winbufnr() != -1 + # If the symbol popup window is already present, close it. + lspserver.peekSymbolPopup->popup_close() + endif + var CbFunc = function(SymbolFilterCB, [lspserver]) + var popupAttrs = { + title: $"{fnamemodify(fname, ':t')} ({fnamemodify(fname, ':h')})", + wrap: false, + moved: 'any', + minheight: 10, + maxheight: 10, + minwidth: 10, + maxwidth: 60, + cursorline: true, + border: [], + mapping: false, + filter: CbFunc + } + lspserver.peekSymbolPopup = popup_atcursor(bnum, popupAttrs) + + # Highlight the symbol name and center the line in the popup + var pwid = lspserver.peekSymbolPopup + var pwbuf = pwid->winbufnr() + var pos: list<number> = [] + var start_col: number + var end_col: number + var rstart = range.start + start_col = util.GetLineByteFromPos(pwbuf, rstart) + 1 + end_col = util.GetLineByteFromPos(pwbuf, range.end) + 1 + pos->add(rstart.line + 1) + pos->extend([start_col, end_col - start_col]) + matchaddpos('Search', [pos], 10, 101, {window: pwid}) + var cmds =<< trim eval END + :setlocal number + [{rstart.line + 1}, 1]->cursor() + :normal! z. + END + win_execute(pwid, cmds, 'silent!') +enddef + +# Jump to the definition, declaration or implementation of a symbol. +# Also, used to peek at the definition, declaration or implementation of a +# symbol. +export def GotoSymbol(lspserver: dict<any>, location: dict<any>, + peekSymbol: bool, cmdmods: string) + if peekSymbol + PeekSymbolLocation(lspserver, location) + else + # Save the current cursor location in the tag stack. + util.PushCursorToTagStack() + util.JumpToLspLocation(location, cmdmods) + endif +enddef + +# Process the LSP server reply message for a 'textDocument/definition' request +# and return a list of Dicts in a format accepted by the 'tagfunc' option. +export def TagFunc(lspserver: dict<any>, + taglocations: list<dict<any>>, + pat: string): list<dict<any>> + var retval: list<dict<any>> + + for tagloc in taglocations + var tagitem = {} + tagitem.name = pat + + var [uri, range] = util.LspLocationParse(tagloc) + tagitem.filename = util.LspUriToFile(uri) + var bnr = util.LspUriToBufnr(uri) + var rstart = range.start + var startByteIdx = util.GetLineByteFromPos(bnr, rstart) + tagitem.cmd = $"/\\%{rstart.line + 1}l\\%{startByteIdx + 1}c" + + retval->add(tagitem) + endfor + + return retval +enddef + +# process SymbolInformation[] +def ProcessSymbolInfoTable(lspserver: dict<any>, + bnr: number, + symbolInfoTable: list<dict<any>>, + symbolTypeTable: dict<list<dict<any>>>, + symbolLineTable: list<dict<any>>) + var fname: string + var symbolType: string + var name: string + var r: dict<dict<number>> + var symInfo: dict<any> + + for syminfo in symbolInfoTable + fname = util.LspUriToFile(syminfo.location.uri) + symbolType = SymbolKindToName(syminfo.kind) + name = syminfo.name + if syminfo->has_key('containerName') + if syminfo.containerName != '' + name ..= $' [{syminfo.containerName}]' + endif + endif + r = syminfo.location.range + lspserver.decodeRange(bnr, r) + + if !symbolTypeTable->has_key(symbolType) + symbolTypeTable[symbolType] = [] + endif + symInfo = {name: name, range: r} + symbolTypeTable[symbolType]->add(symInfo) + symbolLineTable->add(symInfo) + endfor +enddef + +# process DocumentSymbol[] +def ProcessDocSymbolTable(lspserver: dict<any>, + bnr: number, + docSymbolTable: list<dict<any>>, + symbolTypeTable: dict<list<dict<any>>>, + symbolLineTable: list<dict<any>>) + var symbolType: string + var name: string + var r: dict<dict<number>> + var symInfo: dict<any> + var symbolDetail: string + var childSymbols: dict<list<dict<any>>> + + for syminfo in docSymbolTable + name = syminfo.name + symbolType = SymbolKindToName(syminfo.kind) + r = syminfo.selectionRange + lspserver.decodeRange(bnr, r) + if syminfo->has_key('detail') + symbolDetail = syminfo.detail + endif + if !symbolTypeTable->has_key(symbolType) + symbolTypeTable[symbolType] = [] + endif + childSymbols = {} + if syminfo->has_key('children') + ProcessDocSymbolTable(lspserver, bnr, syminfo.children, childSymbols, + symbolLineTable) + endif + symInfo = {name: name, range: r, detail: symbolDetail, + children: childSymbols} + symbolTypeTable[symbolType]->add(symInfo) + symbolLineTable->add(symInfo) + endfor +enddef + +# process the 'textDocument/documentSymbol' reply from the LSP server +# Open a symbols window and display the symbols as a tree +# Result: DocumentSymbol[] | SymbolInformation[] | null +export def DocSymbolOutline(lspserver: dict<any>, docSymbol: any, fname: string) + var bnr = fname->bufnr() + var symbolTypeTable: dict<list<dict<any>>> = {} + var symbolLineTable: list<dict<any>> = [] + + if docSymbol->empty() + # No symbols defined for this file. Clear the outline window. + outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable) + return + endif + + if docSymbol[0]->has_key('location') + # SymbolInformation[] + ProcessSymbolInfoTable(lspserver, bnr, docSymbol, symbolTypeTable, + symbolLineTable) + else + # DocumentSymbol[] + ProcessDocSymbolTable(lspserver, bnr, docSymbol, symbolTypeTable, + symbolLineTable) + endif + + # sort the symbols by line number + symbolLineTable->sort((a, b) => a.range.start.line - b.range.start.line) + outline.UpdateOutlineWindow(fname, symbolTypeTable, symbolLineTable) +enddef + +# Process the list of symbols (LSP interface "SymbolInformation") in +# "symbolInfoTable". For each symbol, create the name to display in the popup +# menu along with the symbol range and return the List. +def GetSymbolsInfoTable(lspserver: dict<any>, + bnr: number, + symbolInfoTable: list<dict<any>>): list<dict<any>> + var symbolTable: list<dict<any>> = [] + var symbolType: string + var name: string + var containerName: string + var r: dict<dict<number>> + + for syminfo in symbolInfoTable + symbolType = SymbolKindToName(syminfo.kind) + name = $'{symbolType} : {syminfo.name}' + if syminfo->has_key('containerName') && !syminfo.containerName->empty() + name ..= $' [{syminfo.containerName}]' + endif + r = syminfo.location.range + lspserver.decodeRange(bnr, r) + + symbolTable->add({name: name, range: r, selectionRange: {}}) + endfor + + return symbolTable +enddef + +# Process the list of symbols (LSP interface "DocumentSymbol") in +# "docSymbolTable". For each symbol, create the name to display in the popup +# menu along with the symbol range and return the List in "symbolTable" +def GetSymbolsDocSymbol(lspserver: dict<any>, + bnr: number, + docSymbolTable: list<dict<any>>, + symbolTable: list<dict<any>>, + parentName: string = '') + var symbolType: string + var name: string + var r: dict<dict<number>> + var sr: dict<dict<number>> + var symInfo: dict<any> + + for syminfo in docSymbolTable + var symName = syminfo.name + symbolType = SymbolKindToName(syminfo.kind)->tolower() + sr = syminfo.selectionRange + lspserver.decodeRange(bnr, sr) + r = syminfo.range + lspserver.decodeRange(bnr, r) + name = $'{symbolType} : {symName}' + if parentName != '' + name ..= $' [{parentName}]' + endif + # TODO: Should include syminfo.detail? Will it clutter the menu? + symInfo = {name: name, range: r, selectionRange: sr} + symbolTable->add(symInfo) + + if syminfo->has_key('children') + # Process all the child symbols + GetSymbolsDocSymbol(lspserver, bnr, syminfo.children, symbolTable, + symName) + endif + endfor +enddef + +# Highlight the name and the range of lines for the symbol at symTbl[symIdx] +def SymbolHighlight(symTbl: list<dict<any>>, symIdx: number) + prop_remove({type: 'LspSymbolNameProp', all: true}) + prop_remove({type: 'LspSymbolRangeProp', all: true}) + if symTbl->empty() + return + endif + + var r = symTbl[symIdx].range + if r->empty() + return + endif + var rangeStart = r.start + var rangeEnd = r.end + var start_lnum = rangeStart.line + 1 + var start_col = rangeStart.character + 1 + var end_lnum = rangeEnd.line + 1 + var end_col: number + var last_lnum = line('$') + if end_lnum > line('$') + end_lnum = last_lnum + end_col = col([last_lnum, '$']) + else + end_col = rangeEnd.character + 1 + endif + prop_add(start_lnum, start_col, + {type: 'LspSymbolRangeProp', + end_lnum: end_lnum, + end_col: end_col}) + cursor(start_lnum, 1) + :normal! z. + + var sr = symTbl[symIdx].selectionRange + if sr->empty() + return + endif + rangeStart = sr.start + rangeEnd = sr.end + prop_add(rangeStart.line + 1, 1, + {type: 'LspSymbolNameProp', + start_col: rangeStart.character + 1, + end_lnum: rangeEnd.line + 1, + end_col: rangeEnd.character + 1}) +enddef + +# Callback invoked when an item is selected in the symbol popup menu +# "symTbl" - list of symbols +# "symInputPopup" - Symbol search input popup window ID +# "save_curpos" - Cursor position before invoking the symbol search. If the +# symbol search is canceled, restore the cursor to this +# position. +def SymbolMenuItemSelected(symPopupMenu: number, + result: number) + var symTblFiltered = symPopupMenu->getwinvar('symbolTableFiltered', []) + var symInputPopup = symPopupMenu->getwinvar('symbolInputPopup', 0) + var save_curpos = symPopupMenu->getwinvar('saveCurPos', []) + + # Restore the cursor to the location where the command was invoked + setpos('.', save_curpos) + + if result > 0 + # A symbol is selected in the popup menu + + # Set the previous cursor location mark. Instead of using setpos(), m' is + # used so that the current location is added to the jump list. + :normal m' + + # Jump to the selected symbol location + var r = symTblFiltered[result - 1].selectionRange + if r->empty() + # SymbolInformation doesn't have the selectionRange field + r = symTblFiltered[result - 1].range + endif + setcursorcharpos(r.start.line + 1, + util.GetCharIdxWithoutCompChar(bufnr(), r.start) + 1) + :normal! zv + endif + symInputPopup->popup_close() + prop_remove({type: 'LspSymbolNameProp', all: true}) + prop_remove({type: 'LspSymbolRangeProp', all: true}) +enddef + +# Key filter function for the symbol popup menu. +def SymbolMenuFilterKey(symPopupMenu: number, + key: string): bool + var keyHandled = true + var updateInputPopup = false + var inputText = symPopupMenu->getwinvar('inputText', '') + var symInputPopup = symPopupMenu->getwinvar('symbolInputPopup', 0) + + if key == "\<BS>" || key == "\<C-H>" + # Erase a character in the input popup + if inputText->len() >= 1 + inputText = inputText[: -2] + updateInputPopup = true + else + keyHandled = false + endif + elseif key == "\<C-U>" + # Erase all the characters in the input popup + inputText = '' + updateInputPopup = true + elseif key == "\<tab>" + || key == "\<C-n>" + || key == "\<Down>" + || key == "\<ScrollWheelDown>" + var ln = getcurpos(symPopupMenu)[1] + win_execute(symPopupMenu, "normal! j") + if ln == getcurpos(symPopupMenu)[1] + win_execute(symPopupMenu, "normal! gg") + endif + elseif key == "\<S-tab>" + || key == "\<C-p>" + || key == "\<Up>" + || key == "\<ScrollWheelUp>" + var ln = getcurpos(symPopupMenu)[1] + win_execute(symPopupMenu, "normal! k") + if ln == getcurpos(symPopupMenu)[1] + win_execute(symPopupMenu, "normal! G") + endif + elseif key == "\<PageDown>" + win_execute(symPopupMenu, "normal! \<C-d>") + elseif key == "\<PageUp>" + win_execute(symPopupMenu, "normal! \<C-u>") + elseif key == "\<C-F>" + || key == "\<C-B>" + || key == "\<C-Home>" + || key == "\<C-End>" + win_execute(symPopupMenu, $"normal! {key}") + elseif key =~ '^\k$' + # A keyword character is typed. Add to the input text and update the + # popup + inputText ..= key + updateInputPopup = true + else + keyHandled = false + endif + + var symTblFiltered: list<dict<any>> = [] + symTblFiltered = symPopupMenu->getwinvar('symbolTableFiltered', []) + + if updateInputPopup + # Update the input popup with the new text and update the symbol popup + # window with the matching symbol names. + symInputPopup->popup_settext(inputText) + + var symbolTable = symPopupMenu->getwinvar('symbolTable') + symTblFiltered = symbolTable->deepcopy() + var symbolMatchPos: list<list<number>> = [] + + # Get the list of symbols fuzzy matching the entered text + if inputText != '' + var t = symTblFiltered->matchfuzzypos(inputText, {key: 'name'}) + symTblFiltered = t[0] + symbolMatchPos = t[1] + endif + + var popupText: list<dict<any>> + var text: list<dict<any>> + if !symbolMatchPos->empty() + # Generate a list of symbol names and the corresponding text properties + # to highlight the matching characters. + popupText = symTblFiltered->mapnew((idx, val): dict<any> => ({ + text: val.name, + props: symbolMatchPos[idx]->mapnew((_, w: number): dict<any> => ({ + col: w + 1, + length: 1, + type: 'LspSymbolMatch'} + ))} + )) + else + popupText = symTblFiltered->mapnew((idx, val): dict<string> => { + return {text: val.name} + }) + endif + symPopupMenu->popup_settext(popupText) + + # Select the first symbol and highlight the corresponding text range + win_execute(symPopupMenu, 'cursor(1, 1)') + SymbolHighlight(symTblFiltered, 0) + endif + + # Save the filtered symbol table and the search text in popup window + # variables + setwinvar(symPopupMenu, 'inputText', inputText) + setwinvar(symPopupMenu, 'symbolTableFiltered', symTblFiltered) + + if !keyHandled + # Use the default handler for the key + symPopupMenu->popup_filter_menu(key) + endif + + # Highlight the name and range of the selected symbol + var lnum = line('.', symPopupMenu) - 1 + if lnum >= 0 + SymbolHighlight(symTblFiltered, lnum) + endif + + return true +enddef + +# Display the symbols popup menu +def SymbolPopupMenu(symbolTable: list<dict<any>>) + var curLine = line('.') + var curSymIdx = 0 + + # Get the names of all the symbols. Also get the index of the symbol under + # the cursor. + var symNames = symbolTable->mapnew((idx, val): string => { + var r = val.range + if !r->empty() && curSymIdx == 0 + if curLine >= r.start.line + 1 && curLine <= r.end.line + 1 + curSymIdx = idx + endif + endif + return val.name + }) + + var symInputPopupAttr = { + title: 'Select Symbol', + wrap: false, + pos: 'topleft', + line: &lines - 14, + col: 10, + minwidth: 60, + minheight: 1, + maxheight: 1, + maxwidth: 60, + fixed: 1, + close: 'button', + border: [] + } + var symInputPopup = popup_create('', symInputPopupAttr) + + var symNamesPopupattr = { + wrap: false, + pos: 'topleft', + line: &lines - 11, + col: 10, + minwidth: 60, + minheight: 10, + maxheight: 10, + maxwidth: 60, + fixed: 1, + border: [0, 0, 0, 0], + callback: SymbolMenuItemSelected, + filter: SymbolMenuFilterKey, + } + var symPopupMenu = popup_menu(symNames, symNamesPopupattr) + + # Save the state in the popup menu window variables + setwinvar(symPopupMenu, 'symbolTable', symbolTable) + setwinvar(symPopupMenu, 'symbolTableFiltered', symbolTable->deepcopy()) + setwinvar(symPopupMenu, 'symbolInputPopup', symInputPopup) + setwinvar(symPopupMenu, 'saveCurPos', getcurpos()) + prop_type_add('LspSymbolMatch', {bufnr: symPopupMenu->winbufnr(), + highlight: 'Title', + override: true}) + + # Start with the symbol under the cursor + var cmds =<< trim eval END + [{curSymIdx + 1}, 1]->cursor() + :normal! z. + END + win_execute(symPopupMenu, cmds, 'silent!') + + # Highlight the name and range of the first symbol + SymbolHighlight(symbolTable, curSymIdx) +enddef + +# process the 'textDocument/documentSymbol' reply from the LSP server +# Result: DocumentSymbol[] | SymbolInformation[] | null +# Display the symbols in a popup window and jump to the selected symbol +export def DocSymbolPopup(lspserver: dict<any>, docSymbol: any, fname: string) + var symList: list<dict<any>> = [] + + if docSymbol->empty() + return + endif + + var bnr = fname->bufnr() + + if docSymbol[0]->has_key('location') + # SymbolInformation[] + symList = GetSymbolsInfoTable(lspserver, bnr, docSymbol) + else + # DocumentSymbol[] + GetSymbolsDocSymbol(lspserver, bnr, docSymbol, symList) + endif + + :redraw! + SymbolPopupMenu(symList) +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/textedit.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/textedit.vim new file mode 100644 index 0000000..ee4cc4a --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/textedit.vim @@ -0,0 +1,321 @@ +vim9script + +import './util.vim' + +# sort the list of edit operations in the descending order of line and column +# numbers. +# 'a': {'A': [lnum, col], 'B': [lnum, col]} +# 'b': {'A': [lnum, col], 'B': [lnum, col]} +def Edit_sort_func(a: dict<any>, b: dict<any>): number + # line number + if a.A[0] != b.A[0] + return b.A[0] - a.A[0] + endif + # column number + if a.A[1] != b.A[1] + return b.A[1] - a.A[1] + endif + + # Assume that the LSP sorted the lines correctly to begin with + return b.idx - a.idx +enddef + +# Replaces text in a range with new text. +# +# CAUTION: Changes in-place! +# +# 'lines': Original list of strings +# 'A': Start position; [line, col] +# 'B': End position [line, col] +# 'new_lines' A list of strings to replace the original +# +# returns the modified 'lines' +def Set_lines(lines: list<string>, A: list<number>, B: list<number>, + new_lines: list<string>): list<string> + var i_0: number = A[0] + + # If it extends past the end, truncate it to the end. This is because the + # way the LSP describes the range including the last newline is by + # specifying a line number after what we would call the last line. + var numlines: number = lines->len() + var i_n = [B[0], numlines - 1]->min() + + if i_0 < 0 || i_0 >= numlines || i_n < 0 || i_n >= numlines + #util.WarnMsg("set_lines: Invalid range, A = " .. A->string() + # .. ", B = " .. B->string() .. ", numlines = " .. numlines + # .. ", new lines = " .. new_lines->string()) + var msg = $"set_lines: Invalid range, A = {A->string()}" + msg ..= $", B = {B->string()}, numlines = {numlines}" + msg ..= $", new lines = {new_lines->string()}" + util.WarnMsg(msg) + return lines + endif + + # save the prefix and suffix text before doing the replacements + var prefix: string = '' + var suffix: string = lines[i_n][B[1] :] + if A[1] > 0 + prefix = lines[i_0][0 : A[1] - 1] + endif + + var new_lines_len: number = new_lines->len() + + #echomsg $"i_0 = {i_0}, i_n = {i_n}, new_lines = {string(new_lines)}" + var n: number = i_n - i_0 + 1 + if n != new_lines_len + if n > new_lines_len + # remove the deleted lines + lines->remove(i_0, i_0 + n - new_lines_len - 1) + else + # add empty lines for newly the added lines (will be replaced with the + # actual lines below) + lines->extend(repeat([''], new_lines_len - n), i_0) + endif + endif + #echomsg $"lines(1) = {string(lines)}" + + # replace the previous lines with the new lines + for i in new_lines_len->range() + lines[i_0 + i] = new_lines[i] + endfor + #echomsg $"lines(2) = {string(lines)}" + + # append the suffix (if any) to the last line + if suffix != '' + var i = i_0 + new_lines_len - 1 + lines[i] = lines[i] .. suffix + endif + #echomsg $"lines(3) = {string(lines)}" + + # prepend the prefix (if any) to the first line + if prefix != '' + lines[i_0] = prefix .. lines[i_0] + endif + #echomsg $"lines(4) = {string(lines)}" + + return lines +enddef + +# Apply set of text edits to the specified buffer +# The text edit logic is ported from the Neovim lua implementation +export def ApplyTextEdits(bnr: number, text_edits: list<dict<any>>): void + if text_edits->empty() + return + endif + + # if the buffer is not loaded, load it and make it a listed buffer + :silent! bnr->bufload() + setbufvar(bnr, '&buflisted', true) + + var start_line: number = 4294967295 # 2 ^ 32 + var finish_line: number = -1 + var updated_edits: list<dict<any>> = [] + var start_row: number + var start_col: number + var end_row: number + var end_col: number + + # create a list of buffer positions where the edits have to be applied. + var idx = 0 + for e in text_edits + # Adjust the start and end columns for multibyte characters + var r = e.range + var rstart: dict<any> = r.start + var rend: dict<any> = r.end + start_row = rstart.line + start_col = util.GetCharIdxWithoutCompChar(bnr, rstart) + end_row = rend.line + end_col = util.GetCharIdxWithoutCompChar(bnr, rend) + start_line = [rstart.line, start_line]->min() + finish_line = [rend.line, finish_line]->max() + + updated_edits->add({A: [start_row, start_col], + B: [end_row, end_col], + idx: idx, + lines: e.newText->split("\n", true)}) + idx += 1 + endfor + + # Reverse sort the edit operations by descending line and column numbers so + # that they can be applied without interfering with each other. + updated_edits->sort('Edit_sort_func') + + var lines: list<string> = bnr->getbufline(start_line + 1, finish_line + 1) + var fix_eol: bool = bnr->getbufvar('&fixeol') + var set_eol = fix_eol && bnr->getbufinfo()[0].linecount <= finish_line + 1 + if !lines->empty() && set_eol && lines[-1]->len() != 0 + lines->add('') + endif + + #echomsg $'lines(1) = {string(lines)}' + #echomsg updated_edits + + for e in updated_edits + var A: list<number> = [e.A[0] - start_line, e.A[1]] + var B: list<number> = [e.B[0] - start_line, e.B[1]] + lines = Set_lines(lines, A, B, e.lines) + endfor + + #echomsg $'lines(2) = {string(lines)}' + + # If the last line is empty and we need to set EOL, then remove it. + if !lines->empty() && set_eol && lines[-1]->len() == 0 + lines->remove(-1) + endif + + #echomsg $'ApplyTextEdits: start_line = {start_line}, finish_line = {finish_line}' + #echomsg $'lines = {string(lines)}' + + # if the buffer is empty, appending lines before the first line adds an + # extra empty line at the end. Delete the empty line after appending the + # lines. + var dellastline: bool = false + if start_line == 0 && bnr->getbufinfo()[0].linecount == 1 && + bnr->getbufline(1)->get(0, '')->empty() + dellastline = true + endif + + # Now we apply the textedits to the actual buffer. + # In theory we could just delete all old lines and append the new lines. + # This would however cause the cursor to change position: It will always be + # on the last line added. + # + # Luckily there is an even simpler solution, that has no cursor sideeffects. + # + # Logically this method is split into the following three cases: + # + # 1. The number of new lines is equal to the number of old lines: + # Just replace the lines inline with setbufline() + # + # 2. The number of new lines is greater than the old ones: + # First append the missing lines at the **end** of the range, then use + # setbufline() again. This does not cause the cursor to change position. + # + # 3. The number of new lines is less than before: + # First use setbufline() to replace the lines that we can replace. + # Then remove superfluous lines. + # + # Luckily, the three different cases exist only logically, we can reduce + # them to a single case practically, because appendbufline() does not append + # anything if an empty list is passed just like deletebufline() does not + # delete anything, if the last line of the range is before the first line. + # We just need to be careful with all indices. + appendbufline(bnr, finish_line + 1, lines[finish_line - start_line + 1 : -1]) + setbufline(bnr, start_line + 1, lines) + deletebufline(bnr, start_line + 1 + lines->len(), finish_line + 1) + + if dellastline + bnr->deletebufline(bnr->getbufinfo()[0].linecount) + endif +enddef + +# interface TextDocumentEdit +def ApplyTextDocumentEdit(textDocEdit: dict<any>) + var bnr: number = util.LspUriToBufnr(textDocEdit.textDocument.uri) + if bnr == -1 + util.ErrMsg($'Text Document edit, buffer {textDocEdit.textDocument.uri} is not found') + return + endif + ApplyTextEdits(bnr, textDocEdit.edits) +enddef + +# interface CreateFile +# Create the "createFile.uri" file +def FileCreate(createFile: dict<any>) + var fname: string = util.LspUriToFile(createFile.uri) + var opts: dict<bool> = createFile->get('options', {}) + var ignoreIfExists: bool = opts->get('ignoreIfExists', true) + var overwrite: bool = opts->get('overwrite', false) + + # LSP Spec: Overwrite wins over `ignoreIfExists` + if fname->filereadable() && ignoreIfExists && !overwrite + return + endif + + fname->fnamemodify(':p:h')->mkdir('p') + []->writefile(fname) + fname->bufadd() +enddef + +# interface DeleteFile +# Delete the "deleteFile.uri" file +def FileDelete(deleteFile: dict<any>) + var fname: string = util.LspUriToFile(deleteFile.uri) + var opts: dict<bool> = deleteFile->get('options', {}) + var recursive: bool = opts->get('recursive', false) + var ignoreIfNotExists: bool = opts->get('ignoreIfNotExists', true) + + if !fname->filereadable() && ignoreIfNotExists + return + endif + + var flags: string = '' + if recursive + # # NOTE: is this a dangerous operation? The LSP server can send a + # # DeleteFile message to recursively delete all the files in the disk. + # flags = 'rf' + util.ErrMsg($'Recursively deleting files is not supported') + return + elseif fname->isdirectory() + flags = 'd' + endif + var bnr: number = fname->bufadd() + fname->delete(flags) + exe $'{bnr}bwipe!' +enddef + +# interface RenameFile +# Rename file "renameFile.oldUri" to "renameFile.newUri" +def FileRename(renameFile: dict<any>) + var old_fname: string = util.LspUriToFile(renameFile.oldUri) + var new_fname: string = util.LspUriToFile(renameFile.newUri) + + var opts: dict<bool> = renameFile->get('options', {}) + var overwrite: bool = opts->get('overwrite', false) + var ignoreIfExists: bool = opts->get('ignoreIfExists', true) + + if new_fname->filereadable() && (!overwrite || ignoreIfExists) + return + endif + + old_fname->rename(new_fname) +enddef + +# interface WorkspaceEdit +export def ApplyWorkspaceEdit(workspaceEdit: dict<any>) + if workspaceEdit->has_key('documentChanges') + for change in workspaceEdit.documentChanges + if change->has_key('kind') + if change.kind == 'create' + FileCreate(change) + elseif change.kind == 'delete' + FileDelete(change) + elseif change.kind == 'rename' + FileRename(change) + else + util.ErrMsg($'Unsupported change in workspace edit [{change.kind}]') + endif + else + ApplyTextDocumentEdit(change) + endif + endfor + return + endif + + if !workspaceEdit->has_key('changes') + return + endif + + for [uri, changes] in workspaceEdit.changes->items() + var bnr: number = util.LspUriToBufnr(uri) + if bnr == 0 + # file is not present + continue + endif + + # interface TextEdit + ApplyTextEdits(bnr, changes) + endfor +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/typehierarchy.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/typehierarchy.vim new file mode 100644 index 0000000..8786b1b --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/typehierarchy.vim @@ -0,0 +1,174 @@ +vim9script + +# Functions for dealing with type hierarchy (super types/sub types) + +import './util.vim' +import './symbol.vim' + +# Parse the type hierarchy in "typeHier" and displays a tree of type names +# in the current buffer. This function is called recursively to display the +# super/sub type hierarchy. +# +# Returns the line number where the next type name should be added. +def TypeTreeGenerate(isSuper: bool, typeHier: dict<any>, pfx_arg: string, + typeTree: list<string>, typeUriMap: list<dict<any>>) + + var itemHasChildren = false + if isSuper + if typeHier->has_key('parents') && !typeHier.parents->empty() + itemHasChildren = true + endif + else + if typeHier->has_key('children') && !typeHier.children->empty() + itemHasChildren = true + endif + endif + + var itemBranchPfx: string + if itemHasChildren + itemBranchPfx = '▾ ' + else + itemBranchPfx = pfx_arg->empty() ? '' : ' ' + endif + + var typestr: string + var kindstr = symbol.SymbolKindToName(typeHier.kind) + if kindstr != '' + typestr = $'{pfx_arg}{itemBranchPfx}{typeHier.name} ({kindstr[0]})' + else + typestr = $'{pfx_arg}{itemBranchPfx}{typeHier.name}' + endif + typeTree->add(typestr) + typeUriMap->add(typeHier) + + # last item to process + if !itemHasChildren + return + endif + + var items: list<dict<any>> + items = isSuper ? typeHier.parents : typeHier.children + + for item in items + TypeTreeGenerate(isSuper, item, $'{pfx_arg}| ', typeTree, typeUriMap) + endfor +enddef + +# Display a popup with the file containing a type and highlight the line and +# the type name. +def UpdateTypeHierFileInPopup(lspserver: dict<any>, typeUriMap: list<dict<any>>) + if lspserver.typeHierPopup->winbufnr() == -1 + return + endif + + lspserver.typeHierFilePopup->popup_close() + + var n = line('.', lspserver.typeHierPopup) - 1 + var fname: string = util.LspUriToFile(typeUriMap[n].uri) + + var bnr = fname->bufadd() + if bnr == 0 + return + endif + + var popupAttrs = { + title: $"{fname->fnamemodify(':t')} ({fname->fnamemodify(':h')})", + wrap: false, + fixed: true, + minheight: 10, + maxheight: 10, + minwidth: winwidth(0) - 38, + maxwidth: winwidth(0) - 38, + cursorline: true, + border: [], + line: 'cursor+1', + col: 1 + } + lspserver.typeHierFilePopup = popup_create(bnr, popupAttrs) + var cmds =<< trim eval END + [{typeUriMap[n].range.start.line + 1}, 1]->cursor() + :normal! z. + END + win_execute(lspserver.typeHierFilePopup, cmds) + + lspserver.typeHierFilePopup->clearmatches() + var start_col = util.GetLineByteFromPos(bnr, + typeUriMap[n].selectionRange.start) + 1 + var end_col = util.GetLineByteFromPos(bnr, typeUriMap[n].selectionRange.end) + var pos = [[typeUriMap[n].selectionRange.start.line + 1, + start_col, end_col - start_col + 1]] + matchaddpos('Search', pos, 10, -1, {window: lspserver.typeHierFilePopup}) +enddef + +def TypeHierPopupFilter(lspserver: dict<any>, typeUriMap: list<dict<any>>, + popupID: number, key: string): bool + popupID->popup_filter_menu(key) + if lspserver.typeHierPopup->winbufnr() == -1 + # popup is closed + if lspserver.typeHierFilePopup->winbufnr() != -1 + lspserver.typeHierFilePopup->popup_close() + endif + lspserver.typeHierFilePopup = -1 + lspserver.typeHierPopup = -1 + else + UpdateTypeHierFileInPopup(lspserver, typeUriMap) + endif + + return true +enddef + +def TypeHierPopupCallback(lspserver: dict<any>, typeUriMap: list<dict<any>>, + popupID: number, selIdx: number) + if lspserver.typeHierFilePopup->winbufnr() != -1 + lspserver.typeHierFilePopup->popup_close() + endif + lspserver.typeHierFilePopup = -1 + lspserver.typeHierPopup = -1 + + if selIdx <= 0 + # popup is canceled + return + endif + + # Save the current cursor location in the tag stack. + util.PushCursorToTagStack() + util.JumpToLspLocation(typeUriMap[selIdx - 1], '') +enddef + +# Show the super or sub type hierarchy items "types" as a tree in a popup +# window +export def ShowTypeHierarchy(lspserver: dict<any>, isSuper: bool, types: dict<any>) + + if lspserver.typeHierPopup->winbufnr() != -1 + # If the type hierarchy popup window is already present, close it. + lspserver.typeHierPopup->popup_close() + endif + + var typeTree: list<string> + var typeUriMap: list<dict<any>> + + # Generate a tree of the type hierarchy items + TypeTreeGenerate(isSuper, types, '', typeTree, typeUriMap) + + # Display a popup window with the type hierarchy tree and a popup window for + # the file. + var popupAttrs = { + title: $'{isSuper ? "Super" : "Sub"}Type Hierarchy', + wrap: 0, + pos: 'topleft', + line: 'cursor+1', + col: winwidth(0) - 34, + minheight: 10, + maxheight: 10, + minwidth: 30, + maxwidth: 30, + mapping: false, + fixed: true, + filter: function(TypeHierPopupFilter, [lspserver, typeUriMap]), + callback: function(TypeHierPopupCallback, [lspserver, typeUriMap]) + } + lspserver.typeHierPopup = popup_menu(typeTree, popupAttrs) + UpdateTypeHierFileInPopup(lspserver, typeUriMap) +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 diff --git a/vim/pack/downloads/opt/lsp/autoload/lsp/util.vim b/vim/pack/downloads/opt/lsp/autoload/lsp/util.vim new file mode 100644 index 0000000..eb0a64f --- /dev/null +++ b/vim/pack/downloads/opt/lsp/autoload/lsp/util.vim @@ -0,0 +1,364 @@ +vim9script + +# Display an info message +export def InfoMsg(msg: string) + :echohl Question + :echomsg $'Info: {msg}' + :echohl None +enddef + +# Display a warning message +export def WarnMsg(msg: string) + :echohl WarningMsg + :echomsg $'Warn: {msg}' + :echohl None +enddef + +# Display an error message +export def ErrMsg(msg: string) + :echohl Error + :echomsg $'Error: {msg}' + :echohl None +enddef + +# Lsp server trace log directory +var lsp_log_dir: string +if has('unix') + lsp_log_dir = '/tmp/' +else + lsp_log_dir = $TEMP .. '\\' +endif + +# Log a message from the LSP server. stderr is true for logging messages +# from the standard error and false for stdout. +export def TraceLog(fname: string, stderr: bool, msg: string) + if stderr + writefile(msg->split("\n"), $'{lsp_log_dir}{fname}', 'a') + else + writefile([msg], $'{lsp_log_dir}{fname}', 'a') + endif +enddef + +# Empty out the LSP server trace logs +export def ClearTraceLogs(fname: string) + writefile([], $'{lsp_log_dir}{fname}') +enddef + +# Open the LSP server debug messages file. +export def ServerMessagesShow(fname: string) + var fullname = $'{lsp_log_dir}{fname}' + if !filereadable(fullname) + WarnMsg($'File {fullname} is not found') + return + endif + var wid = fullname->bufwinid() + if wid == -1 + exe $'split {fullname}' + else + win_gotoid(wid) + endif + setlocal autoread + setlocal bufhidden=wipe + setlocal nomodified + setlocal nomodifiable +enddef + +# Parse a LSP Location or LocationLink type and return a List with two items. +# The first item is the DocumentURI and the second item is the Range. +export def LspLocationParse(lsploc: dict<any>): list<any> + if lsploc->has_key('targetUri') + # LocationLink + return [lsploc.targetUri, lsploc.targetSelectionRange] + else + # Location + return [lsploc.uri, lsploc.range] + endif +enddef + +# Convert a LSP file URI (file://<absolute_path>) to a Vim file name +export def LspUriToFile(uri: string): string + # Replace all the %xx numbers (e.g. %20 for space) in the URI to character + var uri_decoded: string = substitute(uri, '%\(\x\x\)', + '\=nr2char(str2nr(submatch(1), 16))', 'g') + + # File URIs on MS-Windows start with file:///[a-zA-Z]:' + if uri_decoded =~? '^file:///\a:' + # MS-Windows URI + uri_decoded = uri_decoded[8 : ] + uri_decoded = uri_decoded->substitute('/', '\\', 'g') + # On GNU/Linux (pattern not end with `:`) + elseif uri_decoded =~? '^file:///\a' + uri_decoded = uri_decoded[7 : ] + endif + + return uri_decoded +enddef + +# Convert a LSP file URI (file://<absolute_path>) to a Vim buffer number. +# If the file is not in a Vim buffer, then adds the buffer. +# Returns 0 on error. +export def LspUriToBufnr(uri: string): number + return LspUriToFile(uri)->bufadd() +enddef + +# Returns if the URI refers to a remote file (e.g. ssh://) +# Credit: vim-lsp plugin +export def LspUriRemote(uri: string): bool + return uri =~ '^\w\+::' || uri =~ '^[a-z][a-z0-9+.-]*://' +enddef + +var resolvedUris = {} + +# Convert a Vim filename to an LSP URI (file://<absolute_path>) +export def LspFileToUri(fname: string): string + var fname_full: string = fname->fnamemodify(':p') + + if resolvedUris->has_key(fname_full) + return resolvedUris[fname_full] + endif + + var uri: string = fname_full + + if has("win32unix") + # We're in Cygwin, convert POSIX style paths to Windows style. + # The substitution is to remove the '^@' escape character from the end of + # line. + uri = system($'cygpath -m {uri}')->substitute('^\(\p*\).*$', '\=submatch(1)', "") + endif + + var on_windows: bool = false + if uri =~? '^\a:' + on_windows = true + endif + + if on_windows + # MS-Windows + uri = uri->substitute('\\', '/', 'g') + endif + + uri = uri->substitute('\([^A-Za-z0-9-._~:/]\)', + '\=printf("%%%02x", char2nr(submatch(1)))', 'g') + + if on_windows + uri = $'file:///{uri}' + else + uri = $'file://{uri}' + endif + + resolvedUris[fname_full] = uri + return uri +enddef + +# Convert a Vim buffer number to an LSP URI (file://<absolute_path>) +export def LspBufnrToUri(bnr: number): string + return LspFileToUri(bnr->bufname()) +enddef + +# Returns the byte number of the specified LSP position in buffer "bnr". +# LSP's line and characters are 0-indexed. +# Vim's line and columns are 1-indexed. +# Returns a zero-indexed column. +export def GetLineByteFromPos(bnr: number, pos: dict<number>): number + var col: number = pos.character + # When on the first character, we can ignore the difference between byte and + # character + if col <= 0 + return col + endif + + # Need a loaded buffer to read the line and compute the offset + :silent! bnr->bufload() + + var ltext: string = bnr->getbufline(pos.line + 1)->get(0, '') + if ltext->empty() + return col + endif + + var byteIdx = ltext->byteidxcomp(col) + if byteIdx != -1 + return byteIdx + endif + + return col +enddef + +# Get the index of the character at [pos.line, pos.character] in buffer "bnr" +# without counting the composing characters. The LSP server counts composing +# characters as separate characters whereas Vim string indexing ignores the +# composing characters. +export def GetCharIdxWithoutCompChar(bnr: number, pos: dict<number>): number + var col: number = pos.character + # When on the first character, nothing to do. + if col <= 0 + return col + endif + + # Need a loaded buffer to read the line and compute the offset + :silent! bnr->bufload() + + var ltext: string = bnr->getbufline(pos.line + 1)->get(0, '') + if ltext->empty() + return col + endif + + # Convert the character index that includes composing characters as separate + # characters to a byte index and then back to a character index ignoring the + # composing characters. + var byteIdx = ltext->byteidxcomp(col) + if byteIdx != -1 + if byteIdx == ltext->strlen() + # Byte index points to the byte after the last byte. + return ltext->strcharlen() + else + return ltext->charidx(byteIdx, false) + endif + endif + + return col +enddef + +# Get the index of the character at [pos.line, pos.character] in buffer "bnr" +# counting the composing characters as separate characters. The LSP server +# counts composing characters as separate characters whereas Vim string +# indexing ignores the composing characters. +export def GetCharIdxWithCompChar(ltext: string, charIdx: number): number + # When on the first character, nothing to do. + if charIdx <= 0 || ltext->empty() + return charIdx + endif + + # Convert the character index that doesn't include composing characters as + # separate characters to a byte index and then back to a character index + # that includes the composing characters as separate characters + var byteIdx = ltext->byteidx(charIdx) + if byteIdx != -1 + if byteIdx == ltext->strlen() + return ltext->strchars() + else + return ltext->charidx(byteIdx, true) + endif + endif + + return charIdx +enddef + +# push the current location on to the tag stack +export def PushCursorToTagStack() + settagstack(winnr(), {items: [ + { + bufnr: bufnr(), + from: getpos('.'), + matchnr: 1, + tagname: expand('<cword>') + }]}, 't') +enddef + +# Jump to the LSP "location". The "location" contains the file name, line +# number and character number. The user specified window command modifiers +# (e.g. topleft) are in "cmdmods". +export def JumpToLspLocation(location: dict<any>, cmdmods: string) + var [uri, range] = LspLocationParse(location) + var fname = LspUriToFile(uri) + + # jump to the file and line containing the symbol + var bnr: number = fname->bufnr() + if cmdmods->empty() + if bnr == bufnr() + # Set the previous cursor location mark. Instead of using setpos(), m' is + # used so that the current location is added to the jump list. + :normal m' + else + var wid = fname->bufwinid() + if wid != -1 + wid->win_gotoid() + else + if bnr != -1 + # Reuse an existing buffer. If the current buffer has unsaved changes + # and 'hidden' is not set or if the current buffer is a special + # buffer, then open the buffer in a new window. + if (&modified && !&hidden) || &buftype != '' + exe $'belowright sbuffer {bnr}' + else + exe $'buf {bnr}' + endif + else + if (&modified && !&hidden) || &buftype != '' + # if the current buffer has unsaved changes and 'hidden' is not set, + # or if the current buffer is a special buffer, then open the file + # in a new window + exe $'belowright split {fname}' + else + exe $'edit {fname}' + endif + endif + endif + endif + else + if bnr == -1 + exe $'{cmdmods} split {fname}' + else + # Use "sbuffer" so that the 'switchbuf' option settings are used. + exe $'{cmdmods} sbuffer {bnr}' + endif + endif + var rstart = range.start + setcursorcharpos(rstart.line + 1, + GetCharIdxWithoutCompChar(bufnr(), rstart) + 1) + :normal! zv +enddef + +# indexof() function is not present in older Vim 9 versions. So use this +# function. +export def Indexof(list: list<any>, CallbackFn: func(number, any): bool): number + var ix = 0 + for val in list + if CallbackFn(ix, val) + return ix + endif + ix += 1 + endfor + return -1 +enddef + +# Find the nearest root directory containing a file or directory name from the +# list of names in "files" starting with the directory "startDir". +# Based on a similar implementation in the vim-lsp plugin. +# Searches upwards starting with the directory "startDir". +# If a file name ends with '/' or '\', then it is a directory name, otherwise +# it is a file name. +# Returns '' if none of the file and directory names in "files" can be found +# in one of the parent directories. +export def FindNearestRootDir(startDir: string, files: list<any>): string + var foundDirs: dict<bool> = {} + + for file in files + if file->type() != v:t_string || file->empty() + continue + endif + var isDir = file[-1 : ] == '/' || file[-1 : ] == '\' + var relPath: string + if isDir + relPath = finddir(file, $'{startDir};') + else + relPath = findfile(file, $'{startDir};') + endif + if relPath->empty() + continue + endif + var rootDir = relPath->fnamemodify(isDir ? ':p:h:h' : ':p:h') + foundDirs[rootDir] = true + endfor + if foundDirs->empty() + return '' + endif + + # Sort the directory names by length + var sortedList: list<string> = foundDirs->keys()->sort((a, b) => { + return b->len() - a->len() + }) + + # choose the longest matching path (the nearest directory from "startDir") + return sortedList[0] +enddef + +# vim: tabstop=8 shiftwidth=2 softtabstop=2 |