]> Sergey Matveev's repositories - vim-lsp.git/commitdiff
Add the initial support for semantic highlighting
authorYegappan Lakshmanan <yegappan@yahoo.com>
Sun, 3 Dec 2023 05:50:59 +0000 (21:50 -0800)
committerYegappan Lakshmanan <yegappan@yahoo.com>
Sun, 3 Dec 2023 05:50:59 +0000 (21:50 -0800)
README.md
autoload/lsp/buffer.vim
autoload/lsp/capabilities.vim
autoload/lsp/lsp.vim
autoload/lsp/lspserver.vim
autoload/lsp/options.vim
autoload/lsp/semantichighlight.vim [new file with mode: 0644]
doc/lsp.txt

index ec63932e4684eb7150df7e53372688f4e1dbbbb9..09a75d0c57362f17c53063c0adb05be4cb6ac951 100644 (file)
--- 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: {}
        \ })
 ```
index 7d5696abb98494a1beee40298d09b5d5f0232c2e..5a07654879473c59468301b3e58915f7579fd5ec 100644 (file)
@@ -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,
index c1fe839a59715d4ae484698de03936678ba61755..61ac8ca750cab8de7b05a0c509d0118b078a8632 100644 (file)
@@ -168,6 +168,35 @@ export def ProcessServerCaps(lspserver: dict<any>, caps: dict<any>)
     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<any>
          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,
index 8c4dbe039dae2f254ef1e2e19c4f8a968474b99c..3dfd7ab726589eb548983d70b5e1d2d073bd3f7b 100644 (file)
@@ -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<dict<any>> = []
@@ -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
index 25eee8682e3b4519b242e48e3b21e35e99249f35..b7e2dca1f12a431b96d813126c0922b7bb9211df 100644 (file)
@@ -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<any>, chan: channel, msg: any): void
@@ -531,6 +532,44 @@ def WorkspaceConfigGet(lspserver: dict<any>, configItem: dict<any>): dict<any>
   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>)
@@ -1925,6 +1964,7 @@ export def NewLspServer(serverParams: dict<any>): dict<any>
     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]),
index c2d9ca7e70d54c704007da9331eeb540cc9a3605..dbe23dc17965790c3faaffcaf5c245dee7266a27 100644 (file)
@@ -83,6 +83,9 @@ export var lspOptions: dict<any> = {
   # 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 (file)
index 0000000..243aea9
--- /dev/null
@@ -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
index a5f90d2154462e11f5cb90fd688a5cd9e97cad0a..affcd3ea4956f1255acc46df7d73990254c6fac1 100644 (file)
@@ -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