diff --git a/packages/plugin-ext/src/api/plugin-api.ts b/packages/plugin-ext/src/api/plugin-api.ts index f407b48b9bd78..cbd2a7a2de31c 100644 --- a/packages/plugin-ext/src/api/plugin-api.ts +++ b/packages/plugin-ext/src/api/plugin-api.ts @@ -384,6 +384,7 @@ export interface WorkspaceMain { $onTextDocumentContentChange(uri: string, content: string): void; $registerFileSystemWatcher(options: FileWatcherSubscriberOptions): Promise; $unregisterFileSystemWatcher(watcherId: string): Promise; + $updateWorkspaceFolders(start: number, deleteCount?: number, ...rootsToAdd: string[]): Promise; } export interface WorkspaceExt { diff --git a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts index f6b793e971cb1..bed91911901f9 100644 --- a/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts +++ b/packages/plugin-ext/src/hosted/browser/worker/worker-main.ts @@ -27,6 +27,7 @@ import { ExtPluginApi } from '../../../common/plugin-ext-api-contribution'; import { createDebugExtStub } from './debug-stub'; import { EditorsAndDocumentsExtImpl } from '../../../plugin/editors-and-documents'; import { WorkspaceExtImpl } from '../../../plugin/workspace'; +import { MessageRegistryExt } from '../../../plugin/message-registry'; // tslint:disable-next-line:no-any const ctx = self as any; @@ -50,7 +51,8 @@ function initialize(contextPath: string, pluginMetadata: PluginMetadata): void { } const envExt = new EnvExtImpl(rpc); const editorsAndDocuments = new EditorsAndDocumentsExtImpl(rpc); -const workspaceExt = new WorkspaceExtImpl(rpc, editorsAndDocuments); +const messageRegistryExt = new MessageRegistryExt(rpc); +const workspaceExt = new WorkspaceExtImpl(rpc, editorsAndDocuments, messageRegistryExt); const preferenceRegistryExt = new PreferenceRegistryExtImpl(rpc, workspaceExt); const debugExt = createDebugExtStub(rpc); @@ -130,7 +132,8 @@ const apiFactory = createAPIFactory( debugExt, preferenceRegistryExt, editorsAndDocuments, - workspaceExt + workspaceExt, + messageRegistryExt ); let defaultApi: typeof theia; diff --git a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts index e6ccc3a546446..c1532a0e787a0 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts @@ -24,6 +24,7 @@ import { ExtPluginApi } from '../../common/plugin-ext-api-contribution'; import { DebugExtImpl } from '../../plugin/node/debug/debug'; import { EditorsAndDocumentsExtImpl } from '../../plugin/editors-and-documents'; import { WorkspaceExtImpl } from '../../plugin/workspace'; +import { MessageRegistryExt } from '../../plugin/message-registry'; /** * Handle the RPC calls. @@ -42,7 +43,8 @@ export class PluginHostRPC { const envExt = new EnvExtImpl(this.rpc); const debugExt = new DebugExtImpl(this.rpc); const editorsAndDocumentsExt = new EditorsAndDocumentsExtImpl(this.rpc); - const workspaceExt = new WorkspaceExtImpl(this.rpc, editorsAndDocumentsExt); + const messageRegistryExt = new MessageRegistryExt(this.rpc); + const workspaceExt = new WorkspaceExtImpl(this.rpc, editorsAndDocumentsExt, messageRegistryExt); const preferenceRegistryExt = new PreferenceRegistryExtImpl(this.rpc, workspaceExt); this.pluginManager = this.createPluginManager(envExt, preferenceRegistryExt, this.rpc); this.rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, this.pluginManager); @@ -57,7 +59,8 @@ export class PluginHostRPC { debugExt, preferenceRegistryExt, editorsAndDocumentsExt, - workspaceExt + workspaceExt, + messageRegistryExt ); } diff --git a/packages/plugin-ext/src/main/browser/workspace-main.ts b/packages/plugin-ext/src/main/browser/workspace-main.ts index ed18eac945715..bef498bb472e6 100644 --- a/packages/plugin-ext/src/main/browser/workspace-main.ts +++ b/packages/plugin-ext/src/main/browser/workspace-main.ts @@ -194,6 +194,10 @@ export class WorkspaceMainImpl implements WorkspaceMain { this.resourceResolver.onContentChange(uri, content); } + async $updateWorkspaceFolders(start: number, deleteCount?: number, ...rootsToAdd: string[]): Promise { + await this.workspaceService.spliceRoots(start, deleteCount, ...rootsToAdd.map(root => new URI(root))); + } + } /** diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 3097fc37110fd..438dd276d2278 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -131,13 +131,13 @@ export function createAPIFactory( debugExt: DebugExtImpl, preferenceRegistryExt: PreferenceRegistryExtImpl, editorsAndDocumentsExt: EditorsAndDocumentsExtImpl, - workspaceExt: WorkspaceExtImpl + workspaceExt: WorkspaceExtImpl, + messageRegistryExt: MessageRegistryExt ): PluginAPIFactory { const commandRegistry = rpc.set(MAIN_RPC_CONTEXT.COMMAND_REGISTRY_EXT, new CommandRegistryImpl(rpc)); const quickOpenExt = rpc.set(MAIN_RPC_CONTEXT.QUICK_OPEN_EXT, new QuickOpenExtImpl(rpc)); const dialogsExt = new DialogsExtImpl(rpc); - const messageRegistryExt = new MessageRegistryExt(rpc); const windowStateExt = rpc.set(MAIN_RPC_CONTEXT.WINDOW_STATE_EXT, new WindowStateExtImpl()); const notificationExt = rpc.set(MAIN_RPC_CONTEXT.NOTIFICATION_EXT, new NotificationExtImpl(rpc)); const statusBarExt = new StatusBarExtImpl(rpc); @@ -429,6 +429,9 @@ export function createAPIFactory( asRelativePath(pathOrUri: theia.Uri | string, includeWorkspace?: boolean): string | undefined { return workspaceExt.getRelativePath(pathOrUri, includeWorkspace); }, + updateWorkspaceFolders: (index, deleteCount, ...workspaceFoldersToAdd) => + workspaceExt.updateWorkspaceFolders(index, deleteCount || 0, ...workspaceFoldersToAdd) + , registerTaskProvider(type: string, provider: theia.TaskProvider): theia.Disposable { return tasks.registerTaskProvider(type, provider); }, diff --git a/packages/plugin-ext/src/plugin/workspace.ts b/packages/plugin-ext/src/plugin/workspace.ts index 3d929b60a8e0a..6d975495eb857 100644 --- a/packages/plugin-ext/src/plugin/workspace.ts +++ b/packages/plugin-ext/src/plugin/workspace.ts @@ -22,7 +22,8 @@ import { WorkspaceExt, WorkspaceFolderPickOptionsMain, WorkspaceMain, - PLUGIN_RPC_CONTEXT as Ext + PLUGIN_RPC_CONTEXT as Ext, + MainMessageType } from '../api/plugin-api'; import { Path } from '@theia/core/lib/common/path'; import { RPCProtocol } from '../api/rpc-protocol'; @@ -35,6 +36,7 @@ import { normalize } from '../common/paths'; import { relative } from '../common/paths-util'; import { Schemes } from '../common/uri-components'; import { toWorkspaceFolder } from './type-converters'; +import { MessageRegistryExt } from './message-registry'; export class WorkspaceExtImpl implements WorkspaceExt { @@ -47,7 +49,9 @@ export class WorkspaceExtImpl implements WorkspaceExt { private folders: theia.WorkspaceFolder[] | undefined; private documentContentProviders = new Map(); - constructor(rpc: RPCProtocol, private editorsAndDocuments: EditorsAndDocumentsExtImpl) { + constructor(rpc: RPCProtocol, + private editorsAndDocuments: EditorsAndDocumentsExtImpl, + private messageService: MessageRegistryExt) { this.proxy = rpc.getProxy(Ext.WORKSPACE_MAIN); this.fileSystemWatcherManager = new InPluginFileSystemWatcherProxy(this.proxy); } @@ -72,15 +76,20 @@ export class WorkspaceExtImpl implements WorkspaceExt { $onWorkspaceFoldersChanged(event: WorkspaceRootsChangeEvent): void { const newRoots = event.roots || []; const newFolders = newRoots.map((root, index) => this.toWorkspaceFolder(root, index)); - const added = this.foldersDiff(newFolders, this.folders); - const removed = this.foldersDiff(this.folders, newFolders); + const delta = this.deltaFolders(this.folders, newFolders); this.folders = newFolders; - this.workspaceFoldersChangedEmitter.fire({ - added: added, - removed: removed - }); + this.workspaceFoldersChangedEmitter.fire(delta); + } + + private deltaFolders(currentFolders: theia.WorkspaceFolder[] = [], newFolders: theia.WorkspaceFolder[] = []): { + added: theia.WorkspaceFolder[] + removed: theia.WorkspaceFolder[] + } { + const added = this.foldersDiff(newFolders, currentFolders); + const removed = this.foldersDiff(currentFolders, newFolders); + return { added, removed }; } private foldersDiff(folder1: theia.WorkspaceFolder[] = [], folder2: theia.WorkspaceFolder[] = []): theia.WorkspaceFolder[] { @@ -282,6 +291,54 @@ export class WorkspaceExtImpl implements WorkspaceExt { return normalize(result, true); } + updateWorkspaceFolders(start: number, deleteCount: number, ...workspaceFoldersToAdd: { uri: theia.Uri, name?: string }[]): boolean { + const rootsToAdd = new Set(); + if (Array.isArray(workspaceFoldersToAdd)) { + workspaceFoldersToAdd.forEach(folderToAdd => { + const uri = URI.isUri(folderToAdd.uri) && folderToAdd.uri.toString(); + if (uri && !rootsToAdd.has(uri)) { + rootsToAdd.add(uri); + } + }); + } + + if ([start, deleteCount].some(i => typeof i !== 'number' || i < 0)) { + return false; // validate numbers + } + + if (deleteCount === 0 && rootsToAdd.size === 0) { + return false; // nothing to delete or add + } + + const currentWorkspaceFolders = this.workspaceFolders || []; + if (start + deleteCount > currentWorkspaceFolders.length) { + return false; // cannot delete more than we have + } + + // Simulate the updateWorkspaceFolders method on our data to do more validation + const newWorkspaceFolders = currentWorkspaceFolders.slice(0); + newWorkspaceFolders.splice(start, deleteCount, ...[...rootsToAdd].map(uri => ({ uri: URI.parse(uri), name: undefined!, index: undefined! }))); + + for (let i = 0; i < newWorkspaceFolders.length; i++) { + const folder = newWorkspaceFolders[i]; + if (newWorkspaceFolders.some((otherFolder, index) => index !== i && folder.uri.toString() === otherFolder.uri.toString())) { + return false; // cannot add the same folder multiple times + } + } + + const { added, removed } = this.deltaFolders(currentWorkspaceFolders, newWorkspaceFolders); + if (added.length === 0 && removed.length === 0) { + return false; // nothing actually changed + } + + // Trigger on main side + this.proxy.$updateWorkspaceFolders(start, deleteCount, ...rootsToAdd).then(undefined, error => + this.messageService.showMessage(MainMessageType.Error, `Failed to update workspace folders: ${error}`) + ); + + return true; + } + // Experimental API https://github.com/theia-ide/theia/issues/4167 private workspaceWillRenameFileEmitter = new Emitter(); private workspaceDidRenameFileEmitter = new Emitter(); diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 981c8cf160558..ac539c8929ddd 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -3670,48 +3670,48 @@ declare module '@theia/plugin' { export class FileSystemError extends Error { /** - * Create an error to signal that a file or folder wasn't found. - * @param messageOrUri Message or uri. - */ + * Create an error to signal that a file or folder wasn't found. + * @param messageOrUri Message or uri. + */ static FileNotFound(messageOrUri?: string | Uri): FileSystemError; /** - * Create an error to signal that a file or folder already exists, e.g. when - * creating but not overwriting a file. - * @param messageOrUri Message or uri. - */ + * Create an error to signal that a file or folder already exists, e.g. when + * creating but not overwriting a file. + * @param messageOrUri Message or uri. + */ static FileExists(messageOrUri?: string | Uri): FileSystemError; /** - * Create an error to signal that a file is not a folder. - * @param messageOrUri Message or uri. - */ + * Create an error to signal that a file is not a folder. + * @param messageOrUri Message or uri. + */ static FileNotADirectory(messageOrUri?: string | Uri): FileSystemError; /** - * Create an error to signal that a file is a folder. - * @param messageOrUri Message or uri. - */ + * Create an error to signal that a file is a folder. + * @param messageOrUri Message or uri. + */ static FileIsADirectory(messageOrUri?: string | Uri): FileSystemError; /** - * Create an error to signal that an operation lacks required permissions. - * @param messageOrUri Message or uri. - */ + * Create an error to signal that an operation lacks required permissions. + * @param messageOrUri Message or uri. + */ static NoPermissions(messageOrUri?: string | Uri): FileSystemError; /** - * Create an error to signal that the file system is unavailable or too busy to - * complete a request. - * @param messageOrUri Message or uri. - */ + * Create an error to signal that the file system is unavailable or too busy to + * complete a request. + * @param messageOrUri Message or uri. + */ static Unavailable(messageOrUri?: string | Uri): FileSystemError; /** - * Creates a new filesystem error. - * - * @param messageOrUri Message or uri. - */ + * Creates a new filesystem error. + * + * @param messageOrUri Message or uri. + */ constructor(messageOrUri?: string | Uri); } @@ -4147,6 +4147,49 @@ declare module '@theia/plugin' { */ export function asRelativePath(pathOrUri: string | Uri, includeWorkspaceFolder?: boolean): string | undefined; + /** + * This method replaces `deleteCount` [workspace folders](#workspace.workspaceFolders) starting at index `start` + * by an optional set of `workspaceFoldersToAdd` on the `theia.workspace.workspaceFolders` array. This "splice" + * behavior can be used to add, remove and change workspace folders in a single operation. + * + * If the first workspace folder is added, removed or changed, the currently executing extensions (including the + * one that called this method) will be terminated and restarted so that the (deprecated) `rootPath` property is + * updated to point to the first workspace folder. + * + * Use the [`onDidChangeWorkspaceFolders()`](#onDidChangeWorkspaceFolders) event to get notified when the + * workspace folders have been updated. + * + * **Example:** adding a new workspace folder at the end of workspace folders + * ```typescript + * workspace.updateWorkspaceFolders(workspace.workspaceFolders ? workspace.workspaceFolders.length : 0, null, { uri: ...}); + * ``` + * + * **Example:** removing the first workspace folder + * ```typescript + * workspace.updateWorkspaceFolders(0, 1); + * ``` + * + * **Example:** replacing an existing workspace folder with a new one + * ```typescript + * workspace.updateWorkspaceFolders(0, 1, { uri: ...}); + * ``` + * + * It is valid to remove an existing workspace folder and add it again with a different name + * to rename that folder. + * + * **Note:** it is not valid to call [updateWorkspaceFolders()](#updateWorkspaceFolders) multiple times + * without waiting for the [`onDidChangeWorkspaceFolders()`](#onDidChangeWorkspaceFolders) to fire. + * + * @param start the zero-based location in the list of currently opened [workspace folders](#WorkspaceFolder) + * from which to start deleting workspace folders. + * @param deleteCount the optional number of workspace folders to remove. + * @param workspaceFoldersToAdd the optional variable set of workspace folders to add in place of the deleted ones. + * Each workspace is identified with a mandatory URI and an optional name. + * @return true if the operation was successfully started and false otherwise if arguments were used that would result + * in invalid workspace folder state (e.g. 2 folders with the same URI). + */ + export function updateWorkspaceFolders(start: number, deleteCount: number | undefined | null, ...workspaceFoldersToAdd: { uri: Uri, name?: string }[]): boolean; + /** * ~~Register a task provider.~~ *