1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Desktop,Mobile,Web: Add support for overwrite mode in the Markdown editor (#11262)

This commit is contained in:
Henry Heino 2024-10-26 13:12:27 -07:00 committed by GitHub
parent 3732a57af3
commit bed5297829
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 156 additions and 4 deletions

View File

@ -873,6 +873,8 @@ packages/editor/CodeMirror/utils/growSelectionToNode.js
packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/overwriteModeExtension.test.js
packages/editor/CodeMirror/utils/overwriteModeExtension.js
packages/editor/CodeMirror/utils/searchExtension.js
packages/editor/CodeMirror/utils/setupVim.js
packages/editor/SelectionFormatting.js

2
.gitignore vendored
View File

@ -850,6 +850,8 @@ packages/editor/CodeMirror/utils/growSelectionToNode.js
packages/editor/CodeMirror/utils/handlePasteEvent.js
packages/editor/CodeMirror/utils/isCursorAtBeginning.js
packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/overwriteModeExtension.test.js
packages/editor/CodeMirror/utils/overwriteModeExtension.js
packages/editor/CodeMirror/utils/searchExtension.js
packages/editor/CodeMirror/utils/setupVim.js
packages/editor/SelectionFormatting.js

View File

@ -32,6 +32,7 @@ import handlePasteEvent from './utils/handlePasteEvent';
import biDirectionalTextExtension from './utils/biDirectionalTextExtension';
import searchExtension from './utils/searchExtension';
import isCursorAtBeginning from './utils/isCursorAtBeginning';
import overwriteModeExtension from './utils/overwriteModeExtension';
// Newer versions of CodeMirror by default use Chrome's EditContext API.
// While this might be stable enough for desktop use, it causes significant
@ -269,7 +270,9 @@ const createEditor = (
// Apply styles to entire lines (block-display decorations)
decoratorExtension,
biDirectionalTextExtension,
overwriteModeExtension,
props.localisations ? EditorState.phrases.of(props.localisations) : [],

View File

@ -1,16 +1,26 @@
import { EditorView } from '@codemirror/view';
const typeText = (editor: EditorView, text: string) => {
// How CodeMirror does this in their tests:
const selection = editor.state.selection;
const inputHandlers = editor.state.facet(EditorView.inputHandler);
// See how the upstream CodeMirror tests simulate user input:
// https://github.com/codemirror/autocomplete/blob/fb1c899464df4d36528331412cdd316548134cb2/test/webtest-autocomplete.ts#L116
// The important part is the userEvent: input.type.
const selection = editor.state.selection;
editor.dispatch({
const defaultTransaction = editor.state.update({
changes: [{ from: selection.main.head, insert: text }],
selection: { anchor: selection.main.head + text.length },
userEvent: 'input.type',
});
// Allows code that uses input handlers to be tested
for (const handler of inputHandlers) {
if (handler(editor, selection.main.from, selection.main.to, text, () => defaultTransaction)) {
return;
}
}
editor.dispatch(defaultTransaction);
};
export default typeText;

View File

@ -0,0 +1,57 @@
import { EditorSelection } from '@codemirror/state';
import createTestEditor from '../testUtil/createTestEditor';
import overwriteModeExtension, { toggleOverwrite } from './overwriteModeExtension';
import typeText from '../testUtil/typeText';
import pressReleaseKey from '../testUtil/pressReleaseKey';
const createEditor = async (initialText: string, defaultEnabled = false) => {
const editor = await createTestEditor(initialText, EditorSelection.cursor(0), [], [
overwriteModeExtension,
]);
if (defaultEnabled) {
editor.dispatch({ effects: [toggleOverwrite.of(true)] });
}
return editor;
};
describe('overwriteModeExtension', () => {
test('should be disabled by default', async () => {
const editor = await createEditor('Test!');
typeText(editor, 'This should be inserted. ');
expect(editor.state.doc.toString()).toBe('This should be inserted. Test!');
});
test('should overwrite characters while typing', async () => {
const editor = await createEditor('Test!', true);
pressReleaseKey(editor, { key: 'A', code: 'KeyA' });
typeText(editor, 'New');
expect(editor.state.doc.toString()).toBe('Newt!');
});
test('should be toggled by pressing <insert>', async () => {
const editor = await createEditor('Test!');
pressReleaseKey(editor, { key: 'Insert', code: 'Insert' });
typeText(editor, 'Exam');
expect(editor.state.doc.toString()).toBe('Exam!');
pressReleaseKey(editor, { key: 'Insert', code: 'Insert' });
typeText(editor, 'ple');
expect(editor.state.doc.toString()).toBe('Example!');
});
test('should insert text if the cursor is at the end of a line', async () => {
const editor = await createEditor('\nTest', true);
typeText(editor, 'Test! This is a test! ');
typeText(editor, 't');
typeText(editor, 'e');
typeText(editor, 's');
typeText(editor, 't');
expect(editor.state.doc.toString()).toBe('Test! This is a test! test\nTest');
});
});

View File

@ -0,0 +1,78 @@
import { keymap, EditorView } from '@codemirror/view';
import { StateField, Facet, StateEffect } from '@codemirror/state';
const overwriteModeFacet = Facet.define({
combine: values => values[0] ?? false,
enables: facet => [
EditorView.inputHandler.of((
view, _from, _to, text, insert,
) => {
if (view.composing || view.compositionStarted || view.state.readOnly) {
return false;
}
if (view.state.facet(facet) && text) {
const originalTransaction = insert();
const newState1 = originalTransaction.state;
const emptySelections1 = newState1.selection.ranges.filter(
range => range.empty,
);
view.dispatch([
originalTransaction,
newState1.update({
changes: emptySelections1.map(range => {
const line = newState1.doc.lineAt(range.to);
return {
from: range.to,
to: Math.min(line.to, range.to + text.length),
insert: '',
};
}).filter(change => change.from !== change.to || change.insert),
}),
]);
return true;
}
return false;
}),
EditorView.theme({
'&.-overwrite .cm-cursor': {
borderLeftWidth: '0.5em',
},
}),
],
});
export const toggleOverwrite = StateEffect.define<boolean>();
const overwriteModeState = StateField.define({
create: () => false,
update: (oldValue, tr) => {
for (const e of tr.effects) {
if (e.is(toggleOverwrite)) {
return e.value;
}
}
return oldValue;
},
provide: (field) => [
overwriteModeFacet.from(field),
EditorView.editorAttributes.from(field, on => ({
class: on ? '-overwrite' : '',
})),
],
});
const overwriteModeExtension = [
overwriteModeState,
keymap.of([{
key: 'Insert',
run: (view) => {
view.dispatch({
effects: toggleOverwrite.of(!view.state.field(overwriteModeState)),
});
return false;
},
}]),
];
export default overwriteModeExtension;