deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/navigator/.eslintrc.js
Normal file
10
packages/navigator/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
32
packages/navigator/README.md
Normal file
32
packages/navigator/README.md
Normal 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>
|
||||
55
packages/navigator/package.json
Normal file
55
packages/navigator/package.json
Normal 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"
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
74
packages/navigator/src/browser/file-navigator-commands.ts
Normal file
74
packages/navigator/src/browser/file-navigator-commands.ts
Normal 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;
|
||||
}
|
||||
20
packages/navigator/src/browser/index.ts
Normal file
20
packages/navigator/src/browser/index.ts
Normal 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';
|
||||
48
packages/navigator/src/browser/navigator-container.ts
Normal file
48
packages/navigator/src/browser/navigator-container.ts
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
668
packages/navigator/src/browser/navigator-contribution.ts
Normal file
668
packages/navigator/src/browser/navigator-contribution.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
113
packages/navigator/src/browser/navigator-diff.spec.ts
Normal file
113
packages/navigator/src/browser/navigator-diff.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
136
packages/navigator/src/browser/navigator-diff.ts
Normal file
136
packages/navigator/src/browser/navigator-diff.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
148
packages/navigator/src/browser/navigator-filter.spec.ts
Normal file
148
packages/navigator/src/browser/navigator-filter.spec.ts
Normal 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);
|
||||
}
|
||||
165
packages/navigator/src/browser/navigator-filter.ts
Normal file
165
packages/navigator/src/browser/navigator-filter.ts
Normal 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)
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
89
packages/navigator/src/browser/navigator-frontend-module.ts
Normal file
89
packages/navigator/src/browser/navigator-frontend-module.ts
Normal 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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
221
packages/navigator/src/browser/navigator-model.ts
Normal file
221
packages/navigator/src/browser/navigator-model.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
128
packages/navigator/src/browser/navigator-tree.ts
Normal file
128
packages/navigator/src/browser/navigator-tree.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
packages/navigator/src/browser/navigator-widget-factory.ts
Normal file
75
packages/navigator/src/browser/navigator-widget-factory.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
219
packages/navigator/src/browser/navigator-widget.tsx
Normal file
219
packages/navigator/src/browser/navigator-widget.tsx
Normal 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]));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
4
packages/navigator/src/browser/style/files.svg
Normal file
4
packages/navigator/src/browser/style/files.svg
Normal 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 |
43
packages/navigator/src/browser/style/index.css
Normal file
43
packages/navigator/src/browser/style/index.css
Normal 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;
|
||||
}
|
||||
66
packages/navigator/src/common/navigator-preferences.ts
Normal file
66
packages/navigator/src/common/navigator-preferences.ts
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
22
packages/navigator/src/node/navigator-backend-module.ts
Normal file
22
packages/navigator/src/node/navigator-backend-module.ts
Normal 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);
|
||||
});
|
||||
28
packages/navigator/src/package.spec.ts
Normal file
28
packages/navigator/src/package.spec.ts
Normal 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);
|
||||
});
|
||||
3
packages/navigator/test-resources/testFileA.json
Normal file
3
packages/navigator/test-resources/testFileA.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"1": "hello"
|
||||
}
|
||||
3
packages/navigator/test-resources/testFileB.json
Normal file
3
packages/navigator/test-resources/testFileB.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"2": "hello"
|
||||
}
|
||||
22
packages/navigator/tsconfig.json
Normal file
22
packages/navigator/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user