Skip to content

Commit

Permalink
[Refactor] unified event handling of program input and user input by …
Browse files Browse the repository at this point in the history
…introducing `IEditorInputEmulator`
  • Loading branch information
Bistard committed Feb 22, 2025
1 parent 19331a2 commit dcd8627
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 45 deletions.
8 changes: 8 additions & 0 deletions src/editor/common/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,12 @@ export interface IEditorView extends IProseEventBroadcaster {
* The actual editor instance.
*/
readonly editor: RichTextView;

/**
* // TODO
* @param text
* @param from
* @param to
*/
type(text: string, from?: number, to?: number): void;
}
112 changes: 74 additions & 38 deletions src/editor/editorWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ILogService } from "src/base/common/logger";
import { Constructor, isDefined } from "src/base/common/utilities/type";
import { IInstantiationService } from "src/platform/instantiation/common/instantiation";
import { IBrowserLifecycleService, ILifecycleService } from "src/platform/lifecycle/browser/browserLifecycleService";
import { IEditorModel } from "src/editor/common/model";
import { IEditorModel, IModelBuildData } from "src/editor/common/model";
import { EditorType, IEditorView } from "src/editor/common/view";
import { BasicEditorOption, EDITOR_OPTIONS_DEFAULT, EditorOptionsType, IEditorWidgetOptions, toJsonEditorOption } from "src/editor/common/editorConfiguration";
import { EditorModel } from "src/editor/model/editorModel";
Expand All @@ -20,7 +20,8 @@ import { assert, errorToMessage } from "src/base/common/utilities/panic";
import { AsyncResult, err, ok, Result } from "src/base/common/result";
import { EditorDragState } from "src/editor/common/cursorDrop";
import { EditorViewModel } from "src/editor/viewModel/editorViewModel";
import { IEditorViewModel } from "src/editor/common/viewModel";
import { IEditorViewModel, IViewModelBuildData } from "src/editor/common/viewModel";
import { IEditorInputEmulator } from "src/editor/view/inputEmulator";

// region - [interface]

