From d26c0efdb95698b65bf89e617695590d4a234de7 Mon Sep 17 00:00:00 2001 From: zhanba Date: Thu, 18 Jul 2024 12:51:11 +0800 Subject: [PATCH] fix: enable multi-root workspace support (#3848) --- configs/vscode-extensions.json | 10 ++ .../core-browser/src/common/common.command.ts | 1 + .../src/common/common.contribution.ts | 18 ++- .../src/browser/dialog/file-dialog.view.tsx | 16 +- .../src/browser/file-tree-contribution.ts | 145 +++++++++++++----- packages/i18n/src/common/en-US.lang.ts | 5 +- packages/i18n/src/common/zh-CN.lang.ts | 3 +- packages/overlay/src/common/index.ts | 2 +- .../src/browser/terminal.client.ts | 7 +- .../src/browser/terminal.controller.ts | 2 + .../src/browser/terminal.view.ts | 6 +- packages/terminal-next/src/common/resize.ts | 1 + .../src/browser/workspace-preferences.ts | 2 +- .../src/common/mocks/workspace-service.ts | 8 +- .../src/common/workspace.interface.ts | 4 + 15 files changed, 178 insertions(+), 52 deletions(-) diff --git a/configs/vscode-extensions.json b/configs/vscode-extensions.json index f7c90d1b3c..3f11147c1d 100644 --- a/configs/vscode-extensions.json +++ b/configs/vscode-extensions.json @@ -143,6 +143,16 @@ "version": "1.55.2" } ], + "ms-python": [ + { + "name": "python", + "version": "2024.4.1" + }, + { + "name": "debugpy", + "version": "2024.6.0" + } + ], "ms-vscode": [ { "name": "js-debug", diff --git a/packages/core-browser/src/common/common.command.ts b/packages/core-browser/src/common/common.command.ts index 9a09c971ce..6397c91256 100644 --- a/packages/core-browser/src/common/common.command.ts +++ b/packages/core-browser/src/common/common.command.ts @@ -824,6 +824,7 @@ export namespace WORKSPACE_COMMANDS { export const REMOVE_WORKSPACE_FOLDER: Command = { id: 'workspace.removeFolderFromWorkspace', + label: '%workspace.removeFolderFromWorkspace%', category: CATEGORY, }; diff --git a/packages/core-browser/src/common/common.contribution.ts b/packages/core-browser/src/common/common.contribution.ts index ea188f2c5d..28f8429f9c 100644 --- a/packages/core-browser/src/common/common.contribution.ts +++ b/packages/core-browser/src/common/common.contribution.ts @@ -21,7 +21,13 @@ import { MenuId } from '../menu/next/menu-id'; import { PreferenceContribution } from '../preferences'; import { AppConfig } from '../react-providers/config-provider'; -import { COMMON_COMMANDS, EDITOR_COMMANDS, FILE_COMMANDS, TERMINAL_COMMANDS } from './common.command'; +import { + COMMON_COMMANDS, + EDITOR_COMMANDS, + FILE_COMMANDS, + TERMINAL_COMMANDS, + WORKSPACE_COMMANDS, +} from './common.command'; import { ClientAppContribution } from './common.define'; export const inputFocusedContextKey = 'inputFocus'; @@ -155,6 +161,16 @@ export class ClientCommonContribution group: '1_open', when: 'config.application.supportsOpenWorkspace', }, + { + command: WORKSPACE_COMMANDS.ADD_WORKSPACE_FOLDER.id, + group: '1_open', + when: 'config.workspace.supportMultiRootWorkspace', + }, + { + command: WORKSPACE_COMMANDS.SAVE_WORKSPACE_AS_FILE.id, + group: '1_open', + when: 'config.workspace.supportMultiRootWorkspace', + }, { command: EDITOR_COMMANDS.NEW_UNTITLED_FILE.id, group: '2_new', diff --git a/packages/file-tree-next/src/browser/dialog/file-dialog.view.tsx b/packages/file-tree-next/src/browser/dialog/file-dialog.view.tsx index bcf336159d..5383327eb6 100644 --- a/packages/file-tree-next/src/browser/dialog/file-dialog.view.tsx +++ b/packages/file-tree-next/src/browser/dialog/file-dialog.view.tsx @@ -10,7 +10,7 @@ import { Select, TreeNodeType, } from '@opensumi/ide-components'; -import { isMacintosh, localize, path, useInjectable } from '@opensumi/ide-core-browser'; +import { URI, isMacintosh, localize, path, useInjectable } from '@opensumi/ide-core-browser'; import { Progress } from '@opensumi/ide-core-browser/lib/progress/progress-bar'; import { IDialogService, IOpenDialogOptions, ISaveDialogOptions } from '@opensumi/ide-overlay'; @@ -153,7 +153,19 @@ export const FileDialog = ({ options, model, isOpenDialog }: React.PropsWithChil } } else { if ((options as IOpenDialogOptions).canSelectFiles && type === TreeNodeType.TreeNode) { - handleItemClick(item, type); + const filterExts = new Set( + Object.values((options as IOpenDialogOptions).filters ?? {}) + .flat() + .map((item) => `.${item}`), + ); + if (filterExts.size > 0) { + const ext = URI.parse(item.filestat.uri).path.ext; + if (filterExts.has(ext)) { + handleItemClick(item, type); + } + } else { + handleItemClick(item, type); + } } else if ((options as IOpenDialogOptions).canSelectFolders && type === TreeNodeType.CompositeTreeNode) { handleItemClick(item, type); } diff --git a/packages/file-tree-next/src/browser/file-tree-contribution.ts b/packages/file-tree-next/src/browser/file-tree-contribution.ts index 2e9e8e9cf4..9ff4f5c665 100644 --- a/packages/file-tree-next/src/browser/file-tree-contribution.ts +++ b/packages/file-tree-next/src/browser/file-tree-contribution.ts @@ -49,7 +49,7 @@ import { EXPLORER_CONTAINER_ID } from '@opensumi/ide-explorer/lib/browser/explor import { IMainLayoutService, IViewsRegistry, MainLayoutContribution } from '@opensumi/ide-main-layout'; import { ViewContentGroups } from '@opensumi/ide-main-layout/lib/browser/views-registry'; import { IOpenDialogOptions, ISaveDialogOptions, IWindowDialogService } from '@opensumi/ide-overlay'; -import { DEFAULT_WORKSPACE_SUFFIX_NAME, IWorkspaceService, UNTITLED_WORKSPACE } from '@opensumi/ide-workspace'; +import { IWorkspaceService, UNTITLED_WORKSPACE } from '@opensumi/ide-workspace'; import { IFileTreeService, PasteTypes, RESOURCE_VIEW_ID } from '../common'; import { Directory } from '../common/file-tree-node.define'; @@ -129,10 +129,6 @@ export class FileTreeContribution private deleteThrottler: Throttler = new Throttler(); private willDeleteUris: URI[] = []; - get workspaceSuffixName() { - return this.appConfig.workspaceSuffixName || DEFAULT_WORKSPACE_SUFFIX_NAME; - } - initialize() { // 等待排除配置初始化结束后再初始化文件树 this.workspaceService.initFileServiceExclude().then(async () => { @@ -198,7 +194,7 @@ export class FileTreeContribution if (workspace) { const uri = new URI(workspace.uri); resourceTitle = uri.displayName; - if (!workspace.isDirectory && resourceTitle.endsWith(`.${this.workspaceSuffixName}`)) { + if (!workspace.isDirectory && resourceTitle.endsWith(`.${this.workspaceService.workspaceSuffixName}`)) { resourceTitle = resourceTitle.slice(0, resourceTitle.lastIndexOf('.')); if (resourceTitle === UNTITLED_WORKSPACE) { return localize('file.workspace.defaultTip'); @@ -246,6 +242,25 @@ export class FileTreeContribution group: '0_new', }); + menuRegistry.registerMenuItem(MenuId.ExplorerContext, { + command: { + id: WORKSPACE_COMMANDS.ADD_WORKSPACE_FOLDER.id, + label: localize('workspace.addFolderToWorkspace'), + }, + order: 1, + group: '0_workspace', + when: 'config.workspace.supportMultiRootWorkspace', + }); + menuRegistry.registerMenuItem(MenuId.ExplorerContext, { + command: { + id: WORKSPACE_COMMANDS.REMOVE_WORKSPACE_FOLDER.id, + label: localize('workspace.removeFolderFromWorkspace'), + }, + order: 1, + group: '0_workspace', + when: 'config.workspace.supportMultiRootWorkspace', + }); + menuRegistry.registerMenuItem(MenuId.ExplorerContext, { command: { id: FILE_COMMANDS.OPEN_RESOURCES.id, @@ -872,29 +887,33 @@ export class FileTreeContribution this.appConfig.isElectronRenderer, }); - if (this.appConfig.isElectronRenderer) { - commands.registerCommand(FILE_COMMANDS.VSCODE_OPEN_FOLDER, { - execute: (uri?: URI, arg?: boolean | { forceNewWindow?: boolean }) => { - const windowService: IWindowService = this.injector.get(IWindowService); - const options = { newWindow: true }; - if (typeof arg === 'boolean') { - options.newWindow = arg; - } else { - options.newWindow = typeof arg?.forceNewWindow === 'boolean' ? arg.forceNewWindow : true; - } + commands.registerCommand(FILE_COMMANDS.VSCODE_OPEN_FOLDER, { + execute: (uri?: URI, arg?: boolean | { forceNewWindow?: boolean }) => { + const windowService: IWindowService = this.injector.get(IWindowService); + const options = { newWindow: true }; + if (typeof arg === 'boolean') { + options.newWindow = arg; + } else { + options.newWindow = typeof arg?.forceNewWindow === 'boolean' ? arg.forceNewWindow : true; + } - if (uri) { - return windowService.openWorkspace(uri, options); - } + if (uri) { + return windowService.openWorkspace(uri, options); + } - return this.commandService.executeCommand(FILE_COMMANDS.OPEN_FOLDER.id, options); - }, - }); + return this.commandService.executeCommand(FILE_COMMANDS.OPEN_FOLDER.id, options); + }, + isVisible: () => { + const supportsOpenWorkspace = this.preferenceService.get('application.supportsOpenFolder'); + return supportsOpenWorkspace ?? false; + }, + }); - commands.registerCommand(FILE_COMMANDS.OPEN_FOLDER, { - execute: (options: { newWindow: boolean }) => { + commands.registerCommand(FILE_COMMANDS.OPEN_FOLDER, { + execute: (options: { newWindow: boolean }) => { + const windowService: IWindowService = this.injector.get(IWindowService); + if (this.appConfig.isElectronRenderer) { const dialogService: IElectronNativeDialogService = this.injector.get(IElectronNativeDialogService); - const windowService: IWindowService = this.injector.get(IWindowService); dialogService .showOpenDialog({ title: localize('workspace.openDirectory'), @@ -905,17 +924,36 @@ export class FileTreeContribution windowService.openWorkspace(URI.file(paths[0]), options || { newWindow: true }); } }); - }, - }); + } else { + const dialogService: IWindowDialogService = this.injector.get(IWindowDialogService); + dialogService + .showOpenDialog({ + title: localize('workspace.openDirectory'), + canSelectFiles: false, + canSelectFolders: true, + }) + .then((uris) => { + if (uris && uris.length > 0) { + windowService.openWorkspace(uris[0], options || { newWindow: true }); + } + }); + } + }, + isVisible: () => { + const supportsOpenWorkspace = this.preferenceService.get('application.supportsOpenFolder'); + return supportsOpenWorkspace ?? false; + }, + }); - commands.registerCommand(FILE_COMMANDS.OPEN_WORKSPACE, { - execute: (options: { newWindow: boolean }) => { - const supportsOpenWorkspace = this.preferenceService.get('application.supportsOpenWorkspace'); - if (!supportsOpenWorkspace) { - return; - } + commands.registerCommand(FILE_COMMANDS.OPEN_WORKSPACE, { + execute: (options?: { newWindow: boolean }) => { + const supportsOpenWorkspace = this.preferenceService.get('application.supportsOpenWorkspace'); + if (!supportsOpenWorkspace) { + return; + } + const windowService: IWindowService = this.injector.get(IWindowService); + if (this.appConfig.isElectronRenderer) { const dialogService: IElectronNativeDialogService = this.injector.get(IElectronNativeDialogService); - const windowService: IWindowService = this.injector.get(IWindowService); dialogService .showOpenDialog({ title: localize('workspace.openWorkspace'), @@ -923,7 +961,7 @@ export class FileTreeContribution filters: [ { name: localize('workspace.openWorkspaceTitle'), - extensions: [this.workspaceSuffixName], + extensions: [this.workspaceService.workspaceSuffixName], }, ], }) @@ -932,9 +970,31 @@ export class FileTreeContribution windowService.openWorkspace(URI.file(paths[0]), options || { newWindow: true }); } }); - }, - }); - } + } else { + const dialogService: IWindowDialogService = this.injector.get(IWindowDialogService); + dialogService + .showOpenDialog({ + title: localize('workspace.openWorkspace'), + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + filters: { + workspace: [this.workspaceService.workspaceSuffixName], + }, + }) + .then((uris) => { + if (uris && uris.length > 0) { + const workspaceService: IWorkspaceService = this.injector.get(IWorkspaceService); + workspaceService.open(uris[0], { preserveWindow: options?.newWindow ?? false }); + } + }); + } + }, + isVisible: () => { + const supportsOpenWorkspace = this.preferenceService.get('application.supportsOpenWorkspace'); + return supportsOpenWorkspace ?? false; + }, + }); commands.registerCommand(FILE_COMMANDS.REVEAL_IN_EXPLORER, { execute: (uriOrResource?: URI | { uri?: URI }) => { @@ -1178,6 +1238,15 @@ export class FileTreeContribution viewId: RESOURCE_VIEW_ID, order: 5, }); + registry.registerItem({ + id: WORKSPACE_COMMANDS.ADD_WORKSPACE_FOLDER.id, + command: WORKSPACE_COMMANDS.ADD_WORKSPACE_FOLDER.id, + label: localize('workspace.addFolderToWorkspace'), + viewId: RESOURCE_VIEW_ID, + order: 0, + group: 'file_explore_workspace', + when: 'config.workspace.supportMultiRootWorkspace', + }); } private doDelete() { diff --git a/packages/i18n/src/common/en-US.lang.ts b/packages/i18n/src/common/en-US.lang.ts index 4923ce0521..9e03db04d4 100644 --- a/packages/i18n/src/common/en-US.lang.ts +++ b/packages/i18n/src/common/en-US.lang.ts @@ -49,7 +49,7 @@ export const localizationBundle = { 'file.action.new.folder': 'New Folder', 'file.action.refresh': 'Refresh', 'file.open.folder': 'Open Folder', - 'file.open.workspace': 'Open Workspace', + 'file.open.workspace': 'Open Workspace from File ...', 'file.action.collapse': 'Collapse', 'file.confirm.delete': 'Are you sure you want to delete the following files?\n{0}', 'file.confirm.delete.ok': 'Move to trash', @@ -1062,6 +1062,7 @@ export const localizationBundle = { 'terminal.killProcess': 'Kill Process', 'terminal.process.unHealthy': '*This terminal session has been timed out and killed by the system. Please open a new terminal session to proceed with operations.', + 'terminal.selectCWDForNewTerminal': 'Select current working directory for new terminal', 'terminal.focusNext.inTerminalGroup': 'Terminal: Focus Next Terminal in Terminal Group', 'terminal.focusPrevious.inTerminalGroup': 'Terminal: Focus Previous Terminal in Terminal Group', @@ -1215,7 +1216,7 @@ export const localizationBundle = { 'editor.compareAndSave.title': '{0} (on Disk) <=> {1} (Editing) ', 'workspace.openDirectory': 'Open Directory', - 'workspace.addFolderToWorkspace': 'Add Folder Into Workspace ...', + 'workspace.addFolderToWorkspace': 'Add Folder to Workspace ...', 'workspace.removeFolderFromWorkspace': 'Remove Folder From Workspace', 'workspace.saveWorkspaceAsFile': 'Save Workspace As ...', 'workspace.openWorkspace': 'Open Workspace', diff --git a/packages/i18n/src/common/zh-CN.lang.ts b/packages/i18n/src/common/zh-CN.lang.ts index 24561897d9..e4d5140b1f 100644 --- a/packages/i18n/src/common/zh-CN.lang.ts +++ b/packages/i18n/src/common/zh-CN.lang.ts @@ -51,7 +51,7 @@ export const localizationBundle = { 'file.action.collapse': '全部折叠', 'file.location': '在文件树中定位', 'file.open.folder': '打开文件夹', - 'file.open.workspace': '打开工作区', + 'file.open.workspace': '从文件打开工作区', 'file.confirm.delete': '确定删除下面列的文件?\n{0}', 'file.confirm.delete.ok': '移入回收站', 'file.confirm.delete.cancel': '取消', @@ -719,6 +719,7 @@ export const localizationBundle = { 'terminal.toggleTerminal': '切换终端面板', 'terminal.killProcess': '结束进程', 'terminal.process.unHealthy': '*此终端会话已被系统超时回收,请打开新的终端会话来进行操作', + 'terminal.selectCWDForNewTerminal': '为新 terminal 选择当前工作路径', 'view.command.show': '打开 {0}', diff --git a/packages/overlay/src/common/index.ts b/packages/overlay/src/common/index.ts index 20b5a62874..114dac132e 100644 --- a/packages/overlay/src/common/index.ts +++ b/packages/overlay/src/common/index.ts @@ -103,7 +103,7 @@ export interface IDialogOptions { title?: string; defaultUri?: URI; filters?: { - [name: string]: string; + [name: string]: string[]; }; } diff --git a/packages/terminal-next/src/browser/terminal.client.ts b/packages/terminal-next/src/browser/terminal.client.ts index 3df7e324e4..5383ee2e4e 100644 --- a/packages/terminal-next/src/browser/terminal.client.ts +++ b/packages/terminal-next/src/browser/terminal.client.ts @@ -510,7 +510,10 @@ export class TerminalClient extends Disposable implements ITerminalClient { if (this.workspace.isMultiRootWorkspaceOpened) { // 工作区模式下每次新建终端都需要用户手动进行一次路径选择 const roots = this.workspace.tryGetRoots(); - const choose = await this.quickPick.show(roots.map((file) => new URI(file.uri).codeUri.fsPath)); + const choose = await this.quickPick.show( + roots.map((file) => new URI(file.uri).codeUri.fsPath), + { placeholder: localize('terminal.selectCWDForNewTerminal') }, + ); return choose; } else if (this.workspace.workspace) { return new URI(this.workspace.workspace?.uri).codeUri.fsPath; @@ -530,7 +533,7 @@ export class TerminalClient extends Disposable implements ITerminalClient { const widget = this._widget; if (TerminalClient.WORKSPACE_PATH_CACHED.has(widget.group.id)) { this._workspacePath = TerminalClient.WORKSPACE_PATH_CACHED.get(widget.group.id)!; - } else { + } else if (!widget.recovery) { const choose = await this._pickWorkspace(); if (choose) { this._workspacePath = choose; diff --git a/packages/terminal-next/src/browser/terminal.controller.ts b/packages/terminal-next/src/browser/terminal.controller.ts index 11d3c11112..c995389b0a 100644 --- a/packages/terminal-next/src/browser/terminal.controller.ts +++ b/packages/terminal-next/src/browser/terminal.controller.ts @@ -355,6 +355,8 @@ export class TerminalController extends WithEventBus implements ITerminalControl group, typeof session === 'string' ? session : session.client, !!session.task, + false, + true, ); const client = await this.clientFactory(widget); diff --git a/packages/terminal-next/src/browser/terminal.view.ts b/packages/terminal-next/src/browser/terminal.view.ts index 3e835d8cf6..316dfb4880 100644 --- a/packages/terminal-next/src/browser/terminal.view.ts +++ b/packages/terminal-next/src/browser/terminal.view.ts @@ -31,7 +31,7 @@ export class Widget extends Disposable implements IWidget { @observable processName: string | undefined; - constructor(id: string, public reuse: boolean = false) { + constructor(id: string, public reuse: boolean = false, public recovery: boolean = false) { super(); makeObservable(this); this._id = id; @@ -372,8 +372,8 @@ export class TerminalGroupViewService implements ITerminalGroupViewService { this.selectGroup(index); } - createWidget(group: WidgetGroup, id?: string, reuse?: boolean, isSimpleWidget = false) { - const widget = new Widget(id || this.service.generateSessionId(), reuse); + createWidget(group: WidgetGroup, id?: string, reuse?: boolean, isSimpleWidget = false, recovery = false) { + const widget = new Widget(id || this.service.generateSessionId(), reuse, recovery); this._widgets.set(widget.id, widget); widget.group = group; if (!isSimpleWidget) { diff --git a/packages/terminal-next/src/common/resize.ts b/packages/terminal-next/src/common/resize.ts index 27d6ef7f75..c5951cf48f 100644 --- a/packages/terminal-next/src/common/resize.ts +++ b/packages/terminal-next/src/common/resize.ts @@ -11,6 +11,7 @@ export interface IWidget extends Disposable { element: HTMLDivElement; group: IWidgetGroup; reuse: boolean; + recovery: boolean; show: boolean; error: boolean; resize: (dynamic?: number) => void; diff --git a/packages/workspace/src/browser/workspace-preferences.ts b/packages/workspace/src/browser/workspace-preferences.ts index f1fea7caab..7a8fcb4ca8 100644 --- a/packages/workspace/src/browser/workspace-preferences.ts +++ b/packages/workspace/src/browser/workspace-preferences.ts @@ -15,7 +15,7 @@ export const workspacePreferenceSchema: PreferenceSchema = { default: false, }, 'workspace.supportMultiRootWorkspace': { - description: 'Enable the multi-root workspace support to test this feature internally', + description: 'Enable multi-root workspace support', type: 'boolean', default: false, }, diff --git a/packages/workspace/src/common/mocks/workspace-service.ts b/packages/workspace/src/common/mocks/workspace-service.ts index f667a01ea5..fa5f1bc10d 100644 --- a/packages/workspace/src/common/mocks/workspace-service.ts +++ b/packages/workspace/src/common/mocks/workspace-service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@opensumi/di'; import { Deferred, Emitter, URI } from '@opensumi/ide-core-common'; import { FileStat } from '@opensumi/ide-file-service'; -import { IWorkspaceService } from '../../common'; +import { IWorkspaceService, WorkspaceInput } from '../../common'; @Injectable() export class MockWorkspaceService implements IWorkspaceService { @@ -14,12 +14,18 @@ export class MockWorkspaceService implements IWorkspaceService { whenReady: Promise; + workspaceSuffixName: string; + private deferredRoots = new Deferred(); constructor() { this.whenReady = this.init(); } + open(uri: URI, options?: WorkspaceInput): Promise { + throw new Error('Method not implemented.'); + } + async init() { await this.setWorkspace(); } diff --git a/packages/workspace/src/common/workspace.interface.ts b/packages/workspace/src/common/workspace.interface.ts index a65f5281c5..da23182439 100644 --- a/packages/workspace/src/common/workspace.interface.ts +++ b/packages/workspace/src/common/workspace.interface.ts @@ -12,6 +12,8 @@ export interface WorkspaceInput { export const IWorkspaceService = Symbol('IWorkspaceService'); export interface IWorkspaceService { + // 工作区文件后缀,默认后缀为 `sumi-workspace` + workspaceSuffixName: string; // 获取当前的根节点 roots: Promise; // 获取workspace @@ -68,6 +70,8 @@ export interface IWorkspaceService { setWorkspace(workspaceStat: FileStat | undefined): Promise; // 初始化文件服务中 `files.exclude` 和 `watche.exclude` 配置 initFileServiceExclude(): Promise; + // 打开工作区 + open(uri: URI, options?: WorkspaceInput): Promise; } export const IWorkspaceStorageService = Symbol('IWorkspaceStorageService');