diff --git a/.eslintignore b/.eslintignore index a8646c40ae..6f9c09e39b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -531,6 +531,7 @@ packages/app-mobile/components/NoteEditor/NoteEditor.js packages/app-mobile/components/NoteEditor/SearchPanel.js packages/app-mobile/components/NoteEditor/commandDeclarations.js packages/app-mobile/components/NoteEditor/hooks/useCodeMirrorPlugins.js +packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js packages/app-mobile/components/NoteEditor/types.js diff --git a/.gitignore b/.gitignore index 97c85d17c9..1acac8706b 100644 --- a/.gitignore +++ b/.gitignore @@ -511,6 +511,7 @@ packages/app-mobile/components/NoteEditor/NoteEditor.js packages/app-mobile/components/NoteEditor/SearchPanel.js packages/app-mobile/components/NoteEditor/commandDeclarations.js packages/app-mobile/components/NoteEditor/hooks/useCodeMirrorPlugins.js +packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.js packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.js packages/app-mobile/components/NoteEditor/hooks/useKeyboardVisible.js packages/app-mobile/components/NoteEditor/types.js diff --git a/jest.base-setup.js b/jest.base-setup.js index 898cdcc234..2d622dc3c2 100644 --- a/jest.base-setup.js +++ b/jest.base-setup.js @@ -14,3 +14,22 @@ module.exports = () => { global.console = jestConsole; }); }; + +// jsdom extensions +if (typeof document !== 'undefined') { + // Prevents the CodeMirror error "getClientRects is undefined". + // See https://github.com/jsdom/jsdom/issues/3002#issue-652790925 + document.createRange = () => { + const range = new Range(); + range.getBoundingClientRect = jest.fn(); + range.getClientRects = () => { + return { + length: 0, + item: () => null, + [Symbol.iterator]: jest.fn(), + }; + }; + + return range; + }; +} diff --git a/packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.ts b/packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.ts new file mode 100644 index 0000000000..be2e33345e --- /dev/null +++ b/packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.test.ts @@ -0,0 +1,36 @@ +import CommandService from '@joplin/lib/services/CommandService'; +import useEditorCommandHandler from './useEditorCommandHandler'; +import commandDeclarations from '../commandDeclarations'; +import createTestEditorControl from '@joplin/editor/CodeMirror/testUtil/createEditorControl'; +import { renderHook } from '@testing-library/react-native'; +import { defaultState } from '@joplin/lib/reducer'; + + +describe('useEditorCommandHandler', () => { + beforeAll(() => { + const storeMock = { getState: () => defaultState, dispatch: jest.fn() }; + CommandService.instance().initialize(storeMock, false, ()=>({})); + + for (const declaration of commandDeclarations) { + CommandService.instance().registerDeclaration(declaration); + } + }); + it('should support running custom commands with editor.execCommand', async () => { + const editor = createTestEditorControl('Test.'); + renderHook(() => useEditorCommandHandler(editor)); + + const testCommandCallback = jest.fn(); + editor.registerCommand('myCommand', testCommandCallback); + expect(testCommandCallback).not.toHaveBeenCalled(); + + // Should support running commands with arguments + await CommandService.instance().execute('editor.execCommand', { name: 'myCommand', args: ['a', 'b', 'c'] }); + expect(testCommandCallback).toHaveBeenCalledTimes(1); + expect(testCommandCallback).toHaveBeenLastCalledWith('a', 'b', 'c'); + + // Should support running commands without arguments + await CommandService.instance().execute('editor.execCommand', { name: 'myCommand' }); + expect(testCommandCallback).toHaveBeenCalledTimes(2); + expect(testCommandCallback).toHaveBeenLastCalledWith(); + }); +}); diff --git a/packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.ts b/packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.ts index 2428b4ce7c..7e29342451 100644 --- a/packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.ts +++ b/packages/app-mobile/components/NoteEditor/hooks/useEditorCommandHandler.ts @@ -1,5 +1,5 @@ import CommandService, { CommandContext, CommandDeclaration } from '@joplin/lib/services/CommandService'; -import { EditorControl } from '../types'; +import { EditorControl } from '@joplin/editor/types'; import { useEffect } from 'react'; import commandDeclarations, { enabledCondition } from '../commandDeclarations'; import Logger from '@joplin/utils/Logger'; @@ -12,9 +12,13 @@ const commandRuntime = (declaration: CommandDeclaration, editor: EditorControl) // Many editor CodeMirror commands are missing the editor. prefix. let commandName = declaration.name.replace(/^editor\./, ''); - if (declaration.name === 'editor.execCommand') { - commandName = args[0]; - args = args.slice(1); + if (commandName === 'execCommand') { + commandName = args[0]?.name; + args = args[0]?.args ?? []; + + if (!commandName) { + throw new Error('editor.execCommand is missing the name of the command to execute'); + } } if (!(await editor.supportsCommand(commandName))) { diff --git a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.ts b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.ts index 7862d114be..b22ebed9da 100644 --- a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.ts +++ b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.ts @@ -171,4 +171,16 @@ describe('CodeMirror5Emulation', () => { expect(testExtensionFn1).toHaveBeenCalledTimes(1); expect(testExtensionFn2).toHaveBeenCalledTimes(1); }); + + it('defineExtension should register an extension where this points to the editor', () => { + const codeMirror = makeCodeMirrorEmulation('Test...'); + let lastThis = null; + + codeMirror.defineExtension('testExtension', function() { + lastThis = this; + }); + codeMirror.execCommand('testExtension'); + + expect(lastThis).toBe(codeMirror); + }); }); diff --git a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts index 2628ffbc72..64f1d8a112 100644 --- a/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts +++ b/packages/editor/CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts @@ -503,7 +503,7 @@ export default class CodeMirror5Emulation extends BaseCodeMirror5Emulation { if (name in CodeMirror5Emulation.commands) { return CodeMirror5Emulation.commands[name as (keyof typeof CodeMirror5Emulation.commands)](this); } else if (typeof this._userExtensions[name] === 'function') { - return this._userExtensions[name](...args); + return this._userExtensions[name].call(this, ...args); } } } diff --git a/packages/editor/jest.setup.js b/packages/editor/jest.setup.js index 4071541f1f..57e3670553 100644 --- a/packages/editor/jest.setup.js +++ b/packages/editor/jest.setup.js @@ -1,17 +1,2 @@ require('../../jest.base-setup.js')(); -// Prevents the CodeMirror error "getClientRects is undefined". -// See https://github.com/jsdom/jsdom/issues/3002#issue-652790925 -document.createRange = () => { - const range = new Range(); - range.getBoundingClientRect = jest.fn(); - range.getClientRects = () => { - return { - length: 0, - item: () => null, - [Symbol.iterator]: jest.fn(), - }; - }; - - return range; -};