Skip to content

Commit

Permalink
[plugin] fix #4240: implement workspace.updateWorkspaceFolders
Browse files Browse the repository at this point in the history
Signed-off-by: Anton Kosyakov <[email protected]>
  • Loading branch information
akosyakov committed Mar 18, 2019
1 parent ac21820 commit 6894d84
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 38 deletions.
1 change: 1 addition & 0 deletions packages/plugin-ext/src/api/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ export interface WorkspaceMain {
$onTextDocumentContentChange(uri: string, content: string): void;
$registerFileSystemWatcher(options: FileWatcherSubscriberOptions): Promise<string>;
$unregisterFileSystemWatcher(watcherId: string): Promise<void>;
$updateWorkspaceFolders(start: number, deleteCount?: number, ...rootsToAdd: string[]): Promise<void>;
}

export interface WorkspaceExt {
Expand Down
7 changes: 5 additions & 2 deletions packages/plugin-ext/src/hosted/browser/worker/worker-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

Expand Down Expand Up @@ -130,7 +132,8 @@ const apiFactory = createAPIFactory(
debugExt,
preferenceRegistryExt,
editorsAndDocuments,
workspaceExt
workspaceExt,
messageRegistryExt
);
let defaultApi: typeof theia;

Expand Down
7 changes: 5 additions & 2 deletions packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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);
Expand All @@ -57,7 +59,8 @@ export class PluginHostRPC {
debugExt,
preferenceRegistryExt,
editorsAndDocumentsExt,
workspaceExt
workspaceExt,
messageRegistryExt
);
}

Expand Down
4 changes: 4 additions & 0 deletions packages/plugin-ext/src/main/browser/workspace-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ export class WorkspaceMainImpl implements WorkspaceMain {
this.resourceResolver.onContentChange(uri, content);
}

async $updateWorkspaceFolders(start: number, deleteCount?: number, ...rootsToAdd: string[]): Promise<void> {
await this.workspaceService.spliceRoots(start, deleteCount, ...rootsToAdd.map(root => new URI(root)));
}

}

/**
Expand Down
7 changes: 5 additions & 2 deletions packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
},
Expand Down
73 changes: 65 additions & 8 deletions packages/plugin-ext/src/plugin/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {

Expand All @@ -47,7 +49,9 @@ export class WorkspaceExtImpl implements WorkspaceExt {
private folders: theia.WorkspaceFolder[] | undefined;
private documentContentProviders = new Map<string, theia.TextDocumentContentProvider>();

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);
}
Expand All @@ -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[] {
Expand Down Expand Up @@ -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<string>();
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<theia.FileWillRenameEvent>();
private workspaceDidRenameFileEmitter = new Emitter<theia.FileRenameEvent>();
Expand Down
91 changes: 67 additions & 24 deletions packages/plugin/src/theia.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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.~~
*
Expand Down

0 comments on commit 6894d84

Please sign in to comment.