css-variable-lsp 0.3.1

A fast, Rust-based Language Server Protocol implementation for CSS Variables
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# CSS Variable LSP (Rust)

A fast, **Rust**-based [Language Server Protocol][lsp] 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 single static binary that any LSP-aware editor (Zed, VS Code,
Neovim, Helix, …) can launch with no Node.js runtime in sight.

[lsp]: https://microsoft.github.io/language-server-protocol/

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

[zed-ext]: https://github.com/lmn451/zed-css-variables
[crates]: https://crates.io/crates/css-variable-lsp

---

## Table of contents

- [Why a Rust rewrite?]#why-a-rust-rewrite
- [Features]#features
- [Quick start]#quick-start
- [Editor integration]#editor-integration
- [Configuration]#configuration
- [LSP features in detail]#lsp-features-in-detail
- [Architecture]#architecture
- [Performance]#performance
- [Build, test, and ship]#build-test-and-ship
- [Comparison with the TypeScript version]#comparison-with-the-typescript-version
- [License]#license

---

## Why a Rust rewrite?

|                    | TypeScript `css-variable-lsp` | This crate            |
| ------------------ | ----------------------------- | --------------------- |
| Runtime            | Node.js + npm dependencies    | **None** — static bin |
| Binary size        | ~50–100 MB                    | **~6 MB**             |
| 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 single-binary, zero-dependency server 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, `rgb()`, `rgba()`, `hsl()`, `hsla()`, named colors, 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.
- **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 (default)                                |
| -------- | --------------------------------------------------- |
| CSS      | `.css`, `.scss`, `.sass`, `.less`                   |
| HTML-ish | `.html`, `.vue`, `.svelte`, `.astro`, `.ripple`     |
| JS-ish   | `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`, `.mts`, `.cts` |

All of these are configurable via the `--lookup-files` flag.

---

## Quick start

```bash
# Install from crates.io
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

```bash
# 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][zed-ext] extension. Once installed, no further
configuration is required.

### Neovim (with `nvim-lspconfig`)

```lua
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`:

```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](https://github.com/Microsoft/vscode-langservers-extracted):

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

### Generic (stdio JSON-RPC)

```bash
# 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       |
| `--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

```bash
# 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

# 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`:

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

---

## 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, and respects the workspace's `lookup_files`
configuration to decide which file kinds are even parsed.

### 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

```bash
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

```bash
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):

```bash
./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`](docs/release-publishing.md) for the
detailed checklist and [`scripts/smoke-test-release.sh`](scripts/smoke-test-release.sh)
for post-release validation.

---

## Comparison with the TypeScript version

| Feature               | TypeScript                           | Rust                       |
| --------------------- | ------------------------------------ | -------------------------- |
| Runtime               | Node + npm packages                  | **None**                   |
| Binary size           | 50–100 MB                            | **6 MB**                   |
| 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](LICENSE). Originally derived from the TypeScript
`css-variable-lsp` by the same author.