From d7439716e1578079644ed9b97a00d566e4952da4 Mon Sep 17 00:00:00 2001 From: Martin Fleck Date: Tue, 12 Mar 2024 13:42:29 +0100 Subject: [PATCH] Improve lazy-loading of Monaco editor - Use more dynamic imports for Monaco editor -- Expose dynamically imported -- Use type imports where possible to avoid full resolution - Expose as much options for outside configuration as possible - Avoid unnecessary boolean return type - Avoid loading Monaco editor only for key codes - Align react-query versions with process-editor-client Co-authored-by: Olaf Lessenich --- integrations/standalone/index.html | 88 +++++++-- integrations/standalone/mock.html | 88 +++++++-- integrations/standalone/src/index.tsx | 55 ++---- integrations/standalone/src/lazy-app.tsx | 50 ++++++ integrations/standalone/src/mock.tsx | 55 ++---- integrations/standalone/src/url-helper.ts | 8 +- packages/core/src/console-util.ts | 46 +++++ packages/core/src/index.ts | 2 + packages/core/src/ivy-script-client.ts | 8 +- packages/core/src/monaco-util.ts | 168 +++++++++++++++--- .../src/utils => core/src}/promises-util.ts | 0 packages/editor/package.json | 5 +- packages/editor/src/App.css | 49 +++++ packages/editor/src/App.tsx | 6 +- .../MaximizedCodeEditor.tsx | 6 +- .../widgets/code-editor/ScriptArea.tsx | 5 +- .../code-editor/SingleLineCodeEditor.tsx | 10 +- packages/editor/src/context/useTheme.tsx | 6 +- packages/editor/src/index.ts | 1 - .../editor/src/monaco/monaco-editor-util.ts | 117 +++++++++--- yarn.lock | 36 ++-- 21 files changed, 608 insertions(+), 201 deletions(-) create mode 100644 integrations/standalone/src/lazy-app.tsx create mode 100644 packages/core/src/console-util.ts rename packages/{editor/src/utils => core/src}/promises-util.ts (100%) diff --git a/integrations/standalone/index.html b/integrations/standalone/index.html index dd895746d..15ee19ed1 100644 --- a/integrations/standalone/index.html +++ b/integrations/standalone/index.html @@ -1,17 +1,75 @@ - - - - - - - Inscription Editor - - - - -
- - - + + + + + + + + Inscription Editor + + + + + +
+
+
+ + + + \ No newline at end of file diff --git a/integrations/standalone/mock.html b/integrations/standalone/mock.html index 9cd4fe11f..11c7ab344 100644 --- a/integrations/standalone/mock.html +++ b/integrations/standalone/mock.html @@ -1,17 +1,75 @@ - - - - - - - Inscription Editor Mock - - - - -
- - - + + + + + + + + Inscription Editor Mock + + + + + +
+
+
+ + + + \ No newline at end of file diff --git a/integrations/standalone/src/index.tsx b/integrations/standalone/src/index.tsx index f22a2b0bb..eae06f5ee 100644 --- a/integrations/standalone/src/index.tsx +++ b/integrations/standalone/src/index.tsx @@ -1,52 +1,23 @@ -import './index.css'; -import { IvyScriptLanguage, InscriptionClientJsonRpc, MonacoUtil } from '@axonivy/inscription-core'; -import { - App, - AppStateView, - ClientContextProvider, - MonacoEditorUtil, - QueryProvider, - ThemeContextProvider, - initQueryClient, - type ThemeMode -} from '@axonivy/inscription-editor'; -import React from 'react'; +import { InscriptionClientJsonRpc } from '@axonivy/inscription-core'; +import { AppStateView } from '@axonivy/inscription-editor'; import { createRoot } from 'react-dom/client'; +import './index.css'; +import { LazyApp, type LazyAppProps } from './lazy-app'; import { URLParams } from './url-helper'; -async function initMonaco(theme: ThemeMode): Promise { - const monaco = await import('monaco-editor/esm/vs/editor/editor.api'); - const editorWorker = await import('monaco-editor/esm/vs/editor/editor.worker?worker'); - await MonacoUtil.initStandalone(editorWorker.default); - await MonacoEditorUtil.configureInstance(monaco, theme); - return true; -} - export async function start(): Promise { - const server = URLParams.webSocketBase(); - const app = URLParams.app(); - const pmv = URLParams.pmv(); - const pid = URLParams.pid(); - const theme = URLParams.themeMode(); + const props: LazyAppProps = { + server: URLParams.webSocketBase(), + app: URLParams.app(), + pmv: URLParams.pmv(), + pid: URLParams.pid(), + theme: URLParams.themeMode(), + clientCreator: () => InscriptionClientJsonRpc.startWebSocketClient(props.server!) + }; const root = createRoot(document.getElementById('root')!); try { - const isMonacoReady = initMonaco(theme); - IvyScriptLanguage.startWebSocketClient(server, isMonacoReady); - const client = await InscriptionClientJsonRpc.startWebSocketClient(server); - const queryClient = initQueryClient(); - - root.render( - - - - - - - - - - ); + root.render(); } catch (error) { console.error(error); root.render({'An error has occurred: ' + error}); diff --git a/integrations/standalone/src/lazy-app.tsx b/integrations/standalone/src/lazy-app.tsx new file mode 100644 index 000000000..220d2f61f --- /dev/null +++ b/integrations/standalone/src/lazy-app.tsx @@ -0,0 +1,50 @@ +import { IvyScriptLanguage } from '@axonivy/inscription-core'; +import { + App, + ClientContextProvider, + MonacoEditorUtil, + QueryProvider, + ThemeContextProvider, + initQueryClient, + type ThemeMode +} from '@axonivy/inscription-editor'; +import type { InscriptionClient } from '@axonivy/inscription-protocol'; +import type { QueryClient } from '@tanstack/react-query'; +import React, { useEffect, useState } from 'react'; + +export interface LazyAppProps { + clientCreator: () => Promise; + server?: string; + app: string; + pmv: string; + pid: string; + theme: ThemeMode; +} + +export function LazyApp(props: LazyAppProps) { + const [client, setClient] = useState(); + const [queryClient] = useState(initQueryClient()); + + useEffect(() => { + const instance = MonacoEditorUtil.configureInstance({ theme: props.theme, debug: true }); + if (props.server) { + IvyScriptLanguage.startWebSocketClient(props.server, instance); + } + props.clientCreator().then(client => setClient(client)); + }, [props, props.server, props.theme]); + + if (client) { + return ( + + + + + + + + + + ); + } + return
; +} diff --git a/integrations/standalone/src/mock.tsx b/integrations/standalone/src/mock.tsx index 6efcb9267..d2734f454 100644 --- a/integrations/standalone/src/mock.tsx +++ b/integrations/standalone/src/mock.tsx @@ -1,49 +1,30 @@ -import './index.css'; -import { MonacoUtil } from '@axonivy/inscription-core'; -import { - App, - ClientContextProvider, - MonacoEditorUtil, - QueryProvider, - ThemeContextProvider, - initQueryClient, - type ThemeMode -} from '@axonivy/inscription-editor'; -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { URLParams } from './url-helper'; +import { AppStateView } from '@axonivy/inscription-editor'; import type { ElementType } from '@axonivy/inscription-protocol'; +import { createRoot } from 'react-dom/client'; +import './index.css'; +import { LazyApp, type LazyAppProps } from './lazy-app'; import { InscriptionClientMock } from './mock/inscription-client-mock'; - -async function initMonaco(theme: ThemeMode): Promise { - const monaco = await import('monaco-editor/esm/vs/editor/editor.api'); - await MonacoUtil.initStandalone(); - await MonacoEditorUtil.configureInstance(monaco, theme); -} +import { URLParams } from './url-helper'; export async function start(): Promise { - const theme = URLParams.themeMode(); const readonly = URLParams.parameter('readonly') ? true : false; const type = (URLParams.parameter('type') as ElementType) ?? undefined; - initMonaco(theme); + const props: LazyAppProps = { + app: '', + pmv: '', + pid: '1', + theme: URLParams.themeMode(), + clientCreator: async () => new InscriptionClientMock(readonly, type) + }; const root = createRoot(document.getElementById('root')!); - - const inscriptionClient = new InscriptionClientMock(readonly, type); - const queryClient = initQueryClient(); - - root.render( - - - - - - - - - - ); + try { + root.render(); + } catch (error) { + console.error(error); + root.render({'An error has occurred: ' + error}); + } } start(); diff --git a/integrations/standalone/src/url-helper.ts b/integrations/standalone/src/url-helper.ts index cf3a03dc1..68020fe27 100644 --- a/integrations/standalone/src/url-helper.ts +++ b/integrations/standalone/src/url-helper.ts @@ -1,4 +1,4 @@ -import type { ThemeMode } from '@axonivy/inscription-editor'; +import { defaultThemeMode, type ThemeMode } from '@axonivy/inscription-editor'; export namespace URLParams { export function parameter(key: string): string | undefined { @@ -23,7 +23,7 @@ export namespace URLParams { } export function themeMode(): ThemeMode { - return (parameter('theme') as ThemeMode) ?? defaultTheme(); + return (parameter('theme') as ThemeMode) ?? defaultThemeMode(); } const isSecureConnection = () => { @@ -49,8 +49,4 @@ export namespace URLParams { } return 'localhost:8081'; }; - - const defaultTheme = (): ThemeMode => { - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; - }; } diff --git a/packages/core/src/console-util.ts b/packages/core/src/console-util.ts new file mode 100644 index 000000000..f5b036c10 --- /dev/null +++ b/packages/core/src/console-util.ts @@ -0,0 +1,46 @@ +export function logIf(condition?: boolean, message?: any, ...optionalParams: any[]): void { + if (condition) { + console.log(message, ...optionalParams); + } +} + +export function timeIf(condition?: boolean, label?: string): void { + if (condition) { + console.time(label); + } +} + +export function timeEndIf(condition?: boolean, label?: string): void { + if (condition) { + console.timeEnd(label); + } +} + +export function timeLogIf(condition?: boolean, label?: string, ...data: any[]): void { + if (condition) { + console.timeLog(label, ...data); + } +} + +export class ConsoleTimer { + constructor(protected condition: boolean | undefined, protected label: string) {} + + log(message?: any, ...optionalParams: any[]) { + logIf(this.condition, message, ...optionalParams); + } + + start(): ConsoleTimer { + timeIf(this.condition, this.label); + return this; + } + + step(...data: any[]): ConsoleTimer { + timeLogIf(this.condition, this.label, ...data); + return this; + } + + end(): ConsoleTimer { + timeEndIf(this.condition, this.label); + return this; + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index bf395e986..7dc501540 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,3 +1,5 @@ +export * from './console-util'; export * from './ivy-script-client'; export * from './inscription-client-jsonrpc'; export * from './monaco-util'; +export * from './promises-util'; diff --git a/packages/core/src/ivy-script-client.ts b/packages/core/src/ivy-script-client.ts index 43cf448fa..a9d110f54 100644 --- a/packages/core/src/ivy-script-client.ts +++ b/packages/core/src/ivy-script-client.ts @@ -1,14 +1,14 @@ import { createWebSocketConnection, type Connection } from '@axonivy/jsonrpc'; export namespace IvyScriptLanguage { - export async function startWebSocketClient(url: string, isMonacoReady: Promise): Promise { + export async function startWebSocketClient(url: string, isMonacoReady: Promise): Promise { const webSocketUrl = new URL('ivy-script-lsp', url); const connection = await createWebSocketConnection(webSocketUrl); - await isMonacoReady; - return startClient(connection); + return startClient(connection, isMonacoReady); } - export async function startClient(connection: Connection) { + export async function startClient(connection: Connection, isMonacoReady: Promise) { + await isMonacoReady; const { MonacoLanguageClient } = await import('monaco-languageclient'); const client = new MonacoLanguageClient({ name: 'IvyScript Language Client', diff --git a/packages/core/src/monaco-util.ts b/packages/core/src/monaco-util.ts index 4fe01d4eb..d0d19e0b7 100644 --- a/packages/core/src/monaco-util.ts +++ b/packages/core/src/monaco-util.ts @@ -1,37 +1,161 @@ -import 'monaco-editor/esm/vs/editor/editor.all.js'; +import { Deferred } from './promises-util'; -import { initServices, wasVscodeApiInitialized } from 'monaco-languageclient'; +import type * as monacoLanguageClient from 'monaco-languageclient'; +export type MonacoLanguageClient = typeof monacoLanguageClient; -import { buildWorkerDefinition } from 'monaco-editor-workers'; +import type * as monacoEditorWorkers from 'monaco-editor-workers'; +export type MonacoEditorWorkers = typeof monacoEditorWorkers; + +import type * as monacoEditorApi from 'monaco-editor'; +import { ConsoleTimer, logIf } from './console-util'; +export type MonacoEditorApi = typeof monacoEditorApi; + +export type WorkerConstructor = (new (...args: any) => Worker) | (new (...args: any) => Promise); + +// from monaco-editor-workers +export interface MonacoWorkerConfig { + workerPath?: string; + basePath?: string; + useModuleWorker?: boolean; + + // extension + workerType?: 'typescript' | 'javascript' | 'html' | 'handlebars' | 'razor' | 'css' | 'scss' | 'less' | 'json' | 'editor'; + workerConstructor?: WorkerConstructor; + skip?: boolean; + debug?: boolean; +} + +export interface MonacoLanguageClientConfig extends monacoLanguageClient.InitializeServiceConfig { + initializationDelay?: number; + initializationMaxTries?: number; + + skip?: boolean; + debug?: boolean; +} export namespace MonacoUtil { - export async function initStandalone(worker?: new () => Worker) { - await initServices({ userServices: [] }); - buildWorkerDefinition('../../node_modules/monaco-editor-workers/dist/workers', new URL('', window.location.href).href, false); + let monacoLanguageClientPromise: Promise; + export async function monacoLanguageClient(): Promise { + if (!monacoLanguageClientPromise) { + monacoLanguageClientPromise = import('monaco-languageclient'); + } + return monacoLanguageClientPromise; + } + + let monacoEditorWorkersPromise: Promise; + export async function monacoEditorWorkers(): Promise { + if (!monacoEditorWorkersPromise) { + monacoEditorWorkersPromise = import('monaco-editor-workers'); + } + return monacoEditorWorkersPromise; + } + + let monacoEditorApiPromise: Promise; + export async function monacoEditorApi(): Promise { + if (!monacoEditorApiPromise) { + monacoEditorApiPromise = import('monaco-editor'); + } + return monacoEditorApiPromise; + } + + /** + * Imports all services and initializes the VS Code extension API for the language client. + * If complete, the vscodeApiInitialised will be set on the MonacoEnvironment. + * You can query this flag through the 'monacoInitialized' function. + */ + export async function configureLanguageClient(config?: MonacoLanguageClientConfig): Promise { + if (config?.skip) { + logIf(config.debug, 'Skip Monaco Language Client Configuration.'); + return; + } + const timer = new ConsoleTimer(config?.debug, 'Configure Language Client'); + timer.start(); + timer.step('Start initializing Services and VS Code Extension API...'); + const languageClient = await monacoLanguageClient(); + await languageClient.initServices(config); + timer.step('Waiting for VS Code API to be initialized...'); + await monacoInitialized(config?.initializationDelay, config?.initializationMaxTries); + timer.end(); + } + + /** + * Ensures that we have the necessary MonacoEnvironment.getWorker function available. + */ + export async function configureWorkers(config?: MonacoWorkerConfig): Promise { + if (config?.skip) { + logIf(config.debug, 'Skip Monaco Worker Configuration.'); + return; + } + const timer = new ConsoleTimer(config?.debug, 'Configure Monaco Workers').start(); + + // default behavior for MonacoEnvironment.getWorker + timer.step('Start configuring MonacoEnvironment.getWorker...'); + const monacoEditorWorker = await monacoEditorWorkers(); + monacoEditorWorker.buildWorkerDefinition( + config?.workerPath ?? '../../../node_modules/monaco-editor-workers/dist/workers', + config?.basePath ?? import.meta.url, + config?.useModuleWorker ?? false + ); + const defaultGetWorker = self.MonacoEnvironment?.getWorker; + + // overridden behavior for MonacoEnvironment.getWorker if an explicit worker constructor is given + if (config?.workerConstructor) { + timer.step('Override MonacoEnvironment.getWorker with given WorkerConstructor...'); + const WorkerConstructor = config.workerConstructor; - if (worker) { self.MonacoEnvironment = { ...self.MonacoEnvironment, - getWorker() { - return new worker(); + async getWorker(id, label) { + try { + timer.log('[MonacoEnvironment] Create Worker...'); + const worker = await new WorkerConstructor(id, label); + timer.log('[MonacoEnvironment] Success.'); + return worker; + } catch (error) { + console.error(error); + timer.log('[MonacoEnvironment] Default to fallback worker...'); + if (defaultGetWorker) { + const worker = await defaultGetWorker(id, config.workerType ?? label); + timer.log('[MonacoEnvironment] Success.'); + return worker; + } + throw error; + } } }; } + timer.end(); } - export function monacoInitialized() { - return new Promise((resolve, reject) => { - const startTime = Date.now(); - const checkInitialized = () => { - if (wasVscodeApiInitialized()) { - resolve(); - } - if (Date.now() > startTime + 3000) { - reject(); + export async function configureEnvironment(config?: { + worker?: MonacoWorkerConfig; + languageClient?: MonacoLanguageClientConfig; + debug?: boolean; + }): Promise { + await Promise.all([ + MonacoUtil.configureWorkers({ ...config?.worker, debug: config?.worker?.debug ?? config?.debug }), + MonacoUtil.configureLanguageClient({ ...config?.languageClient, debug: config?.languageClient?.debug ?? config?.debug }) + ]); + } + + export async function monacoInitialized(delay: number = 100, maxTries: number = 30): Promise { + const deferred = new Deferred(); + let tries = 0; + const initializationCheck = async () => { + try { + tries += 1; + if ((await monacoLanguageClient()).wasVscodeApiInitialized()) { + deferred.resolve(); + } else if (tries < maxTries) { + setTimeout(initializationCheck, delay); + } else { + deferred.reject(new Error('Monaco initialization timed out.')); } - window.setTimeout(checkInitialized, 100); - }; - checkInitialized(); - }); + } catch (error) { + deferred.reject(error); + } + }; + initializationCheck(); + return deferred.promise; } } diff --git a/packages/editor/src/utils/promises-util.ts b/packages/core/src/promises-util.ts similarity index 100% rename from packages/editor/src/utils/promises-util.ts rename to packages/core/src/promises-util.ts diff --git a/packages/editor/package.json b/packages/editor/package.json index 722279113..651a4326c 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -14,6 +14,7 @@ "src" ], "dependencies": { + "@axonivy/inscription-core": "11.3.0-next", "@axonivy/inscription-protocol": "11.3.0-next", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-accordion": "^1.1.2", @@ -23,8 +24,8 @@ "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-tabs": "^1.0.4", - "@tanstack/react-query": "^5.14.2", - "@tanstack/react-query-devtools": "^5.14.5", + "@tanstack/react-query": "^5.17.9", + "@tanstack/react-query-devtools": "^5.17.9", "@tanstack/react-table": "^8.11.2", "@tanstack/react-virtual": "^3.0.1", "@types/node": "^20.10.5", diff --git a/packages/editor/src/App.css b/packages/editor/src/App.css index 9743ab64d..3c771df62 100644 --- a/packages/editor/src/App.css +++ b/packages/editor/src/App.css @@ -106,3 +106,52 @@ justify-content: center; color: var(--body); } + +/* from https://cssloaders.github.io/, licensed under MIT */ +.loader { + width: 48px; + height: 48px; + border-radius: 50%; + position: relative; + top: 50%; + left: 50%; + animation: rotate 1s linear infinite; +} + +.loader::before { + content: ''; + box-sizing: border-box; + position: absolute; + inset: 0px; + border-radius: 50%; + border: 5px solid #a5a5a5; + animation: prixClipFix 2s linear infinite; +} + +@keyframes rotate { + 100% { + transform: rotate(360deg); + } +} + +@keyframes prixClipFix { + 0% { + clip-path: polygon(50% 50%, 0 0, 0 0, 0 0, 0 0, 0 0); + } + + 25% { + clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 0, 100% 0, 100% 0); + } + + 50% { + clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 100% 100%, 100% 100%); + } + + 75% { + clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 100%); + } + + 100% { + clip-path: polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0); + } +} diff --git a/packages/editor/src/App.tsx b/packages/editor/src/App.tsx index 7b1e58344..d1fd4f4f8 100644 --- a/packages/editor/src/App.tsx +++ b/packages/editor/src/App.tsx @@ -77,7 +77,11 @@ function App(props: InscriptionElementContext) { }); if (isPending) { - return Loading...; + return ( + +
+ + ); } if (isError) { diff --git a/packages/editor/src/components/browser/maximizedCodeEditor/MaximizedCodeEditor.tsx b/packages/editor/src/components/browser/maximizedCodeEditor/MaximizedCodeEditor.tsx index e65aec5f3..df9500ff7 100644 --- a/packages/editor/src/components/browser/maximizedCodeEditor/MaximizedCodeEditor.tsx +++ b/packages/editor/src/components/browser/maximizedCodeEditor/MaximizedCodeEditor.tsx @@ -1,10 +1,10 @@ import './MaximizedCodeEditor.css'; -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import { useBrowser, type BrowserType } from '../useBrowser'; import { monacoAutoFocus, useMonacoEditor } from '../../widgets/code-editor/useCodeEditor'; import CodeEditor from '../../widgets/code-editor/CodeEditor'; import Browser from '../Browser'; -import { MAXIMIZED_MONACO_OPTIONS } from '../../../monaco/monaco-editor-util'; +import { MAXIMIZED_MONACO_OPTIONS, MonacoEditorUtil } from '../../../monaco/monaco-editor-util'; export type MaximizedCodeEditorProps = { editorValue: string; @@ -36,7 +36,7 @@ const MaximizedCodeEditor = ({ } }; const keyActionMountFunc = (editor: monaco.editor.IStandaloneCodeEditor) => { - editor.addCommand(monaco.KeyCode.Escape, () => { + editor.addCommand(MonacoEditorUtil.KeyCode.Escape, () => { if (keyActions?.escape) { keyActions.escape(); } diff --git a/packages/editor/src/components/widgets/code-editor/ScriptArea.tsx b/packages/editor/src/components/widgets/code-editor/ScriptArea.tsx index 28e8db959..5530fbd0e 100644 --- a/packages/editor/src/components/widgets/code-editor/ScriptArea.tsx +++ b/packages/editor/src/components/widgets/code-editor/ScriptArea.tsx @@ -1,11 +1,12 @@ import './ScriptArea.css'; -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import type { CodeEditorAreaProps } from './ResizableCodeEditor'; +import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import ResizableCodeEditor from './ResizableCodeEditor'; import { Browser, useBrowser } from '../../../components/browser'; import { useMonacoEditor } from './useCodeEditor'; import { usePath } from '../../../context'; import MaximizedCodeEditorBrowser from '../../browser/MaximizedCodeEditorBrowser'; +import { MonacoEditorUtil } from '../../../monaco/monaco-editor-util'; type ScriptAreaProps = CodeEditorAreaProps & { maximizeState: { @@ -19,7 +20,7 @@ const ScriptArea = (props: ScriptAreaProps) => { const { setEditor, modifyEditor, getMonacoSelection, getSelectionRange } = useMonacoEditor(); const path = usePath(); const keyActionMountFunc = (editor: monaco.editor.IStandaloneCodeEditor) => { - editor.addCommand(monaco.KeyCode.F2, () => { + editor.addCommand(MonacoEditorUtil.KeyCode.F2, () => { props.maximizeState.setIsMaximizedCodeEditorOpen(true); }); }; diff --git a/packages/editor/src/components/widgets/code-editor/SingleLineCodeEditor.tsx b/packages/editor/src/components/widgets/code-editor/SingleLineCodeEditor.tsx index a3828526f..f1becf771 100644 --- a/packages/editor/src/components/widgets/code-editor/SingleLineCodeEditor.tsx +++ b/packages/editor/src/components/widgets/code-editor/SingleLineCodeEditor.tsx @@ -1,6 +1,6 @@ import { useCallback } from 'react'; -import { SINGLE_LINE_MONACO_OPTIONS } from '../../../monaco/monaco-editor-util'; -import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import { MonacoEditorUtil, SINGLE_LINE_MONACO_OPTIONS } from '../../../monaco/monaco-editor-util'; +import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import type { CodeEditorProps } from './CodeEditor'; import CodeEditor from './CodeEditor'; import { monacoAutoFocus } from './useCodeEditor'; @@ -33,7 +33,7 @@ const SingleLineCodeEditor = ({ onChange, onMountFuncs, editorOptions, keyAction editor.trigger(undefined, 'acceptSelectedSuggestion', undefined); const STATE_OPEN = 3; editor.addCommand( - monaco.KeyCode.Enter, + MonacoEditorUtil.KeyCode.Enter, () => { if (isSuggestWidgetOpen(editor)) { triggerAcceptSuggestion(editor); @@ -44,7 +44,7 @@ const SingleLineCodeEditor = ({ onChange, onMountFuncs, editorOptions, keyAction 'singleLine' ); editor.addCommand( - monaco.KeyCode.Tab, + MonacoEditorUtil.KeyCode.Tab, () => { if (isSuggestWidgetOpen(editor)) { triggerAcceptSuggestion(editor); @@ -60,7 +60,7 @@ const SingleLineCodeEditor = ({ onChange, onMountFuncs, editorOptions, keyAction 'singleLine' ); editor.addCommand( - monaco.KeyCode.Escape, + MonacoEditorUtil.KeyCode.Escape, () => { if (!isSuggestWidgetOpen(editor) && keyActions?.escape) { keyActions.escape(); diff --git a/packages/editor/src/context/useTheme.tsx b/packages/editor/src/context/useTheme.tsx index 95fe1a671..b4c17c534 100644 --- a/packages/editor/src/context/useTheme.tsx +++ b/packages/editor/src/context/useTheme.tsx @@ -1,4 +1,4 @@ -import type { ReactNode} from 'react'; +import type { ReactNode } from 'react'; import { createContext, useContext, useEffect, useState } from 'react'; export type ThemeMode = 'dark' | 'light'; @@ -7,6 +7,10 @@ export type ThemeContext = { setMode: (mode: ThemeMode) => void; }; +export const defaultThemeMode = (): ThemeMode => { + return window?.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +}; + export const ThemeContextInstance = createContext({ mode: 'light', setMode: () => {} }); export const useTheme = (): ThemeContext => useContext(ThemeContextInstance); diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index d288cff0c..c1253dfcc 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -5,4 +5,3 @@ export * from './context'; export * from './monaco/monaco-editor-util'; export * from './query'; export * from './types/lambda'; -export * from './utils/promises-util' diff --git a/packages/editor/src/monaco/monaco-editor-util.ts b/packages/editor/src/monaco/monaco-editor-util.ts index 86ff4a319..c7ff6e023 100644 --- a/packages/editor/src/monaco/monaco-editor-util.ts +++ b/packages/editor/src/monaco/monaco-editor-util.ts @@ -1,12 +1,14 @@ -import type { Monaco } from '@monaco-editor/react'; -import { loader } from '@monaco-editor/react'; -import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; -import type { ThemeMode } from '../context/useTheme'; -import { ivyScriptConf, ivyScriptLang } from './ivy-script-language'; +import type { MonacoEditorApi, MonacoLanguageClientConfig, MonacoWorkerConfig } from '@axonivy/inscription-core'; +import { ConsoleTimer, Deferred, MonacoUtil } from '@axonivy/inscription-core'; +import type { editor } from 'monaco-editor/esm/vs/editor/editor.api'; +import { defaultThemeMode, type ThemeMode } from '../context/useTheme'; import { ivyMacroConf, ivyMacroLang } from './ivy-macro-language'; -import { Deferred } from '../utils/promises-util'; +import { ivyScriptConf, ivyScriptLang } from './ivy-script-language'; + +import type * as monacoEditorReact from '@monaco-editor/react'; +export type MonacoEditorReactApi = typeof monacoEditorReact; -export const MONACO_OPTIONS: monaco.editor.IStandaloneEditorConstructionOptions = { +export const MONACO_OPTIONS: editor.IStandaloneEditorConstructionOptions = { glyphMargin: false, lineNumbers: 'off', minimap: { enabled: false }, @@ -24,14 +26,14 @@ export const MONACO_OPTIONS: monaco.editor.IStandaloneEditorConstructionOptions fixedOverflowWidgets: true }; -export const MAXIMIZED_MONACO_OPTIONS: monaco.editor.IStandaloneEditorConstructionOptions = { +export const MAXIMIZED_MONACO_OPTIONS: editor.IStandaloneEditorConstructionOptions = { ...MONACO_OPTIONS, lineNumbers: 'on', folding: true, showFoldingControls: 'always' }; -export const SINGLE_LINE_MONACO_OPTIONS: monaco.editor.IStandaloneEditorConstructionOptions = { +export const SINGLE_LINE_MONACO_OPTIONS: editor.IStandaloneEditorConstructionOptions = { ...MONACO_OPTIONS, overviewRulerLanes: 0, overviewRulerBorder: false, @@ -55,7 +57,7 @@ export const SINGLE_LINE_MONACO_OPTIONS: monaco.editor.IStandaloneEditorConstruc export namespace MonacoEditorUtil { export const DEFAULT_THEME_NAME = 'axon-input'; - export function themeData(theme: ThemeMode): monaco.editor.IStandaloneThemeData { + export function themeData(theme: ThemeMode = defaultThemeMode()): editor.IStandaloneThemeData { if (theme === 'dark') { return { base: 'vs-dark', @@ -80,42 +82,103 @@ export namespace MonacoEditorUtil { }; } - const instance: Deferred = new Deferred(); - export async function getInstance(): Promise { + const instance: Deferred = new Deferred(); + export async function getInstance(): Promise { return instance.promise; } let configureCalled = false; - export async function configureInstance(monaco: Monaco, theme: ThemeMode): Promise { + export async function configureInstance(configuration?: MonacoConfiguration): Promise { if (configureCalled) { - console.error( - 'MonacoEditorUtil.configureInstance should only be called once. The caller will receive the first, configured instance. If you want to configure additional instances, call "configureMonaco" instead.' + console.warn( + 'MonacoEditorUtil.configureInstance should only be called once. The caller will receive the first, configured instance. If you want to configure additional instances, call "configureMonacoReactEditor" instead.' ); } else { configureCalled = true; - configureMonaco(monaco, theme).then(instance.resolve).catch(instance.reject); + configureMonacoReactEditor(configuration).then(instance.resolve).catch(instance.reject); } return instance.promise; } + + // We want to avoid an import to import { KeyCode } from 'monaco-editor/esm/vs/editor/editor.api'. + // So we replicate the necessary Key codes here since they are very stable. + export enum KeyCode { + Tab = 2, + Enter = 3, + Escape = 9, + F2 = 60 + } + + let monacoEditorReactApiPromise: Promise; + export async function monacoEditorReactApi(): Promise { + if (!monacoEditorReactApiPromise) { + monacoEditorReactApiPromise = import('@monaco-editor/react'); + } + return monacoEditorReactApiPromise; + } + + export async function setTheme(theme?: ThemeMode): Promise { + const monacoApi = await getInstance(); + monacoApi.editor.defineTheme(MonacoEditorUtil.DEFAULT_THEME_NAME, MonacoEditorUtil.themeData(theme)); + } } -export async function configureMonaco(monaco: Monaco, theme: ThemeMode): Promise { - loader.config({ monaco }); - const _monaco = await loader.init(); - _monaco.languages.register({ +// from @monaco-editor/loader +export interface MonacoLoaderConfig { + paths?: { + vs?: string; + }; + 'vs/nls'?: { + availableLanguages?: object; + }; + monaco?: MonacoEditorApi; +} + +export interface MonacoConfiguration { + loader?: MonacoLoaderConfig; + worker?: MonacoWorkerConfig; + languageClient?: MonacoLanguageClientConfig; + theme?: ThemeMode; + debug?: boolean; +} + +export async function configureMonacoReactEditor(configuration?: MonacoConfiguration): Promise { + const timer = new ConsoleTimer(configuration?.debug, 'Configure Monaco React Editor'); + timer.start(); + + timer.step('Start loading Monaco Editor React API...'); + const reactEditorApi = await MonacoEditorUtil.monacoEditorReactApi(); + + timer.step('Start loading Monaco Editor API...'); + const monaco = configuration?.loader?.monaco ?? (await MonacoUtil.monacoEditorApi()); + const reactEditorLoader = reactEditorApi.loader; + reactEditorLoader.config({ ...configuration?.loader, monaco }); + + // configure Monaco environment, must be called after configuring monaco + timer.step('Start configuring Monaco Environment...'); + await MonacoUtil.configureEnvironment({ + languageClient: configuration?.languageClient, + worker: configuration?.worker, + debug: configuration?.debug + }); + + timer.step('Initialize Monaco React Editor...'); + const monacoApi = await reactEditorLoader.init(); + monacoApi.languages.register({ id: 'ivyScript', extensions: ['.ivyScript', '.ivyScript'], aliases: ['IvyScript', 'ivyScript'] }); - _monaco.languages.register({ + monacoApi.languages.register({ id: 'ivyMacro', extensions: ['.ivyMacro', '.ivyMacro'], aliases: [] }); - _monaco.languages.setLanguageConfiguration('ivyScript', ivyScriptConf); - _monaco.languages.setMonarchTokensProvider('ivyScript', ivyScriptLang); - _monaco.languages.setLanguageConfiguration('ivyMacro', ivyMacroConf); - _monaco.languages.setMonarchTokensProvider('ivyMacro', ivyMacroLang); - _monaco.editor.defineTheme(MonacoEditorUtil.DEFAULT_THEME_NAME, MonacoEditorUtil.themeData(theme)); - return _monaco; + monacoApi.languages.setLanguageConfiguration('ivyScript', ivyScriptConf); + monacoApi.languages.setMonarchTokensProvider('ivyScript', ivyScriptLang); + monacoApi.languages.setLanguageConfiguration('ivyMacro', ivyMacroConf); + monacoApi.languages.setMonarchTokensProvider('ivyMacro', ivyMacroLang); + monacoApi.editor.defineTheme(MonacoEditorUtil.DEFAULT_THEME_NAME, MonacoEditorUtil.themeData(configuration?.theme)); + timer.end(); + return monacoApi; } diff --git a/yarn.lock b/yarn.lock index 53fdf967f..b0c5acb70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2500,29 +2500,29 @@ dependencies: "@typescript-eslint/utils" "^5.54.0" -"@tanstack/query-core@5.14.2": - version "5.14.2" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.14.2.tgz#ef0c1a93e142d5cce90b0ac2d0333a88fc0fbb95" - integrity sha512-QmoJvC72sSWs3hgGis8JdmlDvqLfYGWUK4UG6OR9Q6t28JMN9m2FDwKPqoSJ9YVocELCSjMt/FGjEiLfk8000Q== +"@tanstack/query-core@5.27.5": + version "5.27.5" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.27.5.tgz#de4ede2094d490d0147e943212fcda51f09c0f69" + integrity sha512-HuYOo46NhzNX1SwXCmLf/Skr8B7T56cDHUN+iOhnu7+GOkUMThda64GwZpAqQzBT8TOTBQo6RQaNe0gi3Gi2fw== -"@tanstack/query-devtools@5.14.5": - version "5.14.5" - resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.14.5.tgz#7e6db903a595b7b0242fc5cdac9ab5b16cf27f0d" - integrity sha512-xM8BQbE4FBAVEPr8t6MFbOJP+canDFz0hcY9Ho+5Z4LQciC60wE+7ZALx5n3InP4R7IELx8AodS5G3KYzBAwCg== +"@tanstack/query-devtools@5.27.8": + version "5.27.8" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.27.8.tgz#fa886ae72d0fe9fe5932af12ba0ba3ff484a1224" + integrity sha512-K94gnqvEe6TsDvi8eZYP2JrnQJOIymhVXRR+Xa0xcsryNqG+PeMIDmQQqjwIqbDq36qyUlPAyT6LxXVvVv1Nyw== -"@tanstack/react-query-devtools@^5.14.5": - version "5.14.5" - resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.14.5.tgz#26a460fab2d7550558bcedfe0ba54bda67ab2458" - integrity sha512-aDntW1dWc/feVUYwVqfM/r5OXa3RN06VhtYrTYW9fhj0Ks2dJw6Diknh9VpYqhI+gJrhVID95Rv/ZzS6W2lANA== +"@tanstack/react-query-devtools@^5.17.9": + version "5.27.8" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.27.8.tgz#eac277afb80e077566ffba85278b8ce4632bac10" + integrity sha512-nWttSF5qhRxyIYh0D9ybZHgAWCOdsBNZf2s0EskYpAxDDrF3lgf/xTzPPzxoX7Z14bxKruVUEpwjQWZg3f/Z7g== dependencies: - "@tanstack/query-devtools" "5.14.5" + "@tanstack/query-devtools" "5.27.8" -"@tanstack/react-query@^5.14.2": - version "5.14.2" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.14.2.tgz#b66c9710609118c07bfe6fca0147c6735a4c95a5" - integrity sha512-SbOzV7UBW8ED3tOnyn6kqNGscnOAfoxShYlbvaQo/5528mDZKpvrwoL/1du1/ukSC6RMAiKmx95SrYqlwPzWDw== +"@tanstack/react-query@^5.17.9": + version "5.27.5" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.27.5.tgz#2a431c28931bd821d9d96eaf7c6e59b4257ce023" + integrity sha512-VcuQo4CYRGsPsD8/rj9e4WnXN6eU4GKmAs0Yd9a1hLSx6DxAzRaBdrwu6P9lfjpz8bxaYkZRyb5NI+YtLipoYA== dependencies: - "@tanstack/query-core" "5.14.2" + "@tanstack/query-core" "5.27.5" "@tanstack/react-table@^8.11.2": version "8.11.2"