@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
210 lines (174 loc) • 7.92 kB
text/typescript
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. 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 { injectable } from 'inversify';
import { isThenable } from '../common/promise-util';
import { CancellationToken, CancellationTokenSource, Disposable, Emitter, Event } from '../common';
import { TernarySearchTree } from '../common/ternary-search-tree';
import URI from '../common/uri';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.52.1/src/vs/workbench/services/decorations/browser/decorationsService.ts#L24-L23
export interface DecorationsProvider {
readonly onDidChange: Event<URI[]>;
provideDecorations(uri: URI, token: CancellationToken): Decoration | Promise<Decoration | undefined> | undefined;
}
export interface Decoration {
readonly weight?: number;
readonly colorId?: string;
readonly letter?: string;
readonly tooltip?: string;
readonly bubble?: boolean;
}
export interface ResourceDecorationChangeEvent {
affectsResource(uri: URI): boolean;
}
export const DecorationsService = Symbol('DecorationsService');
export interface DecorationsService {
readonly onDidChangeDecorations: Event<Map<string, Decoration>>;
registerDecorationsProvider(provider: DecorationsProvider): Disposable;
getDecoration(uri: URI, includeChildren: boolean): Decoration[];
}
class DecorationDataRequest {
constructor(
readonly source: CancellationTokenSource,
readonly thenable: Promise<void>,
) { }
}
class DecorationProviderWrapper {
readonly data: TernarySearchTree<URI, DecorationDataRequest | Decoration | undefined>;
readonly decorations: Map<string, Decoration> = new Map();
private readonly disposable: Disposable;
constructor(
readonly provider: DecorationsProvider,
readonly onDidChangeDecorationsEmitter: Emitter<Map<string, Decoration>>
) {
this.data = TernarySearchTree.forUris<DecorationDataRequest | Decoration | undefined>(true);
this.disposable = this.provider.onDidChange(async uris => {
this.decorations.clear();
if (!uris) {
this.data.clear();
} else {
for (const uri of uris) {
this.fetchData(new URI(uri.toString()));
const decoration = await provider.provideDecorations(uri, CancellationToken.None);
if (decoration) {
this.decorations.set(uri.toString(), decoration);
}
}
}
this.onDidChangeDecorationsEmitter.fire(this.decorations);
});
}
dispose(): void {
this.disposable.dispose();
this.data.clear();
}
knowsAbout(uri: URI): boolean {
return !!this.data.get(uri) || Boolean(this.data.findSuperstr(uri));
}
getOrRetrieve(uri: URI, includeChildren: boolean, callback: (data: Decoration, isChild: boolean) => void): void {
let item = this.data.get(uri);
if (item === undefined) {
// unknown -> trigger request
item = this.fetchData(uri);
}
if (item && !(item instanceof DecorationDataRequest)) {
// found something (which isn't pending anymore)
callback(item, false);
}
if (includeChildren) {
// (resolved) children
const iter = this.data.findSuperstr(uri);
if (iter) {
let next = iter.next();
while (!next.done) {
const value = next.value;
if (value && !(value instanceof DecorationDataRequest)) {
callback(value, true);
}
next = iter.next();
}
}
}
}
private fetchData(uri: URI): Decoration | undefined {
// check for pending request and cancel it
const pendingRequest = this.data.get(new URI(uri.toString()));
if (pendingRequest instanceof DecorationDataRequest) {
pendingRequest.source.cancel();
this.data.delete(uri);
}
const source = new CancellationTokenSource();
const dataOrThenable = this.provider.provideDecorations(new URI(uri.toString()), source.token);
if (!isThenable<Decoration | Promise<Decoration | undefined> | undefined>(dataOrThenable)) {
// sync -> we have a result now
return this.keepItem(uri, dataOrThenable);
} else {
// async -> we have a result soon
const request = new DecorationDataRequest(source, Promise.resolve(dataOrThenable).then(data => {
if (this.data.get(uri) === request) {
this.keepItem(uri, data);
}
}).catch(err => {
if (!(err instanceof Error && err.name === 'Canceled' && err.message === 'Canceled') && this.data.get(uri) === request) {
this.data.delete(uri);
}
}));
this.data.set(uri, request);
return undefined;
}
}
private keepItem(uri: URI, data: Decoration | undefined): Decoration | undefined {
const deco = data ? data : undefined;
this.data.set(uri, deco);
return deco;
}
}
export class DecorationsServiceImpl implements DecorationsService {
private readonly data: DecorationProviderWrapper[] = [];
private readonly onDidChangeDecorationsEmitter = new Emitter<Map<string, Decoration>>();
readonly onDidChangeDecorations = this.onDidChangeDecorationsEmitter.event;
dispose(): void {
this.onDidChangeDecorationsEmitter.dispose();
}
registerDecorationsProvider(provider: DecorationsProvider): Disposable {
const wrapper = new DecorationProviderWrapper(provider, this.onDidChangeDecorationsEmitter);
this.data.push(wrapper);
return Disposable.create(() => {
// fire event that says 'yes' for any resource
// known to this provider. then dispose and remove it.
this.data.splice(this.data.indexOf(wrapper), 1);
this.onDidChangeDecorationsEmitter.fire(new Map<string, Decoration>());
wrapper.dispose();
});
}
getDecoration(uri: URI, includeChildren: boolean): Decoration[] {
const data: Decoration[] = [];
let containsChildren: boolean = false;
for (const wrapper of this.data) {
wrapper.getOrRetrieve(new URI(uri.toString()), includeChildren, (deco, isChild) => {
if (!isChild || deco.bubble) {
data.push(deco);
containsChildren = isChild || containsChildren;
}
});
}
return data;
}
}