Skip to content

Commit 8086d50

Browse files
committed
ench: improve colorizer
1 parent 028ddb3 commit 8086d50

3 files changed

Lines changed: 214 additions & 4 deletions

File tree

.stylua.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ indent_type = "Spaces"
44
indent_width = 2
55
quote_style = "AutoPreferDouble"
66
no_call_parentheses = false
7+
syntax = "Lua52"

CLAUDE.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
Quicktest is a Neovim plugin that enables contextual test execution with real-time feedback. It supports multiple testing frameworks through an adapter system and provides both split window and popup interfaces for viewing test results.
8+
9+
## Development Commands
10+
11+
### Testing
12+
- `make test` - Run the test suite using Plenary
13+
- Tests are located in `tests/` directory
14+
- Test configuration: `tests/minimal_init.lua`
15+
16+
### Code Formatting
17+
- `stylua --check --glob '**/*.lua' -- lua` - Check Lua code formatting
18+
- `stylua --glob '**/*.lua' -- lua` - Auto-format Lua code
19+
- CI runs stylua checks on all pull requests
20+
21+
## Architecture
22+
23+
### Core Structure
24+
- `lua/quicktest.lua` - Main module entry point with setup and public API
25+
- `lua/quicktest/module.lua` - Core functionality, adapter management, and test execution
26+
- `lua/quicktest/ui.lua` - Window management (split/popup) and display logic
27+
- `plugin/quicktest.lua` - Vim plugin initialization and command definitions
28+
29+
### Adapter System
30+
The plugin uses a modular adapter architecture in `lua/quicktest/adapters/`:
31+
- Each adapter (`golang/`, `vitest/`, `playwright/`, etc.) implements the `QuicktestAdapter` interface
32+
- Adapters define test parameter building for different run types (line, file, dir, all)
33+
- Each adapter has `build_*_run_params` functions and a `run` function that executes tests
34+
- Adapters can use TreeSitter queries for parsing test structures (see `query.lua` files)
35+
36+
### Key Components
37+
- `lua/quicktest/fs_utils.lua` - File system utilities for finding test files and directories
38+
- `lua/quicktest/ts.lua` - TreeSitter integration for parsing test code
39+
- `lua/quicktest/colored_printer.lua` - ANSI color support for test output
40+
- `lua/quicktest/notify.lua` - Notification system
41+
42+
### Test Execution Flow
43+
1. User triggers test run (line/file/dir/all)
44+
2. Module determines appropriate adapter based on buffer and configuration
45+
3. Adapter builds run parameters using TreeSitter parsing or file analysis
46+
4. Test command executes via Plenary Job with real-time output streaming
47+
5. Results display in split window or popup with live scrolling and ANSI colors
48+
49+
### Adapter Development
50+
When creating new adapters:
51+
- Implement the `QuicktestAdapter` interface in `lua/quicktest/adapters/[name]/init.lua`
52+
- Use TreeSitter queries in `query.lua` files for test parsing when applicable
53+
- Follow existing patterns in `golang/` or `vitest/` adapters
54+
- Test adapter functionality with example projects in `tests/support/`
55+
56+
## Commands and API
57+
58+
### Vim Commands
59+
- `:QuicktestRunLine [mode] [adapter] [...args]` - Run test at cursor
60+
- `:QuicktestRunFile [mode] [adapter] [...args]` - Run all tests in file
61+
- `:QuicktestRunDir [mode] [adapter] [...args]` - Run tests in directory
62+
- `:QuicktestRunAll [mode] [adapter] [...args]` - Run all tests in project
63+
64+
### Lua API
65+
- `require('quicktest').run_line(mode)` - Run test at cursor position
66+
- `require('quicktest').run_file(mode)` - Run file tests
67+
- `require('quicktest').run_dir(mode)` - Run directory tests
68+
- `require('quicktest').run_all(mode)` - Run all project tests
69+
- `require('quicktest').run_previous(mode)` - Rerun last test
70+
- `require('quicktest').toggle_win(mode)` - Toggle test window
71+
- `require('quicktest').cancel_current_run()` - Cancel running test
72+
73+
Modes: `'split'`, `'popup'`, or omit for auto-detection.

lua/quicktest/colored_printer.lua

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,45 @@ function ColoredPrinter.new()
99
self.current_fg = nil
1010
self.current_bg = nil
1111
self.current_styles = {}
12+
self.bright_color_bold = false
1213
self:setup_highlight_groups()
1314
return self
1415
end
1516

