deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

View File

@@ -0,0 +1,32 @@
<div align='center'>
<br />
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
<h2>ECLIPSE THEIA - NAVIGATOR EXTENSION</h2>
<hr />
</div>
## Description
The `@theia/navigator` extension contributes the `file explorer` widget.\
The `file explorer` can be used to easily view, open, and manage the files that correspond to a given workspace.
## Additional Information
- [API documentation for `@theia/navigator`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_navigator.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## 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)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,55 @@
{
"name": "@theia/navigator",
"version": "1.68.0",
"description": "Theia - Navigator Extension",
"dependencies": {
"@theia/core": "1.68.0",
"@theia/filesystem": "1.68.0",
"@theia/workspace": "1.68.0",
"minimatch": "^10.0.3",
"tslib": "^2.6.2"
},
"publishConfig": {
"access": "public"
},
"theiaExtensions": [
{
"frontend": "lib/browser/navigator-frontend-module",
"backend": "lib/node/navigator-backend-module"
},
{
"frontendElectron": "lib/electron-browser/electron-navigator-module"
}
],
"keywords": [
"theia-extension"
],
"license": "EPL-2.0 OR GPL-2.0-only 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": {
"build": "theiaext build",
"clean": "theiaext clean",
"compile": "theiaext compile",
"lint": "theiaext lint",
"test": "theiaext test",
"watch": "theiaext watch"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0"
},
"nyc": {
"extends": "../../configs/nyc.json"
},
"gitHead": "21358137e41342742707f660b8e222f940a27652"
}

View File

@@ -0,0 +1,55 @@
// *****************************************************************************
// Copyright (C) 2022 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { FileNavigatorPreferences } from '../common/navigator-preferences';
import { FileTreeWidget } from '@theia/filesystem/lib/browser';
import { Attributes, HTMLAttributes } from '@theia/core/shared/react';
import { TreeNode } from '@theia/core/lib/browser';
@injectable()
export class AbstractNavigatorTreeWidget extends FileTreeWidget {
@inject(FileNavigatorPreferences)
protected readonly navigatorPreferences: FileNavigatorPreferences;
@postConstruct()
protected override init(): void {
super.init();
this.toDispose.push(
this.preferenceService.onPreferenceChanged(preference => {
if (preference.preferenceName === 'explorer.decorations.colors') {
this.update();
}
})
);
}
protected override decorateCaption(node: TreeNode, attrs: HTMLAttributes<HTMLElement>): Attributes & HTMLAttributes<HTMLElement> {
const attributes = super.decorateCaption(node, attrs);
if (this.navigatorPreferences.get('explorer.decorations.colors')) {
return attributes;
} else {
return {
...attributes,
style: {
...attributes.style,
color: undefined,
}
};
}
}
}

View File

@@ -0,0 +1,74 @@
// *****************************************************************************
// Copyright (C) 2022 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { codicon, CommonCommands } from '@theia/core/lib/browser';
import { Command } from '@theia/core/lib/common';
import { WorkspaceCommands } from '@theia/workspace/lib/browser';
export namespace FileNavigatorCommands {
export const REVEAL_IN_NAVIGATOR = Command.toLocalizedCommand({
id: 'navigator.reveal',
label: 'Reveal in Explorer'
}, 'theia/navigator/reveal');
export const TOGGLE_HIDDEN_FILES = Command.toLocalizedCommand({
id: 'navigator.toggle.hidden.files',
label: 'Toggle Hidden Files'
}, 'theia/navigator/toggleHiddenFiles');
export const TOGGLE_AUTO_REVEAL = Command.toLocalizedCommand({
id: 'navigator.toggle.autoReveal',
category: CommonCommands.FILE_CATEGORY,
label: 'Auto Reveal'
}, 'theia/navigator/autoReveal', CommonCommands.FILE_CATEGORY_KEY);
export const REFRESH_NAVIGATOR = Command.toLocalizedCommand({
id: 'navigator.refresh',
category: CommonCommands.FILE_CATEGORY,
label: 'Refresh in Explorer',
iconClass: codicon('refresh')
}, 'theia/navigator/refresh', CommonCommands.FILE_CATEGORY_KEY);
export const COLLAPSE_ALL = Command.toDefaultLocalizedCommand({
id: 'navigator.collapse.all',
category: CommonCommands.FILE_CATEGORY,
label: 'Collapse Folders in Explorer',
iconClass: codicon('collapse-all')
});
export const ADD_ROOT_FOLDER: Command = {
id: 'navigator.addRootFolder'
};
export const FOCUS = Command.toDefaultLocalizedCommand({
id: 'workbench.files.action.focusFilesExplorer',
category: CommonCommands.FILE_CATEGORY,
label: 'Focus on Files Explorer'
});
export const OPEN: Command = {
id: 'navigator.open',
};
export const OPEN_WITH: Command = {
id: 'navigator.openWith',
};
export const NEW_FILE_TOOLBAR: Command = {
id: `${WorkspaceCommands.NEW_FILE.id}.toolbar`,
iconClass: codicon('new-file')
};
export const NEW_FOLDER_TOOLBAR: Command = {
id: `${WorkspaceCommands.NEW_FOLDER.id}.toolbar`,
iconClass: codicon('new-folder')
};
/**
* @deprecated since 1.21.0. Use WorkspaceCommands.COPY_RELATIVE_FILE_COMMAND instead.
*/
export const COPY_RELATIVE_FILE_PATH = WorkspaceCommands.COPY_RELATIVE_FILE_PATH;
}

View File

@@ -0,0 +1,20 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox 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-only WITH Classpath-exception-2.0
// *****************************************************************************
export * from './navigator-model';
export * from './navigator-widget';
export * from './navigator-widget-factory';
export * from './navigator-decorator-service';

View File

@@ -0,0 +1,48 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Container, interfaces } from '@theia/core/shared/inversify';
import { TreeProps, defaultTreeProps } from '@theia/core/lib/browser';
import { createFileTreeContainer } from '@theia/filesystem/lib/browser';
import { FileNavigatorTree } from './navigator-tree';
import { FileNavigatorModel } from './navigator-model';
import { FileNavigatorWidget } from './navigator-widget';
import { NAVIGATOR_CONTEXT_MENU } from './navigator-contribution';
import { NavigatorDecoratorService } from './navigator-decorator-service';
export const FILE_NAVIGATOR_PROPS = <TreeProps>{
...defaultTreeProps,
contextMenuPath: NAVIGATOR_CONTEXT_MENU,
multiSelect: true,
search: true,
globalSelection: true
};
export function createFileNavigatorContainer(parent: interfaces.Container): Container {
const child = createFileTreeContainer(parent, {
tree: FileNavigatorTree,
model: FileNavigatorModel,
widget: FileNavigatorWidget,
decoratorService: NavigatorDecoratorService,
props: FILE_NAVIGATOR_PROPS,
});
return child;
}
export function createFileNavigatorWidget(parent: interfaces.Container): FileNavigatorWidget {
return createFileNavigatorContainer(parent).get(FileNavigatorWidget);
}

View File

@@ -0,0 +1,66 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service';
@injectable()
export class NavigatorContextKeyService {
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
protected _explorerViewletVisible: ContextKey<boolean>;
get explorerViewletVisible(): ContextKey<boolean> {
return this._explorerViewletVisible;
}
protected _explorerViewletFocus: ContextKey<boolean>;
/** True if Explorer view has keyboard focus. */
get explorerViewletFocus(): ContextKey<boolean> {
return this._explorerViewletFocus;
}
protected _filesExplorerFocus: ContextKey<boolean>;
/** True if File Explorer section has keyboard focus. */
get filesExplorerFocus(): ContextKey<boolean> {
return this._filesExplorerFocus;
}
protected _explorerResourceIsFolder: ContextKey<boolean>;
get explorerResourceIsFolder(): ContextKey<boolean> {
return this._explorerResourceIsFolder;
}
protected _isFileSystemResource: ContextKey<boolean>;
/**
* True when the Explorer or editor file is a file system resource that can be handled from a file system provider.
*/
get isFileSystemResource(): ContextKey<boolean> {
return this._isFileSystemResource;
}
@postConstruct()
protected init(): void {
this._explorerViewletVisible = this.contextKeyService.createKey<boolean>('explorerViewletVisible', false);
this._explorerViewletFocus = this.contextKeyService.createKey<boolean>('explorerViewletFocus', false);
this._filesExplorerFocus = this.contextKeyService.createKey<boolean>('filesExplorerFocus', false);
this._explorerResourceIsFolder = this.contextKeyService.createKey<boolean>('explorerResourceIsFolder', false);
this._isFileSystemResource = this.contextKeyService.createKey<boolean>('isFileSystemResource', false);
}
}

View File

