Skip to content

Commit

Permalink
Various minor improvements
Browse files Browse the repository at this point in the history
- Merge *-default.ts implementation files of `FeedbackActionDispatcher` and `HelperLineManager` with the
  the interface definition files. The split is no longer necessary since we resolved potential circular dependency issues
- Ensure that we consistently use the `LazyInjector` over dedicated async providers and deprecate existing provider injection
- Move MaybeActions utility type from feedback-action-dispatcher into the protocol package to enable reuse + add testcases
  • Loading branch information
tortmayr committed Dec 5, 2024
1 parent dd011ea commit 483a189
Show file tree
Hide file tree
Showing 15 changed files with 425 additions and 417 deletions.
4 changes: 3 additions & 1 deletion packages/client/src/base/default.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { GLSPActionHandlerRegistry } from './action-handler-registry';
import { GLSPCommandStack } from './command-stack';
import { EditorContextService } from './editor-context-service';
import { ModifyCssFeedbackCommand } from './feedback/css-feedback';
import { FeedbackActionDispatcher } from './feedback/feedback-action-dispatcher-default';
import { FeedbackActionDispatcher } from './feedback/feedback-action-dispatcher';
import { FeedbackAwareSetModelCommand } from './feedback/set-model-command';
import { FeedbackAwareUpdateModelCommand } from './feedback/update-model-command';
import { FocusStateChangedAction } from './focus/focus-state-change-action';
Expand Down Expand Up @@ -72,6 +72,7 @@ export const defaultModule = new FeatureModule(

bind(EditorContextService).toSelf().inSingletonScope();
bind(TYPES.IDiagramStartup).toService(EditorContextService);
// eslint-disable-next-line deprecation/deprecation
bind(TYPES.IEditorContextServiceProvider).toProvider<EditorContextService>(
ctx => async () => ctx.container.get(EditorContextService)
);
Expand All @@ -80,6 +81,7 @@ export const defaultModule = new FeatureModule(
configureActionHandler(context, SetDirtyStateAction.KIND, EditorContextService);

bind(FocusTracker).toSelf().inSingletonScope();
bind(TYPES.IDiagramStartup).toService(FocusTracker);
configureActionHandler(context, FocusStateChangedAction.KIND, FocusTracker);

// Model update initialization ------------------------------------
Expand Down

This file was deleted.

125 changes: 114 additions & 11 deletions packages/client/src/base/feedback/feedback-action-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,29 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { Action, Command, CommandExecutionContext, Disposable, MaybeFunction, call, asArray as toArray } from '@eclipse-glsp/sprotty';
import {
Action,
ActionHandlerRegistry,
Command,
CommandActionHandler,
CommandExecutionContext,
Disposable,
GModelElement,
IActionDispatcher,
ICommand,
ILogger,
MaybeActions,
TYPES,
toTypeGuard
} from '@eclipse-glsp/sprotty';
import { inject, injectable, preDestroy } from 'inversify';
import { getFeedbackRank } from './feedback-command';
import { FeedbackEmitter } from './feedback-emitter';

export interface IFeedbackEmitter {}

export const feedbackFeature = Symbol('feedbackFeature');

export type MaybeActions = MaybeFunction<Action[] | Action | undefined>;

export namespace MaybeActions {
// eslint-disable-next-line @typescript-eslint/no-shadow
export function asArray(actions?: MaybeActions): Action[] {
const cleanup = actions ? call(actions) : [];
return cleanup ? toArray(cleanup) : [];
}
}

/**
* Dispatcher for actions that are meant to show visual feedback on
* the diagram that is not part of the diagram sent from the server
Expand Down Expand Up @@ -86,3 +92,100 @@ export interface IFeedbackActionDispatcher {
*/
createEmitter(): FeedbackEmitter;
}

@injectable()
export class FeedbackActionDispatcher implements IFeedbackActionDispatcher, Disposable {
protected registeredFeedback: Map<IFeedbackEmitter, Action[]> = new Map();

@inject(TYPES.IActionDispatcher) protected actionDispatcher: IActionDispatcher;

@inject(TYPES.ILogger) protected logger: ILogger;

@inject(ActionHandlerRegistry) protected actionHandlerRegistry: ActionHandlerRegistry;

protected isDisposed = false;

registerFeedback(feedbackEmitter: IFeedbackEmitter, feedbackActions: Action[], cleanupActions?: MaybeActions): Disposable {
if (feedbackEmitter instanceof GModelElement) {
this.logger.log(
this,
// eslint-disable-next-line max-len
'GModelElements as feedback emitters are discouraged, as they usually change between model updates and are considered unstable.'
);
}
if (feedbackActions.length > 0) {
this.registeredFeedback.set(feedbackEmitter, feedbackActions);
this.dispatchFeedback(feedbackActions, feedbackEmitter);
}
return Disposable.create(() => this.deregisterFeedback(feedbackEmitter, cleanupActions));
}

deregisterFeedback(feedbackEmitter: IFeedbackEmitter, cleanupActions?: MaybeActions): void {
this.registeredFeedback.delete(feedbackEmitter);
const actions = MaybeActions.asArray(cleanupActions);
if (actions.length > 0) {
this.dispatchFeedback(actions, feedbackEmitter);
}
}

getRegisteredFeedback(): Action[] {
const result: Action[] = [];
this.registeredFeedback.forEach(actions => result.push(...actions));
return result;
}

getRegisteredFeedbackEmitters(action: Action): IFeedbackEmitter[] {
const result: IFeedbackEmitter[] = [];
this.registeredFeedback.forEach((actions, emitter) => {
if (actions.includes(action)) {
result.push(emitter);
}
});
return result;
}

getFeedbackCommands(): Command[] {
return this.getRegisteredFeedback()
.flatMap(action => this.actionToCommands(action))
.sort((left, right) => getFeedbackRank(left) - getFeedbackRank(right));
}

async applyFeedbackCommands(context: CommandExecutionContext): Promise<void> {
const feedbackCommands = this.getFeedbackCommands() ?? [];
if (feedbackCommands?.length > 0) {
const results = feedbackCommands.map(command => command.execute(context));
await Promise.all(results);
}
}

protected actionToCommands(action: Action): ICommand[] {
return (
this.actionHandlerRegistry
.get(action.kind)
.filter(toTypeGuard(CommandActionHandler))
.map(handler => handler.handle(action)) ?? []
);
}

createEmitter(): FeedbackEmitter {
return new FeedbackEmitter(this);
}

protected async dispatchFeedback(actions: Action[], feedbackEmitter: IFeedbackEmitter): Promise<void> {
try {
if (this.isDisposed) {
return;
}
await this.actionDispatcher.dispatchAll(actions);
this.logger.info(this, `Dispatched feedback actions for ${feedbackEmitter}`);
} catch (reason) {
this.logger.error(this, 'Failed to dispatch feedback actions', reason);
}
}

@preDestroy()
dispose(): void {
this.registeredFeedback.clear();
this.isDisposed = true;
}
}
4 changes: 2 additions & 2 deletions packages/client/src/base/feedback/feedback-emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { Action, Disposable, arrayOf } from '@eclipse-glsp/sprotty';
import { IFeedbackActionDispatcher, IFeedbackEmitter, MaybeActions } from './feedback-action-dispatcher';
import { Action, Disposable, MaybeActions, arrayOf } from '@eclipse-glsp/sprotty';
import type { IFeedbackActionDispatcher, IFeedbackEmitter } from './feedback-action-dispatcher';

// counter for internal id, mainly useful for debugging
let idCounter = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/********************************************************************************
* Copyright (c) 2019-2023 EclipseSource and others.
* Copyright (c) 2019-2024 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
Expand All @@ -13,27 +13,31 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { DeleteElementOperation, GModelRoot, IContextMenuItemProvider, MenuItem, Point, TYPES } from '@eclipse-glsp/sprotty';
import { inject, injectable } from 'inversify';
import { DeleteElementOperation, IContextMenuItemProvider, MenuItem, Point, GModelRoot, TYPES } from '@eclipse-glsp/sprotty';
import { EditorContextService, EditorContextServiceProvider } from '../../base/editor-context-service';

@injectable()
export class DeleteElementContextMenuItemProvider implements IContextMenuItemProvider {
/** @deprecated No longer used. The {@link EditorContextService} is now directly injected.*/
// eslint-disable-next-line deprecation/deprecation
@inject(TYPES.IEditorContextServiceProvider) editorContextServiceProvider: EditorContextServiceProvider;

@inject(EditorContextService)
protected editorContext: EditorContextService;

async getItems(_root: Readonly<GModelRoot>, _lastMousePosition?: Point): Promise<MenuItem[]> {
const editorContextService = await this.editorContextServiceProvider();
return [this.createDeleteMenuItem(editorContextService)];
return [this.createDeleteMenuItem()];
}

protected createDeleteMenuItem(editorContextService: EditorContextService): MenuItem {
protected createDeleteMenuItem(): MenuItem {
return {
id: 'delete',
label: 'Delete',
sortString: 'd',
group: 'edit',
actions: [DeleteElementOperation.create(editorContextService.selectedElements.map(e => e.id))],
isEnabled: () => !editorContextService.isReadonly && editorContextService.selectedElements.length > 0
actions: [DeleteElementOperation.create(this.editorContext.selectedElements.map(e => e.id))],
isEnabled: () => !this.editorContext.isReadonly && this.editorContext.selectedElements.length > 0
};
}
}
Loading

0 comments on commit 483a189

Please sign in to comment.