I've spent countless hours writing and reading C++ code. For many years, Emacs has been my primary editor, and I leverage ccls' (my C++ language server) rainbow semantic highlighting feature.
The feature relies on two custom notification messages
$ccls/publishSemanticHighlight
and
$ccls/publishSkippedRanges
.
$ccls/publishSemanticHighlight
provides a list of symbols,
each with kind information (function, type, or variable) of itself and
its semantic parent (e.g. a member function's parent is a class),
storage duration, and a list of ranges.
1 | struct CclsSemanticHighlightSymbol { |
An editor can use consistent colors to highlight different occurrences of a symbol. Different colors can be assigned to different symbols.
Tobias Pisani created emacs-cquery (the predecessor to emacs-ccls) in Nov 2017. Despite not being a fan of Emacs Lisp, I added the rainbow semantic highlighting feature for my own use in early 2018. My setup also relied heavily on these two settings:
- Bolding and underlining variables of static duration storage
- Italicizing member functions and variables
1 | (setq ccls-sem-highlight-method 'font-lock) |
Key symbol properties (member, static) were visually prominent in my Emacs environment.
My Emacs hacking days are a distant memory – beyond basic configuration tweaks, I haven't touched elisp code since 2018. As my Elisp skills faded, I increasingly turned to Neovim for various editing tasks. Naturally, I wanted to migrate my C++ development workflow to Neovim as well. However, a major hurdle emerged: Neovim lacked the beloved rainbow highlighting I enjoyed in Emacs.
Thankfully, Neovim supports "semantic tokens" from LSP 3.16, a standardized approach adopted by many editors.
I've made changes to ccls (available on a
branch; PR)
to support semantic tokens. This involves adapting the
$ccls/publishSemanticHighlight
code to additionally support
textDocument/semanticTokens/full
and
textDocument/semanticTokens/range
.
I utilize a few token modifiers (static
,
classScope
, functionScope
,
namespaceScope
) for highlighting:
1 | vim.cmd([[ |
While this approach is a significant improvement over relying solely on nvim-treesitter, I'm still eager to implement rainbow semantic tokens. Although LSP semantic tokens don't directly distinguish symbols, we can create custom modifiers to achieve similar results.
1 | tokenModifiers: { |
In the user-provided initialization options, I set
highlight.rainbow
to 10.
ccls assigns the same modifier ID to tokens belonging to the same symbol, aiming for unique IDs for different symbols. While we only have a few predefined IDs (each linked to a specific color), there's a slight possibility of collisions. However, this is uncommon and generally acceptable.
For a token with type variable
, Neovim's built-in LSP
plugin assigns a highlight group
@lsp.typemod.variable.id$i.cpp
where $i
is an
integer between 0 and 9. This allows us to customize a unique foreground
color for each modifier ID.
1 | local func_colors = { |
Now, let's analyze the C++ code above using this configuration.
While the results are visually pleasing, I need help implementing code lens functionality.
Inactive code highlighting
Inactive code regions (skipped ranges in Clang) are typically displayed in grey. While this can be helpful for identifying unused code, it can sometimes hinder understanding the details. I simply disabled the inactive code feature.
1 |
|
Refresh
When opening a large project, the initial indexing or cache loading
process can be time-consuming, often leading to empty lists of semantic
tokens for the initially opened files. While ccls prioritizes indexing
these files, it's unclear how to notify the client to refresh the files.
The existing workspace/semanticTokens/refresh
request,
unfortunately, doesn't accept text document parameters.
In contrast, with $ccls/publishSemanticHighlight
, ccls
proactively sends the notification after an index update (see
main_OnIndexed
).
1 | void main_OnIndexed(DB *db, WorkingFiles *wfiles, IndexUpdate *update) { |
While the semantic token request supports partial results in the specification, Neovim lacks this implementation. Even if it were, I believe a notification message with a text document parameter would be a more efficient and direct approach.
1 | export interface SemanticTokensParams extends WorkDoneProgressParams, |
Other clients
emacs-ccls
Once this feature branch is merged, Emacs users can simply remove the following lines:
1 | (setq ccls-sem-highlight-method 'font-lock) |
How to change lsp-semantic-token-modifier-faces
to
support rainbow semantic tokens in lsp-mode and emacs-ccls?
The general approach is similar to the following, but we need a feature from lsp-mode (https://github.com/emacs-lsp/lsp-mode/issues/4590).
1 | (setq lsp-semantic-tokens-enable t) |
vscode-ccls
We require assistance to eliminate the
$ccls/publishSemanticHighlight
feature and adopt built-in
semantic tokens support. Due to the lack of active maintenance for
vscode-ccls, I'm unable to maintain this plugin for an editor I don't
frequently use.
Misc
I use a trick to switch ccls builds without changing editor configurations.
1 | #!/bin/zsh |
Usage:
1 | echo debug > /tmp/ccls-build |