@@ -0,0 +1,668 @@
// *****************************************************************************
// Copyright (C) 2017-2018 TypeFox 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
import { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import {
CommonCommands,
CompositeTreeNode,
FrontendApplication,
FrontendApplicationContribution,
KeybindingRegistry,
OpenerService,
SelectableTreeNode,
Widget,
NavigatableWidget,
ApplicationShell,
TabBar,
Title,
SHELL_TABBAR_CONTEXT_MENU,
OpenWithService
} from '@theia/core/lib/browser';
import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/file-download-command-contribution';
import {
CommandRegistry,
isOSX,
MenuModelRegistry,
MenuPath,
Mutable,
PreferenceScope,
PreferenceService,
QuickInputService,
} from '@theia/core/lib/common';
import {
DidCreateNewResourceEvent,
WorkspaceCommandContribution,
WorkspaceCommands,
WorkspaceService
} from '@theia/workspace/lib/browser';
import { EXPLORER_VIEW_CONTAINER_ID, EXPLORER_VIEW_CONTAINER_TITLE_OPTIONS } from './navigator-widget-factory';
import { FILE_NAVIGATOR_ID, FileNavigatorWidget } from './navigator-widget';
import { FileNavigatorPreferences } from '../common/navigator-preferences';
import { FileNavigatorFilter } from './navigator-filter';
import { WorkspaceNode } from './navigator-tree';
import { NavigatorContextKeyService } from './navigator-context-key-service';
import {
RenderedToolbarAction,
TabBarToolbarContribution,
TabBarToolbarRegistry
} from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { FileSystemCommands } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution';
import { NavigatorDiff, NavigatorDiffCommands } from './navigator-diff';
import { DirNode, FileNode } from '@theia/filesystem/lib/browser';
import { FileNavigatorModel } from './navigator-model';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
import { SelectionService } from '@theia/core/lib/common/selection-service';
import { OpenEditorsWidget } from './open-editors-widget/navigator-open-editors-widget';
import { OpenEditorsContextMenu } from './open-editors-widget/navigator-open-editors-menus';
import { OpenEditorsCommands } from './open-editors-widget/navigator-open-editors-commands';
import { nls } from '@theia/core/lib/common/nls';
import URI from '@theia/core/lib/common/uri';
import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler';
import { FileNavigatorCommands } from './file-navigator-commands';
import { WorkspacePreferences } from '@theia/workspace/lib/common';
export { FileNavigatorCommands };
/**
* Navigator `More Actions...` toolbar item groups.
* Used in order to group items present in the toolbar.
*/
export namespace NavigatorMoreToolbarGroups {
export const NEW_OPEN = '1_navigator_new_open';
export const TOOLS = '2_navigator_tools';
export const WORKSPACE = '3_navigator_workspace';
}
export const NAVIGATOR_CONTEXT_MENU: MenuPath = ['navigator-context-menu'];
export const SHELL_TABBAR_CONTEXT_REVEAL: MenuPath = [...SHELL_TABBAR_CONTEXT_MENU, '2_reveal'];
/**
* Navigator context menu default groups should be aligned
* with VS Code default groups: https://code.visualstudio.com/api/references/contribution-points#contributes.menus
*/
export namespace NavigatorContextMenu {
export const NAVIGATION = [...NAVIGATOR_CONTEXT_MENU, 'navigation'];
/** @deprecated use NAVIGATION */
export const OPEN = NAVIGATION;
/** @deprecated use NAVIGATION */
export const NEW = NAVIGATION;
export const WORKSPACE = [...NAVIGATOR_CONTEXT_MENU, '2_workspace'];
export const COMPARE = [...NAVIGATOR_CONTEXT_MENU, '3_compare'];
/** @deprecated use COMPARE */
export const DIFF = COMPARE;
export const SEARCH = [...NAVIGATOR_CONTEXT_MENU, '4_search'];
export const CLIPBOARD = [...NAVIGATOR_CONTEXT_MENU, '5_cutcopypaste'];
export const MODIFICATION = [...NAVIGATOR_CONTEXT_MENU, '7_modification'];
/** @deprecated use MODIFICATION */
export const MOVE = MODIFICATION;
/** @deprecated use MODIFICATION */
export const ACTIONS = MODIFICATION;
/** @deprecated use the `FileNavigatorCommands.OPEN_WITH` command */
export const OPEN_WITH = [...NAVIGATION, 'open_with'];
}
export const FILE_NAVIGATOR_TOGGLE_COMMAND_ID = 'fileNavigator:toggle';
@injectable()
export class FileNavigatorContribution extends AbstractViewContribution<FileNavigatorWidget> implements FrontendApplicationContribution, TabBarToolbarContribution {
@inject(ClipboardService)
protected readonly clipboardService: ClipboardService;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(TabBarToolbarRegistry)
protected readonly tabbarToolbarRegistry: TabBarToolbarRegistry;
@inject(NavigatorContextKeyService)
protected readonly contextKeyService: NavigatorContextKeyService;
@inject(MenuModelRegistry)
protected readonly menuRegistry: MenuModelRegistry;
@inject(NavigatorDiff)
protected readonly navigatorDiff: NavigatorDiff;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(SelectionService)
protected readonly selectionService: SelectionService;
@inject(WorkspaceCommandContribution)
protected readonly workspaceCommandContribution: WorkspaceCommandContribution;
@inject(OpenWithService)
protected readonly openWithService: OpenWithService;
@inject(QuickInputService) @optional()
protected readonly quickInputService: QuickInputService;
constructor(
@inject(FileNavigatorPreferences) protected readonly fileNavigatorPreferences: FileNavigatorPreferences,
@inject(OpenerService) protected readonly openerService: OpenerService,
@inject(FileNavigatorFilter) protected readonly fileNavigatorFilter: FileNavigatorFilter,
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService,
@inject(WorkspacePreferences) protected readonly workspacePreferences: WorkspacePreferences
) {
super({
viewContainerId: EXPLORER_VIEW_CONTAINER_ID,
widgetId: FILE_NAVIGATOR_ID,
widgetName: EXPLORER_VIEW_CONTAINER_TITLE_OPTIONS.label,
defaultWidgetOptions: {
area: 'left',
rank: 100
},
toggleCommandId: FILE_NAVIGATOR_TOGGLE_COMMAND_ID,
toggleKeybinding: 'ctrlcmd+shift+e'
});
}
@postConstruct()
protected init(): void {
this.doInit();
}
protected async doInit(): Promise<void> {
await this.fileNavigatorPreferences.ready;
this.shell.onDidChangeCurrentWidget(() => this.onCurrentWidgetChangedHandler());
const updateFocusContextKeys = () => {
const hasFocus = this.shell.activeWidget instanceof FileNavigatorWidget;
this.contextKeyService.explorerViewletFocus.set(hasFocus);
this.contextKeyService.filesExplorerFocus.set(hasFocus);
};
updateFocusContextKeys();
this.shell.onDidChangeActiveWidget(updateFocusContextKeys);
this.workspaceCommandContribution.onDidCreateNewFile(async event => this.onDidCreateNewResource(event));
this.workspaceCommandContribution.onDidCreateNewFolder(async event => this.onDidCreateNewResource(event));
}
private async onDidCreateNewResource(event: DidCreateNewResourceEvent): Promise<void> {
const navigator = this.tryGetWidget();
if (!navigator || !navigator.isVisible) {
return;
}
const model: FileNavigatorModel = navigator.model;
const parent = await model.revealFile(event.parent);
if (DirNode.is(parent)) {
await model.refresh(parent);
}
const node = await model.revealFile(event.uri);
if (SelectableTreeNode.is(node)) {
model.selectNode(node);
if (DirNode.is(node)) {
this.openView({ activate: true });
}
}
}
async initializeLayout(app: FrontendApplication): Promise<void> {
await this.openView();
}
override registerCommands(registry: CommandRegistry): void {
super.registerCommands(registry);
registry.registerCommand(FileNavigatorCommands.FOCUS, {
execute: () => this.openView({ activate: true })
});
registry.registerCommand(FileNavigatorCommands.REVEAL_IN_NAVIGATOR, UriAwareCommandHandler.MonoSelect(this.selectionService, {
execute: async uri => {
if (await this.selectFileNode(uri)) {
this.openView({ activate: false, reveal: true });
}
},
isEnabled: uri => !!this.workspaceService.getWorkspaceRootUri(uri),
isVisible: uri => !!this.workspaceService.getWorkspaceRootUri(uri),
}));
registry.registerCommand(FileNavigatorCommands.TOGGLE_HIDDEN_FILES, {
execute: () => {
this.fileNavigatorFilter.toggleHiddenFiles();
},
isEnabled: () => true,
isVisible: () => true
});
registry.registerCommand(FileNavigatorCommands.TOGGLE_AUTO_REVEAL, {
isEnabled: widget => this.withWidget(widget, () => this.workspaceService.opened),
isVisible: widget => this.withWidget(widget, () => this.workspaceService.opened),
execute: () => {
const autoReveal = !this.fileNavigatorPreferences['explorer.autoReveal'];
this.preferenceService.set('explorer.autoReveal', autoReveal, PreferenceScope.User);
if (autoReveal) {
this.selectWidgetFileNode(this.shell.currentWidget);
}
},
isToggled: () => this.fileNavigatorPreferences['explorer.autoReveal']
});
registry.registerCommand(FileNavigatorCommands.COLLAPSE_ALL, {
execute: widget => this.withWidget(widget, () => this.collapseFileNavigatorTree()),
isEnabled: widget => this.withWidget(widget, () => this.workspaceService.opened),
isVisible: widget => this.withWidget(widget, () => this.workspaceService.opened)
});
registry.registerCommand(FileNavigatorCommands.REFRESH_NAVIGATOR, {
execute: widget => this.withWidget(widget, () => this.refreshWorkspace()),
isEnabled: widget => this.withWidget(widget, () => this.workspaceService.opened),
isVisible: widget => this.withWidget(widget, () => this.workspaceService.opened)
});
registry.registerCommand(FileNavigatorCommands.ADD_ROOT_FOLDER, {
execute: (...args) => registry.executeCommand(WorkspaceCommands.ADD_FOLDER.id, ...args),
isEnabled: (...args) => registry.isEnabled(WorkspaceCommands.ADD_FOLDER.id, ...args),
isVisible: (...args) => {
if (!registry.isVisible(WorkspaceCommands.ADD_FOLDER.id, ...args)) {
return false;
}
const navigator = this.tryGetWidget();
const selection = navigator?.model.getFocusedNode();
// The node that is selected when the user clicks in empty space.
const root = navigator?.getContainerTreeNode();
return selection === root;
}
});
registry.registerCommand(NavigatorDiffCommands.COMPARE_FIRST, {
execute: () => {
this.navigatorDiff.addFirstComparisonFile();
},
isEnabled: () => true,
isVisible: () => true
});
registry.registerCommand(NavigatorDiffCommands.COMPARE_SECOND, {
execute: () => {
this.navigatorDiff.compareFiles();
},
isEnabled: () => this.navigatorDiff.isFirstFileSelected,
isVisible: () => this.navigatorDiff.isFirstFileSelected
});
registry.registerCommand(FileNavigatorCommands.OPEN, {
isEnabled: () => this.getSelectedFileNodes().length > 0,
isVisible: () => this.getSelectedFileNodes().length > 0,
execute: () => {
this.getSelectedFileNodes().forEach(async node => {
const opener = await this.openerService.getOpener(node.uri);
opener.open(node.uri);
});
}
});
registry.registerCommand(FileNavigatorCommands.OPEN_WITH, UriAwareCommandHandler.MonoSelect(this.selectionService, {
isEnabled: uri => this.openWithService.getHandlers(uri).length > 0,
isVisible: uri => this.openWithService.getHandlers(uri).length > 0,
execute: uri => this.openWithService.openWith(uri)
}));
registry.registerCommand(OpenEditorsCommands.CLOSE_ALL_TABS_FROM_TOOLBAR, {
execute: widget => this.withOpenEditorsWidget(widget, () => this.shell.closeMany(this.editorWidgets)),
isEnabled: widget => this.withOpenEditorsWidget(widget, () => true),
isVisible: widget => this.withOpenEditorsWidget(widget, () => true)
});
registry.registerCommand(OpenEditorsCommands.SAVE_ALL_TABS_FROM_TOOLBAR, {
execute: widget => this.withOpenEditorsWidget(widget, () => registry.executeCommand(CommonCommands.SAVE_ALL.id)),
isEnabled: widget => this.withOpenEditorsWidget(widget, () => true),
isVisible: widget => this.withOpenEditorsWidget(widget, () => true)
});
const filterEditorWidgets = (title: Title<Widget>) => {
const { owner } = title;
return NavigatableWidget.is(owner);
};
registry.registerCommand(OpenEditorsCommands.CLOSE_ALL_EDITORS_IN_GROUP_FROM_ICON, {
execute: (tabBarOrArea: ApplicationShell.Area | TabBar<Widget>): void => {
this.shell.closeTabs(tabBarOrArea, filterEditorWidgets);
},
isVisible: () => false
});
registry.registerCommand(OpenEditorsCommands.SAVE_ALL_IN_GROUP_FROM_ICON, {
execute: (tabBarOrArea: ApplicationShell.Area | TabBar<Widget>) => {
this.shell.saveTabs(tabBarOrArea, filterEditorWidgets);
},
isVisible: () => false
});
registry.registerCommand(FileNavigatorCommands.NEW_FILE_TOOLBAR, {
execute: (...args) => registry.executeCommand(WorkspaceCommands.NEW_FILE.id, ...args),
isEnabled: widget => this.withWidget(widget, () => this.workspaceService.opened),
isVisible: widget => this.withWidget(widget, () => this.workspaceService.opened)
});
registry.registerCommand(FileNavigatorCommands.NEW_FOLDER_TOOLBAR, {
execute: (...args) => registry.executeCommand(WorkspaceCommands.NEW_FOLDER.id, ...args),
isEnabled: widget => this.withWidget(widget, () => this.workspaceService.opened),
isVisible: widget => this.withWidget(widget, () => this.workspaceService.opened)
});
}
protected get editorWidgets(): NavigatableWidget[] {
const openEditorsWidget = this.widgetManager.tryGetWidget<OpenEditorsWidget>(OpenEditorsWidget.ID);
return openEditorsWidget?.editorWidgets ?? [];
}
protected getSelectedFileNodes(): FileNode[] {
return this.tryGetWidget()?.model.selectedNodes.filter(FileNode.is) || [];
}
protected withWidget<T>(widget: Widget | undefined = this.tryGetWidget(), cb: (navigator: FileNavigatorWidget) => T): T | false {
if (widget instanceof FileNavigatorWidget && widget.id === FILE_NAVIGATOR_ID) {
return cb(widget);
}
return false;
}
protected withOpenEditorsWidget<T>(widget: Widget, cb: (navigator: OpenEditorsWidget) => T): T | false {
if (widget instanceof OpenEditorsWidget && widget.id === OpenEditorsWidget.ID) {
return cb(widget);
}
return false;
}
override registerMenus(registry: MenuModelRegistry): void {
super.registerMenus(registry);
registry.registerMenuAction(SHELL_TABBAR_CONTEXT_REVEAL, {
commandId: FileNavigatorCommands.REVEAL_IN_NAVIGATOR.id,
label: FileNavigatorCommands.REVEAL_IN_NAVIGATOR.label,
order: '5'
});
registry.registerMenuAction(NavigatorContextMenu.NAVIGATION, {
commandId: FileNavigatorCommands.OPEN.id,
label: nls.localizeByDefault('Open')
});
registry.registerMenuAction(NavigatorContextMenu.NAVIGATION, {
commandId: FileNavigatorCommands.OPEN_WITH.id,
when: '!explorerResourceIsFolder',
label: nls.localizeByDefault('Open With...')
});
registry.registerMenuAction(NavigatorContextMenu.CLIPBOARD, {
commandId: CommonCommands.COPY.id,
order: 'a'
});
registry.registerMenuAction(NavigatorContextMenu.CLIPBOARD, {
commandId: CommonCommands.PASTE.id,
order: 'b'
});
registry.registerMenuAction(NavigatorContextMenu.CLIPBOARD, {
commandId: CommonCommands.COPY_PATH.id,
order: 'c'
});
registry.registerMenuAction(NavigatorContextMenu.CLIPBOARD, {
commandId: WorkspaceCommands.COPY_RELATIVE_FILE_PATH.id,
label: WorkspaceCommands.COPY_RELATIVE_FILE_PATH.label,
order: 'd'
});
registry.registerMenuAction(NavigatorContextMenu.CLIPBOARD, {
commandId: FileDownloadCommands.COPY_DOWNLOAD_LINK.id,
order: 'z'
});
registry.registerMenuAction(NavigatorContextMenu.MODIFICATION, {
commandId: WorkspaceCommands.FILE_RENAME.id
});
registry.registerMenuAction(NavigatorContextMenu.MODIFICATION, {
commandId: WorkspaceCommands.FILE_DELETE.id
});
registry.registerMenuAction(NavigatorContextMenu.MODIFICATION, {
commandId: WorkspaceCommands.FILE_DUPLICATE.id
});
const downloadUploadMenu = [...NAVIGATOR_CONTEXT_MENU, '6_downloadupload'];
registry.registerMenuAction(downloadUploadMenu, {
commandId: FileSystemCommands.UPLOAD.id,
order: 'a'
});
registry.registerMenuAction(downloadUploadMenu, {
commandId: FileDownloadCommands.DOWNLOAD.id,
order: 'b'
});
registry.registerMenuAction(NavigatorContextMenu.NAVIGATION, {
commandId: WorkspaceCommands.NEW_FILE.id,
when: 'explorerResourceIsFolder'
});
registry.registerMenuAction(NavigatorContextMenu.NAVIGATION, {
commandId: WorkspaceCommands.NEW_FOLDER.id,
when: 'explorerResourceIsFolder'
});
registry.registerMenuAction(NavigatorContextMenu.COMPARE, {
commandId: WorkspaceCommands.FILE_COMPARE.id
});
registry.registerMenuAction(NavigatorContextMenu.MODIFICATION, {
commandId: FileNavigatorCommands.COLLAPSE_ALL.id,
label: nls.localizeByDefault('Collapse All'),
order: 'z2'
});
registry.registerMenuAction(NavigatorContextMenu.COMPARE, {
commandId: NavigatorDiffCommands.COMPARE_FIRST.id,
order: 'za'
});
registry.registerMenuAction(NavigatorContextMenu.COMPARE, {
commandId: NavigatorDiffCommands.COMPARE_SECOND.id,
order: 'zb'
});
// Open Editors Widget Menu Items
registry.registerMenuAction(OpenEditorsContextMenu.CLIPBOARD, {
commandId: CommonCommands.COPY_PATH.id,
order: 'a'
});
registry.registerMenuAction(OpenEditorsContextMenu.CLIPBOARD, {
commandId: WorkspaceCommands.COPY_RELATIVE_FILE_PATH.id,
order: 'b'
});
registry.registerMenuAction(OpenEditorsContextMenu.SAVE, {
commandId: CommonCommands.SAVE.id,
order: 'a'
});
registry.registerMenuAction(OpenEditorsContextMenu.COMPARE, {
commandId: NavigatorDiffCommands.COMPARE_FIRST.id,
order: 'a'
});
registry.registerMenuAction(OpenEditorsContextMenu.COMPARE, {
commandId: NavigatorDiffCommands.COMPARE_SECOND.id,
order: 'b'
});
registry.registerMenuAction(OpenEditorsContextMenu.MODIFICATION, {
commandId: CommonCommands.CLOSE_TAB.id,
label: nls.localizeByDefault('Close'),
order: 'a'
});
registry.registerMenuAction(OpenEditorsContextMenu.MODIFICATION, {
commandId: CommonCommands.CLOSE_OTHER_TABS.id,
label: nls.localizeByDefault('Close Others'),
order: 'b'
});
registry.registerMenuAction(OpenEditorsContextMenu.MODIFICATION, {
commandId: CommonCommands.CLOSE_ALL_MAIN_TABS.id,
label: nls.localizeByDefault('Close All'),
order: 'c'
});
registry.registerMenuAction(NavigatorContextMenu.WORKSPACE, {
commandId: FileNavigatorCommands.ADD_ROOT_FOLDER.id,
label: WorkspaceCommands.ADD_FOLDER.label
});
registry.registerMenuAction(NavigatorContextMenu.WORKSPACE, {
commandId: WorkspaceCommands.REMOVE_FOLDER.id
});
}
override registerKeybindings(registry: KeybindingRegistry): void {
super.registerKeybindings(registry);
registry.registerKeybinding({
command: FileNavigatorCommands.REVEAL_IN_NAVIGATOR.id,
keybinding: 'alt+r'
});
registry.registerKeybinding({
command: WorkspaceCommands.FILE_DELETE.id,
keybinding: isOSX ? 'cmd+backspace' : 'del',
when: 'filesExplorerFocus'
});
registry.registerKeybinding({
command: WorkspaceCommands.FILE_RENAME.id,
keybinding: 'f2',
when: 'filesExplorerFocus'
});
registry.registerKeybinding({
command: FileNavigatorCommands.TOGGLE_HIDDEN_FILES.id,
keybinding: 'ctrlcmd+i',
when: 'filesExplorerFocus'
});
}
async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> {
toolbarRegistry.registerItem({
id: FileNavigatorCommands.NEW_FILE_TOOLBAR.id,
command: FileNavigatorCommands.NEW_FILE_TOOLBAR.id,
tooltip: nls.localizeByDefault('New File...'),
priority: 0,
});
toolbarRegistry.registerItem({
id: FileNavigatorCommands.NEW_FOLDER_TOOLBAR.id,
command: FileNavigatorCommands.NEW_FOLDER_TOOLBAR.id,
tooltip: nls.localizeByDefault('New Folder...'),
priority: 1,
});
toolbarRegistry.registerItem({
id: FileNavigatorCommands.REFRESH_NAVIGATOR.id,
command: FileNavigatorCommands.REFRESH_NAVIGATOR.id,
tooltip: nls.localizeByDefault('Refresh Explorer'),
priority: 2,
});
toolbarRegistry.registerItem({
id: FileNavigatorCommands.COLLAPSE_ALL.id,
command: FileNavigatorCommands.COLLAPSE_ALL.id,
tooltip: nls.localizeByDefault('Collapse All'),
priority: 3,
});
// More (...) toolbar items.
this.registerMoreToolbarItem({
id: FileNavigatorCommands.TOGGLE_AUTO_REVEAL.id,
command: FileNavigatorCommands.TOGGLE_AUTO_REVEAL.id,
tooltip: FileNavigatorCommands.TOGGLE_AUTO_REVEAL.label,
group: NavigatorMoreToolbarGroups.TOOLS,
});
this.registerMoreToolbarItem({
id: WorkspaceCommands.ADD_FOLDER.id,
command: WorkspaceCommands.ADD_FOLDER.id,
tooltip: WorkspaceCommands.ADD_FOLDER.label,
group: NavigatorMoreToolbarGroups.WORKSPACE,
});
// Open Editors toolbar items.
toolbarRegistry.registerItem({
id: OpenEditorsCommands.SAVE_ALL_TABS_FROM_TOOLBAR.id,
command: OpenEditorsCommands.SAVE_ALL_TABS_FROM_TOOLBAR.id,
tooltip: OpenEditorsCommands.SAVE_ALL_TABS_FROM_TOOLBAR.label,
priority: 0,
});
toolbarRegistry.registerItem({
id: OpenEditorsCommands.CLOSE_ALL_TABS_FROM_TOOLBAR.id,
command: OpenEditorsCommands.CLOSE_ALL_TABS_FROM_TOOLBAR.id,
tooltip: OpenEditorsCommands.CLOSE_ALL_TABS_FROM_TOOLBAR.label,
priority: 1,
});
}
/**
* Register commands to the `More Actions...` navigator toolbar item.
*/
public registerMoreToolbarItem = (item: Mutable<RenderedToolbarAction> & { command: string }) => {
const commandId = item.command;
const id = 'navigator.tabbar.toolbar.' + commandId;
const command = this.commandRegistry.getCommand(commandId);
this.commandRegistry.registerCommand({ id, iconClass: command && command.iconClass }, {
execute: (w, ...args) => w instanceof FileNavigatorWidget
&& this.commandRegistry.executeCommand(commandId, ...args),
isEnabled: (w, ...args) => w instanceof FileNavigatorWidget
&& this.commandRegistry.isEnabled(commandId, ...args),
isVisible: (w, ...args) => w instanceof FileNavigatorWidget
&& this.commandRegistry.isVisible(commandId, ...args),
isToggled: (w, ...args) => w instanceof FileNavigatorWidget
&& this.commandRegistry.isToggled(commandId, ...args),
});
item.command = id;
this.tabbarToolbarRegistry.registerItem(item);
};
/**
* Reveals and selects node in the file navigator to which given widget is related.
* Does nothing if given widget undefined or doesn't have related resource.
*
* @param widget widget file resource of which should be revealed and selected
*/
async selectWidgetFileNode(widget: Widget | undefined): Promise<boolean> {
return this.selectFileNode(NavigatableWidget.getUri(widget));
}
async selectFileNode(uri?: URI): Promise<boolean> {
if (uri) {
const { model } = await this.widget;
const node = await model.revealFile(uri);
if (SelectableTreeNode.is(node)) {
model.selectNode(node);
return true;
}
}
return false;
}
protected onCurrentWidgetChangedHandler(): void {
if (this.fileNavigatorPreferences['explorer.autoReveal']) {
this.selectWidgetFileNode(this.shell.currentWidget);
}
}
/**
* Collapse file navigator nodes and set focus on first visible node
* - single root workspace: collapse all nodes except root
* - multiple root workspace: collapse all nodes, even roots
*/
async collapseFileNavigatorTree(): Promise<void> {
const { model } = await this.widget;
// collapse all child nodes which are not the root (single root workspace)
// collapse all root nodes (multiple root workspace)
let root = model.root as CompositeTreeNode;
if (WorkspaceNode.is(root) && root.children.length === 1) {
root = root.children[0];
}
root.children.forEach(child => CompositeTreeNode.is(child) && model.collapseAll(child));
// select first visible node
const firstChild = WorkspaceNode.is(root) ? root.children[0] : root;
if (SelectableTreeNode.is(firstChild)) {
model.selectNode(firstChild);
}
}
/**
* force refresh workspace in navigator
*/
async refreshWorkspace(): Promise<void> {
const { model } = await this.widget;
await model.refresh();
}
}

View File

@@ -0,0 +1,36 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
import { TreeDecorator, AbstractTreeDecoratorService } from '@theia/core/lib/browser/tree/tree-decorator';
/**
* Symbol for all decorators that would like to contribute into the navigator.
*/
export const NavigatorTreeDecorator = Symbol('NavigatorTreeDecorator');
/**
* Decorator service for the navigator.
*/
@injectable()
export class NavigatorDecoratorService extends AbstractTreeDecoratorService {
constructor(@inject(ContributionProvider) @named(NavigatorTreeDecorator) protected readonly contributions: ContributionProvider<TreeDecorator>) {
super(contributions.getContributions());
}
}

View File

@@ -0,0 +1,113 @@
// *****************************************************************************
// Copyright (C) 2019 David Saunders 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
const disableJSDOM = enableJSDOM();
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
FrontendApplicationConfigProvider.set({});
import { expect } from 'chai';
import { NavigatorDiff } from './navigator-diff';
import * as path from 'path';
import { Container, ContainerModule } from '@theia/core/shared/inversify';
import { SelectionService, ILogger } from '@theia/core/lib/common';
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
import URI from '@theia/core/lib/common/uri';
import { OpenerService } from '@theia/core/lib/browser';
import { MockOpenerService } from '@theia/core/lib/browser/test/mock-opener-service';
import { MessageService } from '@theia/core/lib/common/message-service';
import { MessageClient } from '@theia/core/lib/common/message-service-protocol';
import { FileUri } from '@theia/core/lib/common/file-uri';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { DiskFileSystemProvider } from '@theia/filesystem/lib/node/disk-file-system-provider';
disableJSDOM();
let testContainer: Container;
beforeEach(() => {
testContainer = new Container();
const module = new ContainerModule((bind, unbind, isBound, rebind) => {
bind(ILogger).to(MockLogger).inSingletonScope();
bind(SelectionService).toSelf().inSingletonScope();
bind(NavigatorDiff).toSelf().inSingletonScope();
bind(OpenerService).to(MockOpenerService);
const fileService = new FileService();
fileService['resourceForError'] = (resource: URI) => resource.toString();
fileService.registerProvider('file', new DiskFileSystemProvider());
bind(FileService).toConstantValue(fileService);
bind(MessageService).toSelf().inSingletonScope();
bind(MessageClient).toSelf().inSingletonScope();
});
testContainer.load(module);
});
describe('NavigatorDiff', () => {
it('should allow a valid first file to be added', async () => {
const diff = testContainer.get(NavigatorDiff);
testContainer.get(SelectionService).selection = [{
uri: new URI(FileUri.create(path.resolve(__dirname, '../../test-resources/testFileA.json')).toString())
}];
const result = await diff.addFirstComparisonFile();
expect(result).to.be.true;
});
it('should reject invalid file when added', async () => {
const diff = testContainer.get(NavigatorDiff);
testContainer.get(SelectionService).selection = [{
uri: new URI(FileUri.create(path.resolve(__dirname, '../../test-resources/nonExistentFile.json')).toString())
}];
const result = await diff.addFirstComparisonFile();
expect(result).to.be.false;
});
it('should run comparison when second file is added', done => {
const diff = testContainer.get(NavigatorDiff);
testContainer.get(SelectionService).selection = [{
uri: new URI(FileUri.create(path.resolve(__dirname, '../../test-resources/testFileA.json')).toString())
}];
diff.addFirstComparisonFile()
.then(result => {
testContainer.get(SelectionService).selection = [{
uri: new URI(FileUri.create(path.resolve(__dirname, '../../test-resources/testFileB.json')).toString())
}];
diff.compareFiles()
.then(compareResult => {
expect(compareResult).to.be.true;
done();
});
});
});
it('should fail to run comparison if first file not added', done => {
const diff = testContainer.get(NavigatorDiff);
testContainer.get(SelectionService).selection = [{
uri: new URI(FileUri.create(path.resolve(__dirname, '../../test-resources/testFileA.json')).toString())
}];
diff.compareFiles()
.then(compareResult => {
expect(compareResult).to.be.false;
done();
});
});
});

View File

@@ -0,0 +1,136 @@
// *****************************************************************************
// Copyright (C) 2019 David Saunders 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { SelectionService, UriSelection } from '@theia/core/lib/common';
import { OpenerService, open } from '@theia/core/lib/browser/opener-service';
import { MessageService } from '@theia/core/lib/common/message-service';
import { Command } from '@theia/core/lib/common/command';
import { DiffUris } from '@theia/core/lib/browser/diff-uris';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileOperationError, FileOperationResult } from '@theia/filesystem/lib/common/files';
export namespace NavigatorDiffCommands {
const COMPARE_CATEGORY = 'Compare';
export const COMPARE_FIRST = Command.toDefaultLocalizedCommand({
id: 'compare:first',
category: COMPARE_CATEGORY,
label: 'Select for Compare'
});
export const COMPARE_SECOND = Command.toDefaultLocalizedCommand({
id: 'compare:second',
category: COMPARE_CATEGORY,
label: 'Compare with Selected'
});
}
@injectable()
export class NavigatorDiff {
@inject(FileService)
protected readonly fileService: FileService;
@inject(OpenerService)
protected openerService: OpenerService;
@inject(MessageService)
protected readonly notifications: MessageService;
@inject(SelectionService)
protected readonly selectionService: SelectionService;
constructor(
) {
}
protected _firstCompareFile: URI | undefined = undefined;
protected get firstCompareFile(): URI | undefined {
return this._firstCompareFile;
}
protected set firstCompareFile(uri: URI | undefined) {
this._firstCompareFile = uri;
this._isFirstFileSelected = true;
}
protected _isFirstFileSelected: boolean;
get isFirstFileSelected(): boolean {
return this._isFirstFileSelected;
}
protected async isDirectory(uri: URI): Promise<boolean> {
try {
const stat = await this.fileService.resolve(uri);
return stat.isDirectory;
} catch (e) {
if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
return true;
}
}
return false;
}
protected async getURISelection(): Promise<URI | undefined> {
const uri = UriSelection.getUri(this.selectionService.selection);
if (!uri) {
return undefined;
}
if (await this.isDirectory(uri)) {
return undefined;
}
return uri;
}
/**
* Adds the initial file for comparison
* @see SelectionService
* @see compareFiles
* @returns Promise<boolean> indicating whether the uri is valid
*/
async addFirstComparisonFile(): Promise<boolean> {
const uriSelected = await this.getURISelection();
if (uriSelected === undefined) {
return false;
}
this.firstCompareFile = uriSelected;
return true;
}
/**
* Compare selected files. First file is selected through addFirstComparisonFile
* @see SelectionService
* @see addFirstComparisonFile
* @returns Promise<boolean> indicating whether the comparison was completed successfully
*/
async compareFiles(): Promise<boolean> {
const uriSelected = await this.getURISelection();
if (this.firstCompareFile === undefined || uriSelected === undefined) {
return false;
}
const diffUri = DiffUris.encode(this.firstCompareFile, uriSelected);
open(this.openerService, diffUri).catch(e => {
this.notifications.error(e.message);
});
return true;
}
}

