-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Igor Vinokur <ivinokur@redhat.com>
- Loading branch information
Showing
17 changed files
with
629 additions
and
324 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} | ||
|
Oops, something went wrong.