diff --git a/packages/libro-code-cell/src/code-cell-view.tsx b/packages/libro-code-cell/src/code-cell-view.tsx index 90be221c7..f6be17c9a 100644 --- a/packages/libro-code-cell/src/code-cell-view.tsx +++ b/packages/libro-code-cell/src/code-cell-view.tsx @@ -16,6 +16,7 @@ import { LibroExecutableCellView, LibroOutputArea, VirtualizedManagerHelper, + LirboContextKey, } from '@difizen/libro-core'; import type { ViewSize } from '@difizen/mana-app'; import { @@ -99,6 +100,7 @@ const CodeEditorViewComponent = forwardRef( @transient() @view('code-editor-cell-view') export class LibroCodeCellView extends LibroExecutableCellView { + @inject(LirboContextKey) protected readonly lirboContextKey: LirboContextKey; override view = CodeEditorViewComponent; viewManager: ViewManager; @@ -124,12 +126,6 @@ export class LibroCodeCellView extends LibroExecutableCellView { @prop() override editorStatus: EditorStatus = EditorStatus.NOTLOADED; - protected editorViewReadyDeferred: Deferred = new Deferred(); - - get editorReady() { - return this.editorViewReadyDeferred.promise; - } - protected outputAreaDeferred = new Deferred(); get outputAreaReady() { return this.outputAreaDeferred.promise; @@ -219,20 +215,16 @@ export class LibroCodeCellView extends LibroExecutableCellView { override onViewMount() { this.createEditor(); - //选中cell时才focus - if (this.parent.model.active?.id === this.id) { - this.focus(!this.parent.model.commandMode); - } } setEditorHost(ref: any) { const editorHostId = this.parent.id + this.id; - this.codeEditorManager.setEditorHostRef(editorHostId, ref); } protected getEditorOption(): CodeEditorViewOptions { const option: CodeEditorViewOptions = { + uuid: `${this.parent.model.id}-${this.model.id}`, editorHostId: this.parent.id + this.id, model: this.model, config: { @@ -258,10 +250,14 @@ export class LibroCodeCellView extends LibroExecutableCellView { const editorView = await this.codeEditorManager.getOrCreateEditorView(option); this.editorView = editorView; - this.editorViewReadyDeferred.resolve(); this.editorStatus = EditorStatus.LOADED; - await this.afterEditorReady(); + editorView.onEditorStatusChange((e) => { + if (e.status === 'ready') { + this.editor = this.editorView!.editor; + this.afterEditorReady(); + } + }); } protected async afterEditorReady() { @@ -272,6 +268,23 @@ export class LibroCodeCellView extends LibroExecutableCellView { ); }); this.editorView?.onModalChange((val) => (this.hasModal = val)); + this.focusEditor(); + } + + protected focusEditor() { + //选中cell、编辑模式、非只读时才focus + if ( + this.editorView?.editor && + this.editorView.editorStatus === 'ready' && + this.parent.model.active?.id === this.id && + !this.parent.model.commandMode && + this.lirboContextKey.commandModeEnabled === true && // 排除弹窗等情况 + this.parent.model.readOnly === false + ) { + this.editorView?.editor.setOption('styleActiveLine', true); + this.editorView?.editor.setOption('highlightActiveLineGutter', true); + this.editorView?.editor.focus(); + } } override shouldEnterEditorMode(e: React.FocusEvent) { @@ -287,32 +300,7 @@ export class LibroCodeCellView extends LibroExecutableCellView { override focus = (toEdit: boolean) => { if (toEdit) { - if (this.parent.model.readOnly === true) { - return; - } - if (!this.editorView) { - this.editorReady - .then(() => { - this.editorView?.editorReady.then(() => { - this.editorView?.editor?.setOption('styleActiveLine', true); - this.editorView?.editor?.setOption('highlightActiveLineGutter', true); - if (this.editorView?.editor?.hasFocus()) { - return; - } - this.editorView?.editor?.focus(); - return; - }); - return; - }) - .catch(console.error); - } else { - this.editorView?.editor?.setOption('styleActiveLine', true); - this.editorView?.editor?.setOption('highlightActiveLineGutter', true); - if (this.editorView?.editor?.hasFocus()) { - return; - } - this.editorView?.editor?.focus(); - } + this.focusEditor(); } else { if (this.container?.current?.parentElement?.contains(document.activeElement)) { return; diff --git a/packages/libro-code-editor/src/code-editor-manager.ts b/packages/libro-code-editor/src/code-editor-manager.ts index a9e068b05..0d29332b8 100644 --- a/packages/libro-code-editor/src/code-editor-manager.ts +++ b/packages/libro-code-editor/src/code-editor-manager.ts @@ -1,32 +1,14 @@ import type { Contribution } from '@difizen/mana-app'; -import { - Priority, - ViewManager, - contrib, - inject, - singleton, - Syringe, -} from '@difizen/mana-app'; +import { Priority, ViewManager, contrib, inject, singleton } from '@difizen/mana-app'; import { CodeEditorInfoManager } from './code-editor-info-manager.js'; import type { IModel } from './code-editor-model.js'; -import type { IEditor, IEditorConfig, IEditorOptions } from './code-editor-protocol.js'; +import { CodeEditorContribution } from './code-editor-protocol.js'; +import type { EditorState } from './code-editor-protocol.js'; import { CodeEditorSettings } from './code-editor-settings.js'; import type { CodeEditorViewOptions } from './code-editor-view.js'; import { CodeEditorView } from './code-editor-view.js'; -/** - * A factory used to create a code editor. - */ -export type CodeEditorFactory = (options: IEditorOptions) => IEditor; - -export const CodeEditorContribution = Syringe.defineToken('CodeEditorContribution'); -export interface CodeEditorContribution { - canHandle(mime: string): number; - factory: CodeEditorFactory; - defaultConfig: IEditorConfig; -} - @singleton() export class CodeEditorManager { @contrib(CodeEditorContribution) @@ -34,6 +16,7 @@ export class CodeEditorManager { @inject(ViewManager) protected readonly viewManager: ViewManager; @inject(CodeEditorInfoManager) protected codeEditorInfoManager: CodeEditorInfoManager; @inject(CodeEditorSettings) protected readonly codeEditorSettings: CodeEditorSettings; + protected stateCache: Map = new Map(); setEditorHostRef(id: string, ref: any) { this.codeEditorInfoManager.setEditorHostRef(id, ref); @@ -72,7 +55,9 @@ export class CodeEditorManager { async getOrCreateEditorView(option: CodeEditorViewOptions): Promise { const factory = this.findCodeEditorProvider(option.model)?.factory; if (!factory) { - throw new Error(`no code editor found for mimetype: ${option.model.mimeType}`); + throw new Error( + `no code editor factory registered for mimetype: ${option.model.mimeType}`, + ); } const editorView = await this.viewManager.getOrCreateView< CodeEditorView, diff --git a/packages/libro-code-editor/src/code-editor-protocol.ts b/packages/libro-code-editor/src/code-editor-protocol.ts index 3172e1e9d..d01f62948 100644 --- a/packages/libro-code-editor/src/code-editor-protocol.ts +++ b/packages/libro-code-editor/src/code-editor-protocol.ts @@ -1,5 +1,6 @@ -import type { JSONObject } from '@difizen/libro-common'; +import type { JSONObject, JSONValue } from '@difizen/libro-common'; import type { Disposable, Event, ThemeType } from '@difizen/mana-app'; +import { Syringe } from '@difizen/mana-app'; import type { IModel } from './code-editor-model.js'; @@ -203,7 +204,8 @@ export type EdgeLocation = 'top' | 'topLine' | 'bottom'; /** * A widget that provides a code editor. */ -export interface IEditor extends ISelectionOwner, Disposable { +export interface IEditor extends ISelectionOwner, Disposable { + editorReady: Promise; /** * A signal emitted when either the top or bottom edge is requested. */ @@ -390,6 +392,8 @@ export interface IEditor extends ISelectionOwner, Disposable { onModalChange: Event; dispose: () => void; + + getState: () => EditorState; } export type EditorTheme = Record; @@ -560,14 +564,9 @@ export type CompletionProvider = ( ) => Promise; /** - * The options used to initialize an editor. + * The options used to initialize an editor state. */ -export interface IEditorOptions { - /** - * The host widget used by the editor. - */ - host: HTMLElement; - +export interface IEditorStateOptions { /** * The model used by the editor. */ @@ -576,7 +575,17 @@ export interface IEditorOptions { /** * The desired uuid for the editor. */ - uuid?: string; + uuid: string; +} + +/** + * The options used to initialize an editor. + */ +export interface IEditorOptions extends IEditorStateOptions { + /** + * The host widget used by the editor. + */ + host: HTMLElement; /** * The default selection style for the editor. @@ -612,3 +621,38 @@ export interface SearchMatch { */ position: number; } + +export interface EditorState { + // monaco model or codemirror state or other editor state + state: T; + cursorPosition?: IPosition; + selections?: IRange[]; + toJSON: () => JSONValue; + dispose: (state: T) => void; +} + +export type EditorStateFactory = ( + options: IEditorStateOptions, +) => EditorState; + +/** + * A factory used to create a code editor. + */ +export type CodeEditorFactory = ( + options: IEditorOptions, + state?: T, +) => IEditor; + +export const CodeEditorContribution = Syringe.defineToken('CodeEditorContribution'); +export interface CodeEditorContribution { + canHandle(mime: string): number; + /** + * editor factory + */ + factory: CodeEditorFactory; + /** + * editor state factory + */ + stateFactory?: EditorStateFactory; + defaultConfig: IEditorConfig; +} diff --git a/packages/libro-code-editor/src/code-editor-state-manager.ts b/packages/libro-code-editor/src/code-editor-state-manager.ts new file mode 100644 index 000000000..086a2cc8e --- /dev/null +++ b/packages/libro-code-editor/src/code-editor-state-manager.ts @@ -0,0 +1,54 @@ +import type { Contribution } from '@difizen/mana-app'; +import { Priority } from '@difizen/mana-app'; +import { contrib } from '@difizen/mana-app'; +import { singleton } from '@difizen/mana-app'; + +import type { IModel } from './code-editor-model.js'; +import type { EditorState, IEditorStateOptions } from './code-editor-protocol.js'; +import { CodeEditorContribution } from './code-editor-protocol.js'; + +@singleton() +export class CodeEditorStateManager { + protected readonly codeEditorProvider: Contribution.Provider; + protected stateCache: Map = new Map(); + + constructor( + @contrib(CodeEditorContribution) + codeEditorProvider: Contribution.Provider, + ) { + this.codeEditorProvider = codeEditorProvider; + } + + protected findCodeEditorProvider(model: IModel) { + const prioritized = Priority.sortSync( + this.codeEditorProvider.getContributions(), + (contribution) => contribution.canHandle(model.mimeType), + ); + const sorted = prioritized.map((c) => c.value); + return sorted[0]; + } + + async getOrCreateEditorState(option: IEditorStateOptions): Promise { + if (this.stateCache.has(option.uuid)) { + const state = this.stateCache.get(option.uuid)!; + return state; + } + const factory = this.findCodeEditorProvider(option.model)?.stateFactory; + if (!factory) { + throw new Error( + `no code editor state factory registered for mimetype: ${option.model.mimeType}`, + ); + } + const state = factory(option); + this.stateCache.set(option.uuid, state); + return state; + } + + updateEditorState(id: string, state: EditorState) { + this.stateCache.set(id, state); + } + + removeEditorState(id: string) { + this.stateCache.delete(id); + } +} diff --git a/packages/libro-code-editor/src/code-editor-view.tsx b/packages/libro-code-editor/src/code-editor-view.tsx index bed35385b..9e447942b 100644 --- a/packages/libro-code-editor/src/code-editor-view.tsx +++ b/packages/libro-code-editor/src/code-editor-view.tsx @@ -1,8 +1,6 @@ -import { getOrigin, prop } from '@difizen/mana-app'; import { inject, transient, - Deferred, Emitter, BaseView, ThemeService, @@ -12,7 +10,6 @@ import { import { forwardRef, memo } from 'react'; import { CodeEditorInfoManager } from './code-editor-info-manager.js'; -import type { CodeEditorFactory } from './code-editor-manager.js'; import type { IModel } from './code-editor-model.js'; import type { CompletionProvider, @@ -21,12 +18,14 @@ import type { IEditor, IEditorSelectionStyle, TooltipProvider, + CodeEditorFactory, } from './code-editor-protocol.js'; import { CodeEditorSettings } from './code-editor-settings.js'; +import { CodeEditorStateManager } from './code-editor-state-manager.js'; export const CodeEditorRender = memo( forwardRef((props, ref) => { - return
; + return
; }), ); @@ -51,6 +50,8 @@ const DROP_TARGET_CLASS = 'jp-mod-dropTarget'; */ const leadingWhitespaceRe = /^\s+$/; +export type CodeEditorViewStatus = 'init' | 'ready' | 'disposed'; + /** * A widget which hosts a code editor. */ @@ -59,6 +60,8 @@ const leadingWhitespaceRe = /^\s+$/; export class CodeEditorView extends BaseView { @inject(ThemeService) protected readonly themeService: ThemeService; @inject(CodeEditorSettings) protected readonly codeEditorSettings: CodeEditorSettings; + @inject(CodeEditorStateManager) + protected readonly codeEditorStateManager: CodeEditorStateManager; codeEditorInfoManager: CodeEditorInfoManager; @@ -79,12 +82,16 @@ export class CodeEditorView extends BaseView { /** * Get the editor wrapped by the widget. */ - @prop() editor: IEditor; - protected editorReadyDeferred: Deferred = new Deferred(); - get editorReady() { - return this.editorReadyDeferred.promise; - } + + editorStatus: CodeEditorViewStatus = 'init'; + + protected editorStatusChangeEmitter = new Emitter<{ + status: CodeEditorViewStatus; + prevState: CodeEditorViewStatus; + }>(); + onEditorStatusChange = this.editorStatusChangeEmitter.event; + /** * Construct a new code editor widget. */ @@ -97,34 +104,60 @@ export class CodeEditorView extends BaseView { this.codeEditorInfoManager = codeEditorInfoManager; } - override async onViewMount() { - const settings = this.codeEditorSettings.getUserEditorSettings(); - + protected getEditorHost() { const editorHostId = this.options.editorHostId; const editorHostRef = editorHostId ? this.codeEditorInfoManager.getEditorHostRef(editorHostId) : undefined; - this.editorHostRef = - editorHostRef && editorHostRef.current ? editorHostRef : this.container; + return editorHostRef && editorHostRef.current ? editorHostRef : this.container; + } + + override async onViewMount() { + const state = await this.codeEditorStateManager.getOrCreateEditorState({ + uuid: this.options.uuid, + model: this.options.model, + }); + + const settings = this.codeEditorSettings.getUserEditorSettings(); + + this.editorHostRef = this.getEditorHost(); if (this.editorHostRef.current && this.options.factory) { - this.editor = this.options.factory({ - ...this.options, - host: this.editorHostRef.current, - model: this.options.model, - uuid: this.options.uuid, - config: { ...settings, ...this.options.config }, - selectionStyle: this.options.selectionStyle, - tooltipProvider: this.options.tooltipProvider, - completionProvider: this.options.completionProvider, - }); - this.editorReadyDeferred.resolve(); + this.editor = this.options.factory( + { + ...this.options, + host: this.editorHostRef.current, + model: this.options.model, + uuid: this.options.uuid, + config: { ...settings, ...this.options.config }, + selectionStyle: this.options.selectionStyle, + tooltipProvider: this.options.tooltipProvider, + completionProvider: this.options.completionProvider, + }, + state, + ); + + await this.editor.editorReady; + + const { cursorPosition, selections } = state; + + const prevState = this.editorStatus; + this.editorStatus = 'ready'; + this.editorStatusChangeEmitter.fire({ status: 'ready', prevState: prevState }); + + if (cursorPosition) { + this.editor.setCursorPosition(cursorPosition); + } + if (selections) { + this.editor.setSelections(selections); + } + this.editor.onModalChange((val) => this.modalChangeEmitter.fire(val)); // this.editor.model.selections.changed(this._onSelectionsChanged); if (this.options.autoFocus) { - getOrigin(this.editor).focus(); + this.editor.focus(); } this.editorHostRef.current.addEventListener('focus', this.onViewActive); @@ -148,9 +181,19 @@ export class CodeEditorView extends BaseView { }; override onViewUnmount = () => { - if (this.editor.dispose) { - this.editor.dispose(); + if (this.editor.hasFocus()) { + // 保存编辑器状态 + const editorState = this.editor.getState(); + this.codeEditorStateManager.updateEditorState(this.options.uuid, editorState); + // focus 到 host 避免进入命令模式 + this.editorHostRef = this.getEditorHost(); + this.editorHostRef?.current?.focus(); } + this.editor.dispose(); + + const prevState = this.editorStatus; + this.editorStatus = 'disposed'; + this.editorStatusChangeEmitter.fire({ status: 'disposed', prevState: prevState }); const node = this.editorHostRef?.current; if (node) { @@ -349,8 +392,9 @@ export interface CodeEditorViewOptions( - 'autocomplete', - cur(state), - ) + .languageDataAt< + CompletionSource | readonly (string | Completion)[] + >('autocomplete', cur(state)) .map(asSource); let active: readonly ActiveSource[] = sources.map((source) => { const value = diff --git a/packages/libro-codemirror/src/editor-contribution.ts b/packages/libro-codemirror/src/editor-contribution.ts index 940bc8839..b2823d5e9 100644 --- a/packages/libro-codemirror/src/editor-contribution.ts +++ b/packages/libro-codemirror/src/editor-contribution.ts @@ -3,7 +3,7 @@ import { CodeEditorContribution } from '@difizen/libro-code-editor'; import { singleton } from '@difizen/mana-app'; import { codeMirrorDefaultConfig } from './editor.js'; -import { codeMirrorEditorFactory } from './factory.js'; +import { codeMirrorEditorFactory, stateFactory } from './factory.js'; @singleton({ contrib: [CodeEditorContribution] }) export class CodeMirrorEditorContribution implements CodeEditorContribution { @@ -13,5 +13,6 @@ export class CodeMirrorEditorContribution implements CodeEditorContribution { return 50; } factory: CodeEditorFactory = codeMirrorEditorFactory; + stateFactory = stateFactory; defaultConfig = codeMirrorDefaultConfig; } diff --git a/packages/libro-codemirror/src/editor.ts b/packages/libro-codemirror/src/editor.ts index 7bb3e57ea..6ec5c9441 100644 --- a/packages/libro-codemirror/src/editor.ts +++ b/packages/libro-codemirror/src/editor.ts @@ -19,6 +19,7 @@ import type { Command, DecorationSet, ViewUpdate } from '@codemirror/view'; import { Decoration, EditorView } from '@codemirror/view'; import { defaultConfig, defaultSelectionStyle } from '@difizen/libro-code-editor'; import type { + EditorState as LibroEditorState, ICoordinate, IEditor, IEditorConfig, @@ -34,13 +35,14 @@ import type { } from '@difizen/libro-code-editor'; import { findFirstArrayIndex, removeAllWhereFromArray } from '@difizen/libro-common'; import type { LSPProvider } from '@difizen/libro-lsp'; -import { Disposable, Emitter } from '@difizen/mana-app'; +import { Deferred, Disposable, Emitter } from '@difizen/mana-app'; import { getOrigin, watch } from '@difizen/mana-app'; import type { SyntaxNodeRef } from '@lezer/common'; import { v4 } from 'uuid'; import type { CodeMirrorConfig } from './config.js'; import { EditorConfiguration } from './config.js'; +import { stateFactory } from './factory.js'; import { ensure } from './mode.js'; import { monitorPlugin } from './monitor.js'; @@ -137,6 +139,8 @@ export const codeMirrorDefaultConfig: Required = { }; export class CodeMirrorEditor implements IEditor { + protected editorReadyDeferred = new Deferred(); + editorReady = this.editorReadyDeferred.promise; // highlight protected highlightEffect: StateEffectType<{ matches: SearchMatch[]; @@ -146,6 +150,8 @@ export class CodeMirrorEditor implements IEditor { protected selectedMatchMark: Decoration; protected highlightField: StateField; + protected editorState: LibroEditorState; + /** * Construct a CodeMirror editor. */ @@ -160,6 +166,8 @@ export class CodeMirrorEditor implements IEditor { host.addEventListener('scroll', this, true); this._uuid = options.uuid || v4(); + this.editorState = + options.state ?? stateFactory({ uuid: options.uuid, model: options.model }); // State and effects for handling the selection marks this._addMark = StateEffect.define(); @@ -314,6 +322,8 @@ export class CodeMirrorEditor implements IEditor { ], ); + this.editorReadyDeferred.resolve(); + // every time the model is switched, we need to re-initialize the editor binding // this.model.sharedModelSwitched.connect(this._initializeEditorBinding, this); @@ -333,6 +343,14 @@ export class CodeMirrorEditor implements IEditor { watch(model, 'mimeType', this._onMimeTypeChanged); } + getState(): LibroEditorState { + return { + ...this.editorState, + cursorPosition: this.getCursorPosition(), + selections: this.getSelections(), + }; + } + /** * Initialize the editor binding. */ @@ -545,14 +563,14 @@ export class CodeMirrorEditor implements IEditor { * Brings browser focus to this editor text. */ focus(): void { - this._editor.focus(); + getOrigin(this._editor).focus(); } /** * Test whether the editor has keyboard focus. */ hasFocus(): boolean { - return this._editor.hasFocus; + return getOrigin(this._editor).hasFocus; } /** @@ -1219,6 +1237,7 @@ export interface IOptions extends IEditorOptions { * The configuration options for the editor. */ config?: Partial; + state?: LibroEditorState; } export function createEditor( diff --git a/packages/libro-codemirror/src/factory.ts b/packages/libro-codemirror/src/factory.ts index 67725fcfa..6a815313c 100644 --- a/packages/libro-codemirror/src/factory.ts +++ b/packages/libro-codemirror/src/factory.ts @@ -1,10 +1,31 @@ -import type { CodeEditorFactory, IEditorOptions } from '@difizen/libro-code-editor'; +import type { + CodeEditorFactory, + EditorState, + EditorStateFactory, + IEditorOptions, +} from '@difizen/libro-code-editor'; import { codeMirrorDefaultConfig, CodeMirrorEditor } from './editor.js'; -export const codeMirrorEditorFactory: CodeEditorFactory = (options: IEditorOptions) => { +export const codeMirrorEditorFactory: CodeEditorFactory = ( + options: IEditorOptions, + state?: EditorState, +) => { return new CodeMirrorEditor({ ...options, config: { ...codeMirrorDefaultConfig, ...options.config }, + state, }); }; + +export const stateFactory: EditorStateFactory = () => { + return { + toJSON: () => { + return {}; + }, + dispose: () => { + // + }, + state: {}, + }; +}; diff --git a/packages/libro-cofine-editor-core/src/e2-editor.ts b/packages/libro-cofine-editor-core/src/e2-editor.ts index e2e35cc8f..a87dee575 100644 --- a/packages/libro-cofine-editor-core/src/e2-editor.ts +++ b/packages/libro-cofine-editor-core/src/e2-editor.ts @@ -19,6 +19,27 @@ export const IsDiff = Symbol('IsDiff'); export class E2Editor< T extends monaco.editor.IStandaloneCodeEditor | monaco.editor.IStandaloneDiffEditor, > { + static createMonacoModel( + value: string, + language?: string, + uri?: monaco.Uri, + ): monaco.editor.ITextModel { + return monaco.editor.createModel(value, language, uri); + } + + static createMonacoEditor( + node: HTMLElement, + options: Options, + ): monaco.editor.IStandaloneCodeEditor { + return monaco.editor.create(node, options); + } + + static createMonacoDiffEditor( + node: HTMLElement, + options: Options, + ): monaco.editor.IStandaloneDiffEditor { + return monaco.editor.createDiffEditor(node, options); + } codeEditor!: T; model!: monaco.editor.ITextModel; modified!: monaco.editor.ITextModel; @@ -49,11 +70,9 @@ export class E2Editor< if (MonacoEnvironment.lazy) { // 资源懒加载场景 if (!isDiff) { - this.model = monaco.editor.createModel( - options.value || '', - options.language, - options.uri, - ); + this.model = + options.model ?? + monaco.editor.createModel(options.value || '', options.language, options.uri); const language = this.model.getLanguageId(); (this as E2Editor).codeEditor = monaco.editor.create(node, { ...options, model: this.model }); @@ -112,7 +131,9 @@ export class E2Editor< this.handleEditorLanguageFeatureBefore(options.language); if (!isDiff) { - this.model = monaco.editor.createModel(options.value || '', options.language); + this.model = + options.model ?? + monaco.editor.createModel(options.value || '', options.language, options.uri); (this as E2Editor).codeEditor = monaco.editor.create(node, { ...options, model: this.model }); this.toDispose.push( diff --git a/packages/libro-cofine-editor/src/editor-contribution.ts b/packages/libro-cofine-editor/src/editor-contribution.ts index 6a70e8d38..fb7e1ff81 100644 --- a/packages/libro-cofine-editor/src/editor-contribution.ts +++ b/packages/libro-cofine-editor/src/editor-contribution.ts @@ -1,11 +1,19 @@ -import type { CodeEditorFactory } from '@difizen/libro-code-editor'; +import type { CodeEditorFactory, EditorStateFactory } from '@difizen/libro-code-editor'; import { CodeEditorContribution } from '@difizen/libro-code-editor'; import { inject, singleton } from '@difizen/mana-app'; -import { libroE2DefaultConfig, LibroE2EditorFactory } from './libro-e2-editor.js'; +import { LanguageSpecRegistry } from './language-specs.js'; +import { + e2StateFactory, + libroE2DefaultConfig, + LibroE2EditorFactory, +} from './libro-e2-editor.js'; @singleton({ contrib: [CodeEditorContribution] }) export class LibroE2EditorContribution implements CodeEditorContribution { + @inject(LanguageSpecRegistry) + protected readonly languageSpecRegistry: LanguageSpecRegistry; + factory: CodeEditorFactory; defaultConfig = libroE2DefaultConfig; @@ -16,6 +24,13 @@ export class LibroE2EditorContribution implements CodeEditorContribution { this.factory = libroE2EditorFactory; } + stateFactory: EditorStateFactory = (options) => { + return e2StateFactory(this.languageSpecRegistry)({ + uuid: options.uuid, + model: options.model, + }); + }; + canHandle(mime: string): number { const mimes = [ 'application/vnd.libro.sql+json', diff --git a/packages/libro-cofine-editor/src/language/lsp/completion-provider.ts b/packages/libro-cofine-editor/src/language/lsp/completion-provider.ts index faf3aaf11..845db4d43 100644 --- a/packages/libro-cofine-editor/src/language/lsp/completion-provider.ts +++ b/packages/libro-cofine-editor/src/language/lsp/completion-provider.ts @@ -140,10 +140,9 @@ export class CompletionProvider if (!original) { return; } + const lspConnection = await this.getLSPConnection(); const itemResult = - await this.lspConnection.clientRequests['completionItem/resolve'].request( - original, - ); + await lspConnection.clientRequests['completionItem/resolve'].request(original); if (token.isCancellationRequested) { return; } diff --git a/packages/libro-cofine-editor/src/language/lsp/diagnostic-provider.ts b/packages/libro-cofine-editor/src/language/lsp/diagnostic-provider.ts index f415da316..111350c27 100644 --- a/packages/libro-cofine-editor/src/language/lsp/diagnostic-provider.ts +++ b/packages/libro-cofine-editor/src/language/lsp/diagnostic-provider.ts @@ -1,6 +1,6 @@ import type { LibroService } from '@difizen/libro-core'; import { EditorCellView } from '@difizen/libro-core'; -import type { LSPConnection, VirtualDocument } from '@difizen/libro-lsp'; +import type { ILSPDocumentConnectionManager } from '@difizen/libro-lsp'; import { DisposableCollection } from '@difizen/mana-app'; import type { Disposable } from '@difizen/mana-app'; import * as monaco from '@difizen/monaco-editor-core'; @@ -28,10 +28,9 @@ export class DiagnosticProvider extends LangaugeFeatureProvider implements Dispo protected toDispose = new DisposableCollection(); constructor( libroService: LibroService, - lspConnection: LSPConnection, - virtualDocument: VirtualDocument, + lspDocumentConnectionManager: ILSPDocumentConnectionManager, ) { - super(libroService, lspConnection, virtualDocument); + super(libroService, lspDocumentConnectionManager); this.processDiagnostic(); } @@ -79,71 +78,79 @@ export class DiagnosticProvider extends LangaugeFeatureProvider implements Dispo } async processDiagnostic() { - const toDispose = this.lspConnection.serverNotifications[ + const lspConnection = await this.getLSPConnection(); + const toDispose = lspConnection.serverNotifications[ 'textDocument/publishDiagnostics' - ].event((e) => { + ].event(async (e) => { this.diagnosticList = []; - e.diagnostics.forEach((diagnostic) => { - const { range } = diagnostic; - // the diagnostic range must be in current editor - const editor = this.getEditorFromLSPPosition(range); - if (!editor || editor.getOption('lspEnabled') !== true) { - return; - } - const model = editor?.monacoEditor?.getModel(); - if (!model) { - return; - } + await Promise.all( + e.diagnostics.map(async (diagnostic) => { + const { range } = diagnostic; + // the diagnostic range must be in current editor + const editor = await this.getEditorFromLSPPosition(range); + if (!editor || editor.getOption('lspEnabled') !== true) { + return; + } + const model = editor?.monacoEditor?.getModel(); + if (!model) { + return; + } - const editorStart = this.virtualDocument.transformVirtualToEditor({ - line: range.start.line, - ch: range.start.character, - isVirtual: true, - }); + const virtualDocument = await this.getVirtualDocument(); + if (!virtualDocument) { + return; + } - const editorEnd = this.virtualDocument.transformVirtualToEditor({ - line: range.end.line, - ch: range.end.character, - isVirtual: true, - }); + const editorStart = virtualDocument.transformVirtualToEditor({ + line: range.start.line, + ch: range.start.character, + isVirtual: true, + }); - if (!editorStart || !editorEnd) { - return; - } + const editorEnd = virtualDocument.transformVirtualToEditor({ + line: range.end.line, + ch: range.end.character, + isVirtual: true, + }); - const markerRange = new MonacoRange( - editorStart.line + 1, - editorStart.ch, - editorEnd.line + 1, - editorEnd.ch, - ); - - const marker: monaco.editor.IMarkerData = { - source: diagnostic.source, - tags: diagnostic.tags, - message: diagnostic.message, - code: String(diagnostic.code), - severity: diagnostic.severity - ? vererityMap[diagnostic.severity] - : monaco.MarkerSeverity.Info, - relatedInformation: diagnostic.relatedInformation?.map((item) => { - return { - message: item.message, - resource: MonacoUri.parse(item.location.uri), - startLineNumber: markerRange.startLineNumber, - startColumn: markerRange.startColumn, - endLineNumber: markerRange.endLineNumber, - endColumn: markerRange.endColumn, - }; - }), - startLineNumber: editorStart.line + 1, - startColumn: editorStart.ch + 1, - endLineNumber: editorEnd.line + 1, - endColumn: editorEnd.ch + 1, - }; - - this.addDiagnostic(model, marker); - }); + if (!editorStart || !editorEnd) { + return; + } + + const markerRange = new MonacoRange( + editorStart.line + 1, + editorStart.ch, + editorEnd.line + 1, + editorEnd.ch, + ); + + const marker: monaco.editor.IMarkerData = { + source: diagnostic.source, + tags: diagnostic.tags, + message: diagnostic.message, + code: String(diagnostic.code), + severity: diagnostic.severity + ? vererityMap[diagnostic.severity] + : monaco.MarkerSeverity.Info, + relatedInformation: diagnostic.relatedInformation?.map((item) => { + return { + message: item.message, + resource: MonacoUri.parse(item.location.uri), + startLineNumber: markerRange.startLineNumber, + startColumn: markerRange.startColumn, + endLineNumber: markerRange.endLineNumber, + endColumn: markerRange.endColumn, + }; + }), + startLineNumber: editorStart.line + 1, + startColumn: editorStart.ch + 1, + endLineNumber: editorEnd.line + 1, + endColumn: editorEnd.ch + 1, + }; + + this.addDiagnostic(model, marker); + }), + ); this.displayDiagnostic(); }); diff --git a/packages/libro-cofine-editor/src/language/lsp/format-provider.ts b/packages/libro-cofine-editor/src/language/lsp/format-provider.ts index fa607dd98..163a9aa60 100644 --- a/packages/libro-cofine-editor/src/language/lsp/format-provider.ts +++ b/packages/libro-cofine-editor/src/language/lsp/format-provider.ts @@ -38,7 +38,8 @@ export class FormatProvider } const { virtualDocument: doc } = provider; - const result = await this.lspConnection.clientRequests[ + const lspConnection = await this.getLSPConnection(); + const result = await lspConnection.clientRequests[ 'textDocument/rangeFormatting' ].request({ // TODO: range transform diff --git a/packages/libro-cofine-editor/src/language/lsp/language-feature-provider.ts b/packages/libro-cofine-editor/src/language/lsp/language-feature-provider.ts index 9fd6b3f30..45573a06c 100644 --- a/packages/libro-cofine-editor/src/language/lsp/language-feature-provider.ts +++ b/packages/libro-cofine-editor/src/language/lsp/language-feature-provider.ts @@ -1,7 +1,7 @@ import type { LibroService } from '@difizen/libro-core'; import { EditorCellView } from '@difizen/libro-core'; -import type { LSPProviderResult } from '@difizen/libro-lsp'; -import type { LSPConnection, VirtualDocument } from '@difizen/libro-lsp'; +import type { ILSPDocumentConnectionManager } from '@difizen/libro-lsp'; +import type { LSPConnection } from '@difizen/libro-lsp'; import type monaco from '@difizen/monaco-editor-core'; import type * as lsp from 'vscode-languageserver-protocol'; @@ -9,19 +9,52 @@ import { LibroE2Editor } from '../../libro-e2-editor.js'; export class LangaugeFeatureProvider { protected libroService: LibroService; - protected lspProvider?: LSPProviderResult; - protected lspConnection: LSPConnection; - virtualDocument: VirtualDocument; + lspDocumentConnectionManager: ILSPDocumentConnectionManager; constructor( libroService: LibroService, - lspConnection: LSPConnection, - virtualDocument: VirtualDocument, + lspDocumentConnectionManager: ILSPDocumentConnectionManager, ) { this.libroService = libroService; - this.lspConnection = lspConnection; - this.virtualDocument = virtualDocument; + this.lspDocumentConnectionManager = lspDocumentConnectionManager; } + async getVirtualDocument() { + const libroView = this.libroService.active; + if (!libroView) { + return; + } + await this.lspDocumentConnectionManager.ready; + const adapter = this.lspDocumentConnectionManager.adapters.get(libroView.model.id); + if (!adapter) { + throw new Error('no adapter'); + } + + await adapter.ready; + + // Get the associated virtual document of the opened document + const virtualDocument = adapter.virtualDocument; + return virtualDocument; + } + + async getLSPConnection() { + const virtualDocument = await this.getVirtualDocument(); + if (!virtualDocument) { + throw new Error('no virtualDocument'); + } + + // Get the LSP connection of the virtual document. + const lspConnection = this.lspDocumentConnectionManager.connections.get( + virtualDocument.uri, + ) as LSPConnection; + + return lspConnection; + } + + /** + * find cell editor from active notebook by model uri + * @param model + * @returns + */ protected getEditorByModel(model: monaco.editor.ITextModel) { const cells = this.libroService.active?.model.cells; if (!cells) { @@ -42,17 +75,23 @@ export class LangaugeFeatureProvider { return (cell as EditorCellView).editor as LibroE2Editor | undefined; } - protected getEditorFromLSPPosition(range: lsp.Range) { - const currentEditor = this.virtualDocument.getEditorAtVirtualLine({ - line: range.start.line, - ch: range.start.character, - isVirtual: true, - }); - const editor = currentEditor.getEditor(); - if (editor instanceof LibroE2Editor) { - return editor; + protected async getEditorFromLSPPosition(range: lsp.Range) { + try { + const virtualDocument = await this.getVirtualDocument(); + const currentEditor = virtualDocument?.getEditorAtVirtualLine({ + line: range.start.line, + ch: range.start.character, + isVirtual: true, + }); + const editor = currentEditor?.getEditor(); + if (editor instanceof LibroE2Editor) { + return editor; + } + return; + } catch (error) { + console.warn(error); + return; } - return; } protected async getProvider(model: monaco.editor.ITextModel) { @@ -63,7 +102,6 @@ export class LangaugeFeatureProvider { } const provider = await editor.lspProvider?.(); - this.lspProvider = provider; return provider; } } diff --git a/packages/libro-cofine-editor/src/language/lsp/lsp-contribution.ts b/packages/libro-cofine-editor/src/language/lsp/lsp-contribution.ts index 8cf60362e..744514e74 100644 --- a/packages/libro-cofine-editor/src/language/lsp/lsp-contribution.ts +++ b/packages/libro-cofine-editor/src/language/lsp/lsp-contribution.ts @@ -3,7 +3,6 @@ import { LanguageOptionsRegistry, } from '@difizen/libro-cofine-editor-core'; import { LibroService } from '@difizen/libro-core'; -import type { LSPConnection } from '@difizen/libro-lsp'; import { ILSPDocumentConnectionManager } from '@difizen/libro-lsp'; import { Disposable, DisposableCollection, inject, singleton } from '@difizen/mana-app'; import * as monaco from '@difizen/monaco-editor-core'; @@ -50,96 +49,52 @@ export class LSPContribution implements EditorHandlerContribution { }; } - async getVirtualDocument() { - const libroView = this.libroService.active; - if (!libroView) { - return; - } - await this.lspDocumentConnectionManager.ready; - const adapter = this.lspDocumentConnectionManager.adapters.get(libroView.model.id); - if (!adapter) { - throw new Error('no adapter'); - } - - await adapter.ready; - - // Get the associated virtual document of the opened document - const virtualDocument = adapter.virtualDocument; - return virtualDocument; - } - - async getLSPConnection() { - const virtualDocument = await this.getVirtualDocument(); - if (!virtualDocument) { - throw new Error('no virtualDocument'); - } - - // Get the LSP connection of the virtual document. - const lspConnection = this.lspDocumentConnectionManager.connections.get( - virtualDocument.uri, - ) as LSPConnection; - - return lspConnection; - } - registerLSPFeature(editor: monaco.editor.IStandaloneCodeEditor) { const model = editor.getModel(); if (!model) { return; } - Promise.all([this.getVirtualDocument(), this.getLSPConnection()]) - .then(([virtualDocument, lspConnection]) => { - if (!lspConnection || !virtualDocument) { - return; - } - this.toDispose.push( - monaco.languages.registerCompletionItemProvider( - this.getLanguageSelector(model), - new CompletionProvider(this.libroService, lspConnection, virtualDocument), - ), - ); - - this.toDispose.push( - monaco.languages.registerHoverProvider( - this.getLanguageSelector(model), - new HoverProvider(this.libroService, lspConnection, virtualDocument), - ), - ); - - const provider = new DiagnosticProvider( - this.libroService, - lspConnection, - virtualDocument, - ); - this.toDispose.push(Disposable.create(() => provider.dispose())); - - this.toDispose.push( - monaco.languages.registerSignatureHelpProvider( - this.getLanguageSelector(model), - new SignatureHelpProvider( - this.libroService, - lspConnection, - virtualDocument, - ), - ), - ); - // const formatProvider = new FormatProvider( - // this.libroService, - // lspConnection, - // virtualDocument, - // ); - // monaco.languages.registerDocumentFormattingEditProvider( - // this.getLanguageSelector(model), - // formatProvider, - // ); - // monaco.languages.registerDocumentRangeFormattingEditProvider( - // this.getLanguageSelector(model), - // formatProvider, - // ); - return; - }) - .catch(console.error); + this.toDispose.push( + monaco.languages.registerCompletionItemProvider( + this.getLanguageSelector(model), + new CompletionProvider(this.libroService, this.lspDocumentConnectionManager), + ), + ); + + this.toDispose.push( + monaco.languages.registerHoverProvider( + this.getLanguageSelector(model), + new HoverProvider(this.libroService, this.lspDocumentConnectionManager), + ), + ); + + const provider = new DiagnosticProvider( + this.libroService, + this.lspDocumentConnectionManager, + ); + this.toDispose.push(Disposable.create(() => provider.dispose())); + + this.toDispose.push( + monaco.languages.registerSignatureHelpProvider( + this.getLanguageSelector(model), + new SignatureHelpProvider(this.libroService, this.lspDocumentConnectionManager), + ), + ); + // const formatProvider = new FormatProvider( + // this.libroService, + // lspConnection, + // virtualDocument, + // ); + // monaco.languages.registerDocumentFormattingEditProvider( + // this.getLanguageSelector(model), + // formatProvider, + // ); + // monaco.languages.registerDocumentRangeFormattingEditProvider( + // this.getLanguageSelector(model), + // formatProvider, + // ); + return; // // SignatureHelp // monaco.languages.registerSignatureHelpProvider(id, new SignatureHelpProvider(this._worker)); diff --git a/packages/libro-cofine-editor/src/libro-e2-editor.ts b/packages/libro-cofine-editor/src/libro-e2-editor.ts index 8c69f9731..88d6bf514 100644 --- a/packages/libro-cofine-editor/src/libro-e2-editor.ts +++ b/packages/libro-cofine-editor/src/libro-e2-editor.ts @@ -1,6 +1,8 @@ import type { CodeEditorFactory, CompletionProvider, + EditorState, + EditorStateFactory, ICoordinate, IEditor, IEditorConfig, @@ -18,6 +20,8 @@ import { NotebookCommands } from '@difizen/libro-core'; import type { LSPProvider } from '@difizen/libro-lsp'; import { CommandRegistry, + Deferred, + getOrigin, inject, ThemeService, transient, @@ -35,6 +39,8 @@ import type { MonacoEditorOptions, MonacoEditorType, MonacoMatch } from './types import { MonacoRange, MonacoUri } from './types.js'; import './index.less'; +import * as monaco from '@difizen/monaco-editor-core'; + export interface LibroE2EditorConfig extends IEditorConfig { /** * The mode to use. @@ -211,6 +217,9 @@ export interface LibroE2EditorOptions extends IEditorOptions { config?: Partial; } +export const LibroE2EditorState = Symbol('LibroE2EditorState'); +export type LibroE2EditorState = EditorState; + export const libroE2DefaultConfig: Required = { ...defaultConfig, theme: { @@ -272,8 +281,39 @@ export const E2EditorClassname = 'libro-e2-editor'; export const LibroE2URIScheme = 'libro-e2'; +export type E2EditorState = monaco.editor.ITextModel | null; + +export const e2StateFactory: ( + languageSpecRegistry: LanguageSpecRegistry, +) => EditorStateFactory = (languageSpecRegistry) => (options) => { + const spec = languageSpecRegistry.languageSpecs.find( + (item) => item.mime === options.model.mimeType, + ); + const uri = MonacoUri.from({ + scheme: LibroE2URIScheme, + path: `${options.uuid}${spec?.ext[0]}`, + }); + const monacoModel = monaco.editor.createModel( + options.model.value, + spec?.language, + uri, + ); + return { + state: monacoModel, + toJSON: () => { + return {}; + }, + dispose: () => { + monacoModel.dispose(); + }, + } as EditorState; +}; + @transient() export class LibroE2Editor implements IEditor { + protected editorReadyDeferred = new Deferred(); + editorReady = this.editorReadyDeferred.promise; + protected readonly themeService: ThemeService; protected readonly languageSpecRegistry: LanguageSpecRegistry; @@ -299,7 +339,7 @@ export class LibroE2Editor implements IEditor { protected _config: LibroE2EditorConfig; - private resizeObserver: ResizeObserver; + private intersectionObserver: IntersectionObserver; private editorContentHeight: number; @@ -322,6 +362,8 @@ export class LibroE2Editor implements IEditor { return this._model; } + editorState: EditorState; + protected _editor?: E2Editor; get editor(): E2Editor | undefined { return this?._editor; @@ -344,6 +386,7 @@ export class LibroE2Editor implements IEditor { protected isLayouting = false; constructor( @inject(LibroE2EditorOptions) options: LibroE2EditorOptions, + @inject(LibroE2EditorState) state: LibroE2EditorState, @inject(ThemeService) themeService: ThemeService, @inject(LanguageSpecRegistry) languageSpecRegistry: LanguageSpecRegistry, @@ -370,6 +413,7 @@ export class LibroE2Editor implements IEditor { this.editorHost = document.createElement('div'); this.host.append(this.editorHost); + this.editorState = state; this.createEditor(this.editorHost, fullConfig); this.onMimeTypeChanged(); @@ -444,6 +488,16 @@ export class LibroE2Editor implements IEditor { }; } + getState(): EditorState { + const cursorPosition = this.getCursorPosition(); + const selections = this.getSelections(); + return { + ...this.editorState, + cursorPosition, + selections, + }; + } + protected async createEditor(host: HTMLElement, config: LibroE2EditorConfig) { if (!this.languageSpec) { return; @@ -459,10 +513,7 @@ export class LibroE2Editor implements IEditor { const editorPorvider = MonacoEnvironment.container.get(EditorProvider); - const uri = MonacoUri.from({ - scheme: LibroE2URIScheme, - path: `${this.uuid}${this.languageSpec.ext[0]}`, - }); + const model = this.editorState.state; const options: MonacoEditorOptions = { ...this.toMonacoOptions(editorConfig), @@ -470,13 +521,11 @@ export class LibroE2Editor implements IEditor { * language ia an uri: */ language: this.languageSpec.language, - uri, theme: this.theme, - value: this.model.value, + model, }; - const e2Editor = editorPorvider.create(host, options); - this._editor = e2Editor; + this._editor = editorPorvider.create(host, options); this.toDispose.push( this.monacoEditor?.onDidChangeModelContent(() => { const value = this.monacoEditor?.getValue(); @@ -503,6 +552,7 @@ export class LibroE2Editor implements IEditor { config.placeholder, this.monacoEditor!, ); + this.editorReadyDeferred.resolve(); // console.log( // 'editor._themeService.getColorTheme()', @@ -517,16 +567,12 @@ export class LibroE2Editor implements IEditor { } protected inspectResize() { - // this.resizeObserver = new ResizeObserver((entries) => { - // entries.forEach((entry) => { - // const isVisible = - // entry.contentRect.width !== 0 && entry.contentRect.height !== 0; - // if (isVisible) { - // this.updateEditorSize(); - // } - // }); - // }); - // this.resizeObserver.observe(this.host); + this.intersectionObserver = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) { + this.updateEditorSize(); + } + }); + this.intersectionObserver.observe(this.host); } protected getEditorNode() { @@ -735,7 +781,9 @@ export class LibroE2Editor implements IEditor { this.monacoEditor?.trigger('source', 'redo', {}); }; focus = () => { - this.monacoEditor?.focus(); + window.requestAnimationFrame(() => { + this.monacoEditor?.focus(); + }); }; hasFocus = () => { return this.monacoEditor?.hasWidgetFocus() ?? false; @@ -933,8 +981,8 @@ export class LibroE2Editor implements IEditor { } disposeResizeObserver = () => { - if (this.resizeObserver) { - this.resizeObserver.disconnect(); + if (this.intersectionObserver) { + getOrigin(this.intersectionObserver).disconnect(); } }; } diff --git a/packages/libro-cofine-editor/src/module.ts b/packages/libro-cofine-editor/src/module.ts index 1c77e268c..7b4a517ac 100644 --- a/packages/libro-cofine-editor/src/module.ts +++ b/packages/libro-cofine-editor/src/module.ts @@ -1,4 +1,4 @@ -import type { IEditorOptions } from '@difizen/libro-code-editor'; +import type { EditorState, IEditorOptions } from '@difizen/libro-code-editor'; import { CodeEditorModule } from '@difizen/libro-code-editor'; import { ManaModule } from '@difizen/mana-app'; @@ -12,6 +12,7 @@ import { LibroE2Editor, LibroE2EditorFactory, LibroE2EditorOptions, + LibroE2EditorState, } from './libro-e2-editor.js'; import { loadE2 } from './libro-e2-preload.js'; import { LibroSQLRequestAPI } from './libro-sql-api.js'; @@ -26,9 +27,10 @@ export const LibroE2EditorModule = ManaModule.create() { token: LibroE2EditorFactory, useFactory: (ctx) => { - return (options: IEditorOptions) => { + return (options: IEditorOptions, editorState: EditorState) => { const child = ctx.container.createChild(); child.register({ token: LibroE2EditorOptions, useValue: options }); + child.register({ token: LibroE2EditorState, useValue: editorState }); return child.get(LibroE2Editor); }; }, diff --git a/packages/libro-cofine-textmate/src/monaco-textmate-service.ts b/packages/libro-cofine-textmate/src/monaco-textmate-service.ts index 849ddb2b1..8fd15d123 100644 --- a/packages/libro-cofine-textmate/src/monaco-textmate-service.ts +++ b/packages/libro-cofine-textmate/src/monaco-textmate-service.ts @@ -53,8 +53,21 @@ export class MonacoTextmateService implements EditorHandlerContribution { beforeCreate() { this.initialize(); } - afterCreate() { - // + afterCreate( + editor: monaco.editor.IStandaloneCodeEditor | monaco.editor.IStandaloneDiffEditor, + ) { + const toDispose = new DisposableCollection( + Disposable.create(() => { + /* mark as not disposed */ + }), + ); + // 激活语言必须在创建编辑器实例后 + const lang = (editor as monaco.editor.IStandaloneCodeEditor) + .getModel() + ?.getLanguageId(); + if (lang) { + this.doActivateLanguage(lang, toDispose); + } } canHandle() { return true; @@ -215,6 +228,14 @@ export class MonacoTextmateService implements EditorHandlerContribution { }); } } + // dont work + // return monaco.editor.onDidCreateEditor((editor) => { + // if (editor.getModel()?.getLanguageId() === language) { + // cb(); + // } + // }); + + // triggered on model create, too early if we create model before editor return monaco.languages.onLanguage(language, cb); } } diff --git a/packages/libro-core/src/libro-context-key.ts b/packages/libro-core/src/libro-context-key.ts index c5c01fb05..20972029f 100644 --- a/packages/libro-core/src/libro-context-key.ts +++ b/packages/libro-core/src/libro-context-key.ts @@ -11,7 +11,7 @@ export class LirboContextKey { protected readonly contextKeyService: ContextKeyService; protected readonly libroService: LibroService; protected toDisposeOnActiveChanged?: Disposable; - protected commandModeEnabled = true; + commandModeEnabled = true; protected isCommandMode = false; active: IContextKey; focus: IContextKey; diff --git a/packages/libro-core/src/libro-view.tsx b/packages/libro-core/src/libro-view.tsx index 637658244..99a34f662 100644 --- a/packages/libro-core/src/libro-view.tsx +++ b/packages/libro-core/src/libro-view.tsx @@ -224,6 +224,11 @@ export const LibroRender = forwardRef(function LibroRender(props if (typeof ref === 'function') { return; } + // focus编辑器host + if (!e.relatedTarget) { + return; + } + // focus编辑器外部区域 if (ref?.current?.contains(e.relatedTarget)) { const dndDom = ref?.current?.getElementsByClassName( 'libro-dnd-cells-container', @@ -653,11 +658,10 @@ export class LibroView extends BaseView implements NotebookView { this.runCells([cell]); } if (this.virtualizedManager.isVirtualized) { - setTimeout(() => { - if (this.activeCell) { - this.model.scrollToCellView({ cellIndex: this.activeCellIndex }); - } - }); + // 通过用户反馈,这里跳动会严重影响体验 + // setTimeout(() => { + // if (this.activeCell) this.model.scrollToCellView({ cellIndex: this.activeCellIndex }); + // }); } else { setTimeout(() => { if (this.activeCell) { diff --git a/packages/libro-jupyter/src/toolbar/index.less b/packages/libro-jupyter/src/toolbar/index.less index edce460d7..4a4e2ac8b 100644 --- a/packages/libro-jupyter/src/toolbar/index.less +++ b/packages/libro-jupyter/src/toolbar/index.less @@ -73,6 +73,11 @@ opacity: var(--mana-libro-kernel-text-opacity); } } + + .libro-kernel-status-checker { + margin-left: 8px; + color: var(--mana-libro-kernel-status-text); + } } .libro-container-and-service-status { diff --git a/packages/libro-kernel/src/libro-kernel-connection-manager.ts b/packages/libro-kernel/src/libro-kernel-connection-manager.ts index 21216d139..2669935e2 100644 --- a/packages/libro-kernel/src/libro-kernel-connection-manager.ts +++ b/packages/libro-kernel/src/libro-kernel-connection-manager.ts @@ -23,12 +23,16 @@ export class LibroKernelConnectionManager { this.kernelConnectionMap = new Map(); } + private mpKey(fileInfo: IContentsModel) { + return fileInfo.path || fileInfo.name; // 优先用path作为key + } + async startNew(fileInfo: IContentsModel): Promise { const connection = await this.sessionManager.startNew(fileInfo); if (!connection) { throw new Error('start new kernel connection failed'); } - this.kernelConnectionMap.set(fileInfo.name, connection); + this.kernelConnectionMap.set(this.mpKey(fileInfo), connection); return connection; } @@ -43,20 +47,19 @@ export class LibroKernelConnectionManager { if (!connection) { throw new Error('change kernel connection failed'); } - this.kernelConnectionMap.set(fileInfo.name, connection); + this.kernelConnectionMap.set(this.mpKey(fileInfo), connection); return connection; } async shutdownKC(fileInfo: IContentsModel) { - const fileName = fileInfo.name; - const kc = this.kernelConnectionMap.get(fileName); + const kc = this.kernelConnectionMap.get(this.mpKey(fileInfo)); if (!kc) { throw new Error('interrupt connection failed'); } await this.sessionManager.shutdownKC(fileInfo, kc); - this.kernelConnectionMap.delete(fileName); + this.kernelConnectionMap.delete(this.mpKey(fileInfo)); } getAllKernelConnections() { @@ -64,7 +67,7 @@ export class LibroKernelConnectionManager { } getKernelConnection(fileInfo: IContentsModel) { - const connection = this.kernelConnectionMap.get(fileInfo.name); + const connection = this.kernelConnectionMap.get(this.mpKey(fileInfo)); if (connection && !connection.isDisposed) { return connection; } diff --git a/packages/libro-kernel/src/session/libro-session-manager.ts b/packages/libro-kernel/src/session/libro-session-manager.ts index 64e0e4dd7..bd64cc0db 100644 --- a/packages/libro-kernel/src/session/libro-session-manager.ts +++ b/packages/libro-kernel/src/session/libro-session-manager.ts @@ -181,7 +181,7 @@ export class LibroSessionManager { kernel: { kernelName: reuseKernelInfo ? reuseKernelInfo.name - : fileInfo.content.metadata.kernelspec?.name || firstKernelSpecName, // 使用ipynb文件原本的kernel name,或者使用kernel spec轮询得到的第一个kernel name + : fileInfo.content?.metadata?.kernelspec?.name || firstKernelSpecName, // 使用ipynb文件原本的kernel name,或者使用kernel spec轮询得到的第一个kernel name }, path: fileInfo.path, type: fileInfo.type, @@ -328,7 +328,7 @@ export class LibroSessionManager { { name: fileInfo.name, kernel: { - kernelName: options.name || fileInfo.content.metadata.kernelspec.name, + kernelName: options.name || fileInfo.content?.metadata?.kernelspec.name, }, path: fileInfo.path, type: fileInfo.type, diff --git a/packages/libro-markdown-cell/src/markdown-cell-view.tsx b/packages/libro-markdown-cell/src/markdown-cell-view.tsx index 63ef084db..b01cec724 100644 --- a/packages/libro-markdown-cell/src/markdown-cell-view.tsx +++ b/packages/libro-markdown-cell/src/markdown-cell-view.tsx @@ -144,6 +144,7 @@ export class MarkdownCellView extends LibroEditorCellView implements CellCollaps protected getEditorOption(): CodeEditorViewOptions { const option: CodeEditorViewOptions = { + uuid: `${this.parent.model.id}-${this.model.id}`, model: this.model, config: { lineNumbers: false, @@ -171,11 +172,14 @@ export class MarkdownCellView extends LibroEditorCellView implements CellCollaps const editorView = await this.codeEditorManager.getOrCreateEditorView(option); this.editorView = editorView; - await editorView.editorReady; this.editorStatus = EditorStatus.LOADED; - await this.afterEditorReady(); + editorView.onEditorStatusChange((e) => { + if (e.status === 'ready') { + this.afterEditorReady(); + } + }); } protected async afterEditorReady() { diff --git a/packages/libro-prompt-cell/src/prompt-cell-view.tsx b/packages/libro-prompt-cell/src/prompt-cell-view.tsx index c110aeab3..ad0adde8c 100644 --- a/packages/libro-prompt-cell/src/prompt-cell-view.tsx +++ b/packages/libro-prompt-cell/src/prompt-cell-view.tsx @@ -17,6 +17,7 @@ import { LibroOutputArea, LibroViewTracker, EditorStatus, + LirboContextKey, } from '@difizen/libro-core'; import type { ExecutionMeta, KernelMessage } from '@difizen/libro-jupyter'; import { KernelError, LibroJupyterModel } from '@difizen/libro-jupyter'; @@ -145,6 +146,7 @@ const PropmtEditorViewComponent = React.forwardRef( @transient() @view('prompt-editor-cell-view') export class LibroPromptCellView extends LibroExecutableCellView { + @inject(LirboContextKey) protected readonly lirboContextKey: LirboContextKey; override view = PropmtEditorViewComponent; declare model: LibroPromptCellModel; @@ -169,12 +171,6 @@ export class LibroPromptCellView extends LibroExecutableCellView { return this.outputAreaDeferred.promise; } - protected editorViewReadyDeferred: Deferred = new Deferred(); - - get editorReady() { - return this.editorViewReadyDeferred.promise; - } - constructor( @inject(ViewOption) options: CellViewOptions, @inject(CellService) cellService: CellService, @@ -237,6 +233,7 @@ export class LibroPromptCellView extends LibroExecutableCellView { protected getEditorOption(): CodeEditorViewOptions { const option: CodeEditorViewOptions = { + uuid: `${this.parent.model.id}-${this.model.id}`, editorHostId: this.parent.id + this.id, model: this.model, config: { @@ -260,7 +257,6 @@ export class LibroPromptCellView extends LibroExecutableCellView { const editorView = await this.codeEditorManager.getOrCreateEditorView(option); this.editorView = editorView; - this.editorViewReadyDeferred.resolve(); this.editorStatus = EditorStatus.LOADED; await this.afterEditorReady(); @@ -289,37 +285,25 @@ export class LibroPromptCellView extends LibroExecutableCellView { this.editorView?.editor?.setOption('highlightActiveLineGutter', false); }; + protected focusEditor() { + //选中cell、编辑模式、非只读时才focus + if ( + this.editorView?.editor && + this.editorView.editorStatus === 'ready' && + this.parent.model.active?.id === this.id && + !this.parent.model.commandMode && + this.lirboContextKey.commandModeEnabled === true && // 排除弹窗等情况 + this.parent.model.readOnly === false + ) { + this.editorView?.editor.setOption('styleActiveLine', true); + this.editorView?.editor.setOption('highlightActiveLineGutter', true); + this.editorView?.editor.focus(); + } + } + override focus = (toEdit: boolean) => { if (toEdit) { - if (this.parent.model.readOnly === true) { - return; - } - if (!this.editorView) { - this.editorReady - .then(async () => { - await this.editorView?.editorReady; - this.editorView?.editor?.setOption('styleActiveLine', true); - this.editorView?.editor?.setOption('highlightActiveLineGutter', true); - if (this.editorView?.editor?.hasFocus()) { - return; - } - this.editorView?.editor?.focus(); - return; - }) - .catch(() => { - // - }); - } else { - if (!this.editorView?.editor) { - return; - } - this.editorView.editor.setOption('styleActiveLine', true); - this.editorView.editor.setOption('highlightActiveLineGutter', true); - if (this.editorView.editor.hasFocus()) { - return; - } - this.editorView.editor.focus(); - } + this.focusEditor(); } else { if (this.container?.current?.parentElement?.contains(document.activeElement)) { return; diff --git a/packages/libro-raw-cell/src/raw-cell-view.tsx b/packages/libro-raw-cell/src/raw-cell-view.tsx index 360c9bf8b..88b55d9d7 100644 --- a/packages/libro-raw-cell/src/raw-cell-view.tsx +++ b/packages/libro-raw-cell/src/raw-cell-view.tsx @@ -3,9 +3,8 @@ import type { CodeEditorViewOptions, CodeEditorView } from '@difizen/libro-code-editor'; import { CodeEditorManager } from '@difizen/libro-code-editor'; import type { CellViewOptions } from '@difizen/libro-core'; -import { CellService, LibroEditorCellView } from '@difizen/libro-core'; +import { CellService, LibroEditorCellView, LirboContextKey } from '@difizen/libro-core'; import { getOrigin, prop, useInject, watch } from '@difizen/mana-app'; -import { Deferred } from '@difizen/mana-app'; import { view, ViewInstance, @@ -44,6 +43,7 @@ const CodeEditorViewComponent = React.forwardRef( @transient() @view('raw-cell-view') export class LibroRawCellView extends LibroEditorCellView { + @inject(LirboContextKey) protected readonly lirboContextKey: LirboContextKey; declare model: LibroRawCellModel; override view = CodeEditorViewComponent; @@ -54,12 +54,6 @@ export class LibroRawCellView extends LibroEditorCellView { @prop() editorView?: CodeEditorView; - protected editorViewReadyDeferred: Deferred = new Deferred(); - - get editorReady() { - return this.editorViewReadyDeferred.promise; - } - constructor( @inject(ViewOption) options: CellViewOptions, @inject(CellService) cellService: CellService, @@ -82,6 +76,8 @@ export class LibroRawCellView extends LibroEditorCellView { protected getEditorOption(): CodeEditorViewOptions { const option: CodeEditorViewOptions = { + uuid: `${this.parent.model.id}-${this.model.id}`, + editorHostId: this.parent.id + this.id, model: this.model, config: { readOnly: this.parent.model.readOnly, @@ -102,7 +98,6 @@ export class LibroRawCellView extends LibroEditorCellView { const editorView = await this.codeEditorManager.getOrCreateEditorView(option); this.editorView = editorView; - await editorView.editorReady; await this.afterEditorReady(); } @@ -124,30 +119,25 @@ export class LibroRawCellView extends LibroEditorCellView { // }; + protected focusEditor() { + //选中cell、编辑模式、非只读时才focus + if ( + this.editorView?.editor && + this.editorView.editorStatus === 'ready' && + this.parent.model.active?.id === this.id && + !this.parent.model.commandMode && + this.lirboContextKey.commandModeEnabled === true && // 排除弹窗等情况 + this.parent.model.readOnly === false + ) { + this.editorView?.editor.setOption('styleActiveLine', true); + this.editorView?.editor.setOption('highlightActiveLineGutter', true); + this.editorView?.editor.focus(); + } + } + override focus = (toEdit: boolean) => { if (toEdit) { - if (this.parent.model.readOnly === true) { - return; - } - if (!this.editorView) { - this.editorReady - .then(() => { - this.editorView?.editorReady.then(() => { - if (this.editorView?.editor.hasFocus()) { - return; - } - this.editorView?.editor.focus(); - return; - }); - return; - }) - .catch(console.error); - } else { - if (this.editorView?.editor.hasFocus()) { - return; - } - this.editorView?.editor.focus(); - } + this.focusEditor(); } else { if (this.container?.current?.parentElement?.contains(document.activeElement)) { return; diff --git a/packages/libro-search-code-cell/src/code-cell-search-provider.ts b/packages/libro-search-code-cell/src/code-cell-search-provider.ts index e0d28eecb..e520167a0 100644 --- a/packages/libro-search-code-cell/src/code-cell-search-provider.ts +++ b/packages/libro-search-code-cell/src/code-cell-search-provider.ts @@ -33,9 +33,11 @@ export class CodeCellSearchProvider extends CodeEditorCellSearchProvider { this.currentProviderIndex = -1; this.outputsProvider = []; this.setupOutputProvider(); - this.cell.outputArea.onUpdate(() => { - this.setupOutputProvider(); - }); + this.toDispose.push( + this.cell.outputArea.onUpdate(() => { + this.setupOutputProvider(); + }), + ); this.toDispose.push( watch(this.cell.model, 'hasOutputHidden', async () => { @@ -220,7 +222,7 @@ export class CodeCellSearchProvider extends CodeEditorCellSearchProvider { return this.genericSearchProviderFactory({ view: output }); }); if (this.isActive && this.query && this.filters?.searchCellOutput !== false) { - this.refresh(); + await this.refresh(); } this.stateChangedEmitter.fire(); }; diff --git a/packages/libro-search-code-cell/src/code-editor-cell-search-provider.ts b/packages/libro-search-code-cell/src/code-editor-cell-search-provider.ts index a3afb2684..b79944756 100644 --- a/packages/libro-search-code-cell/src/code-editor-cell-search-provider.ts +++ b/packages/libro-search-code-cell/src/code-editor-cell-search-provider.ts @@ -3,6 +3,7 @@ import type { IPosition, SearchMatch } from '@difizen/libro-code-editor'; import type { BaseSearchProvider, SearchFilters } from '@difizen/libro-search'; import { searchText } from '@difizen/libro-search'; import type { Event } from '@difizen/mana-app'; +import { Disposable } from '@difizen/mana-app'; import { prop } from '@difizen/mana-app'; import { DisposableCollection, Emitter } from '@difizen/mana-app'; import { watch } from '@difizen/mana-app'; @@ -17,7 +18,7 @@ import { CodeEditorSearchHighlighterFactory } from './code-cell-search-protocol. export class CodeEditorCellSearchProvider implements BaseSearchProvider { protected toDispose = new DisposableCollection(); /** - * CodeMirror search highlighter + * code editor search highlighter */ @prop() protected editorHighlighter: CodeEditorSearchHighlighter; /** @@ -58,14 +59,21 @@ export class CodeEditorCellSearchProvider implements BaseSearchProvider { this.toDispose.push(watch(this.cell.model, 'value', this.updateMatches)); this.toDispose.push( - watch(this.cell, 'editor', async () => { - await this.cell.editorReady; - if (this.cell.hasInputHidden === true) { - this.endQuery(); - } else { - this.startQuery(this.query, this.filters); + this.cell.editorView?.onEditorStatusChange((e) => { + if (e.status === 'ready') { + const editor = this.cell.editorView?.editor; + if (editor) { + this.editorHighlighter.setEditor(editor); + } + if (e.prevState === 'init') { + if (this.cell.hasInputHidden === true) { + this.endQuery(); + } else { + this.startQuery(this.query, this.filters); + } + } } - }), + }) ?? Disposable.NONE, ); } @@ -86,8 +94,7 @@ export class CodeEditorCellSearchProvider implements BaseSearchProvider { } protected async setEditor() { - await this.cell.editorReady; - if (this.cell.editor) { + if (this.cell.editor && this.cell.editorView?.editorStatus === 'ready') { this.editorHighlighter.setEditor(this.cell.editor); } } @@ -363,11 +370,19 @@ export class CodeEditorCellSearchProvider implements BaseSearchProvider { const matches = await searchText(this.query, this.cell.model.value); this.editorHighlighter.matches = matches; if (this.isCellSelected) { - const cursorOffset = this.cell.editor!.getOffsetAt( + const cursorOffset = this.cell.editor?.getOffsetAt( this.cell.editor?.getCursorPosition() ?? { column: 0, line: 0 }, ); - const index = matches.findIndex((item) => item.position >= cursorOffset); - this.currentIndex = index; + if (cursorOffset === undefined) { + return; + } + const index = matches.findIndex( + (item) => item.position + item.text.length >= cursorOffset, + ); + if (index >= 0) { + this.currentIndex = index; + this.editorHighlighter.currentIndex = index; + } } } else { this.editorHighlighter.matches = []; diff --git a/packages/libro-search/src/index.less b/packages/libro-search/src/index.less index 96b1ea032..194db07e9 100644 --- a/packages/libro-search/src/index.less +++ b/packages/libro-search/src/index.less @@ -97,10 +97,10 @@ } .libro-selectedtext { - background-color: rgb(255, 225, 0); + background-color: rgb(168, 172, 149) !important; span { - background-color: rgb(255, 225, 0); + background-color: rgb(168, 172, 149) !important; } } diff --git a/packages/libro-search/src/libro-search-generic-provider.ts b/packages/libro-search/src/libro-search-generic-provider.ts index 22c1e84b7..8599a36d9 100644 --- a/packages/libro-search/src/libro-search-generic-provider.ts +++ b/packages/libro-search/src/libro-search-generic-provider.ts @@ -23,7 +23,7 @@ export const GenericSearchProviderFactory = Symbol('GenericSearchProviderFactory @transient() export class GenericSearchProvider extends AbstractSearchProvider { protected _query: RegExp | null; - protected _currentMatchIndex: number; + @prop() protected _currentMatchIndex: number; @prop() protected _matches: HTMLSearchMatch[] = []; protected _mutationObserver: MutationObserver = new MutationObserver( this._onWidgetChanged.bind(this), @@ -159,6 +159,24 @@ export class GenericSearchProvider extends AbstractSearchProvider { return Promise.resolve(false); } + isMatchChanged(matches: HTMLSearchMatch[], newMatches: HTMLSearchMatch[]): boolean { + if (matches.length !== newMatches.length) { + return true; + } + for (let i = 0; i < matches.length; i++) { + if (matches[i].text !== newMatches[i].text) { + return true; + } + if (matches[i].position !== newMatches[i].position) { + return true; + } + if (matches[i].node !== newMatches[i].node) { + return true; + } + } + return false; + } + /** * Initialize the search using the provided options. Should update the UI * to highlight all matches and "select" whatever the first match should be. @@ -167,10 +185,10 @@ export class GenericSearchProvider extends AbstractSearchProvider { * @param filters Filter parameters to pass to provider */ startQuery = async (query: RegExp | null, filters = {}): Promise => { - await this.endQuery(); this._query = query; if (query === null) { + await this.endQuery(); return Promise.resolve(); } @@ -178,6 +196,12 @@ export class GenericSearchProvider extends AbstractSearchProvider { ? await searchInHTML(query, this.view.container?.current) : []; + if (!this.isMatchChanged(this.matches, matches)) { + return Promise.resolve(); + } + + await this.endQuery(); + // Transform the DOM let nodeIdx = 0; while (nodeIdx < matches.length) { diff --git a/packages/libro-search/src/libro-search-provider.ts b/packages/libro-search/src/libro-search-provider.ts index 4a487b8eb..ee72ad455 100644 --- a/packages/libro-search/src/libro-search-provider.ts +++ b/packages/libro-search/src/libro-search-provider.ts @@ -44,7 +44,7 @@ export class LibroSearchProvider extends AbstractSearchProvider { protected toDispose = new DisposableCollection(); @prop() protected currentProviderIndex: number | undefined = undefined; - @prop() searchCellOutput = true; + @prop() searchCellOutput = false; @prop() protected onlySearchSelectedCells = false; @prop() replaceMode = false; diff --git a/packages/libro-search/src/libro-search-view.tsx b/packages/libro-search/src/libro-search-view.tsx index 4bde0e654..285489ed9 100644 --- a/packages/libro-search/src/libro-search-view.tsx +++ b/packages/libro-search/src/libro-search-view.tsx @@ -8,7 +8,7 @@ import { } from '@ant-design/icons'; import type { LibroView } from '@difizen/libro-core'; import { LirboContextKey } from '@difizen/libro-core'; -import { prop, useInject, watch } from '@difizen/mana-app'; +import { prop, useInject, useObserve, watch } from '@difizen/mana-app'; import { BaseView, view, ViewInstance } from '@difizen/mana-app'; import { inject, transient } from '@difizen/mana-app'; import { l10n } from '@difizen/mana-l10n'; @@ -16,7 +16,7 @@ import { Button, Checkbox, Input, Tag } from 'antd'; import type { CheckboxChangeEvent } from 'antd/es/checkbox'; import type { InputRef } from 'antd/es/input'; import classnames from 'classnames'; -import { forwardRef, useEffect, useRef } from 'react'; +import { forwardRef, memo, useEffect, useRef } from 'react'; import type { LibroSearchProvider } from './libro-search-provider.js'; import { LibroSearchProviderFactory } from './libro-search-provider.js'; @@ -40,23 +40,26 @@ export const ReplaceToggle = () => { ); }; -export const SearchIndex = () => { +export const SearchIndex: React.FC = () => { const instance = useInject(ViewInstance); + const currentMatchIndex = useObserve(instance.currentMatchIndex); + const matchesCount = useObserve(instance.matchesCount); - // TODO: trigger update when current match index changed, matchesCount dont work - useEffect(() => { - // - }, [instance.currentMatchIndex]); + if (instance.isSearching) { + return <>searching...; + } return (
{instance.matchesCount !== undefined - ? `${instance.currentMatchIndex}/${instance.matchesCount}` + ? `${currentMatchIndex ?? '-'}/${matchesCount ?? ' -'}` : '无结果'}
); }; +export const SearchIndexMemo = memo(SearchIndex); + export const SearchContent = () => { const instance = useInject(ViewInstance); const findInputRef = useRef(null); @@ -114,7 +117,7 @@ export const SearchContent = () => { /> - +