View File

@@ -0,0 +1,148 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
const disableJSDOM = enableJSDOM();
import { expect } from 'chai';
import { FileNavigatorFilterPredicate } from './navigator-filter';
disableJSDOM();
interface Input {
readonly patterns: { [key: string]: boolean };
readonly includes: string[];
readonly excludes: string[];
}
describe('navigator-filter-glob', () => {
const pathPrefix = 'file:///some/path/to';
const toItem = (id: string) => ({ id: `${pathPrefix}${id}` });
const itemsToFilter = [
'/.git/',
'/.git/a',
'/.git/b',
'/src/foo/',
'/src/foo/a.js',
'/src/foo/b.js',
'/src/foo/a.ts',
'/src/foo/b.ts',
'/src/foo/test/bar/a.js',
'/src/foo/test/bar/b.js',
'/test/baz/bar/a.js',
'/test/baz/bar/b.js'
].map(toItem);
([
{
patterns: {
'**/.git/**': true
},
includes: [
'/src/foo/'
],
excludes: [
'/.git/',
'/.git/a',
'/.git/b'
]
},
{
patterns: {
'*.js': true
},
includes: [
'/src/foo/a.ts',
'/.git/'
],
excludes: [
'/src/foo/a.js',
'/test/baz/bar/a.js'
]
},
{
patterns: {
'**/test/bar/**': true
},
includes: [
'/test/baz/bar/a.js',
'/test/baz/bar/b.js',
'/.git/'
],
excludes: [
'/src/foo/test/bar/a.js',
'/src/foo/test/bar/b.js'
]
},
{
patterns: {
'*.js': true,
'**/.git/**': true
},
includes: [
'/src/foo/a.ts'
],
excludes: [
'/.git/',
'/src/foo/a.js',
'/test/baz/bar/a.js'
]
},
{
patterns: {
'*.js': false,
'**/.git/**': false
},
includes: [
'/.git/',
'/.git/a',
'/.git/b',
'/src/foo/',
'/src/foo/a.js',
'/src/foo/b.js',
'/src/foo/a.ts',
'/src/foo/b.ts',
'/src/foo/test/bar/a.js',
'/src/foo/test/bar/b.js',
'/test/baz/bar/a.js',
'/test/baz/bar/b.js'
],
excludes: [
]
}
] as Input[]).forEach((test, index) => {
it(`${index < 10 ? `0${index + 1}` : `${index + 1}`} glob-filter: (${Object.keys(test.patterns).map(key => `${key} [${test.patterns[key]}]`).join(', ')}) `, () => {
const filter = new FileNavigatorFilterPredicate(test.patterns);
const result = itemsToFilter.filter(filter.filter.bind(filter));
test.includes.map(toItem).forEach(item => includes(result, item));
test.excludes.map(toItem).forEach(item => excludes(result, item));
});
});
});
function includes<T>(array: T[], item: T, message: string = `Expected ${JSON.stringify(array)} to include ${JSON.stringify(item)}.`): void {
expect(array).to.deep.include(item, message);
}
function excludes<T>(array: T[], item: T, message: string = `Expected ${JSON.stringify(array)} to not include ${JSON.stringify(item)}.`): void {
expect(array).to.not.deep.include(item, message);
}

