Skip to content

Commit 2b869e9

Browse files
committed
Merge color mode (#16) into 1.11.0
# Conflicts: # media/webview.css
2 parents 20bfb74 + 7b3d4d9 commit 2b869e9

10 files changed

Lines changed: 155 additions & 3 deletions

File tree

media/webview.css

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,43 @@ button .codicon {
472472
:is(.ag-theme-alpine-dark, .ag-theme-alpine) .ag-floating-top .ag-row {
473473
background-color: var(--vscode-list-inactiveSelectionBackground, #37373d) !important;
474474
}
475+
476+
/* ── Column Color Mode ──────────────────────────────────────────────────────
477+
Toggled by the toolbar "Color columns" button (features/color-mode.ts), which
478+
adds `cm-on` to #grid-container and injects one `--cm-h` hue per column. Each
479+
column cell is tinted with a translucent overlay so the theme's own foreground
480+
text keeps its contrast and the tint reads as "this column's color".
481+
482+
Why these rules are NOT !important: every existing highlight — range selection,
483+
find match/active, duplicate rows, the focused cell, the frozen-row band — is
484+
!important and therefore wins the cascade over this normal-weight tint, so all
485+
of them keep painting clearly on top. The tint is also translucent (mixed with
486+
`transparent`), so the real row background (striping, hover, the frozen-row
487+
tint) shows through underneath. The '#' row-number gutter (col-id "row-index")
488+
is never matched, only data columns (col-id "col_*").
489+
490+
Lightness (--cm-l), chroma (--cm-c) and alpha adapt to the active theme so the
491+
color is visible without fighting the text on light, dark or high-contrast
492+
themes. Hue varies per column; L/C/alpha do not. color-mix()/oklch() are
493+
supported by VS Code's Chromium; on a very old build the declaration is simply
494+
ignored and the toggle is a harmless no-op. */
495+
body.vscode-light #grid-container.cm-on {
496+
--cm-l: 0.55; --cm-c: 0.15; --cm-body-a: 15%; --cm-head-a: 30%;
497+
}
498+
body.vscode-dark #grid-container.cm-on,
499+
body.vscode-high-contrast #grid-container.cm-on {
500+
--cm-l: 0.74; --cm-c: 0.15; --cm-body-a: 20%; --cm-head-a: 36%;
501+
}
502+
/* High contrast: keep tints faint — HC users expect minimal background noise. */
503+
body.vscode-high-contrast #grid-container.cm-on {
504+
--cm-body-a: 10%; --cm-head-a: 20%;
505+
}
506+
#grid-container.cm-on .ag-cell[col-id^="col_"] {
507+
background-color: color-mix(in oklab, oklch(var(--cm-l) var(--cm-c) var(--cm-h)) var(--cm-body-a), transparent);
508+
}
509+
#grid-container.cm-on .ag-header-cell[col-id^="col_"] {
510+
background-color: color-mix(in oklab, oklch(var(--cm-l) var(--cm-c) var(--cm-h)) var(--cm-head-a), transparent);
511+
}
475512
/* Frozen (pinned-left) column header: a pin marker before the column name,
476513
mirroring the frozen-row pin. The always-pinned '#' gutter (.row-index-header)
477514
is excluded so only user-frozen columns get the marker. */

src/csvEditorProvider.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ export class CsvEditorProvider implements vscode.CustomEditorProvider<CsvDocumen
177177

178178
const fileName = path.basename(document.uri.fsPath);
179179
const zoomIndex = this.context.globalState.get<number>('csvGridEditor.zoomIndex', 4);
180+
const colorMode = this.context.globalState.get<boolean>('csvGridEditor.colorMode', false);
180181

181182
webviewPanel.webview.html = getWebviewContent(
182183
webviewPanel.webview,
@@ -188,7 +189,8 @@ export class CsvEditorProvider implements vscode.CustomEditorProvider<CsvDocumen
188189
fileName,
189190
document.isChunked,
190191
process.platform === 'darwin',
191-
zoomIndex
192+
zoomIndex,
193+
colorMode
192194
);
193195

