Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/super-editor/src/core/extensions/keymap.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const Keymap = Extension.create({
'Ctrl-Alt-Backspace': () => handleDelete(this.editor),
'Alt-Delete': () => handleDelete(this.editor),
'Alt-d': () => handleDelete(this.editor),
'Ctrl-a': () => this.editor.commands.selectTextblockStart(),
'Ctrl-a': () => this.editor.commands.selectAll(),
'Ctrl-e': () => this.editor.commands.selectTextblockEnd(),
'Ctrl-t': () => this.editor.commands.insertTabChar(),
};
Expand Down
47 changes: 47 additions & 0 deletions packages/super-editor/src/core/extensions/keymap.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, it, expect, vi } from 'vitest';

const setupKeymap = async ({ isMacOS, isIOS }) => {
vi.resetModules();
vi.doMock('../utilities/isMacOS.js', () => ({ isMacOS: () => isMacOS }));
vi.doMock('../utilities/isIOS.js', () => ({ isIOS: () => isIOS }));

const { Keymap } = await import('./keymap.js');
const { getExtensionConfigField } = await import('../helpers/getExtensionConfigField.js');

const editor = {
commands: {
selectAll: vi.fn(),
selectTextblockStart: vi.fn(),
},
};

const addShortcuts = getExtensionConfigField(Keymap, 'addShortcuts', {
name: Keymap.name,
editor,
});

const bindings = addShortcuts();
return { bindings, editor };
};

describe('Keymap extension', () => {
it('maps Ctrl-a to selectAll on macOS', async () => {
const { bindings, editor } = await setupKeymap({ isMacOS: true, isIOS: false });

expect(bindings['Ctrl-a']).toBeTypeOf('function');
bindings['Ctrl-a']();

expect(editor.commands.selectAll).toHaveBeenCalledTimes(1);
expect(editor.commands.selectTextblockStart).not.toHaveBeenCalled();
});

it('keeps Mod-a mapped to selectAll on non-mac platforms', async () => {
const { bindings, editor } = await setupKeymap({ isMacOS: false, isIOS: false });

expect(bindings['Mod-a']).toBeTypeOf('function');
bindings['Mod-a']();

expect(editor.commands.selectAll).toHaveBeenCalledTimes(1);
expect(editor.commands.selectTextblockStart).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,21 @@ export function computeSelectionRectsFromDom(
continue;
}

const filterPageEntries = (entries: DomPositionIndexEntry[]) =>
entries.filter((entry) => pageEl.contains(entry.el));

let pageEntries = filterPageEntries(sliceEntries);
if (pageEntries.length === 0 && !rebuiltOnce) {
options.rebuildDomPositionIndex();
rebuiltOnce = true;
sliceEntries = options.domPositionIndex.findEntriesInRange(sliceFrom, sliceTo);
pageEntries = filterPageEntries(sliceEntries);
}

if (pageEntries.length === 0) {
continue;
}
Comment thread
harbournick marked this conversation as resolved.

if (isVerbose) {
debugLog(
'verbose',
Expand All @@ -183,8 +198,33 @@ export function computeSelectionRectsFromDom(
);
}

let startEntry = options.domPositionIndex.findEntryAtPosition(sliceFrom) ?? sliceEntries[0]!;
let endEntry = options.domPositionIndex.findEntryAtPosition(sliceTo) ?? sliceEntries[sliceEntries.length - 1]!;
const pickEntryForPos = (entries: DomPositionIndexEntry[], pos: number, fallbackIndex: number) => {
const direct = entries.find((entry) => pos >= entry.pmStart && pos <= entry.pmEnd);
if (!direct) {
const fallback = entries[fallbackIndex]!;
return fallback;
}
return direct;
};

let startEntry = pickEntryForPos(pageEntries, sliceFrom, 0);
let endEntry = pickEntryForPos(pageEntries, sliceTo, pageEntries.length - 1);

if ((!startEntry?.el?.isConnected || !endEntry?.el?.isConnected) && !rebuiltOnce) {
options.rebuildDomPositionIndex();
rebuiltOnce = true;
sliceEntries = options.domPositionIndex.findEntriesInRange(sliceFrom, sliceTo);
pageEntries = filterPageEntries(sliceEntries);
if (pageEntries.length === 0) {
continue;
}
startEntry = pickEntryForPos(pageEntries, sliceFrom, 0);
endEntry = pickEntryForPos(pageEntries, sliceTo, pageEntries.length - 1);
}

if (!startEntry?.el?.isConnected || !endEntry?.el?.isConnected) {
continue;
}

if (isVerbose) {
debugLog(
Expand All @@ -199,61 +239,6 @@ export function computeSelectionRectsFromDom(
);
}

// If the index is stale (virtualization mount/unmount), rebuild once and retry.
let startContained = pageEl.contains(startEntry.el);
let endContained = pageEl.contains(endEntry.el);
if (!startContained || !endContained) {
if (isVerbose) {
debugLog(
'verbose',
`DOM selection rects: boundary containment ${JSON.stringify({
pageIndex,
sliceFrom,
sliceTo,
startContained,
endContained,
})}`,
);
}
if (!rebuiltOnce) {
options.rebuildDomPositionIndex();
rebuiltOnce = true;
sliceEntries = options.domPositionIndex.findEntriesInRange(sliceFrom, sliceTo);
if (sliceEntries.length === 0) continue;
startEntry = options.domPositionIndex.findEntryAtPosition(sliceFrom) ?? sliceEntries[0]!;
endEntry = options.domPositionIndex.findEntryAtPosition(sliceTo) ?? sliceEntries[sliceEntries.length - 1]!;
startContained = pageEl.contains(startEntry.el);
endContained = pageEl.contains(endEntry.el);
if (isVerbose) {
debugLog(
'verbose',
`DOM selection rects: boundary containment after rebuild ${JSON.stringify({
pageIndex,
sliceFrom,
sliceTo,
startContained,
endContained,
start: entryDebugInfo(startEntry),
end: entryDebugInfo(endEntry),
})}`,
);
}
}
if (!startContained || !endContained) {
debugLog(
'warn',
`DOM selection rects: stale index after rebuild ${JSON.stringify({
pageIndex,
sliceFrom,
sliceTo,
start: entryDebugInfo(startEntry),
end: entryDebugInfo(endEntry),
})}`,
);
return null;
}
}

const doc = pageEl.ownerDocument ?? document;
const range = doc.createRange();
try {
Expand All @@ -280,7 +265,7 @@ export function computeSelectionRectsFromDom(
}
let missingEntries: DomPositionIndexEntry[] | null = null;
if (typeof range.intersectsNode === 'function') {
for (const entry of sliceEntries) {
for (const entry of pageEntries) {
try {
if (!range.intersectsNode(entry.el)) {
missingEntries ??= [];
Expand All @@ -304,7 +289,7 @@ export function computeSelectionRectsFromDom(
})}`,
);
}
rawRects = collectClientRectsByLine(doc, sliceEntries, sliceFrom, sliceTo);
rawRects = collectClientRectsByLine(doc, pageEntries, sliceFrom, sliceTo);
if (dumpRects) {
debugLog(
'verbose',
Expand Down
Loading