View File

@@ -0,0 +1,165 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { Minimatch } from 'minimatch';
import { MaybePromise } from '@theia/core/lib/common/types';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { FileSystemPreferences, FileSystemConfiguration } from '@theia/filesystem/lib/common/filesystem-preferences';
import { FileNavigatorPreferences, FileNavigatorConfiguration } from '../common/navigator-preferences';
import { PreferenceChangeEvent } from '@theia/core';
/**
* Filter for omitting elements from the navigator. For more details on the exclusion patterns,
* one should check either the manual with `man 5 gitignore` or just [here](https://git-scm.com/docs/gitignore).
*/
@injectable()
export class FileNavigatorFilter {
protected readonly emitter: Emitter<void> = new Emitter<void>();
protected filterPredicate: FileNavigatorFilter.Predicate;
protected showHiddenFiles: boolean;
@inject(FileSystemPreferences)
protected readonly filesPreferences: FileSystemPreferences;
constructor(
@inject(FileNavigatorPreferences) protected readonly preferences: FileNavigatorPreferences
) { }
@postConstruct()
protected init(): void {
this.doInit();
}
protected async doInit(): Promise<void> {
this.filterPredicate = this.createFilterPredicate(this.filesPreferences['files.exclude']);
this.filesPreferences.onPreferenceChanged(event => this.onFilesPreferenceChanged(event));
this.preferences.onPreferenceChanged(event => this.onPreferenceChanged(event));
}
async filter<T extends { id: string }>(items: MaybePromise<T[]>): Promise<T[]> {
return (await items).filter(item => this.filterItem(item));
}
get onFilterChanged(): Event<void> {
return this.emitter.event;
}
protected filterItem(item: { id: string }): boolean {
return this.filterPredicate.filter(item);
}
protected fireFilterChanged(): void {
this.emitter.fire(undefined);
}
protected onFilesPreferenceChanged(event: PreferenceChangeEvent<FileSystemConfiguration>): void {
const { preferenceName } = event;
if (preferenceName === 'files.exclude') {
const filesExcludes = this.filesPreferences['files.exclude'];
this.filterPredicate = this.createFilterPredicate(filesExcludes);
this.fireFilterChanged();
}
}
protected onPreferenceChanged(event: PreferenceChangeEvent<FileNavigatorConfiguration>): void {
}
protected createFilterPredicate(exclusions: FileNavigatorFilter.Exclusions): FileNavigatorFilter.Predicate {
return new FileNavigatorFilterPredicate(this.interceptExclusions(exclusions));
}
toggleHiddenFiles(): void {
this.showHiddenFiles = !this.showHiddenFiles;
const filesExcludes = this.filesPreferences['files.exclude'];
this.filterPredicate = this.createFilterPredicate(filesExcludes || {});
this.fireFilterChanged();
}
protected interceptExclusions(exclusions: FileNavigatorFilter.Exclusions): FileNavigatorFilter.Exclusions {
return {
...exclusions,
'**/.*': this.showHiddenFiles
};
}
}
export namespace FileNavigatorFilter {
/**
* File navigator filter predicate.
*/
export interface Predicate {
/**
* Returns `true` if the item should filtered our from the navigator. Otherwise, `true`.
*
* @param item the identifier of a tree node.
*/
filter(item: { id: string }): boolean;
}
export namespace Predicate {
/**
* Wraps a bunch of predicates and returns with a new one that evaluates to `true` if
* each of the wrapped predicates evaluates to `true`. Otherwise, `false`.
*/
export function and(...predicates: Predicate[]): Predicate {
return {
filter: id => predicates.every(predicate => predicate.filter(id))
};
}
}
/**
* Type for the exclusion patterns. The property keys are the patterns, values are whether the exclusion is enabled or not.
*/
export interface Exclusions {
[key: string]: boolean;
}
}
/**
* Concrete filter navigator filter predicate that is decoupled from the preferences.
*/
export class FileNavigatorFilterPredicate implements FileNavigatorFilter.Predicate {
private readonly delegate: FileNavigatorFilter.Predicate;
constructor(exclusions: FileNavigatorFilter.Exclusions) {
const patterns = Object.keys(exclusions).map(pattern => ({ pattern, enabled: exclusions[pattern] })).filter(object => object.enabled).map(object => object.pattern);
this.delegate = FileNavigatorFilter.Predicate.and(...patterns.map(pattern => this.createDelegate(pattern)));
}
filter(item: { id: string }): boolean {
return this.delegate.filter(item);
}
protected createDelegate(pattern: string): FileNavigatorFilter.Predicate {
const delegate = new Minimatch(pattern, { matchBase: true });
return {
filter: item => !delegate.match(item.id)
};
}
}

View File

@@ -0,0 +1,89 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import '../../src/browser/style/index.css';
import '../../src/browser/open-editors-widget/open-editors.css';
import { ContainerModule } from '@theia/core/shared/inversify';
import {
bindViewContribution,
FrontendApplicationContribution,
ApplicationShellLayoutMigration
} from '@theia/core/lib/browser';
import { FileNavigatorWidget, FILE_NAVIGATOR_ID } from './navigator-widget';
import { FileNavigatorContribution } from './navigator-contribution';
import { createFileNavigatorWidget } from './navigator-container';
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
import { bindFileNavigatorPreferences } from '../common/navigator-preferences';
import { FileNavigatorFilter } from './navigator-filter';
import { NavigatorContextKeyService } from './navigator-context-key-service';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { NavigatorDiff } from './navigator-diff';
import { NavigatorLayoutVersion3Migration, NavigatorLayoutVersion5Migration } from './navigator-layout-migrations';
import { NavigatorTabBarDecorator } from './navigator-tab-bar-decorator';
import { TabBarDecorator } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { NavigatorWidgetFactory } from './navigator-widget-factory';
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';
import { NavigatorSymlinkDecorator } from './navigator-symlink-decorator';
import { FileTreeDecoratorAdapter } from '@theia/filesystem/lib/browser';
export default new ContainerModule(bind => {
bindFileNavigatorPreferences(bind);
bind(FileNavigatorFilter).toSelf().inSingletonScope();
bind(NavigatorContextKeyService).toSelf().inSingletonScope();
bindViewContribution(bind, FileNavigatorContribution);
bind(FrontendApplicationContribution).toService(FileNavigatorContribution);
bind(TabBarToolbarContribution).toService(FileNavigatorContribution);
bind(FileNavigatorWidget).toDynamicValue(ctx =>
createFileNavigatorWidget(ctx.container)
);
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: FILE_NAVIGATOR_ID,
createWidget: () => container.get(FileNavigatorWidget)
})).inSingletonScope();
bindContributionProvider(bind, NavigatorTreeDecorator);
bindContributionProvider(bind, OpenEditorsTreeDecorator);
bind(NavigatorTreeDecorator).toService(FileTreeDecoratorAdapter);
bind(OpenEditorsTreeDecorator).toService(FileTreeDecoratorAdapter);
bind(NavigatorDeletedEditorDecorator).toSelf().inSingletonScope();
bind(OpenEditorsTreeDecorator).toService(NavigatorDeletedEditorDecorator);
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: OpenEditorsWidget.ID,
createWidget: () => OpenEditorsWidget.createWidget(container)
})).inSingletonScope();
bind(NavigatorWidgetFactory).toSelf().inSingletonScope();
bind(WidgetFactory).toService(NavigatorWidgetFactory);
bind(ApplicationShellLayoutMigration).to(NavigatorLayoutVersion3Migration).inSingletonScope();
bind(ApplicationShellLayoutMigration).to(NavigatorLayoutVersion5Migration).inSingletonScope();
bind(NavigatorDiff).toSelf().inSingletonScope();
bind(NavigatorTabBarDecorator).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(NavigatorTabBarDecorator);
bind(TabBarDecorator).toService(NavigatorTabBarDecorator);
bind(NavigatorSymlinkDecorator).toSelf().inSingletonScope();
bind(NavigatorTreeDecorator).toService(NavigatorSymlinkDecorator);
bind(OpenEditorsTreeDecorator).toService(NavigatorSymlinkDecorator);
});

View File

@@ -0,0 +1,65 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { ApplicationShellLayoutMigration, WidgetDescription, ApplicationShellLayoutMigrationContext } from '@theia/core/lib/browser/shell/shell-layout-restorer';
import { EXPLORER_VIEW_CONTAINER_ID, EXPLORER_VIEW_CONTAINER_TITLE_OPTIONS } from './navigator-widget-factory';
import { FILE_NAVIGATOR_ID } from './navigator-widget';
@injectable()
export class NavigatorLayoutVersion3Migration implements ApplicationShellLayoutMigration {
readonly layoutVersion = 3.0;
onWillInflateWidget(desc: WidgetDescription, { parent }: ApplicationShellLayoutMigrationContext): WidgetDescription | undefined {
if (desc.constructionOptions.factoryId === FILE_NAVIGATOR_ID && !parent) {
return {
constructionOptions: {
factoryId: EXPLORER_VIEW_CONTAINER_ID
},
innerWidgetState: {
parts: [
{
widget: {
constructionOptions: {
factoryId: FILE_NAVIGATOR_ID
},
innerWidgetState: desc.innerWidgetState
},
partId: {
factoryId: FILE_NAVIGATOR_ID
},
collapsed: false,
hidden: false
}
],
title: EXPLORER_VIEW_CONTAINER_TITLE_OPTIONS
}
};
}
return undefined;
}
}
@injectable()
export class NavigatorLayoutVersion5Migration implements ApplicationShellLayoutMigration {
readonly layoutVersion = 5.0;
onWillInflateWidget(desc: WidgetDescription): WidgetDescription | undefined {
if (desc.constructionOptions.factoryId === EXPLORER_VIEW_CONTAINER_ID && typeof desc.innerWidgetState === 'string') {
desc.innerWidgetState = desc.innerWidgetState.replace(/navigator-tab-icon/g, EXPLORER_VIEW_CONTAINER_TITLE_OPTIONS.iconClass!);
return desc;
}
return undefined;
}
}

View File

