diff --git a/Makefile b/Makefile index 29b00b4..f120ea9 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,11 @@ MINIMAL_SCRIPT=$(SCRIPTS_DIR)/minimal-colorizer.sh MINIMAL_DEV_SCRIPT=$(SCRIPTS_DIR)/minimal-colorizer-dev.sh MINIMAL_TAILWIND_SCRIPT=$(SCRIPTS_DIR)/minimal-tailwind.sh MINIMAL_TAILWIND_DEV_SCRIPT=$(SCRIPTS_DIR)/minimal-tailwind-dev.sh +MINIMAL_CSS_VAR_SCRIPT=$(SCRIPTS_DIR)/minimal-css-var.sh +MINIMAL_CSS_VAR_DEV_SCRIPT=$(SCRIPTS_DIR)/minimal-css-var-dev.sh MINIMAL_COLORIZER=colorizer_minimal MINIMAL_TAILWIND=colorizer_tailwind +MINIMAL_CSS_VAR=colorizer_css_var MINIMAL_TRIE=colorizer_trie TEST_SCRIPT=$(SCRIPTS_DIR)/run_tests.sh @@ -23,6 +26,8 @@ help: @echo " make minimal-dev - Run the minimal script (local)" @echo " make minimal-tailwind - Run the minimal tailwind config (remote)" @echo " make minimal-tailwind-dev - Run the minimal tailwind config (local)" + @echo " make minimal-css-var - Run the minimal css-var config (remote)" + @echo " make minimal-css-var-dev - Run the minimal css-var config (local)" @echo " make fmt - Auto-format Lua files with StyLua" @echo " make fmt-check - Check Lua formatting (no changes)" @echo " make docs - Generate vimdoc and HTML docs" @@ -60,11 +65,21 @@ minimal-tailwind-dev: @echo "Running minimal tailwind config (local)..." @bash $(MINIMAL_TAILWIND_DEV_SCRIPT) +minimal-css-var: + @echo "Running minimal css-var config (remote)..." + @bash $(MINIMAL_CSS_VAR_SCRIPT) + +minimal-css-var-dev: + @echo "Running minimal css-var config (local)..." + @bash $(MINIMAL_CSS_VAR_DEV_SCRIPT) + clean: @echo "Removing test/"$(MINIMAL_COLORIZER) @rm -rf test/$(MINIMAL_COLORIZER) @echo "Removing test/"$(MINIMAL_TAILWIND) @rm -rf test/$(MINIMAL_TAILWIND) + @echo "Removing test/"$(MINIMAL_CSS_VAR) + @rm -rf test/$(MINIMAL_CSS_VAR) @echo "Removing test/tailwind/node_modules" @rm -rf test/tailwind/node_modules @echo "Removing test/trie/"$(MINIMAL_TRIE) @@ -106,4 +121,4 @@ readme: readme-check: @lua scripts/readme/gen_readme.lua --check -.PHONY: help fmt fmt-check test test-file trie trie-test trie-benchmark minimal minimal-dev minimal-tailwind minimal-tailwind-dev clean docs docs-html demo screenshots screenshots-list readme readme-check +.PHONY: help fmt fmt-check test test-file trie trie-test trie-benchmark minimal minimal-dev minimal-tailwind minimal-tailwind-dev minimal-css-var minimal-css-var-dev clean docs docs-html demo screenshots screenshots-list readme readme-check diff --git a/README.md b/README.md index 34157c5..67a2898 100644 --- a/README.md +++ b/README.md @@ -479,8 +479,23 @@ Features: - Resolves aliased variables: `--alias: var(--base)` chains are followed - Handles `var(--name, fallback)` syntax (highlights using the definition) +- Follows `@import` declarations to resolve variables from imported CSS files - Re-scans definitions on every text change +### Cross-file variable resolution + +`css_var` automatically follows `@import` declarations to resolve variables +defined in other files. All standard import syntaxes are supported: + +```css +@import url("variables.css"); +@import url('tokens.css'); +@import "theme.css"; +``` + +Import paths are resolved relative to the current file. Buffer-local +definitions always take precedence over imported ones. + ## Lua API ```lua diff --git a/lua/colorizer/parser/css_var.lua b/lua/colorizer/parser/css_var.lua index 83d108d..ac32a4f 100644 --- a/lua/colorizer/parser/css_var.lua +++ b/lua/colorizer/parser/css_var.lua @@ -52,33 +52,20 @@ end local DEF_PATTERN = "^%-%-([%w_-]+)%s*:%s*()(.+)" ---- Scan buffer lines for CSS custom property definitions ----@param bufnr number ----@param line_start number 0-indexed ----@param line_end number -1 for end of buffer ----@param lines table|nil ----@param color_parser function Parser function to extract colors from values -function M.update_variables(bufnr, line_start, line_end, lines, color_parser) - lines = lines or vim.api.nvim_buf_get_lines(bufnr, line_start, line_end, false) - - if not state[bufnr] then - state[bufnr] = { definitions = {} } - end - - local defs = {} - -- First pass: collect direct color definitions - local recursive = {} +--- Scan lines for CSS custom property definitions into defs/recursive tables. +---@param lines table Lines to scan +---@param defs table Direct color definitions (name -> rgb_hex), mutated +---@param recursive table Recursive references (name -> ref_name), mutated +---@param color_parser function|nil +local function scan_lines_for_defs(lines, defs, recursive, color_parser) for _, line in ipairs(lines) do - -- Find -- at any position in the line (CSS custom properties can be indented) local s = line:find("%-%-") if s then - local name, value_pos, value = line:match(DEF_PATTERN, s) + local name, _, value = line:match(DEF_PATTERN, s) if name and value then - -- Strip trailing semicolons, whitespace, !important value = value:match("^(.-)%s*;?%s*$") value = value and value:match("^(.-)%s*!important%s*$") or value if value and #value > 0 then - -- Check if value references another variable local ref_name = value:match("^var%(%s*%-%-([%w_-]+)") if ref_name then recursive[name] = ref_name @@ -92,6 +79,74 @@ function M.update_variables(bufnr, line_start, line_end, lines, color_parser) end end end +end + +--- Extract @import file paths from CSS lines. +--- Supports @import url("..."), @import url('...'), @import "...", @import '...'. +---@param lines table Lines to scan +---@return string[] import_paths +local function extract_imports(lines) + local paths = {} + for _, line in ipairs(lines) do + -- @import url("path") or @import url('path') + local p = line:match('@import%s+url%(%s*"([^"]+)"') + or line:match("@import%s+url%(%s*'([^']+)'") + -- @import "path" or @import 'path' + or line:match('@import%s+"([^"]+)"') + or line:match("@import%s+'([^']+)'") + if p then + paths[#paths + 1] = p + end + end + return paths +end + +--- Read an imported CSS file relative to the buffer's directory. +---@param bufnr number +---@param import_path string +---@return string[]|nil lines +local function read_import(bufnr, import_path) + local buf_name = vim.api.nvim_buf_get_name(bufnr) + if buf_name == "" then + return nil + end + local buf_dir = vim.fn.fnamemodify(buf_name, ":h") + local full_path = buf_dir .. "/" .. import_path + -- Normalize and check existence + full_path = vim.fn.resolve(full_path) + if vim.fn.filereadable(full_path) ~= 1 then + return nil + end + return vim.fn.readfile(full_path) +end + +--- Scan buffer lines for CSS custom property definitions +---@param bufnr number +---@param line_start number 0-indexed +---@param line_end number -1 for end of buffer +---@param lines table|nil +---@param color_parser function Parser function to extract colors from values +function M.update_variables(bufnr, line_start, line_end, lines, color_parser) + lines = lines or vim.api.nvim_buf_get_lines(bufnr, line_start, line_end, false) + + if not state[bufnr] then + state[bufnr] = { definitions = {} } + end + + local defs = {} + local recursive = {} + + -- Scan imported files first (lower priority — buffer definitions override) + local imports = extract_imports(lines) + for _, import_path in ipairs(imports) do + local import_lines = read_import(bufnr, import_path) + if import_lines then + scan_lines_for_defs(import_lines, defs, recursive, color_parser) + end + end + + -- Scan buffer lines (higher priority — overwrites imported definitions) + scan_lines_for_defs(lines, defs, recursive, color_parser) -- Resolve recursive references (var(--other)) local function resolve(name, seen) diff --git a/scripts/minimal-colorizer-dev.sh b/scripts/minimal-colorizer-dev.sh index 4054cbc..5755738 100644 --- a/scripts/minimal-colorizer-dev.sh +++ b/scripts/minimal-colorizer-dev.sh @@ -1,4 +1,8 @@ #!/usr/bin/env bash cd test || exit + +# Clear Neovim's bytecode cache so local source changes are always picked up +rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}/nvim/luac" + nvim --clean -u minimal-colorizer-dev.lua diff --git a/scripts/minimal-css-var-dev.sh b/scripts/minimal-css-var-dev.sh new file mode 100644 index 0000000..49e4eec --- /dev/null +++ b/scripts/minimal-css-var-dev.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +cd test/css-var || exit + +# Clear Neovim's bytecode cache so local source changes are always picked up +rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}/nvim/luac" + +nvim --clean -u ../minimal-css-var-dev.lua main.css diff --git a/scripts/minimal-css-var.sh b/scripts/minimal-css-var.sh new file mode 100644 index 0000000..b4af756 --- /dev/null +++ b/scripts/minimal-css-var.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +cd test/css-var || exit + +nvim --clean -u ../minimal-css-var.lua main.css diff --git a/scripts/minimal-tailwind-dev.sh b/scripts/minimal-tailwind-dev.sh index e3a5ecc..f3fea06 100644 --- a/scripts/minimal-tailwind-dev.sh +++ b/scripts/minimal-tailwind-dev.sh @@ -7,4 +7,7 @@ if [ ! -d node_modules ]; then npm install fi +# Clear Neovim's bytecode cache so local source changes are always picked up +rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}/nvim/luac" + nvim --clean -u ../minimal-tailwind-dev.lua tailwind.html diff --git a/test/colorizer_css_var b/test/colorizer_css_var new file mode 160000 index 0000000..85c7ff3 --- /dev/null +++ b/test/colorizer_css_var @@ -0,0 +1 @@ +Subproject commit 85c7ff3711b730b4030d03144f6db6375044ae82 diff --git a/test/css-var/main.css b/test/css-var/main.css new file mode 100644 index 0000000..5e7fe80 --- /dev/null +++ b/test/css-var/main.css @@ -0,0 +1,79 @@ +/* Import shared variables — LSP resolves these across files */ +@import url("variables.css"); + +/* Same-buffer definitions (resolved by buffer scanning) */ +:root { + --local-color: #ff6b6b; + --local-accent: #4ecdc4; +} + +/* Cross-file var() references (resolved by LSP) */ +body { + background-color: var(--bg); + color: var(--text); +} + +header { + background-color: var(--primary); + color: var(--primary-light); + border-bottom: 2px solid var(--border); +} + +.alert-success { + background-color: var(--success); + color: var(--bg); +} + +.alert-warning { + background-color: var(--warning); +} + +.alert-danger { + background-color: var(--danger); +} + +.alert-info { + background-color: var(--info); +} + +/* Same-buffer var() references (resolved by buffer scanning) */ +.local-example { + color: var(--local-color); + border: 1px solid var(--local-accent); +} + +/* Mixed: some from this file, some from import */ +.card { + background: var(--bg-muted); + color: var(--text); + border: 1px solid var(--border); + box-shadow: 0 1px 3px var(--overlay); +} + +/* Aliased variables (chain resolution) */ +a { + color: var(--link-color); +} + +a:hover { + color: var(--brand); +} + +/* Var with fallback */ +.fallback-example { + color: var(--undefined-color, #888888); +} + +/* Semantic usage */ +.sidebar { + background-color: var(--bg-muted); + color: var(--text-muted); + accent-color: var(--accent); +} + +/* Direct color values (highlighted by parser, not LSP) */ +.direct-colors { + color: #ff0000; + background: rgb(0, 128, 255); + border-color: hsl(120, 50%, 50%); +} diff --git a/test/css-var/package-lock.json b/test/css-var/package-lock.json new file mode 100644 index 0000000..733e058 --- /dev/null +++ b/test/css-var/package-lock.json @@ -0,0 +1,422 @@ +{ + "name": "css-var", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "vscode-langservers-extracted": "^4" + } + }, + "node_modules/@vscode/l10n": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", + "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, + "license": "MIT" + }, + "node_modules/request-light": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.7.0.tgz", + "integrity": "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/vscode-css-languageservice": { + "version": "6.3.10", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.10.tgz", + "integrity": "sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-html-languageservice": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.6.2.tgz", + "integrity": "sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-json-languageservice": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.7.2.tgz", + "integrity": "sha512-WtKRDtJfFEmLrgtu+ODexOHm/6/krRF0k6t+uvkKIKW1Jh9ZIyxZQwJJwB3qhrEgvAxa37zbUg+vn+UyUK/U2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "jsonc-parser": "^3.3.1", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "9.0.0-next.11", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.11.tgz", + "integrity": "sha512-u6LElQNbSiE9OugEEmrUKwH6+8BpPz2S5MDHvQUqHL//I4Q8GPikKLOUf856UnbLkZdhxaPrExac1lA3XwpIPA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-langservers-extracted": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/vscode-langservers-extracted/-/vscode-langservers-extracted-4.10.0.tgz", + "integrity": "sha512-EFf9uQI4dAKbzMQFjDvVm1xJq1DXAQvBEuEfPGrK/xzfsL5xWTfIuRr90NgfmqwO+IEt6vLZm9EOj6R66xIifg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "core-js": "^3.20.1", + "jsonc-parser": "^3.2.1", + "regenerator-runtime": "^0.13.9", + "request-light": "^0.7.0", + "semver": "^7.6.1", + "typescript": "^4.0.5", + "vscode-css-languageservice": "^6.2.14", + "vscode-html-languageservice": "^5.2.0", + "vscode-json-languageservice": "^5.3.11", + "vscode-languageserver": "^10.0.0-next.3", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-languageserver-types": "^3.17.5", + "vscode-markdown-languageservice": "^0.5.0-alpha.6", + "vscode-nls": "^5.2.0", + "vscode-uri": "^3.0.8" + }, + "bin": { + "vscode-css-language-server": "bin/vscode-css-language-server", + "vscode-eslint-language-server": "bin/vscode-eslint-language-server", + "vscode-html-language-server": "bin/vscode-html-language-server", + "vscode-json-language-server": "bin/vscode-json-language-server", + "vscode-markdown-language-server": "bin/vscode-markdown-language-server" + } + }, + "node_modules/vscode-languageserver": { + "version": "10.0.0-next.17", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-10.0.0-next.17.tgz", + "integrity": "sha512-/bwO/E3RUzIkQ1BQ70gcLdZeM8xvK0JS7gMvtug7yiH0dzTjciqqQTUh3H9NEXsqYEjLzGwiXgRUkt6Z8fQV0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.6-next.17" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.6-next.17", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.17.tgz", + "integrity": "sha512-HW72YcFsuckfK6oPVuysRXhKiIFJoUvXgspPHvCMWpwe2x9aq2oGZDUSvKx4m/qUGB27+iu8ijAxsFlljYl2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "9.0.0-next.11", + "vscode-languageserver-types": "3.17.6-next.6" + } + }, + "node_modules/vscode-languageserver-protocol/node_modules/vscode-languageserver-types": { + "version": "3.17.6-next.6", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.6.tgz", + "integrity": "sha512-aiJY5/yW+xzw7KPNlwi3gQtddq/3EIn5z8X8nCgJfaiAij2R1APKePngv+MUdLdYJBVTLu+Qa0ODsT+pHgYguQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-markdown-languageservice": { + "version": "0.5.0-alpha.11", + "resolved": "https://registry.npmjs.org/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.5.0-alpha.11.tgz", + "integrity": "sha512-P1uBMAD5iylgpcweWCU1kQwk8SZngktnljXsZk1vFPorXv1mrEI7BkBpOUU0fhVssKgvFlCNLkI7KmwZLC7pdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.10", + "node-html-parser": "^6.1.5", + "picomatch": "^2.3.1", + "vscode-languageserver-protocol": "^3.17.1", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/vscode-markdown-languageservice/node_modules/@vscode/l10n": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.10.tgz", + "integrity": "sha512-E1OCmDcDWa0Ya7vtSjp/XfHFGqYJfh+YPC1RkATU71fTac+j1JjCcB3qwSzmlKAighx2WxhLlfhS0RwAN++PFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-markdown-languageservice/node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-markdown-languageservice/node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-nls": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/test/css-var/variables.css b/test/css-var/variables.css new file mode 100644 index 0000000..51da3d4 --- /dev/null +++ b/test/css-var/variables.css @@ -0,0 +1,28 @@ +/* Shared CSS custom property definitions */ +:root { + /* Primary palette */ + --primary: #3b82f6; + --primary-dark: #1d4ed8; + --primary-light: #93c5fd; + + /* Semantic colors */ + --success: #22c55e; + --warning: #eab308; + --danger: #ef4444; + --info: #06b6d4; + + /* Neutral palette */ + --bg: #ffffff; + --bg-muted: #f3f4f6; + --text: #111827; + --text-muted: #6b7280; + --border: #d1d5db; + + /* Using CSS functions */ + --accent: rgb(139, 92, 246); + --overlay: hsl(220, 14%, 20%); + + /* Aliased variables */ + --brand: var(--primary); + --link-color: var(--primary-dark); +} diff --git a/test/minimal-css-var-dev.lua b/test/minimal-css-var-dev.lua new file mode 100644 index 0000000..4f9c451 --- /dev/null +++ b/test/minimal-css-var-dev.lua @@ -0,0 +1,80 @@ +-- Minimal config for troubleshooting CSS custom property (var()) highlighting (local dev colorizer) +-- Run: make minimal-css-var-dev +-- +-- Dependencies are installed automatically via npm in test/css-var/ + +local settings = { + use_remote = false, -- Use local git directory for colorizer + base_dir = "../colorizer_css_var", -- Directory to clone lazy.nvim (relative to test/css-var/) + local_plugin_dir = os.getenv("HOME") .. "/git/nvim-colorizer.lua", + plugins = {}, +} + +if not (vim.uv or vim.loop).fs_stat(settings.base_dir) then + vim.fn.system({ + "git", + "clone", + "--filter=blob:none", + "https://github.com/folke/lazy.nvim.git", + "--branch=stable", + settings.base_dir, + }) +end +vim.opt.rtp:prepend(settings.base_dir) + +-- Configure colorizer +local function configure_colorizer() + vim.opt.termguicolors = true + require("colorizer").setup({ + filetypes = { "*" }, + options = { + parsers = { + css = true, -- enables hex, rgb, hsl, oklch, names, css_var (+ @import scanning) + }, + display = { + mode = "background", + virtualtext = { char = "■" }, + }, + }, + }) +end + +local function add_colorizer() + local base_config = { + event = "BufReadPre", + config = configure_colorizer, + } + if settings.use_remote then + table.insert( + settings.plugins, + vim.tbl_extend("force", base_config, { + "catgoose/nvim-colorizer.lua", + url = "https://github.com/catgoose/nvim-colorizer.lua", + }) + ) + else + local local_dir = settings.local_plugin_dir + if vim.fn.isdirectory(local_dir) == 1 then + vim.opt.rtp:append(local_dir) + table.insert( + settings.plugins, + vim.tbl_extend("force", base_config, { + dir = local_dir, + lazy = false, + }) + ) + else + vim.notify("Local plugin directory not found: " .. local_dir, vim.log.levels.ERROR) + end + end +end + +-- Initialize and setup lazy.nvim +local ok, lazy = pcall(require, "lazy") +if not ok then + vim.notify("Failed to require lazy.nvim", vim.log.levels.ERROR) + return +end + +add_colorizer() +lazy.setup(settings.plugins) diff --git a/test/minimal-css-var.lua b/test/minimal-css-var.lua new file mode 100644 index 0000000..66b6b4a --- /dev/null +++ b/test/minimal-css-var.lua @@ -0,0 +1,80 @@ +-- Minimal config for troubleshooting CSS custom property (var()) highlighting (remote colorizer) +-- Run: make minimal-css-var +-- +-- Dependencies are installed automatically via npm in test/css-var/ + +local settings = { + use_remote = true, -- Use colorizer master or local git directory + base_dir = "../colorizer_css_var", -- Directory to clone lazy.nvim (relative to test/css-var/) + local_plugin_dir = os.getenv("HOME") .. "/git/nvim-colorizer.lua", + plugins = {}, +} + +if not (vim.uv or vim.loop).fs_stat(settings.base_dir) then + vim.fn.system({ + "git", + "clone", + "--filter=blob:none", + "https://github.com/folke/lazy.nvim.git", + "--branch=stable", + settings.base_dir, + }) +end +vim.opt.rtp:prepend(settings.base_dir) + +-- Configure colorizer +local function configure_colorizer() + vim.opt.termguicolors = true + require("colorizer").setup({ + filetypes = { "*" }, + options = { + parsers = { + css = true, -- enables hex, rgb, hsl, oklch, names, css_var (+ @import scanning) + }, + display = { + mode = "background", + virtualtext = { char = "■" }, + }, + }, + }) +end + +local function add_colorizer() + local base_config = { + event = "BufReadPre", + config = configure_colorizer, + } + if settings.use_remote then + table.insert( + settings.plugins, + vim.tbl_extend("force", base_config, { + "catgoose/nvim-colorizer.lua", + url = "https://github.com/catgoose/nvim-colorizer.lua", + }) + ) + else + local local_dir = settings.local_plugin_dir + if vim.fn.isdirectory(local_dir) == 1 then + vim.opt.rtp:append(local_dir) + table.insert( + settings.plugins, + vim.tbl_extend("force", base_config, { + dir = local_dir, + lazy = false, + }) + ) + else + vim.notify("Local plugin directory not found: " .. local_dir, vim.log.levels.ERROR) + end + end +end + +-- Initialize and setup lazy.nvim +local ok, lazy = pcall(require, "lazy") +if not ok then + vim.notify("Failed to require lazy.nvim", vim.log.levels.ERROR) + return +end + +add_colorizer() +lazy.setup(settings.plugins) diff --git a/tests/test_css_lsp.lua b/tests/test_css_lsp.lua new file mode 100644 index 0000000..ca9c870 --- /dev/null +++ b/tests/test_css_lsp.lua @@ -0,0 +1,151 @@ +local helpers = require("tests.helpers") +local eq = helpers.eq +local new_set = helpers.new_set + +local css_var = require("colorizer.parser.css_var") +local config = require("colorizer.config") + +local T = new_set() + +-- Helpers +local function make_buf(lines) + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines or {}) + return buf +end + +local function color_parser(line, i) + local hex = line:sub(i):match("^#(%x%x%x%x%x%x)") + if hex then + return 7, hex:lower() + end + return nil +end + +-- @import scanning -------------------------------------------------------- + +T["imports"] = new_set() + +T["imports"]["resolves variables from @import url() file"] = function() + local tmpdir = vim.fn.tempname() + vim.fn.mkdir(tmpdir, "p") + vim.fn.writefile({ + ":root {", + " --imported-color: #aabb00;", + " --imported-accent: #cc00dd;", + "}", + }, tmpdir .. "/vars.css") + local bufnr = make_buf({ + '@import url("vars.css");', + ":root {", + " --local-color: #112233;", + "}", + }) + vim.api.nvim_buf_set_name(bufnr, tmpdir .. "/main.css") + css_var.update_variables(bufnr, 0, -1, nil, color_parser) + local _, hex1 = css_var.parser("var(--imported-color)", 1, bufnr) + local _, hex2 = css_var.parser("var(--imported-accent)", 1, bufnr) + local _, hex3 = css_var.parser("var(--local-color)", 1, bufnr) + eq("aabb00", hex1) + eq("cc00dd", hex2) + eq("112233", hex3) + css_var.cleanup(bufnr) + vim.fn.delete(tmpdir, "rf") +end + +T["imports"]["local definitions override imported"] = function() + local tmpdir = vim.fn.tempname() + vim.fn.mkdir(tmpdir, "p") + vim.fn.writefile({ " --color: #111111;" }, tmpdir .. "/vars.css") + local bufnr = make_buf({ + '@import url("vars.css");', + " --color: #222222;", + }) + vim.api.nvim_buf_set_name(bufnr, tmpdir .. "/main.css") + css_var.update_variables(bufnr, 0, -1, nil, color_parser) + local _, hex = css_var.parser("var(--color)", 1, bufnr) + eq("222222", hex) + css_var.cleanup(bufnr) + vim.fn.delete(tmpdir, "rf") +end + +T["imports"]["handles @import with single quotes"] = function() + local tmpdir = vim.fn.tempname() + vim.fn.mkdir(tmpdir, "p") + vim.fn.writefile({ " --sq: #aaaaaa;" }, tmpdir .. "/sq.css") + local bufnr = make_buf({ "@import url('sq.css');" }) + vim.api.nvim_buf_set_name(bufnr, tmpdir .. "/main.css") + css_var.update_variables(bufnr, 0, -1, nil, color_parser) + local _, hex = css_var.parser("var(--sq)", 1, bufnr) + eq("aaaaaa", hex) + css_var.cleanup(bufnr) + vim.fn.delete(tmpdir, "rf") +end + +T["imports"]["handles @import without url()"] = function() + local tmpdir = vim.fn.tempname() + vim.fn.mkdir(tmpdir, "p") + vim.fn.writefile({ " --bare: #bbbbbb;" }, tmpdir .. "/bare.css") + local bufnr = make_buf({ '@import "bare.css";' }) + vim.api.nvim_buf_set_name(bufnr, tmpdir .. "/main.css") + css_var.update_variables(bufnr, 0, -1, nil, color_parser) + local _, hex = css_var.parser("var(--bare)", 1, bufnr) + eq("bbbbbb", hex) + css_var.cleanup(bufnr) + vim.fn.delete(tmpdir, "rf") +end + +T["imports"]["missing import file is silently skipped"] = function() + local bufnr = make_buf({ + '@import url("nonexistent.css");', + " --local: #ffffff;", + }) + local tmpdir = vim.fn.tempname() + vim.fn.mkdir(tmpdir, "p") + vim.api.nvim_buf_set_name(bufnr, tmpdir .. "/main.css") + css_var.update_variables(bufnr, 0, -1, nil, color_parser) + local _, hex = css_var.parser("var(--local)", 1, bufnr) + eq("ffffff", hex) + css_var.cleanup(bufnr) + vim.fn.delete(tmpdir, "rf") +end + +T["imports"]["resolves aliased vars across import boundary"] = function() + local tmpdir = vim.fn.tempname() + vim.fn.mkdir(tmpdir, "p") + vim.fn.writefile({ " --base: #abcdef;" }, tmpdir .. "/vars.css") + local bufnr = make_buf({ + '@import url("vars.css");', + " --alias: var(--base);", + }) + vim.api.nvim_buf_set_name(bufnr, tmpdir .. "/main.css") + css_var.update_variables(bufnr, 0, -1, nil, color_parser) + local _, hex = css_var.parser("var(--alias)", 1, bufnr) + eq("abcdef", hex) + css_var.cleanup(bufnr) + vim.fn.delete(tmpdir, "rf") +end + +T["imports"]["unnamed buffer skips import resolution"] = function() + local bufnr = make_buf({ + '@import url("vars.css");', + " --local: #aaaaaa;", + }) + css_var.update_variables(bufnr, 0, -1, nil, color_parser) + local _, hex = css_var.parser("var(--local)", 1, bufnr) + eq("aaaaaa", hex) + css_var.cleanup(bufnr) +end + +-- config ------------------------------------------------------------------ + +T["config"] = new_set() + +T["config"]["css preset enables css_var"] = function() + local opts = config.resolve_options({ + parsers = { css = true }, + }) + eq(true, opts.parsers.css_var.enable) +end + +return T