Skip to content

Commit 991c7dd

Browse files
author
Alexey Perov
committed
Implement M2.1 and M2.2: multiple-selection engine and UX
M2.1 — Multiple-Selection Engine: - Enable native multi-cursor/column selection via allowMultipleSelections, drawSelection, and @codemirror/search in the base extension group - Add @codemirror/search as a direct dependency - Add editorSelectionOps.ts: select-next, select-all, skip, and remove-last occurrence operations with deterministic wrap behavior - Wire occurrence actions into domain APIs, runner adapter, and capability - Verify line operations (move/duplicate/join) are multi-range safe with extended test coverage; document search-nav selection replacement M2.2 — Multiple-Selection Commands and UX: - Add 4 new commands (selectNextOccurrence, selectAllOccurrences, skipOccurrence, undoOccurrence) with definitions, handlers, menu items - Reassign Cmd/Ctrl+D from duplicateLine to selectNextOccurrence; duplicate line moves to Cmd/Ctrl+Alt+D (Shift+D owned by diff preview) - Extend cursor status, app state, and status bar to show selection count (compact 'N selections' segment, aria-labeled, active-pane only) - Add cross-feature validation tests for multi-cursor undo, synchronized typing, occurrence non-overlap, and save-content parity All 2750 tests pass; npm run check reports 0 new errors.
1 parent 369d17d commit 991c7dd

33 files changed

Lines changed: 1073 additions & 48 deletions

app/package-lock.json

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

app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@codemirror/lang-yaml": "^6.1.3",
3131
"@codemirror/language": "^6.12.3",
3232
"@codemirror/legacy-modes": "^6.5.3",
33+
"@codemirror/search": "^6.5.6",
3334
"@codemirror/state": "^6.6.0",
3435
"@codemirror/view": "^6.43.0",
3536
"@lezer/highlight": "^1.2.3",

