From 0e68de42783b610268050962fa0bece040252e54 Mon Sep 17 00:00:00 2001 From: "exe.dev user" Date: Thu, 5 Mar 2026 02:54:32 +0000 Subject: [PATCH] Add Match Row and Delete Row commands Match Row: selects all rows where the cell in the current column equals the current cell's value. Selection spans first-to-last matching row across all columns. Delete Row (Cmd+Backspace): deletes every row in the current selection, or the single current row if nothing is selected. Ensures at least one empty row always remains. Co-authored-by: Shelley --- app_test.go | 2 +- commands.go | 2 ++ frontend/src/main.js | 64 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/app_test.go b/app_test.go index df10e86..d566601 100644 --- a/app_test.go +++ b/app_test.go @@ -98,7 +98,7 @@ func TestParseCSVString(t *testing.T) { func TestGetCommands(t *testing.T) { reg := NewCommandRegistry() cmds := reg.GetCommands() - if len(cmds) != 15 { + if len(cmds) != 17 { t.Errorf("expected 12 commands, got %d", len(cmds)) } // Check that all have IDs diff --git a/commands.go b/commands.go index 8c62e2a..d946fee 100644 --- a/commands.go +++ b/commands.go @@ -33,5 +33,7 @@ func (c *CommandRegistry) GetCommands() []Command { {ID: "sort-asc", Name: "Sort A-Z", Shortcut: ""}, {ID: "sort-desc", Name: "Sort Z-A", Shortcut: ""}, {ID: "sort-advanced", Name: "Sort Advanced", Shortcut: ""}, + {ID: "match-row", Name: "Match Row", Shortcut: ""}, + {ID: "delete-row", Name: "Delete Row", Shortcut: "Cmd+Backspace"}, } } diff --git a/frontend/src/main.js b/frontend/src/main.js index 6213a51..8968f03 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -491,6 +491,13 @@ document.addEventListener('keydown', (e) => { return; } + // Cmd+Backspace deletes rows + if (meta && e.key === 'Backspace') { + e.preventDefault(); + executeCommand('delete-row'); + return; + } + // Delete/Backspace clears selected cells if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); @@ -615,6 +622,8 @@ async function loadCommands() { { ID: 'sort-asc', Name: 'Sort A-Z', Shortcut: '' }, { ID: 'sort-desc', Name: 'Sort Z-A', Shortcut: '' }, { ID: 'sort-advanced', Name: 'Sort Advanced', Shortcut: '' }, + { ID: 'match-row', Name: 'Match Row', Shortcut: '' }, + { ID: 'delete-row', Name: 'Delete Row', Shortcut: 'Cmd+Backspace' }, ]; } } @@ -720,6 +729,8 @@ async function executeCommand(id) { case 'sort-asc': doSort(true); break; case 'sort-desc': doSort(false); break; case 'sort-advanced': openSortAdvanced(); break; + case 'match-row': doMatchRow(); break; + case 'delete-row': doDeleteRow(); break; } } @@ -933,6 +944,59 @@ function doInsertColRight() { setStatus('Inserted column right'); } +// ===== Match & Delete Row ===== +function doMatchRow() { + const col = state.cursor.col; + const val = getCellValue(state.cursor.row, col); + const matchingRows = []; + for (let r = 0; r < state.rows.length; r++) { + if ((state.rows[r][col] || '') === val) { + matchingRows.push(r); + } + } + if (matchingRows.length === 0) { + setStatus('No matching rows'); + return; + } + // Select from first to last matching row, spanning all columns + const first = matchingRows[0]; + const last = matchingRows[matchingRows.length - 1]; + state.cursor = { row: first, col: 0 }; + state.selection = { + startRow: first, + startCol: 0, + endRow: last, + endCol: state.headers.length - 1, + }; + updateSelectionClasses(); + scrollCursorIntoView(); + const colName = state.headers[col] || colLabel(col); + setStatus(`${matchingRows.length} row(s) matching ${colName} = "${val}"`); +} + +function doDeleteRow() { + const sel = normalizeSelection(); + let startRow, endRow; + if (sel) { + startRow = sel.r1; + endRow = sel.r2; + } else { + startRow = state.cursor.row; + endRow = state.cursor.row; + } + const count = endRow - startRow + 1; + state.rows.splice(startRow, count); + // Ensure at least one row remains + if (state.rows.length === 0) { + state.rows.push(new Array(state.headers.length).fill('')); + } + // Adjust cursor + state.cursor.row = Math.min(startRow, state.rows.length - 1); + state.selection = null; + render(); + setStatus(`Deleted ${count} row(s)`); +} + // ===== Sorting ===== function doSort(ascending) { const col = state.cursor.col;