diff --git a/media/webview.css b/media/webview.css
index e01bd73..dda2dce 100644
--- a/media/webview.css
+++ b/media/webview.css
@@ -1157,7 +1157,7 @@ body.vscode-high-contrast #grid-container.cm-on {
.col-chooser-item {
display: flex;
align-items: center;
- gap: 7px;
+ gap: 6px;
padding: 4px 6px;
font-size: 12px;
cursor: pointer;
@@ -1165,6 +1165,31 @@ body.vscode-high-contrast #grid-container.cm-on {
}
.col-chooser-item:hover { background: var(--vscode-list-hoverBackground, #2a2d2e); }
.col-chooser-item input { cursor: pointer; margin: 0; flex: none; accent-color: var(--vscode-focusBorder, #007fd4); }
+.col-chooser-master {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px;
+ margin: 6px 0;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ border-radius: 3px;
+ position: relative;
+}
+.col-chooser-master::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: -4px;
+ height: 1px;
+ background: var(--vscode-menu-border, #454545);
+}
+.col-chooser-master:hover { background: var(--vscode-list-hoverBackground, #2a2d2e); }
+.col-chooser-master input { cursor: pointer; margin: 0; flex: none; accent-color: var(--vscode-focusBorder, #007fd4); }
+.col-chooser-master-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.col-chooser-master-count { font-size: 11px; font-weight: 400; opacity: 0.6; flex: none; }
.col-chooser-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.goto-error {
margin-top: 6px;
diff --git a/src/webview.ts b/src/webview.ts
index c1fcb10..16228c6 100644
--- a/src/webview.ts
+++ b/src/webview.ts
@@ -174,10 +174,11 @@ export function getWebviewContent(
Columns
-
-
-
-
+
diff --git a/src/webview/features/column-chooser.ts b/src/webview/features/column-chooser.ts
index e4fd98d..1fb11a1 100644
--- a/src/webview/features/column-chooser.ts
+++ b/src/webview/features/column-chooser.ts
@@ -3,13 +3,13 @@ import { closeAllPopups } from './popups';
// ── Column chooser (show / hide columns) ────────────────────────────────────
// A toolbar dropdown listing every data column with a checkbox to toggle its
-// visibility. A search box filters the list by column name, and Show all / Hide
-// all flip every column at once (Hide all, then search and re-check the few you
-// want). Visibility is applied via AG Grid (setColumnsVisible) and mirrored into
-// state.hiddenCols (0-based data-column indices) so it survives a grid rebuild —
-// e.g. a paged-view page change re-runs buildGrid, which re-applies `hide` from
-// this set. In-memory only (not persisted across reload), matching the freeze
-// features. Column insert/delete clears the set (see delete-row-col).
+// visibility. A search box filters the list by column name. A pinned tri-state
+// "Select all" master checkbox above the list reflects and controls the columns.
+// Visibility is applied via AG Grid (setColumnsVisible) and mirrored into state.hiddenCols
+// (0-based data-column indices) so it survives a grid rebuild - e.g. a paged-view
+// page change re-runs buildGrid, which re-applies `hide` from this set. In-memory
+// only (not persisted across reload), matching the freeze features. Column
+// insert/delete clears the set (see delete-row-col).
let searchQuery = '';
@@ -18,34 +18,63 @@ function setColHidden(colIndex: number, hidden: boolean): void {
else state.hiddenCols.delete(colIndex);
state.gridApi?.setColumnsVisible(['col_' + colIndex], !hidden);
updateButton();
+ syncMaster();
}
-function allColIds(): string[] {
- const n = (state.data[0] ?? []).length;
- const ids: string[] = [];
- for (let c = 0; c < n; c++) ids.push('col_' + c);
- return ids;
+function colLabel(header: string[], c: number): string {
+ const name = header[c] ?? '';
+ return name !== '' ? name : '(column ' + (c + 1) + ')';
+}
+
+function visibleColIndices(): number[] {
+ const header = state.data[0] ?? [];
+ const q = searchQuery.trim().toLowerCase();
+ const out: number[] = [];
+ for (let c = 0; c < header.length; c++) {
+ if (q && !colLabel(header, c).toLowerCase().includes(q)) continue;
+ out.push(c);
+ }
+ return out;
}
-function showAll(): void {
- state.hiddenCols.clear();
- state.gridApi?.setColumnsVisible(allColIds(), true);
+function setVisibleColsHidden(hidden: boolean): void {
+ const cols = visibleColIndices();
+ if (cols.length === 0) return;
+ if (!hidden && searchQuery.trim() === '') {
+ state.hiddenCols.clear();
+ state.gridApi?.setColumnsVisible(allColIds(), true);
+ } else {
+ for (const c of cols) {
+ if (hidden) state.hiddenCols.add(c);
+ else state.hiddenCols.delete(c);
+ }
+ state.gridApi?.setColumnsVisible(cols.map(c => 'col_' + c), !hidden);
+ }
buildList();
updateButton();
}
-function hideAll(): void {
+function allColIds(): string[] {
const n = (state.data[0] ?? []).length;
- state.hiddenCols.clear();
- for (let c = 0; c < n; c++) state.hiddenCols.add(c);
- state.gridApi?.setColumnsVisible(allColIds(), false);
- buildList();
- updateButton();
+ const ids: string[] = [];
+ for (let c = 0; c < n; c++) ids.push('col_' + c);
+ return ids;
}
-function colLabel(header: string[], c: number): string {
- const name = header[c] ?? '';
- return name !== '' ? name : '(column ' + (c + 1) + ')';
+function syncMaster(): void {
+ const cb = document.getElementById('col-chooser-master-cb') as HTMLInputElement | null;
+ const count = document.getElementById('col-chooser-master-count');
+ const label = document.getElementById('col-chooser-master-label');
+ if (!cb) return;
+ const cols = visibleColIndices();
+ const visible = cols.reduce((n, c) => n + (state.hiddenCols.has(c) ? 0 : 1), 0);
+ const total = cols.length;
+
+ cb.checked = total > 0 && visible === total;
+ cb.indeterminate = visible > 0 && visible < total;
+ cb.disabled = total === 0;
+ if (count) count.textContent = total > 0 ? `${visible} / ${total}` : '';
+ if (label) label.textContent = searchQuery.trim() ? 'Select all matches' : 'Select all';
}
function buildList(): void {
@@ -53,13 +82,9 @@ function buildList(): void {
if (!list) return;
list.innerHTML = '';
const header = state.data[0] ?? [];
- const q = searchQuery.trim().toLowerCase();
- let shown = 0;
- for (let c = 0; c < header.length; c++) {
- const label = colLabel(header, c);
- if (q && !label.toLowerCase().includes(q)) continue;
- shown++;
+ const cols = visibleColIndices();
+ for (const c of cols) {
const row = document.createElement('label');
row.className = 'col-chooser-item';
@@ -71,19 +96,21 @@ function buildList(): void {
const span = document.createElement('span');
span.className = 'col-chooser-label';
- span.textContent = label;
+ span.textContent = colLabel(header, c);
row.appendChild(cb);
row.appendChild(span);
list.appendChild(row);
}
- if (shown === 0) {
+ if (cols.length === 0) {
const empty = document.createElement('div');
empty.className = 'csv-filter-empty';
empty.textContent = 'No matching columns';
list.appendChild(empty);
}
+
+ syncMaster();
}
function updateButton(): void {
@@ -99,10 +126,10 @@ function openChooser(): void {
if (search) search.value = '';
buildList();
pop.classList.remove('hidden');
- const r = btn.getBoundingClientRect();
+ const r = btn.getBoundingClientRect();
const pw = pop.offsetWidth || 220;
const vw = window.innerWidth;
- pop.style.top = (r.bottom + 4) + 'px';
+ pop.style.top = (r.bottom + 4) + 'px';
pop.style.left = Math.max(4, Math.min(r.left, vw - pw - 4)) + 'px';
search?.focus();
}
@@ -121,8 +148,8 @@ export function setupColumnChooser(): void {
if (!wasOpen) openChooser();
});
- document.getElementById('col-chooser-showall')?.addEventListener('click', showAll);
- document.getElementById('col-chooser-hideall')?.addEventListener('click', hideAll);
+ const master = document.getElementById('col-chooser-master-cb') as HTMLInputElement | null;
+ master?.addEventListener('change', () => setVisibleColsHidden(!master.checked));
const search = document.getElementById('col-chooser-search') as HTMLInputElement | null;
search?.addEventListener('input', () => {