Skip to content

About

Fast Rust language server for project-wide CSS custom properties: completion, hover, navigation, rename, diagnostics, and color tools.

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Repository files navigation

CSS Variable LSP (Rust)

A fast, Rust-based Language Server Protocol implementation focused on CSS custom properties (--variables) and the var() function. It is a ground-up rewrite of the original TypeScript css-variable-lsp, designed to ship as a standalone native executable that any LSP-aware editor (Zed, VS Code, Neovim, Helix, …) can launch with no Node.js runtime in sight.

Status: production-ready. Used by the Zed CSS Variables extension in the Zed editor. The server is published to crates.io and ships as a tagged GitHub release for Linux, macOS, and Windows (x86_64 + aarch64).


Table of contents


Why a Rust rewrite?

TypeScript css-variable-lsp This crate
Runtime Node.js + npm dependencies Native, no Node.js
Binary size ~50–100 MB ~8.2 MB (macOS aarch64)
Cold start ~500 ms ~10 ms
Baseline memory 50–100 MB 10–20 MB
Parse time (typ.) Fast (css-tree) Very fast (regex)
Distribution npm package cargo install, GitHub Releases, crates.io

A standalone executable with no Node.js or npm runtime dependency is dramatically easier to embed in editor extensions and CI sandboxes.


Features

Core language features

  • CSS parsing of variable definitions, var() usages, and literal color occurrences (hex, named colors, RGB, HSL, HWB, Lab, LCH, OKLab, OKLCH, relative colors, and color() in sRGB or XYZ), plus chain resolution through var() aliases.
  • HTML parsing of <style> blocks, class="…" / classname="…" attributes, and inline style="…" attributes with full DOM tree tracking.
  • JS / TS / JSX / TSX support via CSS-in-JS extraction from string literals and tagged template literals (styled-components, emotion, etc.), correctly handling template expressions. Open documents are parsed automatically; pass --eager-js to also index unopened JS/TS-family files.
  • Astro font variables are statically indexed from astro.config.{js,mjs,cjs,ts,mts,cts} using Oxc. Configuration code is parsed but never executed. fonts[].cssVariable and legacy experimental.fonts[].cssVariable accept direct literals, bounded immutable const aliases, static object and array aliases, and known spreads. Proven ESM and CommonJS defineConfig helpers are supported.
  • Vite injected CSS variables are indexed from static css.preprocessorOptions.scss.additionalData strings in vite.config.{js,mjs,cjs,ts,mts,cts}. Direct literals, immutable aliases, static structures and spreads, and simple unconditional function returns are accepted. Dynamic values, unrelated Vite options, unknown computed properties, and SCSS control-flow directives are ignored.
  • Safe config lifecycle atomically updates definitions and usages. Oxc's recoverable AST replaces stale state, while catastrophic syntax errors and files over 1 MiB retain the last valid analysis. Completion, hover, and document symbols identify whether a definition came from Astro or Vite.
  • Cascade sorting and CSS specificity calculation including :is(), :not(), :where(), attribute selectors, pseudo-classes, and pseudo-elements.
  • DOM-aware matching so hover can tell the user which definition applies at the cursor's position, not just which one wins globally.