17+
function ColoredPrinter:get_256_color(index)
18+
-- Standard 16 colors (0-15)
19+
if index <= 15 then
20+
local colors = {
21+
"#000000", "#800000", "#008000", "#808000", "#000080", "#800080", "#008080", "#c0c0c0",
22+
"#808080", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff"
23+
}
24+
return colors[index + 1]
25+
end
26+
27+
-- 216 color cube (16-231)
28+
if index <= 231 then
29+
local n = index - 16
30+
local r = math.floor(n / 36)
31+
local g = math.floor((n % 36) / 6)
32+
local b = n % 6
33+
34+
local function color_value(c)
35+
if c == 0 then return 0 end
36+
return 55 + c * 40
37+
end
38+
39+
return string.format("#%02x%02x%02x", color_value(r), color_value(g), color_value(b))
40+
end
41+
42+
-- Grayscale (232-255)
43+
if index <= 255 then
44+
local gray = 8 + (index - 232) * 10
45+
return string.format("#%02x%02x%02x", gray, gray, gray)
46+
end
47+
48+
return "#ffffff" -- fallback
49+
end
50+
1651
function ColoredPrinter:setup_highlight_groups()
1752
local basic_colors = {
1853
["30"] = "Black",
@@ -40,11 +75,45 @@ function ColoredPrinter:setup_highlight_groups()
4075
self.color_groups[code] = group_name
4176
end
4277

78+
-- Add background color groups (40-47, 100-107)
79+
local bg_colors = {
80+
["40"] = "Black",
81+
["41"] = "Red",
82+
["42"] = "Green",
83+
["43"] = "Yellow",
84+
["44"] = "Blue",
85+
["45"] = "Magenta",
86+
["46"] = "Cyan",
87+
["47"] = "White",
88+
["100"] = "Grey",
89+
["101"] = "Red",
90+
["102"] = "Green",
91+
["103"] = "Yellow",
92+
["104"] = "Blue",
93+
["105"] = "Magenta",
94+
["106"] = "Cyan",
95+
["107"] = "White",
96+
}
97+
98+
for code, color in pairs(bg_colors) do
99+
local group_name = "QuicktestAnsiBgColor_" .. code
100+
vim.cmd(string.format("highlight %s ctermbg=%s guibg=%s", group_name, color:lower(), color))
101+
102+
self.color_groups[code] = group_name
103+
end
104+
105+
-- Use Normal highlight group directly to ensure proper default colors
106+
vim.cmd("highlight default QuicktestAnsiColorDefault guifg=NONE guibg=NONE")
43107
vim.cmd("highlight default link QuicktestAnsiColorDefault Normal")
44108
self.color_groups["default"] = "QuicktestAnsiColorDefault"
45109
end
46110

47111
function ColoredPrinter:get_or_create_color_group(fg, bg, styles)
112+
-- If no colors or styles are set, use the default group
113+
if not fg and not bg and (#styles == 0) then
114+
return self.color_groups["default"]
115+
end
116+
48117
local function sanitize(str)
49118
if str then
50119
-- Replace # with "hex" and any non-alphanumeric characters with their hex code
@@ -98,7 +167,7 @@ function ColoredPrinter:get_or_create_color_group(fg, bg, styles)
98167
if bg:match("^#") then
99168
cmd = cmd .. string.format(" guibg=%s", bg)
100169
elseif self.color_groups[bg] then
101-
local bg_color = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(self.color_groups[bg])), "fg#")
170+
local bg_color = vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(self.color_groups[bg])), "bg#")
102171
if bg_color and bg_color ~= "" then
103172
cmd = cmd .. " guibg=" .. bg_color
104173
end
@@ -110,7 +179,11 @@ function ColoredPrinter:get_or_create_color_group(fg, bg, styles)
110179
end
111180

112181
if start_cmd ~= cmd then
113-
vim.cmd(cmd)
182+
local success, err = pcall(vim.cmd, cmd)
183+
if not success then
184+
-- Fallback to basic highlight if command fails
185+
vim.cmd("highlight " .. group_name)
186+
end
114187
end
115188

116189
self.color_groups[color_key] = group_name
@@ -142,28 +215,76 @@ function ColoredPrinter:parse_colors(line)
142215
local index = 1
143216
while index <= #codes do
144217
local code = tonumber(codes[index])
218+
-- Skip empty or invalid codes
219+
if not code then
220+
index = index + 1
221+
goto continue
222+
end
145223
if code == 0 then
146224
self.current_fg, self.current_bg = nil, nil
147225
self.current_styles = {}
226+
self.bright_color_bold = false
148227
elseif code == 1 then
149228
table.insert(self.current_styles, "bold")
229+
-- This is explicit bold, not from bright colors
230+
self.bright_color_bold = false
231+
elseif code == 2 then
232+
-- Dim/faint - implement by setting a gray foreground color
233+
self.current_fg = "#808080"
150234
elseif code == 3 then
151235
table.insert(self.current_styles, "italic")
152236
elseif code == 4 then
153237
table.insert(self.current_styles, "underline")
238+
elseif code == 5 or code == 6 then
239+
-- Blink - not supported in most terminals/Neovim, skip
240+
elseif code == 7 then
241+
table.insert(self.current_styles, "reverse")
242+
elseif code == 8 then
243+
-- Conceal - not directly supported as gui attribute, skip
154244
elseif code == 9 then
155245
table.insert(self.current_styles, "strikethrough")
246+
elseif code == 21 then
247+
table.insert(self.current_styles, "undercurl")
248+
elseif code == 22 then
249+
self.current_styles = vim.tbl_filter(function(s) return s ~= "bold" end, self.current_styles)
250+
-- Reset dim color if it was set
251+
if self.current_fg == "#808080" then
252+
self.current_fg = nil
253+
end
254+
elseif code == 23 then
255+
self.current_styles = vim.tbl_filter(function(s) return s ~= "italic" end, self.current_styles)
256+
elseif code == 24 then
257+
self.current_styles = vim.tbl_filter(function(s) return s ~= "underline" and s ~= "undercurl" end, self.current_styles)
258+
elseif code == 25 then
259+
-- Reset blink (not supported anyway)
260+
elseif code == 27 then
261+
self.current_styles = vim.tbl_filter(function(s) return s ~= "reverse" end, self.current_styles)
262+
elseif code == 28 then
263+
-- Reset conceal (not supported anyway)
264+
elseif code == 29 then
265+
self.current_styles = vim.tbl_filter(function(s) return s ~= "strikethrough" end, self.current_styles)
266+
elseif code == 39 then
267+
self.current_fg = nil -- Reset foreground to default
268+
-- If bold was auto-added by bright color, remove it
269+
if self.bright_color_bold then
270+
self.current_styles = vim.tbl_filter(function(s) return s ~= "bold" end, self.current_styles)
271+
self.bright_color_bold = false
272+
end
273+
elseif code == 49 then
274+
self.current_bg = nil -- Reset background to default
156275
elseif code >= 30 and code <= 37 then
157276
self.current_fg = tostring(code)
158277
elseif code >= 40 and code <= 47 then
159-
self.current_bg = tostring(code - 10)
278+
self.current_bg = tostring(code)
160279
elseif code >= 90 and code <= 97 then
161280
self.current_fg = tostring(code)
162281
if not vim.tbl_contains(self.current_styles, "bold") then
163282
table.insert(self.current_styles, "bold")
164283
end
284+
-- Mark that this bold was auto-added by bright color
285+
self.bright_color_bold = true
165286
elseif code >= 100 and code <= 107 then
166-
self.current_bg = tostring(code - 10)
287+
self.current_bg = tostring(code)
167288
elseif code == 38 or code == 48 then
168289
if codes[index + 1] == "2" then
169290
if #codes >= index + 4 then
@@ -179,9 +300,24 @@ function ColoredPrinter:parse_colors(line)
179300
index = index + 4
180301
end
181302
end
303+
elseif codes[index + 1] == "5" then
304+
if #codes >= index + 2 then
305+
local color_index = tonumber(codes[index + 2])
306+
if color_index and color_index >= 0 and color_index <= 255 then
307+
local color_hex = self:get_256_color(color_index)
308+
if code == 38 then
309+
self.current_fg = color_hex
310+
else
311+
self.current_bg = color_hex
312+
end
313+
index = index + 2
314+
end
315+
end
182316
end
183317
end
184318
index = index + 1
319+
320+
::continue::
185321
end
186322

187323
i = j + 1

0 commit comments

Comments
 (0)