@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
386 lines (334 loc) • 15.6 kB
text/typescript
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, named, postConstruct } from 'inversify';
import * as fileIcons from 'file-icons-js';
import URI from '../common/uri';
import { ContributionProvider } from '../common/contribution-provider';
import { Event, Emitter, Disposable, isObject, Path, Prioritizeable } from '../common';
import { FrontendApplicationContribution } from './frontend-application-contribution';
import { EnvVariablesServer } from '../common/env-variables/env-variables-protocol';
import { ResourceLabelFormatter, ResourceLabelFormatting } from '../common/label-protocol';
import { codicon } from './widgets';
/**
* @internal don't export it, use `LabelProvider.folderIcon` instead.
*/
const DEFAULT_FOLDER_ICON = `${codicon('folder')} default-folder-icon`;
/**
* @internal don't export it, use `LabelProvider.fileIcon` instead.
*/
const DEFAULT_FILE_ICON = `${codicon('file')} default-file-icon`;
export const LabelProviderContribution = Symbol('LabelProviderContribution');
/**
* A {@link LabelProviderContribution} determines how specific elements/nodes are displayed in the workbench.
* Theia views use a common {@link LabelProvider} to determine the label and/or an icon for elements shown in the UI. This includes elements in lists
* and trees, but also view specific locations like headers. The common {@link LabelProvider} collects all {@links LabelProviderContribution} and delegates
* to the contribution with the highest priority. This is determined via calling the {@link LabelProviderContribution.canHandle} function, so contributions
* define which elements they are responsible for.
* As arbitrary views can consume LabelProviderContributions, they must be generic for the covered element type, not view specific. Label providers and
* contributions can be used for arbitrary element and node types, e.g. for markers or domain-specific elements.
*/
export interface LabelProviderContribution {
/**
* Determines whether this contribution can handle the given element and with what priority.
* All contributions are ordered by the returned number if greater than zero. The highest number wins.
* If two or more contributions return the same positive number one of those will be used. It is undefined which one.
*/
canHandle(element: object): number;
/**
* returns an icon class for the given element.
*/
getIcon?(element: object): string | undefined;
/**
* returns a short name for the given element.
*/
getName?(element: object): string | undefined;
/**
* returns a long name for the given element.
*/
getLongName?(element: object): string | undefined;
/**
* A compromise between {@link getName} and {@link getLongName}. Can be used to supplement getName in contexts that allow both a primary display field and extra detail.
*/
getDetails?(element: object): string | undefined;
/**
* Emit when something has changed that may result in this label provider returning a different
* value for one or more properties (name, icon etc).
*/
readonly onDidChange?: Event<DidChangeLabelEvent>;
/**
* Checks whether the given element is affected by the given change event.
* Contributions delegating to the label provider can use this hook
* to perform a recursive check.
*/
affects?(element: object, event: DidChangeLabelEvent): boolean;
}
export interface DidChangeLabelEvent {
affects(element: object): boolean;
}
export interface URIIconReference {
kind: 'uriIconReference';
id: 'file' | 'folder';
uri?: URI
}
export namespace URIIconReference {
export function is(element: unknown): element is URIIconReference {
return isObject(element) && element.kind === 'uriIconReference';
}
export function create(id: URIIconReference['id'], uri?: URI): URIIconReference {
return { kind: 'uriIconReference', id, uri };
}
}
export class DefaultUriLabelProviderContribution implements LabelProviderContribution {
protected formatters: ResourceLabelFormatter[] = [];
protected readonly onDidChangeEmitter = new Emitter<DidChangeLabelEvent>();
protected homePath: string | undefined;
protected readonly envVariablesServer: EnvVariablesServer;
init(): void {
this.envVariablesServer.getHomeDirUri().then(result => {
this.homePath = result;
this.fireOnDidChange();
});
}
canHandle(element: object): number {
if (element instanceof URI || URIIconReference.is(element)) {
return 1;
}
return 0;
}
getIcon(element: URI | URIIconReference): string {
if (URIIconReference.is(element) && element.id === 'folder') {
return this.defaultFolderIcon;
}
const uri = URIIconReference.is(element) ? element.uri : element;
if (uri) {
const iconClass = uri && this.getFileIcon(uri);
return iconClass || this.defaultFileIcon;
}
return '';
}
get defaultFolderIcon(): string {
return DEFAULT_FOLDER_ICON;
}
get defaultFileIcon(): string {
return DEFAULT_FILE_ICON;
}
protected getFileIcon(uri: URI): string | undefined {
const fileIcon = fileIcons.getClassWithColor(uri.displayName);
if (!fileIcon) {
return undefined;
}
return fileIcon + ' theia-file-icons-js';
}
getName(element: URI | URIIconReference): string | undefined {
const uri = this.getUri(element);
return uri && uri.displayName;
}
getLongName(element: URI | URIIconReference): string | undefined {
const uri = this.getUri(element);
if (uri) {
const formatting = this.findFormatting(uri);
if (formatting) {
return this.formatUri(uri, formatting);
}
}
return uri && uri.path.fsPath();
}
getDetails(element: URI | URIIconReference): string | undefined {
const uri = this.getUri(element);
if (uri) {
return this.getLongName(uri.parent);
}
return this.getLongName(element);
}
protected getUri(element: URI | URIIconReference): URI | undefined {
return URIIconReference.is(element) ? element.uri : element;
}
registerFormatter(formatter: ResourceLabelFormatter): Disposable {
this.formatters.push(formatter);
this.fireOnDidChange();
return Disposable.create(() => {
this.formatters = this.formatters.filter(f => f !== formatter);
this.fireOnDidChange();
});
}
get onDidChange(): Event<DidChangeLabelEvent> {
return this.onDidChangeEmitter.event;
}
private fireOnDidChange(): void {
this.onDidChangeEmitter.fire({
affects: (element: URI) => this.canHandle(element) > 0
});
}
// copied and modified from https://github.com/microsoft/vscode/blob/1.44.2/src/vs/workbench/services/label/common/labelService.ts
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
private readonly labelMatchingRegexp = /\${(scheme|authority|path|query)}/g;
protected formatUri(resource: URI, formatting: ResourceLabelFormatting): string {
let label = formatting.label.replace(this.labelMatchingRegexp, (match, token) => {
switch (token) {
case 'scheme': return resource.scheme;
case 'authority': return resource.authority;
case 'path': return resource.path.toString();
case 'query': return resource.query;
default: return '';
}
});
// convert \c:\something => C:\something
if (formatting.normalizeDriveLetter && this.hasDriveLetter(label)) {
label = label.charAt(1).toUpperCase() + label.substring(2);
}
if (formatting.tildify) {
label = Path.tildify(label, this.homePath ? this.homePath : '');
}
if (formatting.authorityPrefix && resource.authority) {
label = formatting.authorityPrefix + label;
}
return label.replace(/\//g, formatting.separator);
}
private hasDriveLetter(path: string): boolean {
return !!(path && path[2] === ':');
}
protected findFormatting(resource: URI): ResourceLabelFormatting | undefined {
let bestResult: ResourceLabelFormatter | undefined;
this.formatters.forEach(formatter => {
if (formatter.scheme === resource.scheme) {
if (!bestResult && !formatter.authority) {
bestResult = formatter;
return;
}
if (!formatter.authority) {
return;
}
if ((formatter.authority.toLowerCase() === resource.authority.toLowerCase()) &&
(!bestResult || !bestResult.authority || formatter.authority.length > bestResult.authority.length ||
((formatter.authority.length === bestResult.authority.length) && formatter.priority))) {
bestResult = formatter;
}
}
});
return bestResult ? bestResult.formatting : undefined;
}
}
/**
* The {@link LabelProvider} determines how elements/nodes are displayed in the workbench. For any element, it can determine a short label, a long label
* and an icon. The {@link LabelProvider} is to be used in lists, trees and tables, but also view specific locations like headers.
* The common {@link LabelProvider} can be extended/adapted via {@link LabelProviderContribution}s. For every element, the {@links LabelProvider} will determine the
* {@link LabelProviderContribution} with the hightest priority and delegate to it. Theia registers default {@link LabelProviderContribution} for common types, e.g.
* the {@link DefaultUriLabelProviderContribution} for elements that have a URI.
* Using the {@link LabelProvider} across the workbench ensures a common look and feel for elements across multiple views. To adapt the way how specific
* elements/nodes are rendered, use a {@link LabelProviderContribution} rather than adapting or sub classing the {@link LabelProvider}. This way, your adaptation
* is applied to all views in Theia that use the {@link LabelProvider}
*/
export class LabelProvider implements FrontendApplicationContribution {
protected readonly onDidChangeEmitter = new Emitter<DidChangeLabelEvent>();
protected readonly contributionProvider: ContributionProvider<LabelProviderContribution>;
/**
* Start listening to contributions.
*
* Don't call this method directly!
* It's called by the frontend application during initialization.
*/
initialize(): void {
const contributions = this.contributionProvider.getContributions();
for (const eventContribution of contributions) {
if (eventContribution.onDidChange) {
eventContribution.onDidChange(event => {
this.onDidChangeEmitter.fire({
// TODO check eventContribution.canHandle as well
affects: element => this.affects(element, event)
});
});
}
}
}
protected affects(element: object, event: DidChangeLabelEvent): boolean {
if (event.affects(element)) {
return true;
}
for (const contribution of this.findContribution(element)) {
if (contribution.affects && contribution.affects(element, event)) {
return true;
}
}
return false;
}
get onDidChange(): Event<DidChangeLabelEvent> {
return this.onDidChangeEmitter.event;
}
/**
* Return a default file icon for the current icon theme.
*/
get fileIcon(): string {
return this.getIcon(URIIconReference.create('file'));
}
/**
* Return a default folder icon for the current icon theme.
*/
get folderIcon(): string {
return this.getIcon(URIIconReference.create('folder'));
}
/**
* Get the icon class from the list of available {@link LabelProviderContribution} for the given element.
* @return the icon class
*/
getIcon(element: object): string {
return this.handleRequest(element, 'getIcon') ?? '';
}
/**
* Get a short name from the list of available {@link LabelProviderContribution} for the given element.
* @return the short name
*/
getName(element: object): string {
return this.handleRequest(element, 'getName') ?? '<unknown>';
}
/**
* Get a long name from the list of available {@link LabelProviderContribution} for the given element.
* @return the long name
*/
getLongName(element: object): string {
return this.handleRequest(element, 'getLongName') ?? '';
}
/**
* Get details from the list of available {@link LabelProviderContribution} for the given element.
* @return the details
* Can be used to supplement {@link getName} in contexts that allow both a primary display field and extra detail.
*/
getDetails(element: object): string {
return this.handleRequest(element, 'getDetails') ?? '';
}
protected handleRequest(element: object, method: keyof Omit<LabelProviderContribution, 'canHandle' | 'onDidChange' | 'affects'>): string | undefined {
for (const contribution of this.findContribution(element, method)) {
const value = contribution[method]?.(element);
if (value !== undefined) {
return value;
}
}
}
protected findContribution(element: object, method?: keyof Omit<LabelProviderContribution, 'canHandle' | 'onDidChange' | 'affects'>): LabelProviderContribution[] {
const candidates = method
? this.contributionProvider.getContributions().filter(candidate => candidate[method])
: this.contributionProvider.getContributions();
return Prioritizeable.prioritizeAllSync(candidates, contrib =>
contrib.canHandle(element)
).map(entry => entry.value);
}
}