194196
// F3: File System Watcher — auto-reload on external changes (non-preview only)
@@ -244,6 +246,9 @@ export class CsvEditorProvider implements vscode.CustomEditorProvider<CsvDocumen
244246
} else if (msg.type === 'zoomChanged') {
245247
this.context.globalState.update('csvGridEditor.zoomIndex', msg.zoomIndex);
246248

249+
} else if (msg.type === 'colorModeChanged') {
250+
this.context.globalState.update('csvGridEditor.colorMode', msg.colorMode);
251+
247252
} else if (msg.type === 'edit' && !document.isPreview) {
248253
document.content = msg.text;
249254
this._onDidChangeCustomDocument.fire({ document });

src/webview.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export function getWebviewContent(
1010
fileName: string = '',
1111
isChunked: boolean = false,
1212
isMac: boolean = false,
13-
zoomIndex: number = 4
13+
zoomIndex: number = 4,
14+
colorMode: boolean = false
1415
): string {
1516
const nonce = getNonce();
1617
const mod = isMac ? '⌘' : 'Ctrl+';
@@ -113,6 +114,7 @@ export function getWebviewContent(
113114
<div class="separator"></div>
114115
<button id="btn-profile" title="Column Profile"><i class="codicon codicon-graph"></i></button>
115116
<button id="btn-columns" title="Show / hide columns"><i class="codicon codicon-checklist"></i></button>
117+
<button id="btn-colormode" title="Color columns — distinct, theme-aware tint per column"><i class="codicon codicon-symbol-color"></i></button>
116118
<div class="separator"></div>
117119
<button id="btn-find-replace" title="Find &amp; Replace (${mod}F)"><i class="codicon codicon-search"></i></button>
118120
<div class="separator"></div>
@@ -267,6 +269,7 @@ export function getWebviewContent(
267269
const FILENAME = '${fileName.replace(/'/g, "\\'")}';
268270
const IS_CHUNKED = ${isChunked ? 'true' : 'false'};
269271
const INITIAL_ZOOM_INDEX = ${zoomIndex};
272+
const INITIAL_COLOR_MODE = ${colorMode ? 'true' : 'false'};
270273
</script>
271274
272275
<!-- Bundled webview logic -->

src/webview/features/color-mode.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { state, getNumCols } from '../state';
2+
3+
/**
4+
* Column color mode. A toolbar toggle that gives every data column a distinct,
5+
* theme-adaptive background tint (header stronger, body fainter) so columns are
6+
* easy to tell apart, similar to Rainbow CSV but readable on any VS Code theme.
7+
*
8+
* How the colors work (see also media/webview.css → "Column Color Mode"):
9+
* - Each column gets one hue via golden-angle rotation (137.5°), which keeps
10+
* adjacent columns far apart on the wheel for any column count, even after a
11+
* column is inserted or deleted. Only the per-column hue is injected here as
12+
* a CSS variable (--cm-h) on that column's cells via their stable AG Grid
13+
* `col-id`; the lightness, chroma and alpha live in CSS and adapt to the
14+
* active theme (light / dark / high-contrast).
15+
* - The tint is a translucent overlay (color-mix in OKLCH) painted over the
16+
* real row background, so the theme's own foreground text keeps its contrast
17+
* and frozen rows, selection, find matches and duplicate highlights — all of
18+
* which are !important and higher in the cascade — keep painting clearly on
19+
* top. The row-number gutter (col-id "row-index") is never tinted.
20+
*
21+
* The toggle is persisted globally (VS Code globalState) exactly like zoom, so
22+
* it is remembered across every CSV file and every session.
23+
*/
24+
25+
const HUE_STYLE_ID = 'cm-col-hues';
26+
// 360 * (1 - 1/phi). Irrational vs 360°, so successive hues land in the largest
27+
// remaining gap and never visibly repeat.
28+
const GOLDEN_ANGLE = 137.50776405003785;
29+
// Start offset so column 0 isn't pure red.
30+
const START_HUE = 25;
31+
32+
function persistColorMode(): void {
33+
vscodeApi.postMessage({ type: 'colorModeChanged', colorMode: state.colorMode });
34+
}
35+
36+
function updateButton(): void {
37+
document.getElementById('btn-colormode')?.classList.toggle('btn-active', state.colorMode);
38+
}
39+
40+
// Toggles the `cm-on` class on the grid container (which gates all tint CSS) and,
41+
// when on, (re)generates the per-column hue rules for the current column count.
42+
// Safe to call before the grid has data (numCols 0 → empty rule set, class only).
43+
// Called on setup, on toggle, and at the end of every buildGrid (column add/delete,
44+
// delimiter change, paging) so freshly built columns pick up their hue.
45+
export function applyColorMode(): void {
46+
const container = document.getElementById('grid-container');
47+
if (!container) return;
48+
container.classList.toggle('cm-on', state.colorMode);
49+
50+
let styleEl = document.getElementById(HUE_STYLE_ID) as HTMLStyleElement | null;
51+
if (!state.colorMode) {
52+
styleEl?.remove();
53+
return;
54+
}
55+
if (!styleEl) {
56+
styleEl = document.createElement('style');
57+
styleEl.id = HUE_STYLE_ID;
58+
document.head.appendChild(styleEl);
59+
}
60+
const numCols = getNumCols(state.data);
61+
let css = '';
62+
for (let c = 0; c < numCols; c++) {
63+
const hue = ((START_HUE + c * GOLDEN_ANGLE) % 360).toFixed(2);
64+
css += `#grid-container.cm-on [col-id="col_${c}"]{--cm-h:${hue};}`;
65+
}
66+
styleEl.textContent = css;
67+
}
68+
69+
function toggleColorMode(): void {
70+
state.colorMode = !state.colorMode;
71+
updateButton();
72+
applyColorMode();
73+
persistColorMode();
74+
}
75+
76+
export function setupColorMode(): void {
77+
state.colorMode = !!INITIAL_COLOR_MODE;
78+
updateButton();
79+
applyColorMode();
80+
document.getElementById('btn-colormode')?.addEventListener('click', toggleColorMode);
81+
}

src/webview/features/theme.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ export function isDarkTheme(): boolean {
1313
export function applyGridTheme(): void {
1414
const container = document.getElementById('grid-container');
1515
if (!container) return;
16-
container.className = isDarkTheme() ? 'ag-theme-alpine-dark' : 'ag-theme-alpine';
16+
// Swap only the AG Grid theme class. Toggling via classList (rather than
17+
// overwriting className) preserves other state classes on the container —
18+
// notably `cm-on` from the column color mode — so switching VS Code theme,
19+
// or a buildGrid rebuild, doesn't silently drop the coloring.
20+
container.classList.remove('ag-theme-alpine', 'ag-theme-alpine-dark');
21+
container.classList.add(isDarkTheme() ? 'ag-theme-alpine-dark' : 'ag-theme-alpine');
1722
}
1823

1924
export function setupTheme(): void {

src/webview/globals.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ declare const DELIMITER: string;
66
declare const FILENAME: string;
77
declare const IS_CHUNKED: boolean;
88
declare const INITIAL_ZOOM_INDEX: number;
9+
declare const INITIAL_COLOR_MODE: boolean;
910

1011
declare const agGrid: {
1112
createGrid(container: HTMLElement, options: unknown): any;

src/webview/grid/builder.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getFindCellClassRules } from '../features/find-replace';
99
import { attachHeaderContextMenus } from '../features/freeze-columns';
1010
import { applyZoom } from '../features/zoom';
1111
import { applyGridTheme } from '../features/theme';
12+
import { applyColorMode } from '../features/color-mode';
1213
import {
1314
getRangeCellClassRules,
1415
onCellMouseDownHandler,
@@ -346,6 +347,10 @@ export function buildGrid(): void {
346347

347348
state.gridApi = agGrid.createGrid(container, gridOptions);
348349
updateButtons();
350+
// Re-apply column color mode for the freshly built columns. buildGrid rebuilds
351+
// the column set (insert/delete, delimiter change, paging), so the per-column
352+
// hue rules must be regenerated to match the current column count.
353+
applyColorMode();
349354

350355
// Double-click on resize handle → auto-size that column. Wired once: the
351356
// container survives rebuilds, so re-adding would accumulate listeners.

src/webview/grid/refresh.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { state, getNumCols } from '../state';
2+
import { applyColorMode } from '../features/color-mode';
23

34
// Splits a freshly-built rowData array into the scrollable body and the frozen
45
// reference rows (AG Grid renders the latter in a fixed pinned-top band). Frozen
@@ -81,6 +82,10 @@ export function refreshGrid(): void {
8182
// refreshGrid only swaps rowData, so the row/column counters in the toolbar
8283
// and status bar would otherwise go stale after a delete/insert/paste/undo.
8384
updateCountsDisplay();
85+
// Keep the per-column color hues in sync with the live column count. Undo/redo
86+
// of a column insert/delete reaches here (not buildGrid), so the hue rules must
87+
// be regenerated for the current numCols, not left at the pre-undo count.
88+
applyColorMode();
8489
}
8590

8691
// Recomputes the "<n> rows × <n> columns" toolbar text and the "<n> records"

src/webview/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { setupPaste } from './features/paste';
1515
import { setupRangeSelect } from './features/range-select';
1616
import { setupRenameColumn } from './features/rename-column';
1717
import { setupColumnChooser } from './features/column-chooser';
18+
import { setupColorMode } from './features/color-mode';
1819
import { setupKeyboard } from './keyboard';
1920
import { setupMessaging } from './messaging';
2021

@@ -35,6 +36,7 @@ setupPaste();
3536
setupRangeSelect();
3637
setupRenameColumn();
3738
setupColumnChooser();
39+
setupColorMode();
3840
setupKeyboard();
3941
setupMessaging();
4042

src/webview/state.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ export const state = {
1414
ZOOM_STEPS: [60, 70, 80, 90, 100, 110, 125, 150, 175, 200],
1515
zoomIndex: 4,
1616
isAutoFitted: false,
17+
18+
// Column color mode — when on, every data column gets a distinct, theme-adaptive
19+
// background tint so columns are easier to tell apart. Persisted globally via
20+
// VS Code globalState (csvGridEditor.colorMode), exactly like zoomIndex, so the
21+
// toggle is remembered across every CSV file and every session. The actual
22+
// colors are pure CSS (features/color-mode.ts + media/webview.css). In-memory
23+
// mirror of the persisted flag.
24+
colorMode: false,
1725
autoFitCache: null as any,
1826
autoFitCacheZoom: -1,
1927

0 commit comments

Comments
 (0)