LSP features

  • Completion for var(--name) and bare --name (with trigger characters -, (, :).
  • Hover showing value, selector, specificity, and which definition wins under the current context.
  • Go to definition and find references across the whole workspace.
  • Rename preserving !important, fallback arguments, and whitespace.
  • Code actions:
    • Create variable in :root (quickfix for undefined var()).
    • Add fallback to var(--name) (configurable, default on).
    • Replace literal color with matching variable (configurable, default on).
  • Diagnostics: undefined var() usage (warning / info / off) and "consider using a variable" hints for literal colors that match an existing variable.
  • Document symbols and workspace symbols (Shift+Shift).
  • Document color and color presentation for color picker integration.
  • File-system watching: workspace re-scans on file create / change / delete / rename events.

File-type coverage

Kind Extensions
CSS .css, .scss, .sass, .less
HTML-ish .html, .vue, .svelte, .astro, .ripple
JS-ish .js, .jsx, .ts, .tsx, .mjs, .cjs, .mts, .cts

CSS and HTML-ish workspace discovery is configurable via --lookup-files. Open JS-ish documents are parsed automatically; use --eager-js to parse those files from disk during workspace scans as well. Standard JS/TS-family extensions are included automatically in eager mode, and ignore_globs still applies. Recognized astro.config.* and vite.config.* files are discovered independently of --lookup-files and statically analyzed.


Quick start

# Install from crates.io
# Building from source requires Rust 1.97 or newer.
cargo install css-variable-lsp

# …or download a release binary for your platform
# (see "Release assets" further down)

# Print the version
css-variable-lsp --version

# Print full CLI help
css-variable-lsp --help

The binary is css-variable-lsp (or css-variable-lsp.exe on Windows). It communicates over stdin/stdout using LSP JSON-RPC — launch it from your editor's language-client configuration; no arguments are required for sensible defaults.

Manual smoke test

# Start the server
./css-variable-lsp

# Send an `initialize` request, then an `initialized` notification,
# then a `shutdown` request and an `exit` notification. Use
# any LSP-aware client (or `nvim --headless`, `helix --health`,
# `zed --foreground`, …) to drive it.

Editor integration

Zed

Zed consumes the release binaries via the zed-css-variables extension. Once installed, no further configuration is required.

Neovim (with nvim-lspconfig)

require('lspconfig').css_variable_lsp.setup({
  cmd = { 'css-variable-lsp' },
  filetypes = { 'css', 'scss', 'sass', 'less', 'html', 'vue', 'svelte', 'astro' },
  init_options = {},
})

Helix

Add to ~/.config/helix/languages.toml:

[language-server.css-variable-lsp]
command = "css-variable-lsp"
args = []

[[language]]
name = "css"
language-servers = ["css-lsp", "css-variable-lsp"]

[[language]]
name = "scss"
language-servers = ["css-lsp", "css-variable-lsp"]

VS Code

Use a generic LSP client extension such as vscode-langservers-extracted:

// .vscode/settings.json
{
  "css.variableLsp.command": "css-variable-lsp",
  "scss.variableLsp.command": "css-variable-lsp"
}

Generic (stdio JSON-RPC)

# The server speaks LSP over stdio — point any LSP client at the binary
# with `css-variable-lsp` as the launch command.

Configuration

The server is configured via CLI flags and environment variables. CLI flags take precedence over environment variables, which take precedence over built-in defaults.

Feature flags

Flag Env var Default Description
--no-color-preview CSS_LSP_COLOR_PREVIEW=0 enabled Disable the LSP color provider
--color-only-variables CSS_LSP_COLOR_ONLY_VARIABLES=1 disabled Only highlight colors on var() calls
--eager-js CSS_LSP_EAGER_JS=1 disabled Parse unopened JS/TS CSS-in-JS files
--lookup-files <globs> CSS_LSP_LOOKUP_FILES *.css, *.html… File globs scanned on the workspace
--ignore-globs <globs> CSS_LSP_IGNORE_GLOBS node_modules, dist… Globs excluded from the scan
--path-display <mode[:N]> CSS_LSP_PATH_DISPLAY relative relative / absolute / abbreviated[:N]
--path-display-length <N> CSS_LSP_PATH_DISPLAY_LENGTH 1 Abbreviation length when mode is abbreviated
--undefined-var-fallback <mode> CSS_LSP_UNDEFINED_VAR_FALLBACK warning warning / info / off
--no-suggest-add-fallback CSS_LSP_SUGGEST_ADD_FALLBACK=0 enabled Suppress the "Add fallback" quickfix
--no-suggest-exact-color-variables CSS_LSP_SUGGEST_EXACT_COLOR_VARIABLES=0 enabled Suppress "replace with var()" suggestions

Singular forms --lookup-file and --ignore-glob (repeatable) are also accepted. Path display modes accept aliases: abbr / fish for abbreviated, and warn / information / omit / none / disabled for the undefined-var-fallback mode.

Examples

# Disable color picker
css-variable-lsp --no-color-preview

# Limit scanning to SCSS and Svelte files
CSS_LSP_LOOKUP_FILES="**/*.scss,**/*.svelte" css-variable-lsp

# Index CSS-in-JS from unopened JS/TS-family files
css-variable-lsp --eager-js

# Use abbreviated paths of length 2, suppress the "Add fallback" quickfix
css-variable-lsp --path-display=abbreviated:2 --no-suggest-add-fallback

# Silent mode for undefined variables (still keep other diagnostics)
css-variable-lsp --undefined-var-fallback=off

Editor configuration

Most clients can pass settings via workspace/didChangeConfiguration. The server accepts the same camelCase keys, either flat or namespaced under cssVariableLsp:

{
  "cssVariableLsp": {
    "lookupFiles":         ["**/*.css", "**/*.scss"],
    "ignoreGlobs":         ["**/node_modules/**", "**/dist/**"],
    "enableColorProvider": true,
    "colorOnlyOnVariables": false,
    "eagerJs": true
  }
}

LSP features in detail

Autocomplete contexts

File kind Where completion triggers Insert text
CSS Inside a rule, after : and before ; var(--name) (or --name inside var()
SCSS/SASS Same as CSS Same as CSS
HTML Inside <style>…</style> or style="…" attribute value Same as CSS
JS / TS Inside string literals and template literal text (not in ${}) Same as CSS

Completion is also triggered on -, (, and : per the upstream TypeScript implementation. Workspace scans follow lookup_files for CSS and HTML-ish files; eager JS mode additionally parses unopened JS/TS-family files.

Hover

  • For definitions: shows value, !important flag, selector, and computed specificity.
  • For usages: lists every definition in cascade order, marks the applicable one (✓ Wins / ✓ Applies here / ✓ Would apply (inline style) / ✓ Applies (DOM match)), and explains why each non-winning definition lost (lower specificity, earlier source, no DOM match, !important overridden).
  • For literal colors: shows a colored swatch when --no-color-preview is not set.

Rename

  • Preserves !important.
  • Preserves fallback arguments (var(--old, red) → var(--new, red)).
  • Updates both definitions and usages across all open files and any file the workspace has indexed.

Diagnostics

Code Severity Trigger Can be disabled?
css-variable-lsp.undefined-variable warning/info var(--name) where --name has no definition in the workspace yes (--undefined-var-fallback)
css-variable-lsp.literal-color-replaceable information Literal color value that matches an existing variable exactly yes (--no-suggest-exact-color-variables)

Diagnostics for undefined-variable are not emitted when the var() call has a fallback (severity drops to info or off depending on configuration), since CSS spec says the fallback will cover the gap.


Architecture

                    ┌──────────────────────────────────┐
                    │           editor client           │
                    └────────────┬──────────────▲────────┘
                  LSP/JSON-RPC   │              │ diagnostics,
                  stdio          ▼              │ hover, completion
                    ┌──────────────────────────────────┐
                    │       src/lsp_server.rs          │
                    │  (tower-lsp handler glue + IoC)  │
                    └────────────┬──────────────▲────────┘
                                 │              │
                    ┌────────────▼──────────────┴────────┐
                    │        src/manager.rs               │
                    │ (thread-safe workspace state:      │
                    │  variables, usages, colors, DOM)   │
                    └────────────┬──────────────▲────────┘
                                 │              │
        ┌────────────────────────┼──────────────┼────────────────────────┐
        │                        │              │                        │
        ▼                        ▼              ▼                        ▼
┌───────────────┐        ┌───────────────┐ ┌──────────────┐      ┌────────────────┐
│ parsers/css   │        │ parsers/html  │ │ parsers/js   │      │ workspace.rs   │
│  (CSS AST)    │        │  (DOM + CSS)  │ │ (CSS-in-JS)  │      │  (walker)      │
└───────────────┘        └───────────────┘ └──────────────┘      └────────────────┘
        │                        │              │                        │
        └────────────────────────┼──────────────┼────────────────────────┘
                                 ▼
                    ┌──────────────────────────────────┐
                    │  specificity / color / path      │
                    │  display / document_kind         │
                    └──────────────────────────────────┘

Module dependency graph

text_utils         (no deps)
   ↓
document_kind      (uses types::Config)
   ↓
completion_context (uses document_kind + text_utils)
   ↓
lsp_server         (uses every other module)

The library is also published as css_variable_lsp, so you can embed the parser / manager in your own tools without the LSP plumbing.


Performance

The crate is built to be cheap to embed in editor extensions and large monorepos. Key numbers (release build, single-threaded, on an M1 Pro laptop, ~10k variable workspace):

Operation Median time
Binary startup ~10 ms
Parse a typical CSS file (500 LoC) <10 ms
Completion with 100 candidates <5 ms
Hover with cascade calculation <10 ms
Workspace re-scan (10k variables) ~1 s
Memory usage at idle (10k variables) ~18 MB

Optimizations include:

  • Memoized regex compilation (std::sync::LazyLock).
  • Selective revalidation: only documents that reference a changed variable name get re-diagnosed.
  • Line-bucketed literal color index for O(1) position lookups.
  • Bounded document count (default 10 000) to prevent OOM on huge repos; the limit is logged, not silently dropped.
  • Async I/O end-to-end (tokio), no blocking fs::* calls on the LSP threads.

Build, test, and ship

Local development

cargo build               # debug build
cargo build --release     # optimized build (matches release artifacts)
cargo test                # run all 180+ unit + integration tests
cargo fmt -- --check       # formatting check
cargo clippy -- -D warnings  # lint check
RUST_LOG=debug CSS_LSP_ENABLE_LOGS=1 cargo run

Running a single test

cargo test test_lsp_rename_preserves_fallbacks
cargo test --test issues_proof_test -- --nocapture

Release assets (local)

Build and package release assets into dist/ (tar.gz on Unix, zip on Windows):

./scripts/build-release-assets.sh
# Build a subset of targets:
./scripts/build-release-assets.sh x86_64-apple-darwin aarch64-apple-darwin

Publish

The repo follows the standard Rust release flow:

  1. Bump version in Cargo.toml.
  2. git tag vX.Y.Z && git push && git push origin vX.Y.Z.
  3. .github/workflows/release.yml builds 6 binaries (Linux, macOS, Windows × x86_64 + aarch64) and attaches them to the GitHub Release.
  4. .github/workflows/publish.yml publishes the crate to crates.io.

See docs/release-publishing.md for the detailed checklist and scripts/smoke-test-release.sh for post-release validation.


Comparison with the TypeScript version

Feature TypeScript Rust
Runtime Node + npm packages Native, no Node.js
Binary size 50–100 MB ~8.2 MB (macOS aarch64)
Cold start ~500 ms ~10 ms
Memory at idle 50–100 MB 10–20 MB
Parser css-tree (full AST) Regex + state machine
Editor integrations Manual per editor Any stdio LSP client
SCSS / SASS / LESS Best-effort regex Best-effort regex
CSS-in-JS None Yes (styled-components, …)
Cross-platform Requires Node.js Single binary per platform
Distribution npm package crates.io + GitHub Releases

The regex-based parser is intentionally pragmatic: it covers the ~95% case for definitions and var() usages in real-world code without dragging in a full CSS grammar. The trade-off is documented under "Known limitations" in CHANGELOG.md.


License

GPL-3.0. Originally derived from the TypeScript css-variable-lsp by the same author. Dependency license notices are listed in THIRD_PARTY_NOTICES.md and are included in release archives.

About

Fast Rust language server for project-wide CSS custom properties: completion, hover, navigation, rename, diagnostics, and color tools.

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages