fix: delete complete Composer selections

This commit is contained in:
2026-09-01 20:34:23 +09:00
parent 00c8df0fc9
commit 8fbe4218c6
4 changed files with 53 additions and 7 deletions
@@ -28,6 +28,7 @@
type ComposerPasteMeasurement, type ComposerPasteMeasurement,
} from "$lib/workspace/console/composer-paste.ts"; } from "$lib/workspace/console/composer-paste.ts";
import { import {
composerDeletionRange,
composerPasteAtoms, composerPasteAtoms,
composerPasteToken, composerPasteToken,
pasteChipLabel, pasteChipLabel,
@@ -255,18 +256,15 @@
direction: "backward" | "forward", direction: "backward" | "forward",
): boolean { ): boolean {
const selection = currentView.state.selection.main; const selection = currentView.state.selection.main;
if (!selection.empty) return false;
const pastes = composerPasteAtoms( const pastes = composerPasteAtoms(
currentView.state.doc.toString(), currentView.state.doc.toString(),
currentView.state.field(pasteRegistry), currentView.state.field(pasteRegistry),
); );
const paste = direction === "backward" const deletion = composerDeletionRange(selection, pastes, direction);
? pastes.find((candidate) => candidate.to === selection.head) if (!deletion) return false;
: pastes.find((candidate) => candidate.from === selection.head);
if (!paste) return false;
currentView.dispatch({ currentView.dispatch({
changes: { from: paste.from, to: paste.to }, changes: deletion,
selection: EditorSelection.cursor(paste.from), selection: EditorSelection.cursor(deletion.from),
annotations: isolateHistory.of("full"), annotations: isolateHistory.of("full"),
userEvent: "delete", userEvent: "delete",
}); });
@@ -1,5 +1,6 @@
import type { Segment } from "$lib/generated/protocol.ts"; import type { Segment } from "$lib/generated/protocol.ts";
import { import {
composerDeletionRange,
type ComposerPaste, type ComposerPaste,
composerPasteToken, composerPasteToken,
pasteChipLabel, pasteChipLabel,
@@ -76,6 +77,30 @@ Deno.test("composer draft preserves mixed Text and Paste order exactly", () => {
assertEquals(snapshot.pastes.map((entry) => entry.key), [11, 12]); assertEquals(snapshot.pastes.map((entry) => entry.key), [11, 12]);
}); });
Deno.test("selection deletion covers mixed Text and every selected paste chip", () => {
const pastes = [
{ ...paste(1, "first"), key: 10, from: 2, to: 5 },
{ ...paste(2, "second"), key: 11, from: 8, to: 11 },
];
assertEquals(
composerDeletionRange({ from: 1, to: 12, head: 12 }, pastes, "backward"),
{ from: 1, to: 12 },
);
assertEquals(
composerDeletionRange({ from: 2, to: 11, head: 2 }, pastes, "forward"),
{ from: 2, to: 11 },
);
assertEquals(
composerDeletionRange({ from: 5, to: 5, head: 5 }, pastes, "backward"),
{ from: 2, to: 5 },
);
assertEquals(
composerDeletionRange({ from: 8, to: 8, head: 8 }, pastes, "forward"),
{ from: 8, to: 11 },
);
});
Deno.test("composer paste chip label is compact and accessible", () => { Deno.test("composer paste chip label is compact and accessible", () => {
assertEquals( assertEquals(
pasteChipLabel({ id: 4, content: "payload", chars: 7, lines: 1 }), pasteChipLabel({ id: 4, content: "payload", chars: 7, lines: 1 }),
@@ -24,6 +24,26 @@ export interface ComposerTextPaste {
content: string; content: string;
} }
export interface ComposerSelection {
from: number;
to: number;
head: number;
}
export function composerDeletionRange(
selection: ComposerSelection,
pastes: readonly ComposerPasteAtom[],
direction: "backward" | "forward",
): { from: number; to: number } | null {
if (selection.from !== selection.to) {
return { from: selection.from, to: selection.to };
}
const paste = direction === "backward"
? pastes.find((candidate) => candidate.to === selection.head)
: pastes.find((candidate) => candidate.from === selection.head);
return paste ? { from: paste.from, to: paste.to } : null;
}
export interface ComposerDraftSnapshot { export interface ComposerDraftSnapshot {
document: string; document: string;
content: string; content: string;
@@ -604,6 +604,9 @@ Deno.test("Worker Console paste chips preserve typed draft and target authority"
composerInput.includes("EditorView.atomicRanges") && composerInput.includes("EditorView.atomicRanges") &&
composerInput.includes('key: "Backspace"') && composerInput.includes('key: "Backspace"') &&
composerInput.includes('key: "Delete"') && composerInput.includes('key: "Delete"') &&
composerInput.includes(
"composerDeletionRange(selection, pastes, direction)",
) &&
composerInput.includes('chip.setAttribute("aria-label", label)') && composerInput.includes('chip.setAttribute("aria-label", label)') &&
composerInput.includes("preserveExactText = false") && composerInput.includes("preserveExactText = false") &&
consolePage.includes("buildComposerSegmentsRequest(value.segments, {") && consolePage.includes("buildComposerSegmentsRequest(value.segments, {") &&