Add Match Row and Delete Row commands
Some checks failed
Build / build-linux (push) Failing after 13s
Build / build-linux-webkit2_41 (push) Failing after 14s
Build / build-macos (push) Successful in 3m22s

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 <shelley@exe.dev>
This commit is contained in:
exe.dev user 2026-03-05 02:54:32 +00:00
parent ab2d281aad
commit 0e68de4278
3 changed files with 67 additions and 1 deletions

View file

@ -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

View file

@ -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"},
}
}

View file

@ -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;