@eclipse-glsp/client
Version:
A sprotty-based client for GLSP
165 lines (137 loc) • 7.14 kB
text/typescript
/********************************************************************************
* Copyright (c) 2023-2025 Business Informatics Group (TU Wien) 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 { GModelElement, GModelRoot, GNode, LabeledAction, SelectAllAction, toArray, TYPES } from '@eclipse-glsp/sprotty';
import { inject, injectable } from 'inversify';
import { debounce, isEqual } from 'lodash';
import {
AutocompleteSuggestionProviderContext,
type AutocompleteSuggestion,
type AutocompleteSuggestionRegistry
} from '../../base/auto-complete/autocomplete-suggestion-provider';
import { BaseAutocompletePalette } from '../../base/auto-complete/base-autocomplete-palette';
import { applyCssClasses, deleteCssClasses } from '../../base/feedback/css-feedback';
import { messages } from '../../base/messages';
import { GEdge } from '../../model';
import { RepositionAction } from '../viewport/reposition';
const CSS_SEARCH_HIDDEN = 'search-hidden';
const CSS_SEARCH_HIGHLIGHTED = 'search-highlighted';
export interface SearchAutocompleteSuggestion extends Required<AutocompleteSuggestion> {}
export namespace SearchAutocompleteSuggestion {
export function is(suggestion: AutocompleteSuggestion): suggestion is SearchAutocompleteSuggestion {
return suggestion.element !== undefined;
}
}
()
export class SearchAutocompletePalette extends BaseAutocompletePalette {
static readonly ID: string = 'glsp.search-autocomplete-palette';
(TYPES.IAutocompleteSuggestionProviderRegistry)
protected readonly suggestionRegistry: AutocompleteSuggestionRegistry;
/**
* Cache the suggestions so that we don't have to retrieve them again for CSS manipulation.
*/
protected cachedSuggestions: SearchAutocompleteSuggestion[] = [];
id(): string {
return SearchAutocompletePalette.ID;
}
/**
* The search context for the search palette. This is used to filter the suggestions
*/
get searchContext(): string[] {
return [AutocompleteSuggestionProviderContext.CANVAS];
}
protected override initializeContents(containerElement: HTMLElement): void {
super.initializeContents(containerElement);
this.autocompleteWidget.inputField.placeholder = messages.search.placeholder;
containerElement.setAttribute('aria-label', messages.search.label);
}
protected async retrieveSuggestions(root: Readonly<GModelRoot>, input: string): Promise<LabeledAction[]> {
const suggestions = await this.provideSearchSuggestions(root, input);
this.cachedSuggestions = suggestions;
return suggestions.map(s => s.action);
}
protected async provideSearchSuggestions(root: Readonly<GModelRoot>, input: string): Promise<SearchAutocompleteSuggestion[]> {
return (
await Promise.all(
this.suggestionRegistry.providersForContext(this.searchContext).flatMap(provider => provider.getSuggestions(root, input))
)
)
.flat(1)
.filter(SearchAutocompleteSuggestion.is);
}
protected override async visibleSuggestionsChanged(root: Readonly<GModelRoot>, labeledActions: LabeledAction[]): Promise<void> {
await this.applyCSS(this.getHiddenElements(root, this.getSuggestionsFromLabeledActions(labeledActions)), CSS_SEARCH_HIDDEN);
await this.deleteCSS(
this.getSuggestionsFromLabeledActions(labeledActions)
.filter(s => s.element)
.map(s => s.element!),
CSS_SEARCH_HIDDEN
);
}
protected override async selectedSuggestionChanged(
root: Readonly<GModelRoot>,
labeledAction?: LabeledAction | undefined
): Promise<void> {
await this.handleSuggestionChanged(root, labeledAction);
}
protected readonly handleSuggestionChanged = debounce(this.doHandleSuggestionChanged, 300, { trailing: true });
protected async doHandleSuggestionChanged(root: Readonly<GModelRoot>, labeledAction?: LabeledAction | undefined): Promise<void> {
await this.deleteAllCSS(root, CSS_SEARCH_HIGHLIGHTED);
if (labeledAction !== undefined) {
const suggestions = this.getSuggestionsFromLabeledActions([labeledAction]);
const actions: RepositionAction[] = [];
suggestions.map(currElem => actions.push(RepositionAction.create([currElem.element.id])));
this.actionDispatcher.dispatchAll(actions);
await this.applyCSS(
suggestions.map(s => s.element),
CSS_SEARCH_HIGHLIGHTED
);
}
}
override show(root: Readonly<GModelRoot>, ...contextElementIds: string[]): void {
this.actionDispatcher.dispatch(SelectAllAction.create(false));
super.show(root, ...contextElementIds);
}
override hide(): void {
if (this.root !== undefined) {
this.deleteAllCSS(this.root, CSS_SEARCH_HIDDEN, CSS_SEARCH_HIGHLIGHTED);
this.autocompleteWidget.inputField.value = '';
}
super.hide();
}
protected applyCSS(elements: GModelElement[], ...add: string[]): Promise<void> {
const actions = elements.map(element => applyCssClasses(element, ...add));
return this.actionDispatcher.dispatchAll(actions);
}
protected deleteCSS(elements: GModelElement[], ...remove: string[]): Promise<void> {
const actions = elements.map(element => deleteCssClasses(element, ...remove));
return this.actionDispatcher.dispatchAll(actions);
}
protected deleteAllCSS(root: Readonly<GModelRoot>, ...remove: string[]): Promise<void> {
const actions = toArray(root.index.all().map(element => deleteCssClasses(element, ...remove)));
return this.actionDispatcher.dispatchAll(actions);
}
protected getSuggestionsFromLabeledActions(labeledActions: LabeledAction[]): SearchAutocompleteSuggestion[] {
return this.cachedSuggestions.filter(c => labeledActions.find(s => isEqual(s, c.action)));
}
protected getHiddenElements(root: Readonly<GModelRoot>, suggestions: SearchAutocompleteSuggestion[]): GModelElement[] {
return toArray(
root.index
.all()
.filter(element => element instanceof GNode || element instanceof GEdge)
.filter(element => suggestions.find(suggestion => suggestion.element.id === element.id) === undefined)
);
}
}