app/src/lib/commands/definitions.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ export const commandDefinitions: CommandDefinition[] = [
328328
category: "Edit",
329329
paletteIntent: "palette",
330330
availability: "document",
331-
binding: { mac: "Cmd+D", windows: "Ctrl+D" },
331+
binding: { mac: "Cmd+Alt+D", windows: "Ctrl+Alt+D" },
332332
},
333333
{
334334
id: "edit.joinLines",
@@ -339,6 +339,46 @@ export const commandDefinitions: CommandDefinition[] = [
339339
availability: "document",
340340
binding: { mac: "Cmd+J", windows: "Ctrl+J" },
341341
},
342+
{
343+
id: "edit.selectNextOccurrence",
344+
label: "Select Next Occurrence",
345+
menuPath: "Edit/Select Next Occurrence",
346+
category: "Edit",
347+
searchTerms: ["find", "match", "multi-cursor"],
348+
paletteIntent: "palette",
349+
availability: "document",
350+
binding: { mac: "Cmd+D", windows: "Ctrl+D" },
351+
},
352+
{
353+
id: "edit.selectAllOccurrences",
354+
label: "Select All Occurrences",
355+
menuPath: "Edit/Select All Occurrences",
356+
category: "Edit",
357+
searchTerms: ["find", "match", "multi-cursor"],
358+
paletteIntent: "palette",
359+
availability: "document",
360+
binding: { mac: "Cmd+Shift+L", windows: "Ctrl+Shift+L" },
361+
},
362+
{
363+
id: "edit.skipOccurrence",
364+
label: "Skip Occurrence",
365+
menuPath: "Edit/Skip Occurrence",
366+
category: "Edit",
367+
searchTerms: ["find", "match"],
368+
paletteIntent: "palette",
369+
availability: "document",
370+
binding: { mac: "none", windows: "none" },
371+
},
372+
{
373+
id: "edit.undoOccurrence",
374+
label: "Remove Last Occurrence",
375+
menuPath: "Edit/Remove Last Occurrence",
376+
category: "Edit",
377+
searchTerms: ["find", "match", "remove", "cursor"],
378+
paletteIntent: "palette",
379+
availability: "document",
380+
binding: { mac: "none", windows: "none" },
381+
},
342382
{
343383
id: "view.toggleWrap",
344384
label: "Toggle Wrap",

app/src/lib/commands/handlers/appViewHandlers.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ function createEditorRunnerMock(): EditorCommandRunner {
176176
moveLineDown: vi.fn(),
177177
duplicateLine: vi.fn(),
178178
joinLines: vi.fn(),
179+
selectNextOccurrence: vi.fn(() => false),
180+
selectAllOccurrences: vi.fn(() => false),
181+
skipOccurrence: vi.fn(() => false),
182+
undoOccurrence: vi.fn(() => false),
179183
setWrap: vi.fn(),
180184
setZoom: vi.fn(),
181185
findNext: vi.fn(() => false),
@@ -549,6 +553,10 @@ describe("edit commands", () => {
549553
dispatchCommand("edit.moveLineDown", context);
550554
dispatchCommand("edit.duplicateLine", context);
551555
dispatchCommand("edit.joinLines", context);
556+
dispatchCommand("edit.selectNextOccurrence", context);
557+
dispatchCommand("edit.selectAllOccurrences", context);
558+
dispatchCommand("edit.skipOccurrence", context);
559+
dispatchCommand("edit.undoOccurrence", context);
552560

553561
expect(editorRunner.undo).toHaveBeenCalled();
554562
expect(editorRunner.redo).toHaveBeenCalled();
@@ -558,6 +566,10 @@ describe("edit commands", () => {
558566
expect(editorRunner.moveLineDown).toHaveBeenCalled();
559567
expect(editorRunner.duplicateLine).toHaveBeenCalled();
560568
expect(editorRunner.joinLines).toHaveBeenCalled();
569+
expect(editorRunner.selectNextOccurrence).toHaveBeenCalled();
570+
expect(editorRunner.selectAllOccurrences).toHaveBeenCalled();
571+
expect(editorRunner.skipOccurrence).toHaveBeenCalled();
572+
expect(editorRunner.undoOccurrence).toHaveBeenCalled();
561573
});
562574

563575
it("no-ops when no editor runner is available", () => {
@@ -746,6 +758,10 @@ describe("command dispatch coverage", () => {
746758
"edit.moveLineDown",
747759
"edit.duplicateLine",
748760
"edit.joinLines",
761+
"edit.selectNextOccurrence",
762+
"edit.selectAllOccurrences",
763+
"edit.skipOccurrence",
764+
"edit.undoOccurrence",
749765
"view.toggleWrap",
750766
"view.zoomIn",
751767
"view.zoomOut",

app/src/lib/commands/handlers/edit.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,16 @@ export const editHandlers: CommandHandlerMap = {
2525
"edit.joinLines": ({ getEditorRunner }) => {
2626
getEditorRunner()?.joinLines();
2727
},
28+
"edit.selectNextOccurrence": ({ getEditorRunner }) => {
29+
getEditorRunner()?.selectNextOccurrence();
30+
},
31+
"edit.selectAllOccurrences": ({ getEditorRunner }) => {
32+
getEditorRunner()?.selectAllOccurrences();
33+
},
34+
"edit.skipOccurrence": ({ getEditorRunner }) => {
35+
getEditorRunner()?.skipOccurrence();
36+
},
37+
"edit.undoOccurrence": ({ getEditorRunner }) => {
38+
getEditorRunner()?.undoOccurrence();
39+
},
2840
};

app/src/lib/commands/handlers/fileHandlers.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ function createEditorRunnerMock(): EditorCommandRunner {
176176
moveLineDown: vi.fn(),
177177
duplicateLine: vi.fn(),
178178
joinLines: vi.fn(),
179+
selectNextOccurrence: vi.fn(() => false),
180+
selectAllOccurrences: vi.fn(() => false),
181+
skipOccurrence: vi.fn(() => false),
182+
undoOccurrence: vi.fn(() => false),
179183
setWrap: vi.fn(),
180184
setZoom: vi.fn(),
181185
findNext: vi.fn(() => false),

app/src/lib/commands/handlers/keymapHandlers.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ function createEditorRunnerMock(): EditorCommandRunner {
175175
moveLineDown: vi.fn(),
176176
duplicateLine: vi.fn(),
177177
joinLines: vi.fn(),
178+
selectNextOccurrence: vi.fn(() => false),
179+
selectAllOccurrences: vi.fn(() => false),
180+
skipOccurrence: vi.fn(() => false),
181+
undoOccurrence: vi.fn(() => false),
178182
setWrap: vi.fn(),
179183
setZoom: vi.fn(),
180184
findNext: vi.fn(() => false),

app/src/lib/commands/handlers/workspaceHandlers.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ function createEditorRunnerMock(): EditorCommandRunner {
175175
moveLineDown: vi.fn(),
176176
duplicateLine: vi.fn(),
177177
joinLines: vi.fn(),
178+
selectNextOccurrence: vi.fn(() => false),
179+
selectAllOccurrences: vi.fn(() => false),
180+
skipOccurrence: vi.fn(() => false),
181+
undoOccurrence: vi.fn(() => false),
178182
setWrap: vi.fn(),
179183
setZoom: vi.fn(),
180184
findNext: vi.fn(() => false),

app/src/lib/components/AppShell.svelte

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
zoomPercent: number;
157157
cursorLine: number;
158158
cursorColumn: number;
159+
selectionCount: number;
159160
decoratePlaintextSymbols: boolean;
160161
showMinimap: boolean;
161162
maxBinaryOpenAsTextBytes: number;
@@ -581,6 +582,14 @@
581582
<span class="status-segment optional-segment optional-cursor">
582583
Ln {editor.cursorLine}, Col {editor.cursorColumn}
583584
</span>
585+
{#if editor.selectionCount > 1}
586+
<span
587+
class="status-segment optional-segment optional-selections"
588+
aria-label={`${editor.selectionCount} selections`}
589+
>
590+
{editor.selectionCount} selections
591+
</span>
592+
{/if}
584593
<span class="status-segment optional-segment optional-encoding">
585594
{#if editor.isImageDocument}
586595
Image

app/src/lib/domain/commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export type AppCommandId =
3434
| "edit.moveLineDown"
3535
| "edit.duplicateLine"
3636
| "edit.joinLines"
37+
| "edit.selectNextOccurrence"
38+
| "edit.selectAllOccurrences"
39+
| "edit.skipOccurrence"
40+
| "edit.undoOccurrence"
3741
| "view.toggleWrap"
3842
| "view.zoomIn"
3943
| "view.zoomOut"

0 commit comments

Comments
 (0)