Skip to content

Commit

Permalink
Apply FileDecoration provider API
Browse files Browse the repository at this point in the history
Signed-off-by: Igor Vinokur <ivinokur@redhat.com>
  • Loading branch information
vinokurig committed May 17, 2021
1 parent 76f74f1 commit e3c430c
Show file tree
Hide file tree
Showing 18 changed files with 629 additions and 325 deletions.
1 change: 0 additions & 1 deletion examples/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
"@theia/file-search": "1.13.0",
"@theia/filesystem": "1.13.0",
"@theia/getting-started": "1.13.0",
"@theia/git": "1.13.0",
"@theia/keymaps": "1.13.0",
"@theia/markers": "1.13.0",
"@theia/messages": "1.13.0",
Expand Down
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

0 comments on commit e3c430c

Please sign in to comment.