Expand All @@ -35,7 +36,11 @@ export interface IEditorWidget extends
| 'onDidDirtyChange'
| 'onDidSave'
| 'onDidSaveError'
| 'save'>
| 'save'
>,
Pick<IEditorView,
| 'type'
>
{
/**
* Is the editor initialized. if not, access to model, viewModel and view
Expand Down Expand Up @@ -322,45 +327,24 @@ export class EditorWidget extends Disposable implements IEditorWidget {
this.__detachData();

// model
this._model = this.instantiationService.createInstance(
EditorModel,
source,
this._options.getOptions(),
);
const build = await this._model.build();
return (await this.__createModel(source)).andThen(([model, modelData]) => {
this._model = model;
const extensions = this._extensions.getExtensions();

// unexpected behavior, we need to let the user know.
if (build.isErr()) {
const error = new Error(`Cannot open editor at '${URI.toFsPath(source)}'. ${errorToMessage(build.unwrapErr(), false)}`);
return err(error);
}
const modelData = build.unwrap();
// view-model
this._viewModel = this.__createViewModel(extensions);
const viewModelData = this._viewModel.build(modelData);

// view-model
const extensionList = this._extensions.getExtensions();
this._viewModel = this.instantiationService.createInstance(
EditorViewModel,
this._model,
extensionList,
);
const viewData = this._viewModel.build(modelData);
// view
this._view = this.__createView(extensions, viewModelData);

// view
this._view = this.instantiationService.createInstance(
EditorView,
this._container.raw,
this._viewModel,
viewData.state,
extensionList,
this._options.getOptions(),
);
// listeners
this.__registerMVVMListeners(this._model, this._viewModel, this._view);

// listeners
this.__registerMVVMListeners(this._model, this._viewModel, this._view);

// cache data
this._editorData = this.__register(new EditorData(this._model, this._viewModel, this._view, undefined));
return ok();
// cache data
this._editorData = this.__register(new EditorData(this._model, this._viewModel, this._view, undefined));
return ok();
});
}

public isOpened(): boolean {
Expand Down Expand Up @@ -410,6 +394,16 @@ export class EditorWidget extends Disposable implements IEditorWidget {
return this.dispose();
}

// region - [viewModel]



// region - [View]

public type(text: string, from?: number, to?: number): void {
this.view.type(text, from, to);
}

// region - [private]

private __detachData(): void {
Expand All @@ -431,6 +425,48 @@ export class EditorWidget extends Disposable implements IEditorWidget {
return assert(this._view, '[EditorWidget] EditorView is not initialized.');
}

private async __createModel(source: URI): Promise<Result<[EditorModel, IModelBuildData], Error>> {
const model = this.instantiationService.createInstance(
EditorModel,
source,
this._options.getOptions(),
);
const build = await model.build();

// unexpected behavior, we need to let the user know.
if (build.isErr()) {
const error = new Error(`Cannot open editor at '${URI.toFsPath(source)}'. ${errorToMessage(build.unwrapErr(), false)}`);
return err(error);
}
const modelData = build.unwrap();

return ok([model, modelData]);
}

private __createViewModel(extensions: EditorExtension[]): EditorViewModel {
return this.instantiationService.createInstance(
EditorViewModel,
this.model,
extensions,
);
}

private __createView(extensions: EditorExtension[], viewModelData: IViewModelBuildData): EditorView {
const inputEmulator: IEditorInputEmulator = {
type: e => this._onTextInput.fire(e),
};

return this.instantiationService.createInstance(
EditorView,
this._container.raw,
this.viewModel,
viewModelData.state,
extensions,
inputEmulator,
this._options.getOptions(),
);
}

private __registerListeners(): void {
this.__register(this.lifecycleService.onBeforeQuit(() => this._options.saveOptions()));

Expand Down
11 changes: 7 additions & 4 deletions src/editor/view/editorView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { IEditorModel } from 'src/editor/common/model';
import { ProseEditorState } from 'src/editor/common/proseMirror';
import { IEditorViewModel } from 'src/editor/common/viewModel';
import { RichTextView } from 'src/editor/view/richTextView';
import { IEditorInputEmulator } from 'src/editor/view/inputEmulator';
import { IInstantiationService, InstantiationService } from 'src/platform/instantiation/common/instantiation';

export class ViewContext {
constructor(
Expand Down Expand Up @@ -85,8 +87,10 @@ export class EditorView extends Disposable implements IEditorView {
viewModel: IEditorViewModel,
initState: ProseEditorState,
extensions: IEditorExtension[],
inputEmulator: IEditorInputEmulator,
options: EditorOptionsType,
@ILogService logService: ILogService,
@IInstantiationService instantiationService: IInstantiationService,
) {
super();

Expand All @@ -99,7 +103,7 @@ export class EditorView extends Disposable implements IEditorView {
// the centre that integrates the editor-related functionalities
const editorElement = document.createElement('div');
editorElement.className = 'editor-container';
this._view = this.__register(new RichTextView(editorElement, this._container, context, initState, extensions));
this._view = this.__register(instantiationService.createInstance(RichTextView, editorElement, this._container, context, initState, extensions, inputEmulator));

// forward: start listening events from view model
this.__registerEventFromViewModel();
Expand Down Expand Up @@ -144,9 +148,8 @@ export class EditorView extends Disposable implements IEditorView {
return this._view.isDestroyed();
}

public override dispose(): void {
super.dispose();
this._container.remove();
public type(text: string, from?: number, to?: number): void {
this._view.type(text, from, to);
}

// [private helper methods]
Expand Down
21 changes: 21 additions & 0 deletions src/editor/view/inputEmulator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { IOnTextInputEvent } from "src/editor/view/proseEventBroadcaster";


/**
* A delegate to simulate certain user's input programmatically.
*
* Consider the following difference:
* 1. The workflow of user input might be:
* Keyboard Event -> Event Broadcasting -> Extension Handing -> Document Updates
* 2. The workflow of program input:
* function call -> Document Updates
*
* This is a problem for program input, the event is not broadcasting, so the
* extensions and others cannot be notified.
*
* This why we need a delegate to emulate related-functions by also broadcasting
* them.
*/
export interface IEditorInputEmulator {
type(event: IOnTextInputEvent): void;
}
38 changes: 35 additions & 3 deletions src/editor/view/richTextView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { ProseEditorState, ProseEditorView } from "src/editor/common/proseMirror
import { ViewContext } from "src/editor/view/editorView";
import { EditorViewProxy, IEditorViewProxy } from "src/editor/view/editorViewProxy";
import { IEditorExtension } from 'src/editor/common/editorExtension';
import { IEditorInputEmulator } from 'src/editor/view/inputEmulator';
import { IOnTextInputEvent } from 'src/editor/view/proseEventBroadcaster';

/**
* An interface only for {@link RichTextView}.
Expand Down Expand Up @@ -33,6 +35,8 @@ export class RichTextView extends EditorViewProxy implements IRichTextView {
protected readonly _container: HTMLElement;
protected readonly _editorContainer: HTMLElement;
protected readonly _context: ViewContext;

private readonly _inputEmulator: IEditorInputEmulator;

// [constructor]

Expand All @@ -42,6 +46,7 @@ export class RichTextView extends EditorViewProxy implements IRichTextView {
context: ViewContext,
editorState: ProseEditorState,
extensions: IEditorExtension[],
inputEmulator: IEditorInputEmulator,
) {
overlayContainer.classList.add('editor-base', 'rich-text');

Expand All @@ -59,18 +64,45 @@ export class RichTextView extends EditorViewProxy implements IRichTextView {
this._editorContainer = overlayContainer;
this._container = domEventElement;
this._context = context;
this._inputEmulator = inputEmulator;

// init changes back to viewModel
// send latest data back to viewModel after initialization
context.viewModel.updateViewChange({
view: view,
transaction: view.state.tr,
});
}

// [public methods]

// [getter]
get container() { return this._container; }
get overlayContainer() { return this._editorContainer; }

// [public methods]

public type(text: string, from?: number, to?: number): void {
const { state } = this._view;
from ??= state.selection.from;
to ??= state.selection.to;
let prevented = false;

const event: IOnTextInputEvent = {
view: this._view,
text,
from,
to,
preventDefault: () => prevented = true,
};
this._inputEmulator.type(event);
if (prevented) {
return;
}

const tr = state.tr.insertText(text, from, to);
const newState = state.apply(tr);

this.render(newState);
}

// [private helper methods]
}

0 comments on commit dcd8627

Please sign in to comment.