diff --git a/.travis.yml b/.travis.yml index 2a0d39e3ee76c..3883b53806037 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ cache: - dev-packages/application-manager/node_modules - dev-packages/application-package/node_modules - dev-packages/electron/node_modules + - examples/api-samples/node_modules - examples/browser/node_modules - examples/electron/node_modules - node_modules diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ffb93e276330..46769e1fa9dcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## v0.13.0 - [core] Switched the frontend application's shutdown hook from `window.unload` to `window.beforeunload`. [#6530](https://github.com/eclipse-theia/theia/issues/6530) +- [core] label providers can now notify that element labels and icons may have changed and should be refreshed [#5884](https://github.com/theia-ide/theia/pull/5884) - [scm] added handling when opening diff-editors to respect preference `workbench.list.openMode` [#6481](https://github.com/eclipse-theia/theia/pull/6481) Breaking changes: diff --git a/examples/api-samples/README.md b/examples/api-samples/README.md new file mode 100644 index 0000000000000..9f2c22e2c2773 --- /dev/null +++ b/examples/api-samples/README.md @@ -0,0 +1,9 @@ +# Theia - Examples - API Samples - Label Provider + +This package contains examples of how to use Theia's internal API. The examples here can provide code that enables new features to be tested. The examples also show how to use certain API. + +The examples here are all deactivated by default. Commands are provided that toggles on the example behavior. These commands are prefixed with "API Sample". + +## License +- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/) +- [δΈ€ (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp) diff --git a/examples/api-samples/compile.tsconfig.json b/examples/api-samples/compile.tsconfig.json new file mode 100644 index 0000000000000..a23513b5e6b13 --- /dev/null +++ b/examples/api-samples/compile.tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ] +} diff --git a/examples/api-samples/package.json b/examples/api-samples/package.json new file mode 100644 index 0000000000000..f4881547529bb --- /dev/null +++ b/examples/api-samples/package.json @@ -0,0 +1,39 @@ +{ + "private": true, + "name": "@theia/api-samples", + "version": "0.12.0", + "description": "Theia - Example code to demonstrate Theia API", + "dependencies": { + "@theia/core": "^0.12.0" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/api-samples-frontend-module" + } + ], + "keywords": [ + "theia-extension" + ], + "license": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0", + "repository": { + "type": "git", + "url": "https://github.com/eclipse-theia/theia.git" + }, + "bugs": { + "url": "https://github.com/eclipse-theia/theia/issues" + }, + "homepage": "https://github.com/eclipse-theia/theia", + "files": [ + "lib", + "src" + ], + "scripts": { + "prepare": "yarn run clean && yarn run build", + "clean": "theiaext clean", + "build": "theiaext build", + "watch": "theiaext watch" + }, + "devDependencies": { + "@theia/ext-scripts": "^0.12.0" + } +} diff --git a/examples/api-samples/src/browser/api-samples-contribution.ts b/examples/api-samples/src/browser/api-samples-contribution.ts new file mode 100644 index 0000000000000..01092689a7e21 --- /dev/null +++ b/examples/api-samples/src/browser/api-samples-contribution.ts @@ -0,0 +1,55 @@ +/******************************************************************************** + * Copyright (C) 2019 Arm 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, inject } from 'inversify'; +import { Command, CommandContribution, CommandRegistry, CommandHandler } from '@theia/core'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser'; +import { SampleDynamicLabelProviderContribution } from './sample-dynamic-label-provider-contribution'; + +export namespace ExampleLabelProviderCommands { + const EXAMPLE_CATEGORY = 'Examples'; + export const TOGGLE_SAMPLE: Command = { + id: 'example_label_provider.toggle', + category: EXAMPLE_CATEGORY, + label: 'Toggle Dynamically-Changing Labels' + }; +} + +@injectable() +export class ApiSamplesContribution implements FrontendApplicationContribution, CommandContribution { + + @inject(SampleDynamicLabelProviderContribution) + protected readonly labelProviderContribution: SampleDynamicLabelProviderContribution; + + initialize(): void { } + + registerCommands(commands: CommandRegistry): void { + commands.registerCommand(ExampleLabelProviderCommands.TOGGLE_SAMPLE, new ExampleLabelProviderCommandHandler(this.labelProviderContribution)); + } + +} + +export class ExampleLabelProviderCommandHandler implements CommandHandler { + + constructor(private readonly labelProviderContribution: SampleDynamicLabelProviderContribution) { + } + + // tslint:disable-next-line:no-any + execute(...args: any[]): any { + this.labelProviderContribution.toggle(); + } + +} diff --git a/examples/api-samples/src/browser/api-samples-frontend-module.ts b/examples/api-samples/src/browser/api-samples-frontend-module.ts new file mode 100644 index 0000000000000..2c2fe27d83186 --- /dev/null +++ b/examples/api-samples/src/browser/api-samples-frontend-module.ts @@ -0,0 +1,28 @@ +/******************************************************************************** + * Copyright (C) 2019 Arm 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 { ContainerModule } from 'inversify'; +import { CommandContribution } from '@theia/core'; +import { LabelProviderContribution } from '@theia/core/lib/browser/label-provider'; +import { ApiSamplesContribution } from './api-samples-contribution'; +import { SampleDynamicLabelProviderContribution } from './sample-dynamic-label-provider-contribution'; + +export default new ContainerModule(bind => { + bind(CommandContribution).to(ApiSamplesContribution).inSingletonScope(); + + bind(SampleDynamicLabelProviderContribution).toSelf().inSingletonScope(); + bind(LabelProviderContribution).toService(SampleDynamicLabelProviderContribution); +}); diff --git a/examples/api-samples/src/browser/sample-dynamic-label-provider-contribution.ts b/examples/api-samples/src/browser/sample-dynamic-label-provider-contribution.ts new file mode 100644 index 0000000000000..ae7087af40f28 --- /dev/null +++ b/examples/api-samples/src/browser/sample-dynamic-label-provider-contribution.ts @@ -0,0 +1,91 @@ +/******************************************************************************** + * Copyright (C) 2019 Arm 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 { DefaultUriLabelProviderContribution, DidChangeLabelEvent, FILE_ICON } from '@theia/core/lib/browser/label-provider'; +import URI from '@theia/core/lib/common/uri'; +import { injectable } from 'inversify'; +import { Emitter, Event } from '@theia/core'; + +@injectable() +export class SampleDynamicLabelProviderContribution extends DefaultUriLabelProviderContribution { + + protected isActive: boolean = false; + + constructor() { + super(); + const outer = this; + + setInterval(() => { + if (this.isActive) { + outer.x++; + outer.fireLabelsDidChange(); + } + }, 1000); + } + + canHandle(element: object): number { + if (element.toString().includes('test')) { + return 30; + } + return 0; + } + + toggle(): void { + this.isActive = !this.isActive; + this.fireLabelsDidChange(); + } + + private fireLabelsDidChange(): void { + this.onDidChangeEmitter.fire({ + affects: (element: URI) => element.toString().includes('test') + }); + } + + private getUri(element: URI): URI { + return new URI(element.toString()); + } + + async getIcon(element: URI): Promise { + const uri = this.getUri(element); + const icon = super.getFileIcon(uri); + if (!icon) { + return FILE_ICON; + } + return icon; + } + + protected readonly onDidChangeEmitter = new Emitter(); + private x: number = 0; + + getName(element: URI): string { + const uri = this.getUri(element); + if (this.isActive && uri.toString().includes('test')) { + return super.getName(uri) + '-' + this.x.toString(10); + } else { + return super.getName(uri); + } + } + + getLongName(element: URI): string { + const uri = this.getUri(element); + return super.getLongName(uri); + } + + get onDidChange(): Event { + return this.onDidChangeEmitter.event; + } + +} diff --git a/examples/browser/package.json b/examples/browser/package.json index 11a536e63b62f..1f3a45df3706f 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -14,6 +14,7 @@ } }, "dependencies": { + "@theia/api-samples": "^0.12.0", "@theia/callhierarchy": "^0.12.0", "@theia/console": "^0.12.0", "@theia/core": "^0.12.0", diff --git a/examples/electron/package.json b/examples/electron/package.json index 624f287f22ec6..c9836b3d413a7 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -12,6 +12,7 @@ } }, "dependencies": { + "@theia/api-samples": "^0.12.0", "@theia/callhierarchy": "^0.12.0", "@theia/console": "^0.12.0", "@theia/core": "^0.12.0", diff --git a/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree-widget.tsx b/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree-widget.tsx index b369ba2d92b34..76065f0ee1bcc 100644 --- a/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree-widget.tsx +++ b/packages/callhierarchy/src/browser/callhierarchy-tree/callhierarchy-tree-widget.tsx @@ -59,6 +59,9 @@ export class CallHierarchyTreeWidget extends TreeWidget { this.toDispose.push(this.model.onOpenNode((node: TreeNode) => { this.openEditor(node, false); })); + this.toDispose.push( + this.labelProvider.onDidChange(() => this.update()) + ); } initializeModel(selection: Location | undefined, languageId: string | undefined): void { diff --git a/packages/core/src/browser/diff-uris.ts b/packages/core/src/browser/diff-uris.ts index ab4075c8f90df..6a0aa808c2b12 100644 --- a/packages/core/src/browser/diff-uris.ts +++ b/packages/core/src/browser/diff-uris.ts @@ -102,4 +102,9 @@ export class DiffUriLabelProviderContribution implements LabelProviderContributi getIcon(uri: URI): string { return 'fa fa-columns'; } + + getConstituentUris(uri: URI): URI[] { + return DiffUris.decode(uri); + } + } diff --git a/packages/core/src/browser/label-provider.ts b/packages/core/src/browser/label-provider.ts index 75e3421e46c3c..b9f85c792bcb4 100644 --- a/packages/core/src/browser/label-provider.ts +++ b/packages/core/src/browser/label-provider.ts @@ -19,6 +19,7 @@ import * as fileIcons from 'file-icons-js'; import URI from '../common/uri'; import { ContributionProvider } from '../common/contribution-provider'; import { Prioritizeable, MaybePromise } from '../common/types'; +import { Event, Emitter } from '../common'; export const FOLDER_ICON = 'fa fa-folder'; export const FILE_ICON = 'fa fa-file'; @@ -48,6 +49,17 @@ export interface LabelProviderContribution { */ getLongName?(element: object): string; + /** + * 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; + + readonly getConstituentUris?: (compositeElement: object) => URI[]; +} + +export interface DidChangeLabelEvent { + affects(element: object): boolean; } @injectable() @@ -88,10 +100,67 @@ export class DefaultUriLabelProviderContribution implements LabelProviderContrib @injectable() export class LabelProvider { + protected readonly onDidChangeEmitter = new Emitter(); + constructor( @inject(ContributionProvider) @named(LabelProviderContribution) protected readonly contributionProvider: ContributionProvider - ) { } + ) { + const contributions = this.contributionProvider.getContributions(); + for (const contribution of contributions) { + if (contribution.onDidChange) { + contribution.onDidChange(event => { + const affects = (uri: URI) => this.affects(uri, event, contribution); + this.onDidChangeEmitter.fire({ affects }); + }); + } + } + } + + /** + * When the given event occurs, determine if the given URI could in any + * way be affected. + * + * If the event directly indicates that it affects the URI then of course we + * return `true`. However there may be label provider contributions that delegate + * back to the label provider. These contributors do not, and should not, listen for + * label provider events because that would cause infinite recursion. + * + * @param uri + * @param event + */ + protected affects(element: object, event: DidChangeLabelEvent, originatingContribution: LabelProviderContribution): boolean { + + const contribs = this.findContribution(element); + const possibleContribsWithDups = [ + contribs.find(c => c.getIcon !== undefined), + contribs.find(c => c.getName !== undefined), + contribs.find(c => c.getLongName !== undefined), + ]; + const possibleContribsWithoutDups = [...new Set(possibleContribsWithDups)]; + for (const possibleContrib of possibleContribsWithoutDups) { + if (possibleContrib) { + if (possibleContrib === originatingContribution) { + if (event.affects(element)) { + return true; + } + } + if (possibleContrib.getConstituentUris) { + const constituentUris: URI[] = possibleContrib.getConstituentUris(element); + for (const constituentUri of constituentUris) { + if (this.affects(constituentUri, event, originatingContribution)) { + return true; + } + } + } + } + } + return false; + } + + get onDidChange(): Event { + return this.onDidChangeEmitter.event; + } async getIcon(element: object): Promise { const contribs = this.findContribution(element); @@ -117,7 +186,7 @@ export class LabelProvider { if (!contrib) { return ''; } - return contrib!.getLongName!(element); + return contrib.getLongName!(element); } protected findContribution(element: object): LabelProviderContribution[] { @@ -126,5 +195,4 @@ export class LabelProvider { ); return prioritized.map(c => c.value); } - } diff --git a/packages/core/src/browser/tree/tree.ts b/packages/core/src/browser/tree/tree.ts index 94be7e09cacf9..0c7045d667d20 100644 --- a/packages/core/src/browser/tree/tree.ts +++ b/packages/core/src/browser/tree/tree.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { injectable } from 'inversify'; -import { Event, Emitter, Disposable, DisposableCollection, WaitUntilEvent } from '../../common'; +import { Event, Emitter, Disposable, DisposableCollection, WaitUntilEvent, Mutable } from '../../common'; export const Tree = Symbol('Tree'); @@ -206,7 +206,7 @@ export class TreeImpl implements Tree { protected readonly toDispose = new DisposableCollection(); protected nodes: { - [id: string]: TreeNode | undefined + [id: string]: Mutable | undefined } = {}; constructor() { diff --git a/packages/debug/src/browser/debug-session-manager.ts b/packages/debug/src/browser/debug-session-manager.ts index 9325fbe66cc77..ece6394526099 100644 --- a/packages/debug/src/browser/debug-session-manager.ts +++ b/packages/debug/src/browser/debug-session-manager.ts @@ -147,6 +147,14 @@ export class DebugSessionManager { this.debugTypeKey = this.contextKeyService.createKey('debugType', undefined); this.inDebugModeKey = this.contextKeyService.createKey('inDebugMode', this.inDebugMode); this.breakpoints.onDidChangeMarkers(uri => this.fireDidChangeBreakpoints({ uri })); + this.labelProvider.onDidChange(event => { + for (const uriString of this.breakpoints.getUris()) { + const uri = new URI(uriString); + if (event.affects(uri)) { + this.fireDidChangeBreakpoints({ uri }); + } + } + }); } get inDebugMode(): boolean { diff --git a/packages/editor/src/browser/editor-widget-factory.ts b/packages/editor/src/browser/editor-widget-factory.ts index 4e07734147435..0a01b0b37ae1c 100644 --- a/packages/editor/src/browser/editor-widget-factory.ts +++ b/packages/editor/src/browser/editor-widget-factory.ts @@ -43,15 +43,27 @@ export class EditorWidgetFactory implements WidgetFactory { } protected async createEditor(uri: URI): Promise { - const icon = await this.labelProvider.getIcon(uri); - return this.editorProvider(uri).then(textEditor => { - const newEditor = new EditorWidget(textEditor, this.selectionService); - newEditor.id = this.id + ':' + uri.toString(); - newEditor.title.closable = true; - newEditor.title.label = this.labelProvider.getName(uri); - newEditor.title.iconClass = icon + ' file-icon'; - newEditor.title.caption = uri.path.toString(); - return newEditor; + const textEditor = await this.editorProvider(uri); + const newEditor = new EditorWidget(textEditor, this.selectionService); + + await this.setLabels(newEditor, uri); + const labelListener = this.labelProvider.onDidChange(async event => { + if (uri && event.affects(uri)) { + this.setLabels(newEditor, uri); + } }); + newEditor.onDispose(() => labelListener.dispose()); + + newEditor.id = this.id + ':' + uri.toString(); + newEditor.title.closable = true; + newEditor.title.caption = uri.path.toString(); + return newEditor; + } + + private async setLabels(editor: EditorWidget, uri: URI): Promise { + const icon = await this.labelProvider.getIcon(uri); + editor.title.label = this.labelProvider.getName(uri); + editor.title.iconClass = icon + ' file-icon'; + } } diff --git a/packages/editor/src/browser/editor-widget.ts b/packages/editor/src/browser/editor-widget.ts index a126339bfda9f..e344793e8d8bd 100644 --- a/packages/editor/src/browser/editor-widget.ts +++ b/packages/editor/src/browser/editor-widget.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { Disposable, SelectionService } from '@theia/core/lib/common'; +import { Disposable, SelectionService, Event } from '@theia/core/lib/common'; import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget } from '@theia/core/lib/browser'; import URI from '@theia/core/lib/common/uri'; import { TextEditor } from './editor'; @@ -84,4 +84,8 @@ export class EditorWidget extends BaseWidget implements SaveableSource, Navigata this.editor.restoreViewState(oldState); } + get onDispose(): Event { + return this.toDispose.onDispose; + } + } diff --git a/packages/filesystem/src/browser/file-tree/file-tree.ts b/packages/filesystem/src/browser/file-tree/file-tree.ts index 3466ea959d8eb..8d78df4c0638b 100644 --- a/packages/filesystem/src/browser/file-tree/file-tree.ts +++ b/packages/filesystem/src/browser/file-tree/file-tree.ts @@ -14,11 +14,11 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject } from 'inversify'; +import { injectable, inject, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { TreeNode, CompositeTreeNode, SelectableTreeNode, ExpandableTreeNode, TreeImpl } from '@theia/core/lib/browser'; import { FileSystem, FileStat } from '../../common'; -import { LabelProvider } from '@theia/core/lib/browser/label-provider'; +import { LabelProvider, DidChangeLabelEvent } from '@theia/core/lib/browser/label-provider'; import { UriSelection } from '@theia/core/lib/common/selection'; import { FileSelection } from '../file-selection'; @@ -28,6 +28,34 @@ export class FileTree extends TreeImpl { @inject(FileSystem) protected readonly fileSystem: FileSystem; @inject(LabelProvider) protected readonly labelProvider: LabelProvider; + @postConstruct() + protected initFileTree(): void { + this.toDispose.push( + this.labelProvider.onDidChange((event: DidChangeLabelEvent) => this.doUpdateElement(event)) + ); + } + + protected async doUpdateElement(event: DidChangeLabelEvent): Promise { + let isAnyAffectedNodes = false; + for (const nodeId of Object.keys(this.nodes)) { + const mutableNode = this.nodes[nodeId]; + + const nodeWithPossibleUri = mutableNode; + if (mutableNode && FileStatNode.is(nodeWithPossibleUri)) { + const uri = nodeWithPossibleUri.uri; + if (event.affects(uri)) { + mutableNode.name = this.labelProvider.getName(uri); + mutableNode.description = this.labelProvider.getLongName(uri); + mutableNode.icon = await this.labelProvider.getIcon(uri); + isAnyAffectedNodes = true; + } + } + } + if (isAnyAffectedNodes) { + this.fireChanged(); + } + } + async resolveChildren(parent: CompositeTreeNode): Promise { if (FileStatNode.is(parent)) { const fileStat = await this.resolveFileStat(parent); @@ -54,14 +82,14 @@ export class FileTree extends TreeImpl { return []; } const result = await Promise.all(fileStat.children.map(async child => - await this.toNode(child, parent) + this.toNode(child, parent) )); return result.sort(DirNode.compare); } protected async toNode(fileStat: FileStat, parent: CompositeTreeNode): Promise { const uri = new URI(fileStat.uri); - const name = await this.labelProvider.getName(uri); + const name = this.labelProvider.getName(uri); const icon = await this.labelProvider.getIcon(fileStat); const id = this.toNodeId(uri, parent); const node = this.getNode(id); diff --git a/packages/git/src/browser/diff/git-diff-widget.tsx b/packages/git/src/browser/diff/git-diff-widget.tsx index f339c4f691844..71e0dc4c68e5d 100644 --- a/packages/git/src/browser/diff/git-diff-widget.tsx +++ b/packages/git/src/browser/diff/git-diff-widget.tsx @@ -68,6 +68,12 @@ export class GitDiffWidget extends GitNavigableListWidget imp this.setContent(this.options); } })); + this.toDispose.push(this.labelProvider.onDidChange(event => { + const affectsFiles = this.fileChangeNodes.some(node => event.affects(new URI(node.uri))); + if (this.options && affectsFiles) { + this.setContent(this.options); + } + })); } protected getScrollContainer(): MaybePromise { diff --git a/packages/git/src/browser/git-uri-label-contribution.ts b/packages/git/src/browser/git-uri-label-contribution.ts index 875f31d3b20aa..aad6c49b1f032 100644 --- a/packages/git/src/browser/git-uri-label-contribution.ts +++ b/packages/git/src/browser/git-uri-label-contribution.ts @@ -23,7 +23,7 @@ import { MaybePromise } from '@theia/core'; @injectable() export class GitUriLabelProviderContribution implements LabelProviderContribution { - constructor( @inject(LabelProvider) protected labelProvider: LabelProvider) { + constructor(@inject(LabelProvider) protected labelProvider: LabelProvider) { } canHandle(element: object): number { @@ -45,6 +45,14 @@ export class GitUriLabelProviderContribution implements LabelProviderContributio return this.labelProvider.getIcon(this.toFileUri(uri)); } + getConstituentUris(element: URI): URI[] { + const fileUri = this.toFileUri(element); + return [ + fileUri, + fileUri.withoutQuery().withoutFragment(), + ]; + } + protected toFileUri(uri: URI): URI { return uri.withScheme('file'); } diff --git a/packages/git/src/browser/history/git-history-widget.tsx b/packages/git/src/browser/history/git-history-widget.tsx index 37fe84c55bcb4..6052f7438bd93 100644 --- a/packages/git/src/browser/history/git-history-widget.tsx +++ b/packages/git/src/browser/history/git-history-widget.tsx @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject } from 'inversify'; +import { injectable, inject, postConstruct } from 'inversify'; import { DiffUris } from '@theia/core/lib/browser/diff-uris'; import { OpenerService, open, StatefulWidget, SELECTED_CLASS, WidgetManager, ApplicationShell } from '@theia/core/lib/browser'; import { CancellationTokenSource } from '@theia/core/lib/common/cancellation'; @@ -33,6 +33,7 @@ import { GitNavigableListWidget } from '../git-navigable-list-widget'; import { GitFileChangeNode } from '../git-file-change-node'; import * as React from 'react'; import { AlertMessage } from '@theia/core/lib/browser/widgets/alert-message'; +import { DidChangeLabelEvent } from '@theia/core/lib/browser/label-provider'; export interface GitCommitNode extends GitCommitDetails { fileChanges?: GitFileChange[]; @@ -89,6 +90,30 @@ export class GitHistoryWidget extends GitNavigableListWidget this.cancelIndicator = new CancellationTokenSource(); } + @postConstruct() + protected init(): void { + this.toDispose.push(this.labelProvider.onDidChange(event => this.refreshLabels(event))); + } + + protected async refreshLabels(event: DidChangeLabelEvent): Promise { + let isAnyAffectedNodes = false; + for (let i = 0; i < this.gitNodes.length; i++) { + const gitNode = this.gitNodes[i]; + if (GitFileChangeNode.is(gitNode)) { + const uri = new URI(gitNode.uri); + if (event.affects(uri)) { + const label = this.labelProvider.getName(uri); + const icon = await this.labelProvider.getIcon(uri); + this.gitNodes[i] = { ...gitNode, label, icon }; + isAnyAffectedNodes = true; + } + } + } + if (isAnyAffectedNodes) { + this.update(); + } + } + protected onAfterAttach(msg: Message): void { super.onAfterAttach(msg); this.addGitListNavigationKeyListeners(this.node); diff --git a/packages/markers/src/browser/marker-tree.ts b/packages/markers/src/browser/marker-tree.ts index 0f6376345b0e6..96e3727d7228d 100644 --- a/packages/markers/src/browser/marker-tree.ts +++ b/packages/markers/src/browser/marker-tree.ts @@ -39,6 +39,24 @@ export abstract class MarkerTree extends TreeImpl { super(); this.toDispose.push(markerManager.onDidChangeMarkers(uri => this.refreshMarkerInfo(uri))); + this.toDispose.push(labelProvider.onDidChange(async event => { + let isAnyAffectedNodes = false; + for (const nodeId of Object.keys(this.nodes)) { + const node = this.nodes[nodeId]; + const markerInfoNode = node; + if (node && MarkerInfoNode.is(markerInfoNode)) { + const uri = markerInfoNode.uri; + if (event.affects(uri)) { + node.name = this.labelProvider.getName(uri); + node.icon = await this.labelProvider.getIcon(uri); + isAnyAffectedNodes = true; + } + } + } + if (isAnyAffectedNodes) { + this.fireChanged(); + } + })); this.root = { visible: false, diff --git a/packages/navigator/src/browser/navigator-tree.spec.ts b/packages/navigator/src/browser/navigator-tree.spec.ts index 7b3edee244ee8..68b13523c5bd1 100644 --- a/packages/navigator/src/browser/navigator-tree.spec.ts +++ b/packages/navigator/src/browser/navigator-tree.spec.ts @@ -69,6 +69,7 @@ describe('FileNavigatorTree', () => { let mockLabelProvider: LabelProvider; const mockFilterChangeEmitter: Emitter = new Emitter(); + const mockLabelChangeEmitter: Emitter = new Emitter(); let navigatorTree: FileNavigatorTree; @@ -91,6 +92,7 @@ describe('FileNavigatorTree', () => { testContainer.bind(LabelProvider).toConstantValue(mockLabelProvider); sinon.stub(mockFileNavigatorFilter, 'onFilterChanged').value(mockFilterChangeEmitter.event); + sinon.stub(mockLabelProvider, 'onDidChange').value(mockLabelChangeEmitter.event); setup(); navigatorTree = testContainer.get(FileNavigatorTree); diff --git a/packages/navigator/src/browser/navigator-tree.ts b/packages/navigator/src/browser/navigator-tree.ts index ff72d6111fe33..95e4adab51387 100644 --- a/packages/navigator/src/browser/navigator-tree.ts +++ b/packages/navigator/src/browser/navigator-tree.ts @@ -28,6 +28,7 @@ export class FileNavigatorTree extends FileTree { @postConstruct() protected init(): void { + super.initFileTree(); this.toDispose.push(this.filter.onFilterChanged(() => this.refresh())); } diff --git a/packages/scm/src/browser/scm-contribution.ts b/packages/scm/src/browser/scm-contribution.ts index 49a5bf57c2e92..afee96b6b60fd 100644 --- a/packages/scm/src/browser/scm-contribution.ts +++ b/packages/scm/src/browser/scm-contribution.ts @@ -93,6 +93,7 @@ export class ScmContribution extends AbstractViewContribution impleme this.scmService.onDidRemoveRepository(() => this.updateStatusBar()); this.scmService.onDidChangeSelectedRepository(() => this.updateStatusBar()); this.scmService.onDidChangeStatusBarCommands(() => this.updateStatusBar()); + this.labelProvider.onDidChange(() => this.updateStatusBar()); this.updateContextKeys(); this.shell.currentChanged.connect(() => this.updateContextKeys()); diff --git a/packages/scm/src/browser/scm-widget.tsx b/packages/scm/src/browser/scm-widget.tsx index ca8cdfd321a60..a5242d8f36165 100644 --- a/packages/scm/src/browser/scm-widget.tsx +++ b/packages/scm/src/browser/scm-widget.tsx @@ -88,6 +88,7 @@ export class ScmWidget extends ReactWidget implements StatefulWidget { protected init(): void { this.refresh(); this.toDispose.push(this.scmService.onDidChangeSelectedRepository(() => this.refresh())); + this.toDispose.push(this.labelProvider.onDidChange(() => this.update())); } protected readonly toDisposeOnRefresh = new DisposableCollection(); diff --git a/tsconfig.json b/tsconfig.json index 20d702ac00d07..42d04f1a9c071 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -129,6 +129,9 @@ ], "@theia/task/lib/*": [ "packages/task/src/*" + ], + "@theia/api-samples/lib/*": [ + "examples/api-samples/src/*" ] }, "plugins": [ @@ -140,6 +143,7 @@ "include": [ "dev-packages/*/src", "packages/*/src", + "examples/*/src", "examples/*/test" ] }