Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply FileDecoration provider API #8911

Merged
merged 1 commit into from
May 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 214 additions & 0 deletions packages/core/src/browser/decorations-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/********************************************************************************
* 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 WITH Classpath-exception-2.0
********************************************************************************/

import { injectable } from 'inversify';
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;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isThenable<T>(obj: any): obj is Promise<T> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return obj && typeof (<Promise<any>>obj).then === 'function';
}
}

private keepItem(uri: URI, data: Decoration | undefined): Decoration | undefined {
const deco = data ? data : undefined;
this.data.set(uri, deco);
return deco;
}
}

@injectable()
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;
}
}
2 changes: 2 additions & 0 deletions packages/core/src/browser/frontend-application-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import { LanguageService } from './language-service';
import { EncodingRegistry } from './encoding-registry';
import { EncodingService } from '../common/encoding-service';
import { AuthenticationService, AuthenticationServiceImpl } from '../browser/authentication-service';
import { DecorationsService, DecorationsServiceImpl } from './decorations-service';

export { bindResourceProvider, bindMessageService, bindPreferenceService };

Expand Down Expand Up @@ -338,4 +339,5 @@ export const frontendApplicationModule = new ContainerModule((bind, unbind, isBo
bind(ContextMenuContext).toSelf().inSingletonScope();

bind(AuthenticationService).to(AuthenticationServiceImpl).inSingletonScope();
bind(DecorationsService).to(DecorationsServiceImpl).inSingletonScope();
});
5 changes: 5 additions & 0 deletions packages/git/src/browser/git-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import { GitPreferences } from './git-preferences';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { ScmInputIssueType } from '@theia/scm/lib/browser/scm-input';
import { DecorationsService } from '@theia/core/lib/browser/decorations-service';
import { GitDecorationProvider } from './git-decoration-provider';

export namespace GIT_COMMANDS {
export const CLONE = {
Expand Down Expand Up @@ -257,11 +259,14 @@ export class GitContribution implements CommandContribution, MenuContribution, T
@inject(CommandRegistry) protected readonly commands: CommandRegistry;
@inject(ProgressService) protected readonly progressService: ProgressService;
@inject(GitPreferences) protected readonly gitPreferences: GitPreferences;
@inject(DecorationsService) protected readonly decorationsService: DecorationsService;
@inject(GitDecorationProvider) protected readonly gitDecorationProvider: GitDecorationProvider;

onStart(): void {
this.updateStatusBar();
this.repositoryTracker.onGitEvent(() => this.updateStatusBar());
this.syncService.onDidChange(() => this.updateStatusBar());
this.decorationsService.registerDecorationsProvider(this.gitDecorationProvider);
}

registerMenus(menus: MenuModelRegistry): void {
Expand Down
66 changes: 66 additions & 0 deletions packages/git/src/browser/git-decoration-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/********************************************************************************
* 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 WITH Classpath-exception-2.0
********************************************************************************/

import { inject, injectable } from '@theia/core/shared/inversify';
import { GitFileChange, GitFileStatus, GitStatusChangeEvent } from '../common';
import { CancellationToken, Emitter, Event } from '@theia/core/lib/common';
import { Decoration, DecorationsProvider } from '@theia/core/lib/browser/decorations-service';
import { GitRepositoryTracker } from './git-repository-tracker';
import URI from '@theia/core/lib/common/uri';

@injectable()
export class GitDecorationProvider implements DecorationsProvider {

private readonly onDidChangeDecorationsEmitter = new Emitter<URI[]>();
readonly onDidChange: Event<URI[]> = this.onDidChangeDecorationsEmitter.event;

private decorations = new Map<string, Decoration>();

constructor(@inject(GitRepositoryTracker) protected readonly gitRepositoryTracker: GitRepositoryTracker) {
this.gitRepositoryTracker.onGitEvent((event: GitStatusChangeEvent | undefined) => {
this.onGitEvent(event);
});
}

private async onGitEvent(event: GitStatusChangeEvent | undefined): Promise<void> {
if (!event) {
return;
}

const newDecorations = new Map<string, Decoration>();
this.collectDecorationData(event.status.changes, newDecorations);

const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()]));
this.decorations = newDecorations;
this.onDidChangeDecorationsEmitter.fire([...uris.values()].map(value => new URI(value)));
}

private collectDecorationData(changes: GitFileChange[], bucket: Map<string, Decoration>): void {
changes.forEach(change => {
const color = GitFileStatus.getColor(change.status, change.staged);
bucket.set(change.uri, {
colorId: color.substring(12, color.length - 1).replace(/-/g, '.'),
tooltip: GitFileStatus.toString(change.status),
letter: GitFileStatus.toAbbreviation(change.status, change.staged)
});
});
}

provideDecorations(uri: URI, token: CancellationToken): Decoration | Promise<Decoration | undefined> | undefined {
return this.decorations.get(uri.toString());
}
}

Loading