@@ -0,0 +1,221 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { FileNode, FileTreeModel } from '@theia/filesystem/lib/browser';
import { OpenerService, open, TreeNode, ExpandableTreeNode, CompositeTreeNode, SelectableTreeNode } from '@theia/core/lib/browser';
import { FileNavigatorTree, WorkspaceRootNode, WorkspaceNode } from './navigator-tree';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { ProgressService } from '@theia/core/lib/common/progress-service';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { Disposable } from '@theia/core/lib/common/disposable';
@injectable()
export class FileNavigatorModel extends FileTreeModel {
@inject(OpenerService) protected readonly openerService: OpenerService;
@inject(FileNavigatorTree) protected override readonly tree: FileNavigatorTree;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@inject(FrontendApplicationStateService) protected readonly applicationState: FrontendApplicationStateService;
@inject(ProgressService)
protected readonly progressService: ProgressService;
@postConstruct()
protected override init(): void {
super.init();
this.reportBusyProgress();
this.initializeRoot();
}
protected readonly pendingBusyProgress = new Map<string, Deferred<void>>();
protected reportBusyProgress(): void {
this.toDispose.push(this.onDidChangeBusy(node => {
const pending = this.pendingBusyProgress.get(node.id);
if (pending) {
if (!node.busy) {
pending.resolve();
this.pendingBusyProgress.delete(node.id);
}
return;
}
if (node.busy) {
const progress = new Deferred<void>();
this.pendingBusyProgress.set(node.id, progress);
this.progressService.withProgress('', 'explorer', () => progress.promise);
}
}));
this.toDispose.push(Disposable.create(() => {
for (const pending of this.pendingBusyProgress.values()) {
pending.resolve();
}
this.pendingBusyProgress.clear();
}));
}
protected async initializeRoot(): Promise<void> {
await Promise.all([
this.applicationState.reachedState('initialized_layout'),
this.workspaceService.roots
]);
await this.updateRoot();
if (this.toDispose.disposed) {
return;
}
this.toDispose.push(this.workspaceService.onWorkspaceChanged(() => this.updateRoot()));
this.toDispose.push(this.workspaceService.onWorkspaceLocationChanged(() => this.updateRoot()));
if (this.selectedNodes.length) {
return;
}
const root = this.root;
if (CompositeTreeNode.is(root) && root.children.length === 1) {
const child = root.children[0];
if (SelectableTreeNode.is(child) && !child.selected && ExpandableTreeNode.is(child)) {
this.selectNode(child);
this.expandNode(child);
}
}
}
previewNode(node: TreeNode): void {
if (FileNode.is(node)) {
open(this.openerService, node.uri, { mode: 'reveal', preview: true });
}
}
protected override doOpenNode(node: TreeNode): void {
if (node.visible === false) {
return;
} else if (FileNode.is(node)) {
open(this.openerService, node.uri);
}
}
override *getNodesByUri(uri: URI): IterableIterator<TreeNode> {
const workspace = this.root;
if (WorkspaceNode.is(workspace)) {
for (const root of workspace.children) {
const id = this.tree.createId(root, uri);
const node = this.getNode(id);
if (node) {
yield node;
}
}
}
}
protected async updateRoot(): Promise<void> {
this.root = await this.createRoot();
}
protected async createRoot(): Promise<TreeNode | undefined> {
if (this.workspaceService.opened) {
const stat = this.workspaceService.workspace;
const isMulti = (stat) ? !stat.isDirectory : false;
const workspaceNode = isMulti
? this.createMultipleRootNode()
: WorkspaceNode.createRoot();
const roots = await this.workspaceService.roots;
for (const root of roots) {
workspaceNode.children.push(
await this.tree.createWorkspaceRoot(root, workspaceNode)
);
}
return workspaceNode;
}
}
/**
* Create multiple root node used to display
* the multiple root workspace name.
*
* @returns `WorkspaceNode`
*/
protected createMultipleRootNode(): WorkspaceNode {
const workspace = this.workspaceService.workspace;
let name = workspace
? workspace.resource.path.name
: 'untitled';
name += ' (Workspace)';
return WorkspaceNode.createRoot(name);
}
/**
* Move the given source file or directory to the given target directory.
*/
override async move(source: TreeNode, target: TreeNode): Promise<URI | undefined> {
if (source.parent && WorkspaceRootNode.is(source)) {
// do not support moving a root folder
return undefined;
}
return super.move(source, target);
}
/**
* Reveals node in the navigator by given file uri.
*
* @param uri uri to file which should be revealed in the navigator
* @returns file tree node if the file with given uri was revealed, undefined otherwise
*/
async revealFile(uri: URI): Promise<TreeNode | undefined> {
if (!uri.path.isAbsolute) {
return undefined;
}
let node = this.getNodeClosestToRootByUri(uri);
// success stop condition
// we have to reach workspace root because expanded node could be inside collapsed one
if (WorkspaceRootNode.is(node)) {
if (ExpandableTreeNode.is(node)) {
if (!node.expanded) {
node = await this.expandNode(node);
}
return node;
}
// shouldn't happen, root node is always directory, i.e. expandable
return undefined;
}
// fail stop condition
if (uri.path.isRoot) {
// file system root is reached but workspace root wasn't found, it means that
// given uri is not in workspace root folder or points to not existing file.
return undefined;
}
if (await this.revealFile(uri.parent)) {
if (node === undefined) {
// get node if it wasn't mounted into navigator tree before expansion
node = this.getNodeClosestToRootByUri(uri);
}
if (ExpandableTreeNode.is(node) && !node.expanded) {
node = await this.expandNode(node);
}
return node;
}
return undefined;
}
protected getNodeClosestToRootByUri(uri: URI): TreeNode | undefined {
const nodes = [...this.getNodesByUri(uri)];
return nodes.length > 0
? nodes.reduce((node1, node2) => // return the node closest to the workspace root
node1.id.length >= node2.id.length ? node1 : node2
) : undefined;
}
}

View File

@@ -0,0 +1,68 @@
// *****************************************************************************
// 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { Emitter, Event, nls } from '@theia/core';
import { TreeDecorator, Tree, TreeDecoration, DepthFirstTreeIterator } from '@theia/core/lib/browser';
import { FileStatNode } from '@theia/filesystem/lib/browser';
import { DecorationsService } from '@theia/core/lib/browser/decorations-service';
@injectable()
export class NavigatorSymlinkDecorator implements TreeDecorator {
readonly id = 'theia-navigator-symlink-decorator';
@inject(DecorationsService)
protected readonly decorationsService: DecorationsService;
@postConstruct()
protected init(): void {
this.decorationsService.onDidChangeDecorations(() => {
this.fireDidChangeDecorations((tree: Tree) => this.collectDecorator(tree));
});
}
async decorations(tree: Tree): Promise<Map<string, TreeDecoration.Data>> {
return this.collectDecorator(tree);
}
protected collectDecorator(tree: Tree): Map<string, TreeDecoration.Data> {
const result = new Map<string, TreeDecoration.Data>();
if (tree.root === undefined) {
return result;
}
for (const node of new DepthFirstTreeIterator(tree.root)) {
if (FileStatNode.is(node) && node.fileStat.isSymbolicLink) {
const decorations: TreeDecoration.Data = {
tailDecorations: [{ data: '⤷', tooltip: nls.localizeByDefault('Symbolic Link') }]
};
result.set(node.id, decorations);
}
}
return result;
}
protected readonly onDidChangeDecorationsEmitter = new Emitter<(tree: Tree) => Map<string, TreeDecoration.Data>>();
get onDidChangeDecorations(): Event<(tree: Tree) => Map<string, TreeDecoration.Data>> {
return this.onDidChangeDecorationsEmitter.event;
}
fireDidChangeDecorations(event: (tree: Tree) => Map<string, TreeDecoration.Data>): void {
this.onDidChangeDecorationsEmitter.fire(event);
}
}

View File

@@ -0,0 +1,71 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { TabBarDecorator } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { ApplicationShell, FrontendApplication, FrontendApplicationContribution, Saveable, Title, ViewContainer, Widget } from '@theia/core/lib/browser';
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { OpenEditorsWidget } from './open-editors-widget/navigator-open-editors-widget';
@injectable()
export class NavigatorTabBarDecorator implements TabBarDecorator, FrontendApplicationContribution {
readonly id = 'theia-navigator-tabbar-decorator';
protected applicationShell: ApplicationShell;
protected readonly emitter = new Emitter<void>();
private readonly toDispose = new DisposableCollection();
private readonly toDisposeOnDirtyChanged = new Map<string, Disposable>();
onStart(app: FrontendApplication): void {
this.applicationShell = app.shell;
if (!!this.getDirtyEditorsCount()) {
this.fireDidChangeDecorations();
}
this.toDispose.pushAll([
this.applicationShell.onDidAddWidget(widget => {
const saveable = Saveable.get(widget);
if (saveable) {
this.toDisposeOnDirtyChanged.set(widget.id, saveable.onDirtyChanged(() => this.fireDidChangeDecorations()));
}
}),
this.applicationShell.onDidRemoveWidget(widget => this.toDisposeOnDirtyChanged.get(widget.id)?.dispose())
]);
}
decorate(title: Title<Widget>): WidgetDecoration.Data[] {
const { owner } = title;
if (owner instanceof ViewContainer && owner.getParts().find(part => part.wrapped instanceof OpenEditorsWidget)) {
const changes = this.getDirtyEditorsCount();
return changes > 0 ? [{ badge: changes }] : [];
} else {
return [];
}
}
protected getDirtyEditorsCount(): number {
return this.applicationShell.widgets.filter(widget => Saveable.isDirty(widget)).length;
}
get onDidChangeDecorations(): Event<void> {
return this.emitter.event;
}
protected fireDidChangeDecorations(): void {
this.emitter.fire(undefined);
}
}

View File

@@ -0,0 +1,128 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { FileTree, DirNode } from '@theia/filesystem/lib/browser';
import { FileStat } from '@theia/filesystem/lib/common/files';
import URI from '@theia/core/lib/common/uri';
import { TreeNode, CompositeTreeNode, SelectableTreeNode, CompressionToggle } from '@theia/core/lib/browser';
import { FileNavigatorFilter } from './navigator-filter';
import { EXPLORER_COMPACT_FOLDERS, FileNavigatorPreferences } from '../common/navigator-preferences';
@injectable()
export class FileNavigatorTree extends FileTree {
@inject(FileNavigatorFilter) protected readonly filter: FileNavigatorFilter;
@inject(FileNavigatorPreferences) protected readonly navigatorPreferences: FileNavigatorPreferences;
@inject(CompressionToggle) protected readonly compressionToggle: CompressionToggle;
@postConstruct()
protected init(): void {
this.toDispose.push(this.filter.onFilterChanged(() => this.refresh()));
this.navigatorPreferences.ready.then(() => this.toggleCompression());
this.toDispose.push(this.navigatorPreferences.onPreferenceChanged(({ preferenceName }) => {
if (preferenceName === EXPLORER_COMPACT_FOLDERS) {
this.toggleCompression();
}
}));
}
protected toggleCompression(): void {
this.compressionToggle.compress = this.navigatorPreferences.get(EXPLORER_COMPACT_FOLDERS, true);
this.refresh();
}
override async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
if (WorkspaceNode.is(parent)) {
return parent.children;
}
return this.filter.filter(super.resolveChildren(parent));
}
protected override toNodeId(uri: URI, parent: CompositeTreeNode): string {
const workspaceRootNode = WorkspaceRootNode.find(parent);
if (workspaceRootNode) {
return this.createId(workspaceRootNode, uri);
}
return super.toNodeId(uri, parent);
}
createId(root: WorkspaceRootNode, uri: URI): string {
const id = super.toNodeId(uri, root);
return id === root.id ? id : `${root.id}:${id}`;
}
async createWorkspaceRoot(rootFolder: FileStat, workspaceNode: WorkspaceNode): Promise<WorkspaceRootNode> {
const node = this.toNode(rootFolder, workspaceNode) as WorkspaceRootNode;
Object.assign(node, {
visible: workspaceNode.name !== WorkspaceNode.name,
});
return node;
}
}
/**
* File tree root node for multi-root workspaces.
*/
export interface WorkspaceNode extends CompositeTreeNode, SelectableTreeNode {
children: WorkspaceRootNode[];
}
export namespace WorkspaceNode {
export const id = 'WorkspaceNodeId';
export const name = 'WorkspaceNode';
export function is(node: TreeNode | undefined): node is WorkspaceNode {
return CompositeTreeNode.is(node) && node.id === WorkspaceNode.id;
}
/**
* Create a `WorkspaceNode` that can be used as a `Tree` root.
*/
export function createRoot(multiRootName?: string): WorkspaceNode {
return {
id: WorkspaceNode.id,
name: multiRootName || WorkspaceNode.name,
parent: undefined,
children: [],
visible: false,
selected: false
};
}
}
/**
* A node representing a folder from a multi-root workspace.
*/
export interface WorkspaceRootNode extends DirNode {
parent: WorkspaceNode;
}
export namespace WorkspaceRootNode {
export function is(node: unknown): node is WorkspaceRootNode {
return DirNode.is(node) && WorkspaceNode.is(node.parent);
}
export function find(node: TreeNode | undefined): WorkspaceRootNode | undefined {
if (node) {
if (is(node)) {
return node;
}
return find(node.parent);
}
}
}

View File

