From 9bfc47d00b13686088a81e518704934bb9327edc Mon Sep 17 00:00:00 2001 From: Yegappan Lakshmanan Date: Sat, 2 Dec 2023 21:50:59 -0800 Subject: [PATCH] Add the initial support for semantic highlighting --- README.md | 59 +++---- autoload/lsp/buffer.vim | 1 + autoload/lsp/capabilities.vim | 54 ++++++ autoload/lsp/lsp.vim | 16 ++ autoload/lsp/lspserver.vim | 40 +++++ autoload/lsp/options.vim | 3 + autoload/lsp/semantichighlight.vim | 264 +++++++++++++++++++++++++++++ doc/lsp.txt | 9 +- 8 files changed, 415 insertions(+), 31 deletions(-) create mode 100644 autoload/lsp/semantichighlight.vim diff --git a/README.md b/README.md index ec63932..09a75d0 100644 --- a/README.md +++ b/README.md @@ -110,45 +110,46 @@ Some of the LSP plugin features can be enabled or disabled by using the LspOptio Here is an example of configuration with default values: ```viml call LspOptionsSet(#{ - \ aleSupport: false, - \ autoComplete: true, - \ autoHighlight: false, - \ autoHighlightDiags: true, - \ autoPopulateDiags: false, + \ aleSupport: v:false, + \ autoComplete: v:true, + \ autoHighlight: v:false, + \ autoHighlightDiags: v:true, + \ autoPopulateDiags: v:false, \ completionMatcher: 'case', \ completionMatcherValue: 1, \ diagSignErrorText: 'E>', \ diagSignHintText: 'H>', \ diagSignInfoText: 'I>', \ diagSignWarningText: 'W>', - \ echoSignature: false, - \ hideDisabledCodeActions: false, - \ highlightDiagInline: true, - \ hoverInPreview: false, - \ ignoreMissingServer: false, - \ keepFocusInDiags: true, - \ keepFocusInReferences: true, - \ completionTextEdit: true, + \ echoSignature: v:false, + \ hideDisabledCodeActions: v:false, + \ highlightDiagInline: v:true, + \ hoverInPreview: v:false, + \ ignoreMissingServer: v:false, + \ keepFocusInDiags: v:true, + \ keepFocusInReferences: v:true, + \ completionTextEdit: v:true, \ diagVirtualTextAlign: 'above', - \ noNewlineInCompletion: false, + \ noNewlineInCompletion: v:false, \ omniComplete: null, - \ outlineOnRight: false, + \ outlineOnRight: v:false, \ outlineWinSize: 20, - \ showDiagInBalloon: true, - \ showDiagInPopup: true, - \ showDiagOnStatusLine: false, - \ showDiagWithSign: true, - \ showDiagWithVirtualText: false, - \ showInlayHints: false, - \ showSignature: true, - \ snippetSupport: false, - \ ultisnipsSupport: false, - \ useBufferCompletion: false, - \ usePopupInCodeAction: false, - \ useQuickfixForLocations: false, - \ vsnipSupport: false, + \ semanticHighlight: v:true, + \ showDiagInBalloon: v:true, + \ showDiagInPopup: v:true, + \ showDiagOnStatusLine: v:false, + \ showDiagWithSign: v:true, + \ showDiagWithVirtualText: v:false, + \ showInlayHints: v:false, + \ showSignature: v:true, + \ snippetSupport: v:false, + \ ultisnipsSupport: v:false, + \ useBufferCompletion: v:false, + \ usePopupInCodeAction: v:false, + \ useQuickfixForLocations: v:false, + \ vsnipSupport: v:false, \ bufferCompletionTimeout: 100, - \ customCompletionKinds: false, + \ customCompletionKinds: v:false, \ completionKinds: {} \ }) ``` diff --git a/autoload/lsp/buffer.vim b/autoload/lsp/buffer.vim index 7d5696a..5a07654 100644 --- a/autoload/lsp/buffer.vim +++ b/autoload/lsp/buffer.vim @@ -53,6 +53,7 @@ var SupportedCheckFns = { 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, diff --git a/autoload/lsp/capabilities.vim b/autoload/lsp/capabilities.vim index c1fe839..61ac8ca 100644 --- a/autoload/lsp/capabilities.vim +++ b/autoload/lsp/capabilities.vim @@ -168,6 +168,35 @@ export def ProcessServerCaps(lspserver: dict, caps: dict) 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 @@ -404,6 +433,31 @@ export def GetClientCaps(): dict 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, diff --git a/autoload/lsp/lsp.vim b/autoload/lsp/lsp.vim index 8c4dbe0..3dfd7ab 100644 --- a/autoload/lsp/lsp.vim +++ b/autoload/lsp/lsp.vim @@ -21,6 +21,7 @@ import './outline.vim' import './signature.vim' import './codeaction.vim' import './inlayhints.vim' +import './semantichighlight.vim' # LSP server information var LSPServers: list> = [] @@ -48,6 +49,7 @@ def LspInitOnce() inlayhints.InitOnce() signature.InitOnce() symbol.InitOnce() + semantichighlight.InitOnce() lspInitializedOnce = true enddef @@ -424,6 +426,11 @@ def BufferInit(lspserverId: number, bnr: number): void 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') @@ -503,7 +510,16 @@ export def BufferLoadedInWin(bnr: number) 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 diff --git a/autoload/lsp/lspserver.vim b/autoload/lsp/lspserver.vim index 25eee86..b7e2dca 100644 --- a/autoload/lsp/lspserver.vim +++ b/autoload/lsp/lspserver.vim @@ -25,6 +25,7 @@ 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, chan: channel, msg: any): void @@ -531,6 +532,44 @@ def WorkspaceConfigGet(lspserver: dict, configItem: dict): dict return config enddef +# Update semantic highlighting for buffer "bnr" +# Request: textDocument/semanticTokens/full or +# textDocument/semanticTokens/full/delta +def SemanticHighlightUpdate(lspserver: dict, 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 = { + 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) @@ -1925,6 +1964,7 @@ export def NewLspServer(serverParams: dict): dict 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]), diff --git a/autoload/lsp/options.vim b/autoload/lsp/options.vim index c2d9ca7..dbe23dc 100644 --- a/autoload/lsp/options.vim +++ b/autoload/lsp/options.vim @@ -83,6 +83,9 @@ export var lspOptions: dict = { # 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, diff --git a/autoload/lsp/semantichighlight.vim b/autoload/lsp/semantichighlight.vim new file mode 100644 index 0000000..243aea9 --- /dev/null +++ b/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 = { + '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, 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) + if semTokens.edits->empty() + return + endif + + # Need to sort the edits and apply the last edit first. + semTokens.edits->sort((a: dict, b: dict) => 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, bnr: number, tokens: list): dict>> + var props: dict>> = {} + 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 = + lspserver.semanticTokensLegend.tokenTypes + var lspserverTokenMods: list = + 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, bnr: number, semTokens: dict) + + 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>> + 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 = 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, 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> = [] + + 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/doc/lsp.txt b/doc/lsp.txt index a5f90d2..affcd3e 100644 --- a/doc/lsp.txt +++ b/doc/lsp.txt @@ -3,7 +3,7 @@ Author: Yegappan Lakshmanan (yegappan AT yahoo DOT com) For Vim version 9.0 and above -Last change: July 26, 2023 +Last change: Dec 2, 2023 ============================================================================== CONTENTS *lsp-contents* @@ -426,7 +426,7 @@ language server and to set the language server options. For example: > ] autocmd VimEnter * LspAddServer(lspServers) - var lspOpts = {'autoHighlightDiags': true} + var lspOpts = {autoHighlightDiags: true} autocmd VimEnter * LspOptionsSet(lspOpts) < *lsp-options* *LspOptionsSet()* @@ -567,6 +567,11 @@ outlineOnRight |Boolean| option. Open the outline window on the outlineWinSize |Number| option. The size of the symbol Outline window. By default this is set to 20. + *lsp-opt-semanticHighlight* +semanticHighlight |Boolean| option. Enables or disables semantic + highlighting. + By default this is set to false. + *lsp-opt-showDiagInBalloon* showDiagInBalloon |Boolean| option. When the mouse is over a range of text referenced by a diagnostic, display the -- 2.48.1