diff --git a/packages/filesystem/src/browser/filesystem-frontend-contribution.ts b/packages/filesystem/src/browser/filesystem-frontend-contribution.ts index 3b702e88d4137..693761f5a577b 100644 --- a/packages/filesystem/src/browser/filesystem-frontend-contribution.ts +++ b/packages/filesystem/src/browser/filesystem-frontend-contribution.ts @@ -17,7 +17,7 @@ import { injectable, inject } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; import { environment } from '@theia/core/shared/@theia/application-package/lib/environment'; -import { MaybePromise, SelectionService, isCancelled } from '@theia/core/lib/common'; +import { MaybePromise, SelectionService, isCancelled, Emitter } from '@theia/core/lib/common'; import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common/command'; import { FrontendApplicationContribution, ApplicationShell, @@ -77,6 +77,9 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri @inject(FileService) protected readonly fileService: FileService; + protected onDidChangeEditorFileEmitter = new Emitter<{ editor: NavigatableWidget, type: FileChangeType }>(); + readonly onDidChangeEditorFile = this.onDidChangeEditorFileEmitter.event; + protected readonly userOperations = new Map>(); protected queueUserOperation(event: UserFileOperationEvent): void { const moveOperation = new Deferred(); @@ -301,6 +304,7 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri } if (!deleted) { widget.title.label += this.deletedSuffix; + this.onDidChangeEditorFileEmitter.fire({ editor: widget, type: FileChangeType.DELETED }); } const widgets = toClose.get(uriString) || []; widgets.push(widget); @@ -308,6 +312,7 @@ export class FileSystemFrontendContribution implements FrontendApplicationContri } else if (event.contains(uri, FileChangeType.ADDED)) { if (deleted) { widget.title.label = widget.title.label.substr(0, label.length - this.deletedSuffix.length); + this.onDidChangeEditorFileEmitter.fire({ editor: widget, type: FileChangeType.ADDED }); } } } diff --git a/packages/navigator/src/browser/navigator-frontend-module.ts b/packages/navigator/src/browser/navigator-frontend-module.ts index 9d833ada290bc..231e3274f1409 100644 --- a/packages/navigator/src/browser/navigator-frontend-module.ts +++ b/packages/navigator/src/browser/navigator-frontend-module.ts @@ -41,6 +41,7 @@ import { bindContributionProvider } from '@theia/core/lib/common'; import { OpenEditorsTreeDecorator } from './open-editors-widget/navigator-open-editors-decorator-service'; import { OpenEditorsWidget } from './open-editors-widget/navigator-open-editors-widget'; import { NavigatorTreeDecorator } from './navigator-decorator-service'; +import { NavigatorDeletedEditorDecorator } from './open-editors-widget/navigator-deleted-editor-decorator'; export default new ContainerModule(bind => { bindFileNavigatorPreferences(bind); @@ -63,6 +64,8 @@ export default new ContainerModule(bind => { })).inSingletonScope(); bindContributionProvider(bind, NavigatorTreeDecorator); bindContributionProvider(bind, OpenEditorsTreeDecorator); + bind(NavigatorDeletedEditorDecorator).toSelf().inSingletonScope(); + bind(OpenEditorsTreeDecorator).toService(NavigatorDeletedEditorDecorator); bind(WidgetFactory).toDynamicValue(({ container }) => ({ id: OpenEditorsWidget.ID, diff --git a/packages/navigator/src/browser/open-editors-widget/navigator-deleted-editor-decorator.ts b/packages/navigator/src/browser/open-editors-widget/navigator-deleted-editor-decorator.ts new file mode 100644 index 0000000000000..5e0a8c9973438 --- /dev/null +++ b/packages/navigator/src/browser/open-editors-widget/navigator-deleted-editor-decorator.ts @@ -0,0 +1,91 @@ +/******************************************************************************** + * Copyright (C) 2021 Ericsson 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, postConstruct } from '@theia/core/shared/inversify'; +import { ApplicationShell, DepthFirstTreeIterator, NavigatableWidget, Tree, TreeDecoration, TreeDecorator } from '@theia/core/lib/browser'; +import { FileSystemFrontendContribution } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution'; +import { Emitter } from '@theia/core'; +import { FileChangeType, FileStatNode } from '@theia/filesystem/lib/browser'; + +@injectable() +export class NavigatorDeletedEditorDecorator implements TreeDecorator { + + @inject(FileSystemFrontendContribution) + protected readonly fileSystemContribution: FileSystemFrontendContribution; + @inject(ApplicationShell) + protected readonly shell: ApplicationShell; + + readonly id = 'theia-deleted-editor-decorator'; + protected readonly onDidChangeDecorationsEmitter = new Emitter(); + readonly onDidChangeDecorations = this.onDidChangeDecorationsEmitter.event; + protected deletedURIs = new Set(); + + @postConstruct() + init(): void { + this.fileSystemContribution.onDidChangeEditorFile(({ editor, type }) => { + const uri = editor.getResourceUri()?.toString(); + if (uri) { + if (type === FileChangeType.DELETED) { + this.deletedURIs.add(uri); + } else if (type === FileChangeType.ADDED) { + this.deletedURIs.delete(uri); + } + this.fireDidChangeDecorations((tree: Tree) => this.collectDecorators(tree)); + } + }); + this.shell.onDidAddWidget(() => { + const newDeletedURIs = new Set(); + this.shell.widgets.forEach(widget => { + if (NavigatableWidget.is(widget)) { + const uri = widget.getResourceUri()?.toString(); + if (uri && this.deletedURIs.has(uri)) { + newDeletedURIs.add(uri); + } + } + }); + this.deletedURIs = newDeletedURIs; + }); + } + + decorations(tree: Tree): Map { + return this.collectDecorators(tree); + } + + protected collectDecorators(tree: Tree): Map { + const result = new Map(); + if (tree.root === undefined) { + return result; + } + for (const node of new DepthFirstTreeIterator(tree.root)) { + if (FileStatNode.is(node)) { + const uri = node.uri.toString(); + if (this.deletedURIs.has(uri)) { + const deletedDecoration: TreeDecoration.Data = { + fontData: { + style: 'line-through', + } + }; + result.set(node.id, deletedDecoration); + } + } + } + return result; + } + + protected fireDidChangeDecorations(event: (tree: Tree) => Map): void { + this.onDidChangeDecorationsEmitter.fire(event); + } +} diff --git a/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-tree-model.ts b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-tree-model.ts index 2b257b0d9aeb1..ec11ca3399359 100644 --- a/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-tree-model.ts +++ b/packages/navigator/src/browser/open-editors-widget/navigator-open-editors-tree-model.ts @@ -31,6 +31,8 @@ import { import { WorkspaceService } from '@theia/workspace/lib/browser'; import debounce = require('@theia/core/shared/lodash.debounce'); import { DisposableCollection } from '@theia/core/lib/common'; +import { FileStat } from '@theia/filesystem/lib/common/files'; + export interface OpenEditorNode extends FileStatNode { widget: Widget; }; @@ -60,6 +62,8 @@ export class OpenEditorsModel extends FileTreeModel { // Last collection of editors before a layout modification, used to detect changes in widget ordering protected _lastEditorWidgetsByArea = new Map(); + protected cachedFileStats = new Map(); + get editorWidgets(): NavigatableWidget[] { const editorWidgets: NavigatableWidget[] = []; this._editorWidgetsByArea.forEach(widgets => editorWidgets.push(...widgets)); @@ -84,7 +88,15 @@ export class OpenEditorsModel extends FileTreeModel { this.selectNode(nodeToSelect); } })); - this.toDispose.push(this.applicationShell.onDidAddWidget(() => this.updateOpenWidgets())); + this.toDispose.push(this.applicationShell.onDidAddWidget(async () => { + await this.updateOpenWidgets(); + const existingWidgetIds = new Set(this.editorWidgets.map(widget => widget.id)); + this.cachedFileStats.forEach((_fileStat, id) => { + if (!existingWidgetIds.has(id)) { + this.cachedFileStats.delete(id); + } + }); + })); this.toDispose.push(this.applicationShell.onDidRemoveWidget(() => this.updateOpenWidgets())); // Check for tabs rearranged in main and bottom this.applicationShell.mainPanel.layoutModified.connect(() => this.doUpdateOpenWidgets('main')); @@ -187,7 +199,19 @@ export class OpenEditorsModel extends FileTreeModel { for (const widget of widgetsInArea) { const uri = widget.getResourceUri(); if (uri) { - const fileStat = await this.fileService.resolve(uri); + let fileStat: FileStat; + try { + fileStat = await this.fileService.resolve(uri); + this.cachedFileStats.set(widget.id, fileStat); + } catch { + const cachedStat = this.cachedFileStats.get(widget.id); + if (cachedStat) { + fileStat = cachedStat; + } else { + continue; + } + } + const openEditorNode: OpenEditorNode = { id: widget.id, fileStat,