Skip to content

Commit 20bfb74

Browse files
committed
Merge PRs #11-#14 for 1.11.0 (codicons, select-all, popup fix, unfreeze-all)
2 parents b411134 + 32524af commit 20bfb74

8 files changed

Lines changed: 171 additions & 82 deletions

File tree

media/webview.css

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -472,11 +472,22 @@ 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-
/* Frozen (pinned-left) column header: a 📌 marker before the column name,
475+
/* Frozen (pinned-left) column header: a pin marker before the column name,
476476
mirroring the frozen-row pin. The always-pinned '#' gutter (.row-index-header)
477477
is excluded so only user-frozen columns get the marker. */
478478
:is(.ag-theme-alpine-dark, .ag-theme-alpine) .ag-pinned-left-header .ag-header-cell:not(.row-index-header) .ag-header-cell-text::before {
479-
content: '📌 ';
479+
content: "\eba0";
480+
font-family: "codicon";
481+
font-weight: normal;
482+
font-style: normal;
483+
font-size: 16px;
484+
line-height: 1;
485+
display: inline-block;
486+
vertical-align: middle;
487+
margin-right: 4px;
488+
-webkit-font-smoothing: antialiased;
489+
-moz-osx-font-smoothing: grayscale;
490+
text-rendering: auto;
480491
}
481492
:is(.ag-theme-alpine-dark, .ag-theme-alpine) .ag-header-cell {
482493
font-weight: 600 !important;
@@ -790,8 +801,37 @@ button .codicon {
790801
user-select: none;
791802
cursor: pointer;
792803
}
804+
.row-index-header { cursor: pointer; }
793805
.row-index-header .ag-header-cell-text::before { display: none !important; }
794806

807+
:is(.ag-theme-alpine-dark, .ag-theme-alpine) .ag-cell.row-index-pinned {
808+
padding: 0 6px;
809+
}
810+
:is(.ag-theme-alpine-dark, .ag-theme-alpine) .ag-cell.row-index-pinned .ag-cell-value {
811+
width: 100%;
812+
}
813+
.row-pin-cell {
814+
display: flex;
815+
align-items: center;
816+
justify-content: space-between;
817+
width: 100%;
818+
gap: 4px;
819+
}
820+
.row-pin-cell .codicon {
821+
font-size: 16px;
822+
width: 16px;
823+
height: 16px;
824+
line-height: 16px;
825+
flex-shrink: 0;
826+
font-weight: normal;
827+
-webkit-font-smoothing: antialiased;
828+
-moz-osx-font-smoothing: grayscale;
829+
}
830+
.row-pin-num {
831+
overflow: hidden;
832+
text-overflow: ellipsis;
833+
}
834+
795835
/* ── F2: Delimiter Badge ── */
796836
.delim-badge {
797837
font-size: 11px;
@@ -830,11 +870,12 @@ button .codicon {
830870
box-shadow: 0 3px 8px rgba(0,0,0,.35);
831871
}
832872
.col-context-menu.hidden { display: none; }
833-
.col-ctx-item { padding: 5px 12px; font-size: 12px; cursor: pointer; color: var(--vscode-menu-foreground, #ccc); }
873+
.col-ctx-item { display: flex; align-items: center; gap: 8px; padding: 5px 12px; font-size: 12px; cursor: pointer; color: var(--vscode-menu-foreground, #ccc); }
834874
.col-ctx-item:hover { background: var(--vscode-menu-selectionBackground, #094771); color: var(--vscode-menu-selectionForeground, #fff); }
835875
.col-ctx-separator { height: 1px; margin: 3px 0; background: var(--vscode-menu-border, #454545); pointer-events: none; }
836876
.col-ctx-item.danger { color: var(--vscode-errorForeground, #f48771); }
837877
.col-ctx-item.danger:hover { background: var(--vscode-inputValidation-errorBackground, #5a1d1d); color: #fff; }
878+
.col-ctx-item .codicon { font-size: 16px; width: 16px; flex-shrink: 0; font-weight: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
838879

839880
/* ── Row Context Menu ── */
840881
.row-context-menu {
@@ -848,10 +889,11 @@ button .codicon {
848889
box-shadow: 0 3px 8px rgba(0,0,0,.35);
849890
}
850891
.row-context-menu.hidden { display: none; }
851-
.row-ctx-item { padding: 5px 12px; font-size: 12px; cursor: pointer; color: var(--vscode-menu-foreground, #ccc); }
892+
.row-ctx-item { display: flex; align-items: center; gap: 8px; padding: 5px 12px; font-size: 12px; cursor: pointer; color: var(--vscode-menu-foreground, #ccc); }
852893
.row-ctx-item:hover { background: var(--vscode-menu-selectionBackground, #094771); color: var(--vscode-menu-selectionForeground, #fff); }
853894
.row-ctx-item.danger { color: var(--vscode-errorForeground, #f48771); }
854895
.row-ctx-item.danger:hover { background: var(--vscode-inputValidation-errorBackground, #5a1d1d); color: #fff; }
896+
.row-ctx-item .codicon { font-size: 16px; width: 16px; flex-shrink: 0; font-weight: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
855897

856898
/* AG Grid context menu danger items */
857899
.ag-menu-option.ctx-danger .ag-menu-option-text { color: var(--vscode-errorForeground, #f48771); }

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/webview.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,22 +220,23 @@ export function getWebviewContent(
220220
221221
<!-- Column context menu [F5] -->
222222
<div id="col-context-menu" class="col-context-menu hidden">
223-
<div id="col-ctx-insert-left" class="col-ctx-item"${isPreview ? ' style="display:none;"' : ''}>&#x2B05;&#xFE0F; Insert column left</div>
224-
<div id="col-ctx-insert-right" class="col-ctx-item"${isPreview ? ' style="display:none;"' : ''}>&#x27A1;&#xFE0F; Insert column right</div>
223+
<div id="col-ctx-insert-left" class="col-ctx-item"${isPreview ? ' style="display:none;"' : ''}><i class="codicon codicon-arrow-left"></i><span class="col-ctx-label">Insert column left</span></div>
224+
<div id="col-ctx-insert-right" class="col-ctx-item"${isPreview ? ' style="display:none;"' : ''}><i class="codicon codicon-arrow-right"></i><span class="col-ctx-label">Insert column right</span></div>
225225
<div class="col-ctx-separator"${isPreview ? ' style="display:none;"' : ''}></div>
226-
<div id="col-ctx-rename" class="col-ctx-item"${isPreview ? ' style="display:none;"' : ''}>&#x270F;&#xFE0F; Rename column</div>
226+
<div id="col-ctx-rename" class="col-ctx-item"${isPreview ? ' style="display:none;"' : ''}><i class="codicon codicon-edit"></i><span class="col-ctx-label">Rename column</span></div>
227227
<div class="col-ctx-separator"${isPreview ? ' style="display:none;"' : ''}></div>
228-
<div id="col-ctx-select" class="col-ctx-item">&#x2630; Select column</div>
228+
<div id="col-ctx-select" class="col-ctx-item"><i class="codicon codicon-list-selection"></i><span class="col-ctx-label">Select column</span></div>
229229
<div class="col-ctx-separator"></div>
230-
<div id="col-ctx-freeze" class="col-ctx-item">&#x1F4CC; Freeze column</div>
231-
<div id="col-ctx-unfreeze" class="col-ctx-item" style="display:none;">&#x1F4CC; Unfreeze column</div>
230+
<div id="col-ctx-freeze" class="col-ctx-item"><i class="codicon codicon-pinned"></i><span class="col-ctx-label">Freeze column</span></div>
231+
<div id="col-ctx-unfreeze" class="col-ctx-item" style="display:none;"><i class="codicon codicon-pin"></i><span class="col-ctx-label">Unfreeze column</span></div>
232+
<div id="col-ctx-unfreeze-all" class="col-ctx-item" style="display:none;"><i class="codicon codicon-pin"></i><span class="col-ctx-label">Unfreeze all columns</span></div>
232233
<div class="col-ctx-separator"${isPreview ? ' style="display:none;"' : ''}></div>
233-
<div id="col-ctx-delete" class="col-ctx-item danger"${isPreview ? ' style="display:none;"' : ''}>&#x2715; Delete column</div>
234+
<div id="col-ctx-delete" class="col-ctx-item danger"${isPreview ? ' style="display:none;"' : ''}><i class="codicon codicon-trash"></i><span class="col-ctx-label">Delete column</span></div>
234235
</div>
235236
236237
<!-- Row context menu -->
237238
<div id="row-context-menu" class="row-context-menu hidden">
238-
<div id="row-ctx-delete" class="row-ctx-item danger">&#x2715; Delete row</div>
239+
<div id="row-ctx-delete" class="row-ctx-item danger"><i class="codicon codicon-trash"></i><span class="row-ctx-label">Delete row</span></div>
239240
</div>
240241
241242
<!-- Pagination bar [F7] -->

src/webview/features/delete-row-col.ts

Lines changed: 53 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,17 @@ function insertColumns(baseIndex: number, position: 'left' | 'right', count: num
163163
}
164164

165165
// ── Custom context menu ───────────────────────────────────────────────────────
166+
function makeRowItem(label: string, iconClass: string, danger = false): HTMLDivElement {
167+
const item = document.createElement('div');
168+
item.className = 'row-ctx-item' + (danger ? ' danger' : '');
169+
const icon = document.createElement('i');
170+
icon.className = 'codicon ' + iconClass;
171+
const span = document.createElement('span');
172+
span.className = 'row-ctx-label';
173+
span.textContent = label;
174+
item.append(icon, span);
175+
return item;
176+
}
166177

167178
function hideMenu(): void {
168179
document.getElementById('row-context-menu')?.classList.add('hidden');
@@ -185,15 +196,11 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
185196
// ── Copy ──────────────────────────────────────────────────────────────────
186197
if (hasMultiSelection()) {
187198
// A range is selected — offer range copy, with and without header row.
188-
const copyRange = document.createElement('div');
189-
copyRange.className = 'row-ctx-item';
190-
copyRange.textContent = 'Copy';
199+
const copyRange = makeRowItem('Copy', 'codicon-copy');
191200
copyRange.addEventListener('click', () => { copySelection(false); hideMenu(); });
192201
menu.appendChild(copyRange);
193202

194-
const copyWithHeader = document.createElement('div');
195-
copyWithHeader.className = 'row-ctx-item';
196-
copyWithHeader.textContent = 'Copy with header';
203+
const copyWithHeader = makeRowItem('Copy with header', 'codicon-copy');
197204
copyWithHeader.addEventListener('click', () => { copySelection(true); hideMenu(); });
198205
menu.appendChild(copyWithHeader);
199206

@@ -207,9 +214,7 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
207214
const raw = node?.data?.[colId];
208215
const value = raw != null ? String(raw) : '';
209216

210-
const copyItem = document.createElement('div');
211-
copyItem.className = 'row-ctx-item';
212-
copyItem.textContent = 'Copy';
217+
const copyItem = makeRowItem('Copy', 'codicon-copy');
213218
copyItem.addEventListener('click', () => {
214219
navigator.clipboard.writeText(value).catch(() => {});
215220
hideMenu();
@@ -227,30 +232,13 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
227232
// the two are mutually exclusive (runDetect() drops any frozen row).
228233
if (state.dupRowSet.size === 0 && !state.dupShowOnly) {
229234
if (isPinnedRow) {
230-
// Right-clicked a pinned (frozen) row. pinnedOrig is resolved from the
231-
// DOM by the contextmenu handler. Only show the per-row item when it
232-
// resolved, so an unresolved click can NEVER fall back to clearing all
233-
// freezes. "Unfreeze all rows" is a separate, explicit action.
235+
// Right-clicked a pinned (frozen) row -> unfreeze just that row.
234236
const clickedOrig = pinnedOrig;
235237
if (clickedOrig != null) {
236-
const item = document.createElement('div');
237-
item.className = 'row-ctx-item';
238-
item.textContent = '📌 Unfreeze row';
238+
const item = makeRowItem('Unfreeze row', 'codicon-pin');
239239
item.addEventListener('click', () => { unfreezeRow(clickedOrig); hideMenu(); });
240240
menu.appendChild(item);
241241
}
242-
if (frozenRowCount() > 1) {
243-
const all = document.createElement('div');
244-
all.className = 'row-ctx-item';
245-
all.textContent = '📌 Unfreeze all rows';
246-
all.addEventListener('click', () => { unfreezeAllRows(); hideMenu(); });
247-
menu.appendChild(all);
248-
}
249-
if (clickedOrig != null || frozenRowCount() > 1) {
250-
const sep = document.createElement('div');
251-
sep.className = 'col-ctx-separator';
252-
menu.appendChild(sep);
253-
}
254242
} else if (rowIndex !== null) {
255243
// Body rows are never frozen themselves (a frozen row moves to the
256244
// pinned band), so the body menu only offers Freeze. With a multi-row
@@ -265,17 +253,27 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
265253
}
266254

267255
if (origs.length > 0) {
268-
const item = document.createElement('div');
269-
item.className = 'row-ctx-item';
270-
item.textContent = origs.length > 1 ? `📌 Freeze ${origs.length} rows` : '📌 Freeze row';
256+
const item = makeRowItem(origs.length > 1 ? `Freeze ${origs.length} rows` : 'Freeze row', 'codicon-pinned');
271257
item.addEventListener('click', () => { freezeRows(origs); hideMenu(); });
272258
menu.appendChild(item);
273-
274-
const sep = document.createElement('div');
275-
sep.className = 'col-ctx-separator';
276-
menu.appendChild(sep);
277259
}
278260
}
261+
262+
// "Unfreeze all rows (N)" sits below the per-row items, shown on any row
263+
// while more than one row is frozen - mirrors "Unfreeze all columns".
264+
if (frozenRowCount() > 1) {
265+
const all = makeRowItem(`Unfreeze all rows (${frozenRowCount()})`, 'codicon-pin');
266+
all.addEventListener('click', () => { unfreezeAllRows(); hideMenu(); });
267+
menu.appendChild(all);
268+
}
269+
270+
// One separator for the freeze group, if it has any item.
271+
const hasFreezeItem = (isPinnedRow && pinnedOrig != null) || (!isPinnedRow && rowIndex !== null) || frozenRowCount() > 1;
272+
if (hasFreezeItem) {
273+
const sep = document.createElement('div');
274+
sep.className = 'col-ctx-separator';
275+
menu.appendChild(sep);
276+
}
279277
}
280278

281279
// ── Insert row(s) above/below ─────────────────────────────────────────────
@@ -289,18 +287,14 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
289287
const topEdge = inSel ? Math.min(...selectedRows) : rowIndex;
290288
const bottomEdge = inSel ? Math.max(...selectedRows) : rowIndex;
291289

292-
const insertAbove = document.createElement('div');
293-
insertAbove.className = 'row-ctx-item';
294-
insertAbove.textContent = count > 1 ? `Insert ${count} rows above` : 'Insert row above';
290+
const insertAbove = makeRowItem(count > 1 ? `Insert ${count} rows above` : 'Insert row above', 'codicon-arrow-up');
295291
insertAbove.addEventListener('click', () => {
296292
insertRows(topEdge, 'above', count);
297293
hideMenu();
298294
});
299295
menu.appendChild(insertAbove);
300296

301-
const insertBelow = document.createElement('div');
302-
insertBelow.className = 'row-ctx-item';
303-
insertBelow.textContent = count > 1 ? `Insert ${count} rows below` : 'Insert row below';
297+
const insertBelow = makeRowItem(count > 1 ? `Insert ${count} rows below` : 'Insert row below', 'codicon-arrow-down');
304298
insertBelow.addEventListener('click', () => {
305299
insertRows(bottomEdge, 'below', count);
306300
hideMenu();
@@ -323,10 +317,7 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
323317
? selectedRows
324318
: [rowIndex];
325319

326-
const label = rowIndices.length > 1 ? `Delete ${rowIndices.length} rows` : 'Delete row';
327-
const delRowItem = document.createElement('div');
328-
delRowItem.className = 'row-ctx-item danger';
329-
delRowItem.textContent = label;
320+
const delRowItem = makeRowItem(rowIndices.length > 1 ? `Delete ${rowIndices.length} rows` : 'Delete row', 'codicon-trash', true);
330321
delRowItem.addEventListener('click', () => {
331322
deleteRows(rowIndices);
332323
hideMenu();
@@ -340,9 +331,7 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
340331
const selectedCols = getSelectedColIndices();
341332
const useMulti = selectedCols.length > 1 && selectedCols.includes(colIndex);
342333

343-
const delColItem = document.createElement('div');
344-
delColItem.className = 'row-ctx-item danger';
345-
delColItem.textContent = useMulti ? `Delete ${selectedCols.length} columns` : 'Delete column';
334+
const delColItem = makeRowItem(useMulti ? `Delete ${selectedCols.length} columns` : 'Delete column', 'codicon-trash', true);
346335
delColItem.addEventListener('click', () => {
347336
if (useMulti) deleteColumns(selectedCols);
348337
else if (colId) deleteColumn(colId);
@@ -351,6 +340,18 @@ function showContextMenu(x: number, y: number, rowIndex: number | null, colId: s
351340
menu.appendChild(delColItem);
352341
}
353342

343+
// Remove orphan separators: drop a leading/trailing separator and any two
344+
// that ended up adjacent (e.g. a middle group rendered nothing).
345+
const items = Array.from(menu.children);
346+
let lastWasSep = true;
347+
for (const el of items) {
348+
const isSep = el.classList.contains('col-ctx-separator');
349+
if (isSep && lastWasSep) { el.remove(); continue; }
350+
lastWasSep = isSep;
351+
}
352+
const last = menu.lastElementChild;
353+
if (last?.classList.contains('col-ctx-separator')) last.remove();
354+
354355
if (menu.children.length === 0) return;
355356

356357
// Position — keep menu on screen
@@ -431,8 +432,10 @@ export function setupDeleteRowCol(): void {
431432
const rowIndex = riStr != null ? parseInt(riStr, 10) : null;
432433

433434
// For a pinned (frozen) row, resolve which row it is independently of AG
434-
// Grid's pinned-row index scheme: the '#' gutter cell renders "📌<origIndex>"
435-
// (builder.ts valueGetter), so read that from the matching row in the band.
435+
// Grid's pinned-row index scheme: the '#' gutter cell renders a pin icon +
436+
// the origIndex (builder.ts cellRenderer). The icon is a ::before glyph with
437+
// no text node, so the cell's textContent is just the number - read it from
438+
// the matching row in the band.
436439
let pinnedOrig: number | null = null;
437440
if (isPinnedRow && agRow && riStr != null) {
438441
const ft = agRow.closest('.ag-floating-top');

0 commit comments

Comments
 (0)