@@ -0,0 +1,75 @@
// *****************************************************************************
// Copyright (C) 2021 SAP SE or an SAP affiliate company 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable } from '@theia/core/shared/inversify';
import {
codicon,
ViewContainer,
ViewContainerTitleOptions,
WidgetFactory,
WidgetManager
} from '@theia/core/lib/browser';
import { FILE_NAVIGATOR_ID } from './navigator-widget';
import { OpenEditorsWidget } from './open-editors-widget/navigator-open-editors-widget';
import { nls } from '@theia/core/lib/common/nls';
export const EXPLORER_VIEW_CONTAINER_ID = 'explorer-view-container';
export const EXPLORER_VIEW_CONTAINER_TITLE_OPTIONS: ViewContainerTitleOptions = {
label: nls.localizeByDefault('Explorer'),
iconClass: codicon('files'),
closeable: true
};
@injectable()
export class NavigatorWidgetFactory implements WidgetFactory {
static ID = EXPLORER_VIEW_CONTAINER_ID;
readonly id = NavigatorWidgetFactory.ID;
protected openEditorsWidgetOptions: ViewContainer.Factory.WidgetOptions = {
order: 0,
canHide: true,
initiallyCollapsed: true,
// this property currently has no effect (https://github.com/eclipse-theia/theia/issues/7755)
weight: 20
};
protected fileNavigatorWidgetOptions: ViewContainer.Factory.WidgetOptions = {
order: 1,
canHide: false,
initiallyCollapsed: false,
weight: 80,
disableDraggingToOtherContainers: true
};
@inject(ViewContainer.Factory)
protected readonly viewContainerFactory: ViewContainer.Factory;
@inject(WidgetManager) protected readonly widgetManager: WidgetManager;
async createWidget(): Promise<ViewContainer> {
const viewContainer = this.viewContainerFactory({
id: EXPLORER_VIEW_CONTAINER_ID,
progressLocationId: 'explorer'
});
viewContainer.setTitleOptions(EXPLORER_VIEW_CONTAINER_TITLE_OPTIONS);
const openEditorsWidget = await this.widgetManager.getOrCreateWidget(OpenEditorsWidget.ID);
const navigatorWidget = await this.widgetManager.getOrCreateWidget(FILE_NAVIGATOR_ID);
viewContainer.addWidget(navigatorWidget, this.fileNavigatorWidgetOptions);
viewContainer.addWidget(openEditorsWidget, this.openEditorsWidgetOptions);
return viewContainer;
}
}

View File

@@ -0,0 +1,219 @@
// *****************************************************************************
// Copyright (C) 2017 TypeFox 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@lumino/messaging';
import URI from '@theia/core/lib/common/uri';
import { CommandService } from '@theia/core/lib/common';
import { Key, TreeModel, ContextMenuRenderer, ExpandableTreeNode, TreeProps, TreeNode } from '@theia/core/lib/browser';
import { DirNode, FileStatNodeData } from '@theia/filesystem/lib/browser';
import { WorkspaceService, WorkspaceCommands } from '@theia/workspace/lib/browser';
import { WorkspaceNode, WorkspaceRootNode } from './navigator-tree';
import { FileNavigatorModel } from './navigator-model';
import { isOSX, environment } from '@theia/core';
import * as React from '@theia/core/shared/react';
import { NavigatorContextKeyService } from './navigator-context-key-service';
import { nls } from '@theia/core/lib/common/nls';
import { AbstractNavigatorTreeWidget } from './abstract-navigator-tree-widget';
export const FILE_NAVIGATOR_ID = 'files';
export const LABEL = nls.localizeByDefault('No Folder Opened');
export const CLASS = 'theia-Files';
@injectable()
export class FileNavigatorWidget extends AbstractNavigatorTreeWidget {
@inject(CommandService) protected readonly commandService: CommandService;
@inject(NavigatorContextKeyService) protected readonly contextKeyService: NavigatorContextKeyService;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
constructor(
@inject(TreeProps) props: TreeProps,
@inject(FileNavigatorModel) override readonly model: FileNavigatorModel,
@inject(ContextMenuRenderer) contextMenuRenderer: ContextMenuRenderer,
) {
super(props, model, contextMenuRenderer);
this.id = FILE_NAVIGATOR_ID;
this.addClass(CLASS);
}
@postConstruct()
protected override init(): void {
super.init();
// This ensures that the context menu command to hide this widget receives the label 'Folders'
// regardless of the name of workspace. See ViewContainer.updateToolbarItems.
const dataset = { ...this.title.dataset, visibilityCommandLabel: nls.localizeByDefault('Folders') };
this.title.dataset = dataset;
this.updateSelectionContextKeys();
this.toDispose.pushAll([
this.model.onSelectionChanged(() =>
this.updateSelectionContextKeys()
),
this.model.onExpansionChanged(node => {
if (node.expanded && node.children.length === 1) {
const child = node.children[0];
if (ExpandableTreeNode.is(child) && !child.expanded) {
this.model.expandNode(child);
}
}
})
]);
}
protected override doUpdateRows(): void {
super.doUpdateRows();
this.title.label = LABEL;
if (WorkspaceNode.is(this.model.root)) {
if (this.model.root.name === WorkspaceNode.name) {
const rootNode = this.model.root.children[0];
if (WorkspaceRootNode.is(rootNode)) {
this.title.label = this.toNodeName(rootNode);
this.title.caption = this.labelProvider.getLongName(rootNode.uri);
}
} else {
this.title.label = this.toNodeName(this.model.root);
this.title.caption = this.title.label;
}
} else {
this.title.caption = this.title.label;
}
}
override getContainerTreeNode(): TreeNode | undefined {
const root = this.model.root;
if (this.workspaceService.isMultiRootWorkspaceOpened) {
return root;
}
if (WorkspaceNode.is(root)) {
return root.children[0];
}
return undefined;
}
protected override renderTree(model: TreeModel): React.ReactNode {
if (this.model.root && this.isEmptyMultiRootWorkspace(model)) {
return this.renderEmptyMultiRootWorkspace();
}
return super.renderTree(model);
}
protected override shouldShowWelcomeView(): boolean {
return this.model.root === undefined;
}
protected override onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.addClipboardListener(this.node, 'copy', e => this.handleCopy(e));
this.addClipboardListener(this.node, 'paste', e => this.handlePaste(e));
}
protected handleCopy(event: ClipboardEvent): void {
const uris = this.model.selectedFileStatNodes.map(node => node.uri.toString());
if (uris.length > 0 && event.clipboardData) {
event.clipboardData.setData('text/plain', uris.join('\n'));
event.preventDefault();
}
}
protected handlePaste(event: ClipboardEvent): void {
if (event.clipboardData) {
const raw = event.clipboardData.getData('text/plain');
if (!raw) {
return;
}
const target = this.model.selectedFileStatNodes[0];
if (!target) {
return;
}
for (const file of raw.split('\n')) {
event.preventDefault();
const source = new URI(file);
this.model.copy(source, target);
}
}
}
protected canOpenWorkspaceFileAndFolder: boolean = isOSX || !environment.electron.is();
protected readonly openWorkspace = () => this.doOpenWorkspace();
protected doOpenWorkspace(): void {
this.commandService.executeCommand(WorkspaceCommands.OPEN_WORKSPACE.id);
}
protected readonly openFolder = () => this.doOpenFolder();
protected doOpenFolder(): void {
this.commandService.executeCommand(WorkspaceCommands.OPEN_FOLDER.id);
}
protected readonly addFolder = () => this.doAddFolder();
protected doAddFolder(): void {
this.commandService.executeCommand(WorkspaceCommands.ADD_FOLDER.id);
}
protected readonly keyUpHandler = (e: React.KeyboardEvent) => {
if (Key.ENTER.keyCode === e.keyCode) {
(e.target as HTMLElement).click();
}
};
/**
* When a multi-root workspace is opened, a user can remove all the folders from it.
* Instead of displaying an empty navigator tree, this will show a button to add more folders.
*/
protected renderEmptyMultiRootWorkspace(): React.ReactNode {
return <div className='theia-navigator-container'>
<div className='center'>{nls.localizeByDefault('You have not yet added a folder to the workspace.\n{0}', '')}</div>
<div className='open-workspace-button-container'>
<button className='theia-button open-workspace-button' title={nls.localizeByDefault('Add Folder to Workspace')}
onClick={this.addFolder}
onKeyUp={this.keyUpHandler}>
{nls.localizeByDefault('Open Folder')}
</button>
</div>
</div>;
}
protected isEmptyMultiRootWorkspace(model: TreeModel): boolean {
return WorkspaceNode.is(model.root) && model.root.children.length === 0;
}
protected override tapNode(node?: TreeNode): void {
if (node && this.corePreferences['workbench.list.openMode'] === 'singleClick') {
this.model.previewNode(node);
}
super.tapNode(node);
}
protected override onAfterShow(msg: Message): void {
super.onAfterShow(msg);
this.contextKeyService.explorerViewletVisible.set(true);
}
protected override onAfterHide(msg: Message): void {
super.onAfterHide(msg);
this.contextKeyService.explorerViewletVisible.set(false);
}
protected updateSelectionContextKeys(): void {
this.contextKeyService.explorerResourceIsFolder.set(DirNode.is(this.model.selectedNodes[0]));
// As `FileStatNode` only created if `FileService.resolve` was successful, we can safely assume that
// a valid `FileSystemProvider` is available for the selected node. So we skip an additional check
// for provider availability here and check the node type.
this.contextKeyService.isFileSystemResource.set(FileStatNodeData.is(this.model.selectedNodes[0]));
}
}

View File

@@ -0,0 +1,92 @@
// *****************************************************************************
// 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-only 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 { FileStatNode } from '@theia/filesystem/lib/browser';
import { FileChangeType } from '@theia/filesystem/lib/common/files';
@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<string>();
@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<string>();
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<string, TreeDecoration.Data> {
return this.collectDecorators(tree);
}
protected collectDecorators(tree: Tree): Map<string, TreeDecoration.Data> {
const result = new Map<string, TreeDecoration.Data>();
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<string, TreeDecoration.Data>): void {
this.onDidChangeDecorationsEmitter.fire(event);
}
}

View File

@@ -0,0 +1,48 @@
// *****************************************************************************
// 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { CommonCommands } from '@theia/core/lib/browser';
import { Command } from '@theia/core/lib/common';
export namespace OpenEditorsCommands {
export const CLOSE_ALL_TABS_FROM_TOOLBAR = Command.toDefaultLocalizedCommand({
id: 'navigator.close.all.editors.toolbar',
category: CommonCommands.FILE_CATEGORY,
label: 'Close All Editors',
iconClass: 'codicon codicon-close-all'
});
export const SAVE_ALL_TABS_FROM_TOOLBAR = Command.toDefaultLocalizedCommand({
id: 'navigator.save.all.editors.toolbar',
category: CommonCommands.FILE_CATEGORY,
label: 'Save All',
iconClass: 'codicon codicon-save-all'
});
export const CLOSE_ALL_EDITORS_IN_GROUP_FROM_ICON = Command.toDefaultLocalizedCommand({
id: 'navigator.close.all.in.area.icon',
category: CommonCommands.VIEW_CATEGORY,
label: 'Close Group',
iconClass: 'codicon codicon-close-all'
});
export const SAVE_ALL_IN_GROUP_FROM_ICON = Command.toDefaultLocalizedCommand({
id: 'navigator.save.all.in.area.icon',
category: CommonCommands.FILE_CATEGORY,
label: 'Save All in Group',
iconClass: 'codicon codicon-save-all'
});
}

View File

@@ -0,0 +1,28 @@
// *****************************************************************************
// 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { TreeDecorator, AbstractTreeDecoratorService } from '@theia/core/lib/browser/tree/tree-decorator';
import { ContributionProvider } from '@theia/core/lib/common';
export const OpenEditorsTreeDecorator = Symbol('OpenEditorsTreeDecorator');
@injectable()
export class OpenEditorsTreeDecoratorService extends AbstractTreeDecoratorService {
constructor(@inject(ContributionProvider) @named(OpenEditorsTreeDecorator) protected readonly contributions: ContributionProvider<TreeDecorator>) {
super(contributions.getContributions());
}
}

View File

@@ -0,0 +1,26 @@
// *****************************************************************************
// 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { MenuPath } from '@theia/core/lib/common';
export const OPEN_EDITORS_CONTEXT_MENU: MenuPath = ['open-editors-context-menu'];
export namespace OpenEditorsContextMenu {
export const NAVIGATION = [...OPEN_EDITORS_CONTEXT_MENU, '1_navigation'];
export const CLIPBOARD = [...OPEN_EDITORS_CONTEXT_MENU, '2_clipboard'];
export const SAVE = [...OPEN_EDITORS_CONTEXT_MENU, '3_save'];
export const COMPARE = [...OPEN_EDITORS_CONTEXT_MENU, '4_compare'];
export const MODIFICATION = [...OPEN_EDITORS_CONTEXT_MENU, '5_modification'];
}

View File

