Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion media/webview.css
Original file line number Diff line number Diff line change
Expand Up @@ -1157,14 +1157,39 @@ 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;
border-radius: 3px;
}
.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;
Expand Down
9 changes: 5 additions & 4 deletions src/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,11 @@ export function getWebviewContent(
<span class="goto-label" style="margin:0;">Columns</span>
</div>
<input id="col-chooser-search" class="csv-filter-input" type="text" placeholder="Search columns…" spellcheck="false">
<div class="csv-filter-actions">
<button id="col-chooser-showall" type="button" class="csv-filter-link">Show all</button>
<button id="col-chooser-hideall" type="button" class="csv-filter-link">Hide all</button>
</div>
<label id="col-chooser-master" class="col-chooser-master">
<input id="col-chooser-master-cb" type="checkbox">
<span id="col-chooser-master-label" class="col-chooser-master-label">Select all</span>
<span id="col-chooser-master-count" class="col-chooser-master-count"></span>
</label>
<div id="col-chooser-list" class="col-chooser-list"></div>
</div>

Expand Down
99 changes: 63 additions & 36 deletions src/webview/features/column-chooser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';

Expand All @@ -18,48 +18,73 @@ 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[] {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small cleanup: visibleColIndices() now owns the search predicate (q && !colLabel(...).toLowerCase().includes(q)), but buildList() still runs a byte-identical copy of it in its own render loop. No bug today, but the master count (from visibleColIndices) and the rendered rows (from buildList) derive 'which columns match' from two copies of the rule. If the match logic changes in one place they will disagree. Could buildList iterate over visibleColIndices() so there is one source of truth.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildList() now iterates visibleColIndices(), so the search-match rule lives in one place.

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 {

@Robin-Reiche Robin-Reiche Jun 18, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommendation after a closer look: keep this scoped behavior (it is the Excel model #19 asked for) and just make the scope visible. The only gap is that the label stays 'Select all' while the action is scoped to the search. In syncMaster, when searchQuery is non-empty, set the master label to 'Select all matches' and back to 'Select all' when the search is empty. That makes the N / N count and the checked state honest. The global escape hatch already exists since clearing the search box restores the global master, so no separate Show-all button is needed (that was the clutter #19 wanted gone).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brought back the global reveal behavior. Checking the master box with an empty search now clears all hidden items (Show all). When filtered, it stays scoped to the search results.

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 {
const list = document.getElementById('col-chooser-list');
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';

Expand All @@ -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 {
Expand All @@ -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();
}
Expand All @@ -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', () => {
Expand Down