@eclipse-glsp/client
Version:
A sprotty-based client for GLSP
276 lines (250 loc) • 11.9 kB
text/typescript
/********************************************************************************
* Copyright (c) 2020-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
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import {
Action,
ActionHandlerRegistry,
Args,
CenterAction,
IActionDispatcher,
IActionHandler,
ICommand,
ILogger,
MessageAction,
NavigateToExternalTargetAction,
NavigateToTargetAction,
NavigationTarget,
RequestNavigationTargetsAction,
SelectAction,
SelectAllAction,
SetNavigationTargetsAction,
SetResolvedNavigationTargetAction,
SeverityLevel,
StatusAction,
TYPES,
hasObjectProp,
hasStringProp
} from '@eclipse-glsp/sprotty';
import { inject, injectable } from 'inversify';
import { EditorContextService, EditorContextServiceProvider } from '../../base/editor-context-service';
import { NavigationTargetResolver } from './navigation-target-resolver';
/**
* Action for triggering a navigation of a certain target type.
*
* Examples for target types could be `documentation`, `implementation`, etc.
* but this is up to the domain-specific diagram implementation to decide.
* Such an action will eventually trigger a `RequestNavigationTargetsAction`
* (see `NavigationActionHandler`) in order to request the navigation targets
* from the server.
*
* This action is typically triggered by a user action.
*/
export interface NavigateAction extends Action {
kind: typeof NavigateAction.KIND;
/**
* Navigation target type, such as `documentation`, `implementation`, etc.
*/
targetTypeId: string;
/**
* Additional arguments for customization.
*/
args?: Args;
}
export namespace NavigateAction {
export const KIND = 'navigate';
export function is(object: any): object is NavigateAction {
return Action.hasKind(object, KIND) && hasStringProp(object, 'targetTypeId');
}
export function create(targetTypeId: string, options: { args?: Args } = {}): NavigateAction {
return {
kind: KIND,
targetTypeId,
...options
};
}
}
/**
* Action to trigger the processing of additional navigation arguments.
* The resolution of a `NavigationTarget` may entail additional arguments. In this case, this action is
* triggered allowing the client to react to those arguments. The default `NavigationActionHandler` will
* only process the arguments' keys `info`, `warning`, and `error` to present them to the user.
* Customizations, however, may add domain-specific arguments and register custom handler to also process
* other arguments and trigger some additional behavior (e.g. update other views, etc.).
*/
export interface ProcessNavigationArgumentsAction extends Action {
kind: typeof ProcessNavigationArgumentsAction.KIND;
/**
* The navigation arguments.
*/
args: Args;
}
export namespace ProcessNavigationArgumentsAction {
export const KIND = 'processNavigationArguments';
export function is(object: any): object is ProcessNavigationArgumentsAction {
return Action.hasKind(object, KIND) && hasObjectProp(object, 'args');
}
export function create(args: Args): ProcessNavigationArgumentsAction {
return {
kind: KIND,
args
};
}
}
/**
* Default handler for all actions that are related to the navigation.
*
* For a `NavigateAction` this handler triggers a `RequestNavigationTargetAction` to obtain the actual
* navigation targets for the navigation type that is specified in the `NavigateAction`.
* Once the navigation targets are available, it will trigger a `NavigateToTargetAction` to actually
* perform the navigation.
*
* In other scenarios, clients may also trigger the `NavigateToTargetAction` directly, e.g. when opening
* the diagram.
*
* Depending on the URI and arguments of the navigation target we may encounter three cases:
* *(a)* the navigation target already specifies element IDs, in which case this action handler navigates
* to the specified elements directly, by the selecting them and centering them in the viewport.
* *(b)* the arguments of the navigation targets don't contain element IDs, but other arguments, the
* navigation target needs to be resolved into actual element IDs by the `NavigationTargetResolver`.
* This can for instance be useful, if the navigation deals with queries or some other more complex
* logic that can't be dealt with on the client.
* *(c)* the target isn't resolved by the `NavigationTargetResolver`, e.g. because the `uri` doesn't match
* the URI of the current diagram. In this case, the navigation request is forwarded by dispatching
* a `NavigateToExternalTargetAction`.
*/
export class NavigationActionHandler implements IActionHandler {
readonly notificationTimeout = 5000;
protected logger: ILogger;
protected dispatcher: IActionDispatcher;
/** @deprecated No longer in used. The {@link ActionHandlerRegistry} is now directly injected */
// eslint-disable-next-line deprecation/deprecation
protected actionHandlerRegistryProvider: () => Promise<ActionHandlerRegistry>;
/** @deprecated No longer in used. The {@link EditorContextService} is now directly injected */
// eslint-disable-next-line deprecation/deprecation
protected editorContextService: EditorContextServiceProvider;
protected actionHandlerRegistry: ActionHandlerRegistry;
protected resolver: NavigationTargetResolver;
protected editorContext: EditorContextService;
handle(action: Action): ICommand | Action | void {
if (NavigateAction.is(action)) {
this.handleNavigateAction(action);
} else if (NavigateToTargetAction.is(action)) {
this.handleNavigateToTarget(action);
} else if (ProcessNavigationArgumentsAction.is(action)) {
this.processNavigationArguments(action.args);
} else if (NavigateToExternalTargetAction.is(action)) {
this.handleNavigateToExternalTarget(action);
}
}
protected async handleNavigateAction(action: NavigateAction): Promise<void> {
try {
const editorContext = this.editorContext.get(action.args);
const response = await this.dispatcher.request(
RequestNavigationTargetsAction.create({ targetTypeId: action.targetTypeId, editorContext })
);
if (SetNavigationTargetsAction.is(response) && response.targets && response.targets.length === 1) {
if (response.targets.length > 1) {
this.logger.warn(
this,
'Processing of multiple targets is not supported yet. ' + 'Only the first is being processed.',
response.targets
);
}
return this.dispatcher.dispatch(NavigateToTargetAction.create(response.targets[0]));
}
this.warnAboutFailedNavigation('No valid navigation target found');
} catch (reason) {
this.logger.error(this, 'Failed to obtain navigation target', reason, action);
}
}
protected async handleNavigateToTarget(action: NavigateToTargetAction): Promise<void> {
try {
const resolvedElements = await this.resolveElements(action);
if (this.containsElementIdsOrArguments(resolvedElements)) {
this.navigateTo(resolvedElements);
this.handleResolutionArguments(resolvedElements);
return;
} else {
this.navigateToExternal(action.target);
return;
}
} catch (reason) {
this.logger.error(this, 'Failed to navigate', reason, action);
}
}
protected resolveElements(action: NavigateToTargetAction): Promise<SetResolvedNavigationTargetAction | undefined> {
return this.resolver.resolve(action.target);
}
protected containsElementIdsOrArguments(
target: SetResolvedNavigationTargetAction | undefined
): target is SetResolvedNavigationTargetAction {
return target !== undefined && (this.containsElementIds(target.elementIds) || this.containsArguments(target.args));
}
protected containsElementIds(elementIds: string[] | undefined): elementIds is string[] {
return elementIds !== undefined && elementIds.length > 0;
}
protected containsArguments(args: Args | undefined): args is Args {
return args !== undefined && args !== undefined && Object.keys(args).length > 0;
}
protected navigateTo(target: SetResolvedNavigationTargetAction): void {
const elementIds = target.elementIds;
if (!this.containsElementIds(elementIds)) {
return;
}
this.dispatcher.dispatchAll([
SelectAllAction.create(false),
SelectAction.create({ selectedElementsIDs: elementIds }),
CenterAction.create(elementIds)
]);
}
protected handleResolutionArguments(target: SetResolvedNavigationTargetAction): void {
const args = target.args;
if (!this.containsArguments(args)) {
return;
}
this.dispatcher.dispatch(ProcessNavigationArgumentsAction.create(args));
}
protected navigateToExternal(target: NavigationTarget): Promise<void> {
return this.dispatcher.dispatch(NavigateToExternalTargetAction.create(target));
}
protected processNavigationArguments(args: Args): void {
if (args.info && args.info.toString().length > 0) {
this.notify('INFO', args.info.toString());
}
if (args.warning && args.warning.toString().length > 0) {
this.notify('WARNING', args.warning.toString());
}
if (args.error && args.error.toString().length > 0) {
this.notify('ERROR', args.error.toString());
}
}
protected async handleNavigateToExternalTarget(action: NavigateToExternalTargetAction): Promise<void> {
const handlers = this.actionHandlerRegistry.get(NavigateToExternalTargetAction.KIND);
if (handlers.length === 1) {
// we are the only handler so we know nobody took care of it
this.warnAboutFailedNavigation('Could not resolve or navigate to target', action.target);
}
}
protected warnAboutFailedNavigation(msg: string, target?: NavigationTarget): void {
const message = `${msg}` + (target ? `: '${target.uri}' (arguments: ${JSON.stringify(target.args)})` : '');
this.logger.warn(this, msg, target);
this.notify('WARNING', message);
}
private notify(severity: SeverityLevel, message: string): void {
const timeout = this.notificationTimeout;
this.dispatcher.dispatchAll([StatusAction.create(message, { severity, timeout }), MessageAction.create(message, { severity })]);
}
}