@@ -0,0 +1,254 @@
// *****************************************************************************
// 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { FileNode, FileStatNode, FileTreeModel } from '@theia/filesystem/lib/browser';
import {
ApplicationShell,
CompositeTreeNode,
open,
NavigatableWidget,
OpenerService,
SelectableTreeNode,
TreeNode,
Widget,
ExpandableTreeNode,
TabBar
} from '@theia/core/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import debounce = require('@theia/core/shared/lodash.debounce');
import { nls } from '@theia/core/lib/common';
import { FileStat } from '@theia/filesystem/lib/common/files';
export interface OpenEditorNode extends FileStatNode {
widget: Widget;
};
export namespace OpenEditorNode {
export function is(node: unknown): node is OpenEditorNode {
return FileStatNode.is(node) && 'widget' in node;
}
}
@injectable()
export class OpenEditorsModel extends FileTreeModel {
static GROUP_NODE_ID_PREFIX = 'group-node';
static AREA_NODE_ID_PREFIX = 'area-node';
@inject(ApplicationShell) protected readonly applicationShell: ApplicationShell;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@inject(OpenerService) protected readonly openerService: OpenerService;
// Returns the collection of editors belonging to a tabbar group in the main area
protected _editorWidgetsByGroup = new Map<number, { widgets: NavigatableWidget[], tabbar: TabBar<Widget> }>();
// Returns the collection of editors belonging to an area grouping (main, left, right bottom)
protected _editorWidgetsByArea = new Map<ApplicationShell.Area, NavigatableWidget[]>();
// Last collection of editors before a layout modification, used to detect changes in widget ordering
protected _lastEditorWidgetsByArea = new Map<ApplicationShell.Area, NavigatableWidget[]>();
protected cachedFileStats = new Map<string, FileStat>();
get editorWidgets(): NavigatableWidget[] {
const editorWidgets: NavigatableWidget[] = [];
this._editorWidgetsByArea.forEach(widgets => editorWidgets.push(...widgets));
return editorWidgets;
}
getTabBarForGroup(id: number): TabBar<Widget> | undefined {
return this._editorWidgetsByGroup.get(id)?.tabbar;
}
@postConstruct()
protected override init(): void {
super.init();
this.setupHandlers();
this.initializeRoot();
}
protected setupHandlers(): void {
this.toDispose.push(this.applicationShell.onDidChangeCurrentWidget(({ newValue }) => {
const nodeToSelect = this.tree.getNode(newValue?.id);
if (nodeToSelect && SelectableTreeNode.is(nodeToSelect)) {
this.selectNode(nodeToSelect);
}
}));
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'));
this.applicationShell.bottomPanel.layoutModified.connect(() => this.doUpdateOpenWidgets('bottom'));
}
protected async initializeRoot(): Promise<void> {
await this.updateOpenWidgets();
this.fireChanged();
}
protected updateOpenWidgets = debounce(this.doUpdateOpenWidgets, 250);
protected async doUpdateOpenWidgets(layoutModifiedArea?: ApplicationShell.Area): Promise<void> {
this._lastEditorWidgetsByArea = this._editorWidgetsByArea;
this._editorWidgetsByArea = new Map<ApplicationShell.Area, NavigatableWidget[]>();
let doRebuild = true;
const areas: ApplicationShell.Area[] = ['main', 'bottom', 'left', 'right', 'top', 'secondaryWindow'];
areas.forEach(area => {
const editorWidgetsForArea = this.applicationShell.getWidgets(area).filter((widget): widget is NavigatableWidget => NavigatableWidget.is(widget));
if (editorWidgetsForArea.length) {
this._editorWidgetsByArea.set(area, editorWidgetsForArea);
}
});
if (this._lastEditorWidgetsByArea.size === 0) {
this._lastEditorWidgetsByArea = this._editorWidgetsByArea;
}
// `layoutModified` can be triggered when tabs are clicked, even if they are not rearranged.
// This will check for those instances and prevent a rebuild if it is unnecessary. Rebuilding
// the tree if there is no change can cause the tree's selection to flicker.
if (layoutModifiedArea) {
doRebuild = this.shouldRebuildTreeOnLayoutModified(layoutModifiedArea);
}
if (doRebuild) {
this.root = await this.buildRootFromOpenedWidgets(this._editorWidgetsByArea);
}
}
protected shouldRebuildTreeOnLayoutModified(area: ApplicationShell.Area): boolean {
const currentOrdering = this._editorWidgetsByArea.get(area);
const previousOrdering = this._lastEditorWidgetsByArea.get(area);
if (currentOrdering?.length === 1) {
return true;
}
if (currentOrdering?.length !== previousOrdering?.length) {
return true;
}
if (!!currentOrdering && !!previousOrdering) {
return !currentOrdering.every((widget, index) => widget === previousOrdering[index]);
}
return true;
}
protected tryCreateWidgetGroupMap(): Map<Widget, CompositeTreeNode> {
const mainTabBars = this.applicationShell.mainAreaTabBars;
this._editorWidgetsByGroup = new Map();
const widgetGroupMap = new Map<Widget, CompositeTreeNode>();
if (mainTabBars.length > 1) {
mainTabBars.forEach((tabbar, index) => {
const groupNumber = index + 1;
const newCaption = nls.localizeByDefault('Group {0}', groupNumber);
const groupNode = {
parent: undefined,
id: `${OpenEditorsModel.GROUP_NODE_ID_PREFIX}:${groupNumber}`,
name: newCaption,
children: []
};
const widgets: NavigatableWidget[] = [];
tabbar.titles.map(title => {
const { owner } = title;
widgetGroupMap.set(owner, groupNode);
if (NavigatableWidget.is(owner)) {
widgets.push(owner);
}
});
this._editorWidgetsByGroup.set(groupNumber, { widgets, tabbar });
});
}
return widgetGroupMap;
}
protected async buildRootFromOpenedWidgets(widgetsByArea: Map<ApplicationShell.Area, NavigatableWidget[]>): Promise<CompositeTreeNode> {
const rootNode: CompositeTreeNode = {
id: 'open-editors:root',
parent: undefined,
visible: false,
children: [],
};
const mainAreaWidgetGroups = this.tryCreateWidgetGroupMap();
for (const [area, widgetsInArea] of widgetsByArea.entries()) {
const areaNode: CompositeTreeNode & ExpandableTreeNode = {
id: `${OpenEditorsModel.AREA_NODE_ID_PREFIX}:${area}`,
parent: rootNode,
name: ApplicationShell.areaLabels[area],
expanded: true,
children: []
};
for (const widget of widgetsInArea) {
const uri = widget.getResourceUri();
if (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,
uri,
selected: false,
parent: undefined,
name: widget.title.label,
icon: widget.title.iconClass,
widget
};
// only show groupings for main area widgets if more than one tabbar
if ((area === 'main') && (mainAreaWidgetGroups.size > 1)) {
const groupNode = mainAreaWidgetGroups.get(widget);
if (groupNode) {
CompositeTreeNode.addChild(groupNode, openEditorNode);
CompositeTreeNode.addChild(areaNode, groupNode);
}
} else {
CompositeTreeNode.addChild(areaNode, openEditorNode);
}
}
}
// If widgets are only in the main area and in a single tabbar, then don't show area node
if (widgetsByArea.size === 1 && widgetsByArea.has('main') && area === 'main') {
areaNode.children.forEach(child => CompositeTreeNode.addChild(rootNode, child));
} else {
CompositeTreeNode.addChild(rootNode, areaNode);
}
}
return rootNode;
}
protected override doOpenNode(node: TreeNode): void {
if (node.visible === false) {
return;
} else if (FileNode.is(node)) {
open(this.openerService, node.uri);
}
}
}

View File

@@ -0,0 +1,274 @@
// *****************************************************************************
// 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as React from '@theia/core/shared/react';
import { injectable, interfaces, Container, postConstruct, inject } from '@theia/core/shared/inversify';
import {
ApplicationShell,
codicon,
ContextMenuRenderer,
defaultTreeProps,
NavigatableWidget,
NodeProps,
Saveable,
TabBar,
TreeDecoration,
TreeDecoratorService,
TreeModel,
TreeNode,
TreeProps,
TreeWidget,
TREE_NODE_CONTENT_CLASS,
Widget,
} from '@theia/core/lib/browser';
import { OpenEditorNode, OpenEditorsModel } from './navigator-open-editors-tree-model';
import { createFileTreeContainer, FileTreeModel, FileTreeWidget } from '@theia/filesystem/lib/browser';
import { OpenEditorsTreeDecoratorService } from './navigator-open-editors-decorator-service';
import { OPEN_EDITORS_CONTEXT_MENU } from './navigator-open-editors-menus';
import { CommandService } from '@theia/core/lib/common';
import { OpenEditorsCommands } from './navigator-open-editors-commands';
import { nls } from '@theia/core/lib/common/nls';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { AbstractNavigatorTreeWidget } from '../abstract-navigator-tree-widget';
export const OPEN_EDITORS_PROPS: TreeProps = {
...defaultTreeProps,
virtualized: false,
contextMenuPath: OPEN_EDITORS_CONTEXT_MENU,
leftPadding: 22
};
export interface OpenEditorsNodeRow extends TreeWidget.NodeRow {
node: OpenEditorNode;
}
@injectable()
export class OpenEditorsWidget extends AbstractNavigatorTreeWidget {
static ID = 'theia-open-editors-widget';
static LABEL = nls.localizeByDefault('Open Editors');
@inject(ApplicationShell) protected readonly applicationShell: ApplicationShell;
@inject(CommandService) protected readonly commandService: CommandService;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
static createContainer(parent: interfaces.Container): Container {
const child = createFileTreeContainer(parent);
child.unbind(FileTreeModel);
child.bind(OpenEditorsModel).toSelf();
child.rebind(TreeModel).toService(OpenEditorsModel);
child.unbind(FileTreeWidget);
child.bind(OpenEditorsWidget).toSelf();
child.rebind(TreeProps).toConstantValue(OPEN_EDITORS_PROPS);
child.bind(OpenEditorsTreeDecoratorService).toSelf().inSingletonScope();
child.rebind(TreeDecoratorService).toService(OpenEditorsTreeDecoratorService);
return child;
}
static createWidget(parent: interfaces.Container): OpenEditorsWidget {
return OpenEditorsWidget.createContainer(parent).get(OpenEditorsWidget);
}
constructor(
@inject(TreeProps) props: TreeProps,
@inject(OpenEditorsModel) override readonly model: OpenEditorsModel,
@inject(ContextMenuRenderer) contextMenuRenderer: ContextMenuRenderer
) {
super(props, model, contextMenuRenderer);
}
@postConstruct()
override init(): void {
super.init();
this.id = OpenEditorsWidget.ID;
this.title.label = OpenEditorsWidget.LABEL;
this.addClass(OpenEditorsWidget.ID);
this.update();
}
get editorWidgets(): NavigatableWidget[] {
return this.model.editorWidgets;
}
// eslint-disable-next-line no-null/no-null
protected activeTreeNodePrefixElement: string | undefined | null;
protected override renderNode(node: OpenEditorNode, props: NodeProps): React.ReactNode {
if (!TreeNode.isVisible(node)) {
return undefined;
}
const attributes = this.createNodeAttributes(node, props);
const isEditorNode = !(node.id.startsWith(OpenEditorsModel.GROUP_NODE_ID_PREFIX) || node.id.startsWith(OpenEditorsModel.AREA_NODE_ID_PREFIX));
const content = <div className={`${TREE_NODE_CONTENT_CLASS}`}>
{this.renderExpansionToggle(node, props)}
{isEditorNode && this.renderPrefixIcon(node)}
{this.decorateIcon(node, this.renderIcon(node, props))}
<div className='noWrapInfo theia-TreeNodeSegmentGrow'>
{this.renderCaptionAffixes(node, props, 'captionPrefixes')}
{this.renderCaption(node, props)}
{this.renderCaptionAffixes(node, props, 'captionSuffixes')}
</div>
{this.renderTailDecorations(node, props)}
{(this.isGroupNode(node) || this.isAreaNode(node)) && this.renderInteractables(node, props)}
</div>;
return React.createElement('div', attributes, content);
}
protected override getDecorationData<K extends keyof TreeDecoration.Data>(node: TreeNode, key: K): Required<Pick<TreeDecoration.Data, K>>[K][] {
const contributed = super.getDecorationData(node, key);
if (key === 'captionSuffixes' && OpenEditorNode.is(node)) {
(contributed as Array<Array<TreeDecoration.CaptionAffix>>).push(this.getWorkspaceDecoration(node));
}
return contributed;
}
protected getWorkspaceDecoration(node: OpenEditorNode): TreeDecoration.CaptionAffix[] {
const color = this.getDecorationData(node, 'fontData').find(data => data.color)?.color;
return [{
fontData: { color },
data: this.labelProvider.getDetails(node.fileStat),
}];
}
protected isGroupNode(node: OpenEditorNode): boolean {
return node.id.startsWith(OpenEditorsModel.GROUP_NODE_ID_PREFIX);
}
protected isAreaNode(node: OpenEditorNode): boolean {
return node.id.startsWith(OpenEditorsModel.AREA_NODE_ID_PREFIX);
}
protected override doRenderNodeRow({ node, depth }: OpenEditorsNodeRow): React.ReactNode {
let groupClass = '';
if (this.isGroupNode(node)) {
groupClass = 'group-node';
} else if (this.isAreaNode(node)) {
groupClass = 'area-node';
}
return <div className={`open-editors-node-row ${this.getPrefixIconClass(node)}${groupClass}`}>
{this.renderNode(node, { depth })}
</div>;
}
protected renderInteractables(node: OpenEditorNode, props: NodeProps): React.ReactNode {
return (<div className='open-editors-inline-actions-container'>
<div className='open-editors-inline-action'>
<a className='codicon codicon-save-all'
title={OpenEditorsCommands.SAVE_ALL_IN_GROUP_FROM_ICON.label}
onClick={this.handleGroupActionIconClicked}
data-id={node.id}
id={OpenEditorsCommands.SAVE_ALL_IN_GROUP_FROM_ICON.id}
/>
</div>
<div className='open-editors-inline-action' >
<a className='codicon codicon-close-all'
title={OpenEditorsCommands.CLOSE_ALL_EDITORS_IN_GROUP_FROM_ICON.label}
onClick={this.handleGroupActionIconClicked}
data-id={node.id}
id={OpenEditorsCommands.CLOSE_ALL_EDITORS_IN_GROUP_FROM_ICON.id}
/>
</div>
</div>
);
}
protected handleGroupActionIconClicked = async (e: React.MouseEvent<HTMLAnchorElement>) => this.doHandleGroupActionIconClicked(e);
protected async doHandleGroupActionIconClicked(e: React.MouseEvent<HTMLAnchorElement>): Promise<void> {
e.stopPropagation();
const groupName = e.currentTarget.getAttribute('data-id');
const command = e.currentTarget.id;
if (groupName && command) {
const groupFromTarget: string | number | undefined = groupName.split(':').pop();
const areaOrTabBar = this.sanitizeInputFromClickHandler(groupFromTarget);
if (areaOrTabBar) {
return this.commandService.executeCommand(command, areaOrTabBar);
}
}
}
protected sanitizeInputFromClickHandler(groupFromTarget?: string): ApplicationShell.Area | TabBar<Widget> | undefined {
let areaOrTabBar: ApplicationShell.Area | TabBar<Widget> | undefined;
if (groupFromTarget) {
if (ApplicationShell.isValidArea(groupFromTarget)) {
areaOrTabBar = groupFromTarget;
} else {
const groupAsNum = parseInt(groupFromTarget);
if (!isNaN(groupAsNum)) {
areaOrTabBar = this.model.getTabBarForGroup(groupAsNum);
}
}
}
return areaOrTabBar;
}
protected renderPrefixIcon(node: OpenEditorNode): React.ReactNode {
return (
<div className='open-editors-prefix-icon-container'>
<div data-id={node.id}
className={`open-editors-prefix-icon dirty ${codicon('circle-filled', true)}`}
/>
<div data-id={node.id}
onClick={this.closeEditor}
className={`open-editors-prefix-icon close ${codicon('close', true)}`}
/>
</div>);
}
protected getPrefixIconClass(node: OpenEditorNode): string {
const saveable = Saveable.get(node.widget);
if (saveable) {
return saveable.dirty ? 'dirty' : '';
}
return '';
}
protected closeEditor = async (e: React.MouseEvent<HTMLDivElement>) => this.doCloseEditor(e);
protected async doCloseEditor(e: React.MouseEvent<HTMLDivElement>): Promise<void> {
const widgetId = e.currentTarget.getAttribute('data-id');
if (widgetId) {
await this.applicationShell.closeWidget(widgetId);
}
}
protected override tapNode(node?: TreeNode): void {
if (OpenEditorNode.is(node)) {
this.applicationShell.activateWidget(node.widget.id);
}
super.tapNode(node);
}
protected override handleContextMenuEvent(node: OpenEditorNode | undefined, event: React.MouseEvent<HTMLElement>): void {
super.handleContextMenuEvent(node, event);
if (node) {
// Since the CommonCommands used in the context menu act on the shell's activeWidget, this is necessary to ensure
// that the EditorWidget is activated, not the Navigator itself
this.applicationShell.activateWidget(node.widget.id);
}
}
protected override getPaddingLeft(node: TreeNode): number {
if (node.id.startsWith(OpenEditorsModel.AREA_NODE_ID_PREFIX)) {
return 0;
}
return this.props.leftPadding;
}
// The state of this widget is derived from external factors. No need to store or restore it.
override storeState(): object { return {}; }
override restoreState(): void { }
}

