From 08da37e236ce9576d76657a643e3fde15c6fdf8e Mon Sep 17 00:00:00 2001 From: Joshua Tye <21010072+catgoose@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:58:30 -0500 Subject: [PATCH] feat: adds multiple mode selection --- README.md | 24 ++- lua/colorizer/buffer.lua | 174 ++++++++++++------ lua/colorizer/config.lua | 42 ++++- scripts/screenshots/configs.lua | 26 +++ test/expect.lua | 45 ++++- tests/test_buffer_highlight.lua | 113 ++++++++++++ tests/test_config.lua | 46 +++++ ...st_css_lsp.lua => test_css_var_import.lua} | 0 tests/test_new_options.lua | 58 +++--- 9 files changed, 429 insertions(+), 99 deletions(-) rename tests/{test_css_lsp.lua => test_css_var_import.lua} (100%) diff --git a/README.md b/README.md index 67a28981..bed5f222 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ require("colorizer").setup({ custom = {}, -- list of custom parser definitions }, display = { - mode = "background", -- "background"|"foreground"|"underline"|"virtualtext" + mode = "background", -- string or list: "background"|"foreground"|"underline"|"virtualtext" background = { bright_fg = "#000000", -- text color on bright backgrounds dark_fg = "#ffffff", -- text color on dark backgrounds @@ -328,6 +328,28 @@ See `:help vim.lsp.document_color.enable()` for details. > **Note:** This only applies to Neovim 0.12+. Neovim 0.10 and 0.11 do not > have this feature and are unaffected. +## Combined display modes + +`display.mode` accepts a list to apply multiple modes simultaneously: + +```lua +require("colorizer").setup({ + options = { + display = { + mode = { "background", "virtualtext" }, -- colored background + color swatch + }, + }, +}) +``` + +Non-virtualtext modes (`background`, `foreground`, `underline`) merge into a +single extmark since their highlight attributes don't overlap. `virtualtext` +always gets its own extmark. Any combination of the four modes is valid. + +> **Note:** `background` and `foreground` both set the `fg` attribute. +> When combined, `background` wins (auto-contrast text is needed for +> readability). Use `background` + `underline` if you want both effects. + ## Highlight priority Colorizer uses extmark priorities from `display.priority` to control which diff --git a/lua/colorizer/buffer.lua b/lua/colorizer/buffer.lua index ba0e14cf..a249a7a1 100644 --- a/lua/colorizer/buffer.lua +++ b/lua/colorizer/buffer.lua @@ -32,9 +32,10 @@ local function make_highlight_name(rgb, mode) return table.concat({ hl_state.name_prefix, const.highlight_mode_names[mode], rgb }, "_") end ---- Create a highlight with the given rgb_hex and mode +--- Create a highlight with the given rgb_hex and a single mode. +--- Used for virtualtext's hl_mode (always a single string). ---@param rgb_hex string RGB hex code ----@param mode 'background'|'foreground' Mode of the highlight +---@param mode string Single mode name ---@param bg_opts table|nil Background display options { bright_fg, dark_fg } local function create_highlight(rgb_hex, mode, bg_opts) mode = mode or "background" @@ -45,12 +46,10 @@ local function create_highlight(rgb_hex, mode, bg_opts) table.concat({ const.highlight_mode_names[mode], rgb_hex, bright_fg, dark_fg }, "_") local highlight_name = hl_state.cache[cache_key] - -- Look up in our cache. if highlight_name then return highlight_name end - -- Create the highlight highlight_name = make_highlight_name(rgb_hex, mode) if mode == "foreground" then vim.api.nvim_set_hl(0, highlight_name, { fg = "#" .. rgb_hex }) @@ -66,6 +65,62 @@ local function create_highlight(rgb_hex, mode, bg_opts) return highlight_name end +--- Create a combined highlight merging multiple non-virtualtext modes. +---@param rgb_hex string RGB hex code +---@param modes string[] Sorted list of mode names (no "virtualtext") +---@param bg_opts table|nil Background display options { bright_fg, dark_fg } +local function create_combined_highlight(rgb_hex, modes, bg_opts) + -- Fast path: single mode delegates to existing function + if #modes == 1 then + return create_highlight(rgb_hex, modes[1], bg_opts) + end + + rgb_hex = rgb_hex:lower() + local bright_fg = bg_opts and bg_opts.bright_fg or "#000000" + local dark_fg = bg_opts and bg_opts.dark_fg or "#ffffff" + + -- Build sorted mode key for caching (modes already sorted by config validation) + local mode_keys = {} + for _, m in ipairs(modes) do + mode_keys[#mode_keys + 1] = const.highlight_mode_names[m] + end + local mode_key = table.concat(mode_keys, "_") + + local cache_key = table.concat({ mode_key, rgb_hex, bright_fg, dark_fg }, "_") + local highlight_name = hl_state.cache[cache_key] + if highlight_name then + return highlight_name + end + + highlight_name = table.concat({ hl_state.name_prefix, mode_key, rgb_hex }, "_") + + -- Merge attributes from all modes + local hl_def = {} + local mode_set = {} + for _, m in ipairs(modes) do + mode_set[m] = true + end + + if mode_set["foreground"] then + hl_def.fg = "#" .. rgb_hex + end + if mode_set["underline"] then + hl_def.sp = "#" .. rgb_hex + hl_def.underline = true + end + if mode_set["background"] then + -- background overrides foreground's fg (auto-contrast needed for readability) + local rr, gg, bb = rgb_hex:sub(1, 2), rgb_hex:sub(3, 4), rgb_hex:sub(5, 6) + local r, g, b = tonumber(rr, 16), tonumber(gg, 16), tonumber(bb, 16) + hl_def.fg = color.is_bright(r, g, b) and bright_fg or dark_fg + hl_def.bg = "#" .. rgb_hex + end + + vim.api.nvim_set_hl(0, highlight_name, hl_def) + hl_state.cache[cache_key] = highlight_name + return highlight_name +end + local function slice_line(bufnr, line, start_col, end_col) local lines = vim.api.nvim_buf_get_lines(bufnr, line, line + 1, false) if #lines == 0 then @@ -112,66 +167,75 @@ function M.add_highlight(bufnr, ns_id, line_start, line_end, data, opts, hl_opts local bg_opts = d.background local tw = opts.parsers.tailwind or {} - if d.mode == "background" or d.mode == "foreground" or d.mode == "underline" then - local tw_lsp = tw.lsp - local tw_both = tw.enable and tw_lsp and tw_lsp.enable and hl_opts.tailwind_lsp - for linenr, hls in pairs(data) do - -- When LSP data supersedes name-based tailwind matches, clear the - -- default namespace for this line to avoid hidden duplicate extmarks. - if tw_both then - vim.api.nvim_buf_clear_namespace(bufnr, const.namespace.default, linenr, linenr + 1) - end - for _, hl in ipairs(hls) do - if tw_both and tw.update_names then - local txt = slice_line(bufnr, linenr, hl.range[1], hl.range[2]) - if txt and not hl_state.updated_colors[txt] then - hl_state.updated_colors[txt] = true - names.update_color(txt, hl.rgb_hex, "tailwind_names") - end - end - local hlname = create_highlight(hl.rgb_hex, d.mode, bg_opts) - vim.api.nvim_buf_set_extmark(bufnr, ns_id, linenr, hl.range[1], { - end_col = hl.range[2], - hl_group = hlname, - priority = priority, - }) - end + -- Normalize mode to table (may be string from pre-resolved defaults) + local mode_list = type(d.mode) == "table" and d.mode or { d.mode } + + -- Split mode list into non-virtualtext modes and virtualtext flag + local non_vt_modes = {} + local has_virtualtext = false + for _, m in ipairs(mode_list) do + if m == "virtualtext" then + has_virtualtext = true + else + non_vt_modes[#non_vt_modes + 1] = m end - elseif d.mode == "virtualtext" then - local vt = d.virtualtext - -- Reuse a single opts table across iterations to reduce allocations - local extmark_opts = { + end + + -- Virtualtext setup (reusable tables to reduce allocations) + local vt, vt_extmark_opts, vt_entry, vt_list + if has_virtualtext then + vt = d.virtualtext + vt_extmark_opts = { virt_text = nil, hl_mode = "combine", priority = 0, virt_text_pos = nil, end_col = nil, } - -- Reuse a single inner table for virt_text entries - local virt_text_entry = { "", "" } - local virt_text_list = { virt_text_entry } - local tw_lsp2 = tw.lsp - local tw_both = tw.enable and tw_lsp2 and tw_lsp2.enable and hl_opts.tailwind_lsp - for linenr, hls in pairs(data) do - if tw_both then + vt_entry = { "", "" } + vt_list = { vt_entry } + end + + local tw_lsp = tw.lsp + local tw_both = tw.enable and tw_lsp and tw_lsp.enable and hl_opts.tailwind_lsp + + for linenr, hls in pairs(data) do + -- When LSP data supersedes name-based tailwind matches, clear the + -- default namespace for this line to avoid hidden duplicate extmarks. + if tw_both then + if has_virtualtext then vim.api.nvim_buf_clear_namespace(bufnr, ns_id, linenr, linenr + 1) - vim.api.nvim_buf_clear_namespace(bufnr, const.namespace.default, linenr, linenr + 1) end - for _, hl in ipairs(hls) do - if tw_both and tw.update_names then - local txt = slice_line(bufnr, linenr, hl.range[1], hl.range[2]) - if txt and not hl_state.updated_colors[txt] then - hl_state.updated_colors[txt] = true - names.update_color(txt, hl.rgb_hex, "tailwind_names") - end + vim.api.nvim_buf_clear_namespace(bufnr, const.namespace.default, linenr, linenr + 1) + end + for _, hl in ipairs(hls) do + if tw_both and tw.update_names then + local txt = slice_line(bufnr, linenr, hl.range[1], hl.range[2]) + if txt and not hl_state.updated_colors[txt] then + hl_state.updated_colors[txt] = true + names.update_color(txt, hl.rgb_hex, "tailwind_names") end + end + + -- Non-virtualtext: one extmark with combined highlight group + if #non_vt_modes > 0 then + local hlname = create_combined_highlight(hl.rgb_hex, non_vt_modes, bg_opts) + vim.api.nvim_buf_set_extmark(bufnr, ns_id, linenr, hl.range[1], { + end_col = hl.range[2], + hl_group = hlname, + priority = priority, + }) + end + + -- Virtualtext: separate extmark + if has_virtualtext then local hlname = create_highlight(hl.rgb_hex, vt.hl_mode, bg_opts) local start_col = hl.range[2] - virt_text_entry[2] = hlname + vt_entry[2] = hlname if vt.position == "before" or vt.position == "after" then - extmark_opts.virt_text_pos = "inline" + vt_extmark_opts.virt_text_pos = "inline" local vt_char = vt.char or const.defaults.virtualtext - virt_text_entry[1] = string.format( + vt_entry[1] = string.format( "%s%s%s", vt.position == "before" and vt_char or " ", vt.position == "before" and " " or "", @@ -181,13 +245,13 @@ function M.add_highlight(bufnr, ns_id, line_start, line_end, data, opts, hl_opts start_col = hl.range[1] end else - extmark_opts.virt_text_pos = nil - virt_text_entry[1] = vt.char or const.defaults.virtualtext + vt_extmark_opts.virt_text_pos = nil + vt_entry[1] = vt.char or const.defaults.virtualtext end - extmark_opts.virt_text = virt_text_list - extmark_opts.end_col = start_col + vt_extmark_opts.virt_text = vt_list + vt_extmark_opts.end_col = start_col pcall(function() - vim.api.nvim_buf_set_extmark(bufnr, ns_id, linenr, start_col, extmark_opts) + vim.api.nvim_buf_set_extmark(bufnr, ns_id, linenr, start_col, vt_extmark_opts) end) end end diff --git a/lua/colorizer/config.lua b/lua/colorizer/config.lua index 3346676f..b2f753f6 100644 --- a/lua/colorizer/config.lua +++ b/lua/colorizer/config.lua @@ -790,10 +790,30 @@ end --- Validate new-format options. Validates enums, processes names.custom, checks hook types. ---@param opts table New-format options (fully merged with defaults) function M.validate_new_options(opts) - -- Validate display.mode enum + -- Validate display.mode: accept string or list of strings, normalize to sorted table local valid_modes = { background = true, foreground = true, underline = true, virtualtext = true } - if not valid_modes[opts.display.mode] then - opts.display.mode = default_options.display.mode + local mode = opts.display.mode + if type(mode) == "string" then + if not valid_modes[mode] then + mode = default_options.display.mode + end + opts.display.mode = { mode } + elseif type(mode) == "table" then + local seen = {} + local cleaned = {} + for _, m in ipairs(mode) do + if valid_modes[m] and not seen[m] then + seen[m] = true + cleaned[#cleaned + 1] = m + end + end + if #cleaned == 0 then + cleaned = { default_options.display.mode } + end + table.sort(cleaned) + opts.display.mode = cleaned + else + opts.display.mode = { default_options.display.mode } end -- Normalize tailwind.lsp to table form @@ -1077,13 +1097,15 @@ local function validate_options(opts) if opts.virtualtext_inline ~= "before" and opts.virtualtext_inline ~= "after" then opts.virtualtext_inline = plugin_user_default_options.virtualtext_inline end - if - opts.mode ~= "background" - and opts.mode ~= "foreground" - and opts.mode ~= "underline" - and opts.mode ~= "virtualtext" - then - opts.mode = plugin_user_default_options.mode + if type(opts.mode) ~= "table" then + if + opts.mode ~= "background" + and opts.mode ~= "foreground" + and opts.mode ~= "underline" + and opts.mode ~= "virtualtext" + then + opts.mode = plugin_user_default_options.mode + end end if opts.virtualtext_mode ~= "background" and opts.virtualtext_mode ~= "foreground" then opts.virtualtext_mode = plugin_user_default_options.virtualtext_mode diff --git a/scripts/screenshots/configs.lua b/scripts/screenshots/configs.lua index 48fdb4bf..87c04402 100644 --- a/scripts/screenshots/configs.lua +++ b/scripts/screenshots/configs.lua @@ -366,6 +366,28 @@ M.configs = { description = "custom priority (default=50, lsp=300)", display = { mode = "background", priority = { default = 50, lsp = 300 } }, }), + + -- ── Combined display modes ────────────────────────────────────── + display_bg_vt = cfg("display.css", { css = true }, { + label = "display_bg_vt", + description = "combined: background + virtualtext", + display = { mode = { "background", "virtualtext" }, virtualtext = { position = "after" } }, + }), + display_fg_underline = cfg("display.css", { css = true }, { + label = "display_fg_underline", + description = "combined: foreground + underline", + display = { mode = { "foreground", "underline" } }, + }), + display_bg_underline = cfg("display.css", { css = true }, { + label = "display_bg_underline", + description = "combined: background + underline", + display = { mode = { "background", "underline" } }, + }), + display_bg_underline_vt = cfg("display.css", { css = true }, { + label = "display_bg_underline_vt", + description = "combined: background + underline + virtualtext (eol)", + display = { mode = { "background", "underline", "virtualtext" } }, + }), } --- Ordered categories for --list, iteration, and -- filtering. @@ -428,6 +450,10 @@ M.categories = { "display_vt_char_block", "display_bg_contrast", "display_priority", + "display_bg_vt", + "display_fg_underline", + "display_bg_underline", + "display_bg_underline_vt", }, }, } diff --git a/test/expect.lua b/test/expect.lua index 87e87594..c4c3a4a5 100644 --- a/test/expect.lua +++ b/test/expect.lua @@ -33,7 +33,13 @@ local opts = { return colors.palette end, }, - hex = { default = true }, + hex = { + default = true, + rrggbbaa = true, + hash_aarrggbb = true, + aarrggbb = true, + no_hash = false, + }, rgb = { enable = true }, hsl = { enable = true }, oklch = { enable = true }, @@ -41,17 +47,30 @@ local opts = { lab = { enable = true }, lch = { enable = true }, css_color = { enable = true }, + hsluv = { enable = true }, tailwind = { enable = true }, sass = { enable = true, parsers = { css = true } }, xterm = { enable = true }, + xcolor = { enable = true }, + css_var_rgb = { enable = true }, + css_var = { enable = true, parsers = { css = true } }, }, display = { - mode = "background", -- "background"|"foreground"|"underline"|"virtualtext" + mode = { "background", "virtualtext" }, -- combined: colored background + color swatch + background = { + bright_fg = "#000000", + dark_fg = "#ffffff", + }, virtualtext = { char = "■", - position = "eol", + position = "after", hl_mode = "foreground", }, + priority = { + default = 150, + lsp = 200, + }, + disable_document_color = true, }, hooks = { -- Example: skip black and white @@ -68,6 +87,7 @@ local opts = { end, }, always_update = false, + debounce_ms = 0, }, } @@ -152,7 +172,7 @@ SUCCESS CASES: \e[38;5;42m, #00d75f \e[38;5;42m #00d75f \e[38;5;43m #00d787 - #00d75f +[38;5;42m #00d75f \e[30;0m #000000 \e[31;0m #800000 @@ -302,6 +322,11 @@ hsl(255, 100%, 100%, 1) hsl(255000, 100000%, 100000%, 1000) hsla(300, 50, 50%, 0.5) hsla(300,50%,50,0.5) hsla(300,50,50,0.5) +HSLuv: +hsluv(0 100 50) hsluv(120 100 50) hsluv(240 100 50) +hsluv(60 80 70) hsluv(300 60 30) hsluv(180 50 80) +hsluv(0 100 50 / 0.5) hsluv(120 100 50 / 50%) + OKLCH: oklch(0.5 0.2 180) oklch(0.628 0.258 29.234) oklch(0.519 0.176 142.495) oklch(0.452 0.313 264.052) @@ -354,6 +379,18 @@ color(a98-rgb 1 0 0) color(a98-rgb 0 1 0) color(a98-rgb 0.5 0.5 0.5) color(prophoto-rgb 1 1 1) color(prophoto-rgb 0 0 0) color(prophoto-rgb 0.5 0.3 0.8) color(rec2020 1 0 0) color(rec2020 0 1 0) color(rec2020 0.5 0.5 0.5) +CSS custom properties (var): +:root { --primary: #3b82f6; --accent: #ef4444; } +color: var(--primary); +background: var(--accent); + +CSS variable RGB (--name: R,G,B): +--ctp-flamingo: 240,198,198; +--theme-red: 255,0,0; + +LaTeX xcolor: +red!50 blue!30!green red!25!blue!75 + Xterm ANSI background 256-color: \e[48;5;0m \e[48;5;15m \e[48;5;42m \e[48;5;196m \e[48;5;255m diff --git a/tests/test_buffer_highlight.lua b/tests/test_buffer_highlight.lua index 0c8fe37e..b851d7e6 100644 --- a/tests/test_buffer_highlight.lua +++ b/tests/test_buffer_highlight.lua @@ -252,4 +252,117 @@ T["priority"]["tailwind_lsp priority matches user"] = function() vim.api.nvim_buf_delete(buf, { force = true }) end +-- combined display modes ------------------------------------------------------ + +T["combined modes"] = new_set() + +T["combined modes"]["table mode works like string for single mode"] = function() + local buf = make_buf({ "#FF0000 text" }) + local ns = vim.api.nvim_create_namespace("test_combined_single") + local opts = config.resolve_options({ parsers = { css = true }, display = { mode = { "background" } } }) + local data = buffer.parse_lines(buf, { "#FF0000 text" }, 0, opts) + buffer.add_highlight(buf, ns, 0, 1, data, opts) + local marks = vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, { details = true }) + eq(true, #marks > 0) + eq(true, marks[1][4].hl_group:find("mb") ~= nil) + vim.api.nvim_buf_delete(buf, { force = true }) +end + +T["combined modes"]["background + underline produces one extmark with both attrs"] = function() + local buf = make_buf({ "#FF0000 text" }) + local ns = vim.api.nvim_create_namespace("test_combined_bg_ul") + local opts = config.resolve_options({ + parsers = { css = true }, + display = { mode = { "background", "underline" } }, + }) + local data = buffer.parse_lines(buf, { "#FF0000 text" }, 0, opts) + buffer.add_highlight(buf, ns, 0, 1, data, opts) + local marks = vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, { details = true }) + eq(1, #marks) -- single extmark, not two + local hl_name = marks[1][4].hl_group + -- Should contain both mode codes + eq(true, hl_name:find("mb") ~= nil) + eq(true, hl_name:find("mu") ~= nil) + -- Verify actual highlight attributes + local hl = vim.api.nvim_get_hl(0, { name = hl_name }) + eq(true, hl.bg ~= nil) -- background set + eq(true, hl.sp ~= nil) -- underline sp set + eq(true, hl.underline == true) + vim.api.nvim_buf_delete(buf, { force = true }) +end + +T["combined modes"]["foreground + underline produces one extmark"] = function() + local buf = make_buf({ "#00FF00 text" }) + local ns = vim.api.nvim_create_namespace("test_combined_fg_ul") + local opts = config.resolve_options({ + parsers = { css = true }, + display = { mode = { "foreground", "underline" } }, + }) + local data = buffer.parse_lines(buf, { "#00FF00 text" }, 0, opts) + buffer.add_highlight(buf, ns, 0, 1, data, opts) + local marks = vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, { details = true }) + eq(1, #marks) + local hl = vim.api.nvim_get_hl(0, { name = marks[1][4].hl_group }) + eq(true, hl.fg ~= nil) -- foreground color set + eq(true, hl.sp ~= nil) -- underline sp set + eq(true, hl.underline == true) + vim.api.nvim_buf_delete(buf, { force = true }) +end + +T["combined modes"]["background + virtualtext produces two extmarks"] = function() + local buf = make_buf({ "#FF0000 text" }) + local ns = vim.api.nvim_create_namespace("test_combined_bg_vt") + local opts = config.resolve_options({ + parsers = { css = true }, + display = { mode = { "background", "virtualtext" }, virtualtext = { position = "eol" } }, + }) + local data = buffer.parse_lines(buf, { "#FF0000 text" }, 0, opts) + buffer.add_highlight(buf, ns, 0, 1, data, opts) + local marks = vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, { details = true }) + eq(2, #marks) -- one hl_group extmark + one virtualtext extmark + -- One should have hl_group (background), the other virt_text + local has_hl = false + local has_vt = false + for _, m in ipairs(marks) do + if m[4].hl_group then + has_hl = true + end + if m[4].virt_text then + has_vt = true + end + end + eq(true, has_hl) + eq(true, has_vt) + vim.api.nvim_buf_delete(buf, { force = true }) +end + +T["combined modes"]["virtualtext-only table works"] = function() + local buf = make_buf({ "#FF0000 text" }) + local ns = vim.api.nvim_create_namespace("test_combined_vt_only") + local opts = config.resolve_options({ + parsers = { css = true }, + display = { mode = { "virtualtext" } }, + }) + local data = buffer.parse_lines(buf, { "#FF0000 text" }, 0, opts) + buffer.add_highlight(buf, ns, 0, 1, data, opts) + local marks = vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, { details = true }) + eq(1, #marks) -- only virtualtext + eq(true, marks[1][4].virt_text ~= nil) + vim.api.nvim_buf_delete(buf, { force = true }) +end + +T["combined modes"]["all four modes produces two extmarks"] = function() + local buf = make_buf({ "#0000FF text" }) + local ns = vim.api.nvim_create_namespace("test_combined_all") + local opts = config.resolve_options({ + parsers = { css = true }, + display = { mode = { "background", "foreground", "underline", "virtualtext" } }, + }) + local data = buffer.parse_lines(buf, { "#0000FF text" }, 0, opts) + buffer.add_highlight(buf, ns, 0, 1, data, opts) + local marks = vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, { details = true }) + eq(2, #marks) -- one combined non-vt + one vt + vim.api.nvim_buf_delete(buf, { force = true }) +end + return T diff --git a/tests/test_config.lua b/tests/test_config.lua index dacaa0d4..5b079e83 100644 --- a/tests/test_config.lua +++ b/tests/test_config.lua @@ -99,4 +99,50 @@ T["get_setup_options"]["custom filetypes are preserved"] = function() eq("css", opts.filetypes[2]) end +-- display.mode table validation ----------------------------------------------- + +T["display.mode"] = new_set() + +T["display.mode"]["string normalizes to single-element table"] = function() + local opts = config.resolve_options({ parsers = { css = true }, display = { mode = "foreground" } }) + eq({ "foreground" }, opts.display.mode) +end + +T["display.mode"]["table passes through sorted and deduped"] = function() + local opts = config.resolve_options({ + parsers = { css = true }, + display = { mode = { "underline", "background", "background" } }, + }) + eq({ "background", "underline" }, opts.display.mode) +end + +T["display.mode"]["empty table falls back to default"] = function() + local opts = config.resolve_options({ parsers = { css = true }, display = { mode = {} } }) + eq({ "background" }, opts.display.mode) +end + +T["display.mode"]["invalid entries filtered out"] = function() + local opts = config.resolve_options({ + parsers = { css = true }, + display = { mode = { "background", "invalid", "underline" } }, + }) + eq({ "background", "underline" }, opts.display.mode) +end + +T["display.mode"]["all-invalid falls back to default"] = function() + local opts = config.resolve_options({ + parsers = { css = true }, + display = { mode = { "nope", "bad" } }, + }) + eq({ "background" }, opts.display.mode) +end + +T["display.mode"]["all four modes accepted"] = function() + local opts = config.resolve_options({ + parsers = { css = true }, + display = { mode = { "virtualtext", "underline", "foreground", "background" } }, + }) + eq({ "background", "foreground", "underline", "virtualtext" }, opts.display.mode) +end + return T diff --git a/tests/test_css_lsp.lua b/tests/test_css_var_import.lua similarity index 100% rename from tests/test_css_lsp.lua rename to tests/test_css_var_import.lua diff --git a/tests/test_new_options.lua b/tests/test_new_options.lua index be75a5fb..287e9788 100644 --- a/tests/test_new_options.lua +++ b/tests/test_new_options.lua @@ -82,7 +82,7 @@ T["translate_options"]["translates display options"] = function() virtualtext_inline = true, virtualtext_mode = "background", }) - eq("foreground", new.display.mode) + eq("foreground", new.display.mode) -- translate_options doesn't normalize mode to table eq("X", new.display.virtualtext.char) eq("after", new.display.virtualtext.position) eq("background", new.display.virtualtext.hl_mode) @@ -123,7 +123,7 @@ end T["translate_filetypes"]["handles overrides"] = function() local new = config.translate_filetypes({ "*", html = { mode = "foreground" } }) eq("*", new.enable[1]) - eq("foreground", new.overrides.html.display.mode) + eq({ "foreground" }, new.overrides.html.display.mode) end T["translate_filetypes"]["passes through new format"] = function() @@ -178,7 +178,7 @@ T["validate_new_options"]["resets invalid display.mode"] = function() local opts = vim.deepcopy(config.default_options) opts.display.mode = "invalid" config.validate_new_options(opts) - eq("background", opts.display.mode) + eq({ "background" }, opts.display.mode) end T["validate_new_options"]["normalizes invalid tailwind.lsp to table"] = function() @@ -899,7 +899,7 @@ T["translate_filetypes"]["handles multiple overrides"] = function() css = { RGB = true }, }) eq("*", new.enable[1]) - eq("foreground", new.overrides.html.display.mode) + eq({ "foreground" }, new.overrides.html.display.mode) eq(true, new.overrides.css.parsers.hex.rgb) end @@ -986,14 +986,14 @@ T["validate_new_options"]["valid display.mode is preserved"] = function() local opts = vim.deepcopy(config.default_options) opts.display.mode = "virtualtext" config.validate_new_options(opts) - eq("virtualtext", opts.display.mode) + eq({ "virtualtext" }, opts.display.mode) end T["validate_new_options"]["underline display.mode is preserved"] = function() local opts = vim.deepcopy(config.default_options) opts.display.mode = "underline" config.validate_new_options(opts) - eq("underline", opts.display.mode) + eq({ "underline" }, opts.display.mode) end T["validate_new_options"]["tailwind.lsp boolean true normalizes to table"] = function() @@ -1236,7 +1236,7 @@ T["resolve_options"]["validates after merge"] = function() eq(true, result.parsers.names.enable) -- Other defaults should be preserved eq(true, result.parsers.hex.default) - eq("background", result.display.mode) + eq({ "background" }, result.display.mode) end T["resolve_options"]["preserves display settings"] = function() @@ -1244,7 +1244,7 @@ T["resolve_options"]["preserves display settings"] = function() parsers = { names = { enable = true } }, display = { mode = "foreground" }, }) - eq("foreground", result.display.mode) + eq({ "foreground" }, result.display.mode) end T["resolve_options"]["preserves underline display mode"] = function() @@ -1252,7 +1252,7 @@ T["resolve_options"]["preserves underline display mode"] = function() parsers = { names = { enable = true } }, display = { mode = "underline" }, }) - eq("underline", result.display.mode) + eq({ "underline" }, result.display.mode) end -- Option interpretation: hex.default = true (with no other hex keys) must enable @@ -1392,7 +1392,7 @@ T["get_setup_options new format"]["display options propagate"] = function() }, }, }) - eq("virtualtext", s.options.display.mode) + eq({ "virtualtext" }, s.options.display.mode) eq("X", s.options.display.virtualtext.char) eq("before", s.options.display.virtualtext.position) eq("background", s.options.display.virtualtext.hl_mode) @@ -1407,7 +1407,7 @@ T["get_setup_options new format"]["hoists top-level display to options"] = funct mode = "virtualtext", }, }) - eq("virtualtext", s.options.display.mode) + eq({ "virtualtext" }, s.options.display.mode) -- Should also get sensible parser defaults (legacy baseline) eq(true, s.options.parsers.names.enable) eq(true, s.options.parsers.hex.rrggbb) @@ -1427,7 +1427,7 @@ T["get_setup_options new format"]["hoists top-level parsers and display together display = { mode = "foreground" }, }) eq(true, s.options.parsers.names.enable) - eq("foreground", s.options.display.mode) + eq({ "foreground" }, s.options.display.mode) end T["get_setup_options new format"]["top-level hoist preserves filetypes"] = function() @@ -1435,7 +1435,7 @@ T["get_setup_options new format"]["top-level hoist preserves filetypes"] = funct filetypes = { "lua", "css" }, display = { mode = "virtualtext" }, }) - eq("virtualtext", s.options.display.mode) + eq({ "virtualtext" }, s.options.display.mode) eq("lua", s.filetypes[1]) eq("css", s.filetypes[2]) end @@ -1536,7 +1536,7 @@ T["config entry paths"]["setup({ options = { display only } }) detects colors"] local s = config.get_setup_options({ options = { display = { mode = "virtualtext" } }, }) - eq("virtualtext", s.options.display.mode) + eq({ "virtualtext" }, s.options.display.mode) eq(true, s.options.parsers.names.enable) eq(true, s.options.parsers.hex.rrggbb) end @@ -1545,7 +1545,7 @@ T["config entry paths"]["top-level display only detects colors"] = function() local s = config.get_setup_options({ display = { mode = "foreground" }, }) - eq("foreground", s.options.display.mode) + eq({ "foreground" }, s.options.display.mode) eq(true, s.options.parsers.names.enable) eq(true, s.options.parsers.hex.rrggbb) end @@ -1557,7 +1557,7 @@ T["config entry paths"]["top-level display virtualtext detects colors"] = functi virtualtext = { char = "X", position = "after" }, }, }) - eq("virtualtext", s.options.display.mode) + eq({ "virtualtext" }, s.options.display.mode) eq("X", s.options.display.virtualtext.char) eq("after", s.options.display.virtualtext.position) eq(true, s.options.parsers.names.enable) @@ -1568,28 +1568,28 @@ T["config entry paths"]["legacy mode only detects colors"] = function() local s = config.get_setup_options({ user_default_options = { mode = "foreground" }, }) - eq("foreground", s.options.display.mode) + eq({ "foreground" }, s.options.display.mode) eq(true, s.options.parsers.names.enable) eq(true, s.options.parsers.hex.rrggbb) end T["config entry paths"]["resolve_options with display only detects colors"] = function() local result = config.resolve_options({ display = { mode = "virtualtext" } }) - eq("virtualtext", result.display.mode) + eq({ "virtualtext" }, result.display.mode) eq(true, result.parsers.names.enable) eq(true, result.parsers.hex.rrggbb) end T["config entry paths"]["resolve_options with legacy mode only detects colors"] = function() local result = config.resolve_options({ mode = "virtualtext" }) - eq("virtualtext", result.display.mode) + eq({ "virtualtext" }, result.display.mode) eq(true, result.parsers.names.enable) eq(true, result.parsers.hex.rrggbb) end T["config entry paths"]["resolve_options with legacy underline mode"] = function() local result = config.resolve_options({ mode = "underline" }) - eq("underline", result.display.mode) + eq({ "underline" }, result.display.mode) eq(true, result.parsers.names.enable) eq(true, result.parsers.hex.rrggbb) end @@ -1649,7 +1649,7 @@ T["config entry paths"]["setup options disable + display mode"] = function() display = { mode = "virtualtext" }, }, }) - eq("virtualtext", s.options.display.mode) + eq({ "virtualtext" }, s.options.display.mode) eq(false, s.options.parsers.names.enable) eq(false, s.options.parsers.hex.rgb) end @@ -1726,7 +1726,7 @@ T["get_setup_options legacy"]["no arguments uses plugin defaults"] = function() eq(true, s.options.parsers.names.enable) eq(true, s.options.parsers.hex.rrggbb) eq(true, s.options.parsers.hex.rgb) - eq("background", s.options.display.mode) + eq({ "background" }, s.options.display.mode) eq(true, s.user_default_options.names) eq(true, s.user_default_options.RGB) eq(true, s.user_default_options.RRGGBB) @@ -1749,7 +1749,7 @@ T["get_setup_options legacy"]["sparse user_default_options preserves plugin defa eq(true, s.options.parsers.names.enable) eq(true, s.options.parsers.hex.rrggbb) eq(true, s.options.parsers.hex.rgb) - eq("foreground", s.options.display.mode) + eq({ "foreground" }, s.options.display.mode) end T["get_setup_options legacy"]["single override does not clobber other defaults"] = function() @@ -1830,7 +1830,7 @@ T["new-only options"]["options wins over user_default_options"] = function() }) -- options takes precedence eq(false, s.options.parsers.names.enable) - eq("foreground", s.options.display.mode) + eq({ "foreground" }, s.options.display.mode) end -- matcher cache tests ------------------------------------------------------- @@ -2244,13 +2244,13 @@ T["resolve_options"]["nil returns copy of defaults"] = function() local result = config.resolve_options(nil) eq(true, result.parsers.hex.default) eq(true, result.parsers.names.enable) - eq("background", result.display.mode) + eq("background", result.display.mode) -- raw defaults, not validated end T["resolve_options"]["empty table returns defaults"] = function() local result = config.resolve_options({}) eq(true, result.parsers.hex.default) - eq("background", result.display.mode) + eq({ "background" }, result.display.mode) end -- as_flat tests -------------------------------------------------------------- @@ -2326,7 +2326,7 @@ T["validate_new_options"]["invalid display mode resets to default"] = function() local opts = vim.deepcopy(config.default_options) opts.display.mode = "invalid_mode" config.validate_new_options(opts) - eq("background", opts.display.mode) + eq({ "background" }, opts.display.mode) end T["validate_new_options"]["invalid virtualtext position resets to default"] = function() @@ -2543,7 +2543,7 @@ T["roundtrip"]["new -> flat -> resolve preserves enabled parsers"] = function() eq(true, restored.parsers.hex.default) eq(true, restored.parsers.hex.rrggbb) eq(true, restored.parsers.rgb.enable) - eq("foreground", restored.display.mode) + eq({ "foreground" }, restored.display.mode) end T["roundtrip"]["new -> flat -> resolve preserves display settings"] = function() @@ -2557,7 +2557,7 @@ T["roundtrip"]["new -> flat -> resolve preserves display settings"] = function() local flat = config.as_flat(original) local restored = config.resolve_options(flat) - eq("virtualtext", restored.display.mode) + eq({ "virtualtext" }, restored.display.mode) eq("X", restored.display.virtualtext.char) eq("before", restored.display.virtualtext.position) eq("background", restored.display.virtualtext.hl_mode)