fix: delete complete Composer selections
This commit is contained in:
@@ -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, {") &&
|
||||||
|
|||||||
Reference in New Issue
Block a user