View File

@@ -0,0 +1,104 @@
/********************************************************************************
* 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-only WITH Classpath-exception-2.0
********************************************************************************/
:root {
--theia-open-editors-icon-width: 20px;
}
.theia-open-editors-widget .theia-caption-suffix {
margin-left: var(--theia-ui-padding);
font-size: var(--theia-ui-font-size0);
}
.theia-open-editors-widget
.open-editors-node-row
.open-editors-prefix-icon-container {
min-width: var(--theia-open-editors-icon-width);
}
.theia-open-editors-widget
.open-editors-node-row
.open-editors-prefix-icon.dirty,
.theia-open-editors-widget
.open-editors-node-row.dirty:hover
.open-editors-prefix-icon.dirty {
display: none;
}
.theia-open-editors-widget
.open-editors-node-row.dirty
.open-editors-prefix-icon.dirty {
display: block;
}
.theia-open-editors-widget
.open-editors-node-row
.open-editors-prefix-icon.close {
display: none;
}
.theia-open-editors-widget
.open-editors-node-row:not(.dirty)
.theia-mod-selected
.open-editors-prefix-icon.close,
.theia-open-editors-widget
.open-editors-node-row:hover
.open-editors-prefix-icon.close {
display: block;
}
.theia-open-editors-widget .open-editors-node-row.group-node,
.theia-open-editors-widget .open-editors-node-row.area-node {
font-weight: 700;
text-transform: uppercase;
font-size: var(--theia-ui-font-size0);
}
.theia-open-editors-widget .open-editors-node-row.area-node {
font-style: italic;
}
.theia-open-editors-widget .open-editors-inline-actions-container {
display: flex;
justify-content: flex-end;
margin-left: 3px;
min-height: 16px;
}
.theia-open-editors-widget .open-editors-inline-action a {
color: var(--theia-icon-foreground);
}
.theia-open-editors-widget .open-editors-inline-action {
padding: 0px 3px;
font-size: var(--theia-ui-font-size1);
margin: 0 2px;
cursor: pointer;
display: flex;
align-items: center;
}
.theia-open-editors-widget
.open-editors-node-row
.open-editors-inline-actions-container {
visibility: hidden;
}
.theia-open-editors-widget
.open-editors-node-row:hover
.open-editors-inline-actions-container {
visibility: visible;
}

View File

@@ -0,0 +1,4 @@
<!--Copyright (c) Microsoft Corporation. All rights reserved.-->
<!--Copyright (C) 2019 TypeFox and others.-->
<!--Licensed under the MIT License. See License.txt in the project root for license information.-->
<svg fill="#F6F6F6" height="28" viewBox="0 0 28 28" width="28" xmlns="http://www.w3.org/2000/svg"><path d="m14.965 7h-8.91642s-2.04858.078-2.04858 2v15s0 2 2.04858 2l11.26722-.004c2.0486.004 2.0486-1.996 2.0486-1.996v-11.491zm-1.7464 2v5h4.0972v10h-11.26722v-15zm5.6428-6h-8.6993s-2.06493.016-2.0803 2h8.2097v.454l4.0265 4.546h1.095v12c2.0485 0 2.0485-1.995 2.0485-1.995v-11.357z" fill="#F6F6F6"/></svg>

After

Width:  |  Height:  |  Size: 615 B

View File

@@ -0,0 +1,43 @@
/********************************************************************************
* Copyright (C) 2017 TypeFox 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-only WITH Classpath-exception-2.0
********************************************************************************/
.theia-Files {
height: 100%;
}
.theia-navigator-container {
font-size: var(--theia-ui-font-size1);
padding: 5px;
position: relative;
}
.theia-navigator-container .open-workspace-button-container {
margin: auto;
margin-top: 5px;
display: flex;
justify-content: center;
align-self: center;
}
.theia-navigator-container .center {
text-align: center;
}
.lm-Widget .open-workspace-button {
padding: 4px 12px;
width: calc(100% - var(--theia-ui-padding) * 4);
margin-left: 0;
}

View File

@@ -0,0 +1,66 @@
// *****************************************************************************
// Copyright (C) 2018 TypeFox 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { interfaces } from '@theia/core/shared/inversify';
import { createPreferenceProxy, PreferenceContribution, PreferenceProxy, PreferenceSchema, PreferenceService } from '@theia/core';
import { nls } from '@theia/core/lib/common/nls';
export const EXPLORER_COMPACT_FOLDERS = 'explorer.compactFolders';
export const FileNavigatorConfigSchema: PreferenceSchema = {
properties: {
'explorer.autoReveal': {
type: 'boolean',
description: nls.localizeByDefault('Controls whether the Explorer should automatically reveal and select files when opening them.'),
default: true
},
'explorer.decorations.colors': {
type: 'boolean',
description: nls.localizeByDefault('Controls whether file decorations should use colors.'),
default: true
},
[EXPLORER_COMPACT_FOLDERS]: {
type: 'boolean',
// eslint-disable-next-line max-len
description: nls.localizeByDefault('Controls whether the Explorer should render folders in a compact form. In such a form, single child folders will be compressed in a combined tree element. Useful for Java package structures, for example.'),
default: true,
}
},
};
export interface FileNavigatorConfiguration {
'explorer.autoReveal': boolean;
'explorer.decorations.colors': boolean;
[EXPLORER_COMPACT_FOLDERS]: boolean;
}
export const FileNavigatorPreferenceContribution = Symbol('FileNavigatorPreferenceContribution');
export const FileNavigatorPreferences = Symbol('NavigatorPreferences');
export type FileNavigatorPreferences = PreferenceProxy<FileNavigatorConfiguration>;
export function createNavigatorPreferences(preferences: PreferenceService, schema: PreferenceSchema = FileNavigatorConfigSchema): FileNavigatorPreferences {
return createPreferenceProxy(preferences, schema);
}
export function bindFileNavigatorPreferences(bind: interfaces.Bind): void {
bind(FileNavigatorPreferences).toDynamicValue(ctx => {
const preferences = ctx.container.get<PreferenceService>(PreferenceService);
const contribution = ctx.container.get<PreferenceContribution>(FileNavigatorPreferenceContribution);
return createNavigatorPreferences(preferences, contribution.schema);
}).inSingletonScope();
bind(FileNavigatorPreferenceContribution).toConstantValue({ schema: FileNavigatorConfigSchema });
bind(PreferenceContribution).toService(FileNavigatorPreferenceContribution);
}

View File

@@ -0,0 +1,119 @@
// *****************************************************************************
// Copyright (C) 2021 EclipseSource 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Command, CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry, SelectionService, URI } from '@theia/core';
import { CommonCommands, KeybindingContribution, KeybindingRegistry, OpenWithService } from '@theia/core/lib/browser';
import { WidgetManager } from '@theia/core/lib/browser/widget-manager';
import { nls } from '@theia/core/lib/common';
import { FileUri } from '@theia/core/lib/common/file-uri';
import { isOSX, isWindows } from '@theia/core/lib/common/os';
import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler';
import '@theia/core/lib/electron-common/electron-api';
import { inject, injectable } from '@theia/core/shared/inversify';
import { FileStatNode } from '@theia/filesystem/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { FILE_NAVIGATOR_ID, FileNavigatorWidget } from '../browser';
import { NavigatorContextMenu, SHELL_TABBAR_CONTEXT_REVEAL } from '../browser/navigator-contribution';
export const OPEN_CONTAINING_FOLDER = Command.toDefaultLocalizedCommand({
id: 'revealFileInOS',
category: CommonCommands.FILE_CATEGORY,
label: isWindows ? 'Reveal in File Explorer' :
isOSX ? 'Reveal in Finder' :
/* linux */ 'Open Containing Folder'
});
export const OPEN_WITH_SYSTEM_APP = Command.toLocalizedCommand({
id: 'openWithSystemApp',
category: CommonCommands.FILE_CATEGORY,
label: 'Open With System Editor'
}, 'theia/navigator/openWithSystemEditor');
@injectable()
export class ElectronNavigatorMenuContribution implements MenuContribution, CommandContribution, KeybindingContribution {
@inject(SelectionService)
protected readonly selectionService: SelectionService;
@inject(WidgetManager)
protected readonly widgetManager: WidgetManager;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(OpenWithService)
protected readonly openWithService: OpenWithService;
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(OPEN_CONTAINING_FOLDER, UriAwareCommandHandler.MonoSelect(this.selectionService, {
execute: async uri => {
window.electronTheiaCore.showItemInFolder(FileUri.fsPath(uri));
},
isEnabled: uri => !!this.workspaceService.getWorkspaceRootUri(uri),
isVisible: uri => !!this.workspaceService.getWorkspaceRootUri(uri),
}));
commands.registerCommand(OPEN_WITH_SYSTEM_APP, UriAwareCommandHandler.MonoSelect(this.selectionService, {
execute: async uri => {
this.openWithSystemApplication(uri);
}
}));
this.openWithService.registerHandler({
id: 'system-editor',
label: nls.localize('theia/navigator/systemEditor', 'System Editor'),
providerName: nls.localizeByDefault('Built-in'),
// Low priority to avoid conflicts with other open handlers.
canHandle: uri => (uri.scheme === 'file') ? 10 : 0,
open: uri => {
this.openWithSystemApplication(uri);
return {};
}
});
}
protected openWithSystemApplication(uri: URI): void {
window.electronTheiaCore.openWithSystemApp(FileUri.fsPath(uri));
}
registerMenus(menus: MenuModelRegistry): void {
menus.registerMenuAction(NavigatorContextMenu.NAVIGATION, {
commandId: OPEN_CONTAINING_FOLDER.id,
label: OPEN_CONTAINING_FOLDER.label
});
menus.registerMenuAction(SHELL_TABBAR_CONTEXT_REVEAL, {
commandId: OPEN_CONTAINING_FOLDER.id,
label: OPEN_CONTAINING_FOLDER.label,
order: '4'
});
}
registerKeybindings(keybindings: KeybindingRegistry): void {
keybindings.registerKeybinding({
command: OPEN_CONTAINING_FOLDER.id,
keybinding: 'ctrlcmd+alt+p',
when: 'filesExplorerFocus'
});
}
protected getSelectedFileStatNodes(): FileStatNode[] {
const navigator = this.tryGetNavigatorWidget();
return navigator ? navigator.model.selectedNodes.filter(FileStatNode.is) : [];
}
tryGetNavigatorWidget(): FileNavigatorWidget | undefined {
return this.widgetManager.tryGetWidget(FILE_NAVIGATOR_ID);
}
}

View File

@@ -0,0 +1,26 @@
// *****************************************************************************
// Copyright (C) 2021 EclipseSource 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { CommandContribution, MenuContribution } from '@theia/core';
import { ContainerModule } from '@theia/core/shared/inversify';
import { KeybindingContribution } from '@theia/core/lib/browser';
import { ElectronNavigatorMenuContribution } from './electron-navigator-menu-contribution';
export default new ContainerModule(bind => {
bind(MenuContribution).to(ElectronNavigatorMenuContribution).inSingletonScope();
bind(CommandContribution).to(ElectronNavigatorMenuContribution).inSingletonScope();
bind(KeybindingContribution).to(ElectronNavigatorMenuContribution).inSingletonScope();
});

View File

@@ -0,0 +1,22 @@
// *****************************************************************************
// Copyright (C) 2025 STMicroelectronics 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-only WITH Classpath-exception-2.0
// *****************************************************************************
import { ContainerModule } from '@theia/core/shared/inversify';
import { bindFileNavigatorPreferences } from '../common/navigator-preferences';
export default new ContainerModule(bind => {
bindFileNavigatorPreferences(bind);
});

View File

@@ -0,0 +1,28 @@
// *****************************************************************************
// Copyright (C) 2017 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-only WITH Classpath-exception-2.0
// *****************************************************************************
/* note: this bogus test file is required so that
we are able to run mocha unit tests on this
package, without having any actual unit tests in it.
This way a coverage report will be generated,
showing 0% coverage, instead of no report.
This file can be removed once we have real unit
tests in place. */
describe('navigator package', () => {
it('support code coverage statistics', () => true);
});

View File

@@ -0,0 +1,3 @@
{
"1": "hello"
}

View File

@@ -0,0 +1,3 @@
{
"2": "hello"
}

View File

@@ -0,0 +1,22 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../core"
},
{
"path": "../filesystem"
},
{
"path": "../workspace"
}
]
}