Skip to content

Commit d8a6ab4

Browse files
author
colin-grant-work
authored
Open new, multi-root workspace from CLI (#11034)
Allows users to input multiple folders to open as a single workspace.
1 parent f4409b0 commit d8a6ab4

File tree

4 files changed

+68
-35
lines changed

4 files changed

+68
-35
lines changed

packages/core/src/common/promise-util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { CancellationToken, CancellationError, cancelled } from './cancellation'
2424
*/
2525
export class Deferred<T = void> {
2626
state: 'resolved' | 'rejected' | 'unresolved' = 'unresolved';
27-
resolve: (value: T) => void;
27+
resolve: (value: T | PromiseLike<T>) => void;
2828
reject: (err?: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
2929

3030
promise = new Promise<T>((resolve, reject) => {

packages/workspace/src/browser/workspace-service.ts

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
1818
import URI from '@theia/core/lib/common/uri';
19-
import { WorkspaceServer, THEIA_EXT, CommonWorkspaceUtils } from '../common';
19+
import { WorkspaceServer, CommonWorkspaceUtils } from '../common';
2020
import { WindowService } from '@theia/core/lib/browser/window/window-service';
2121
import { DEFAULT_WINDOW_HASH } from '@theia/core/lib/common/window';
2222
import {
@@ -438,24 +438,16 @@ export class WorkspaceService implements FrontendApplicationContribution {
438438
}
439439

440440
async getUntitledWorkspace(): Promise<URI> {
441-
const configDirURI = await this.envVariableServer.getConfigDirUri();
442-
let uri;
443-
let attempts = 0;
444-
do {
445-
attempts++;
446-
uri = new URI(configDirURI).resolve(`workspaces/Untitled-${Math.round(Math.random() * 1000)}.${THEIA_EXT}`);
447-
if (attempts === 10) {
448-
this.messageService.warn(nls.localize(
449-
'theia/workspace/untitled-cleanup',
450-
'There appear to be many untitled workspace files. Please check {0} and remove any unused files.',
451-
new URI(configDirURI).resolve('workspaces').path.toString())
452-
);
453-
}
454-
if (attempts === 50) {
455-
throw new Error('Workspace Service: too many attempts to find unused filename.');
456-
}
457-
} while (await this.fileService.exists(uri));
458-
return uri;
441+
const configDirURI = new URI(await this.envVariableServer.getConfigDirUri());
442+
return this.utils.getUntitledWorkspaceUri(
443+
configDirURI,
444+
uri => this.fileService.exists(uri).then(exists => !exists),
445+
() => this.messageService.warn(nls.localize(
446+
'theia/workspace/untitled-cleanup',
447+
'There appear to be many untitled workspace files. Please check {0} and remove any unused files.',
448+
configDirURI.resolve('workspaces').path.fsPath())
449+
),
450+
);
459451
}
460452

461453
protected async writeWorkspaceFile(workspaceFile: FileStat | undefined, workspaceData: WorkspaceData): Promise<FileStat | undefined> {

packages/workspace/src/common/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import URI from '@theia/core/lib/common/uri';
2020
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
2121
import { injectable } from '@theia/core/shared/inversify';
2222
import { FileStat } from '@theia/filesystem/lib/common/files';
23+
import { MaybePromise } from '@theia/core';
2324

2425
export const THEIA_EXT = 'theia-workspace';
2526
export const VSCODE_EXT = 'code-workspace';
@@ -47,4 +48,21 @@ export class CommonWorkspaceUtils {
4748
isUntitledWorkspace(candidate?: URI): boolean {
4849
return !!candidate && this.isWorkspaceFile(candidate) && candidate.path.base.startsWith('Untitled');
4950
}
51+
52+
async getUntitledWorkspaceUri(configDirUri: URI, isAcceptable: (candidate: URI) => MaybePromise<boolean>, warnOnHits?: () => unknown): Promise<URI> {
53+
const parentDir = configDirUri.resolve('workspaces');
54+
let uri;
55+
let attempts = 0;
56+
do {
57+
attempts++;
58+
uri = parentDir.resolve(`Untitled-${Math.round(Math.random() * 1000)}.${THEIA_EXT}`);
59+
if (attempts === 10) {
60+
warnOnHits?.();
61+
}
62+
if (attempts === 50) {
63+
throw new Error('Workspace Service: too many attempts to find unused filename.');
64+
}
65+
} while (!(await isAcceptable(uri)));
66+
return uri;
67+
}
5068
}

packages/workspace/src/node/default-workspace-server.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,55 @@ import URI from '@theia/core/lib/common/uri';
2929
@injectable()
3030
export class WorkspaceCliContribution implements CliContribution {
3131

32+
@inject(EnvVariablesServer) protected readonly envVariablesServer: EnvVariablesServer;
33+
@inject(CommonWorkspaceUtils) protected readonly workspaceUtils: CommonWorkspaceUtils;
34+
3235
workspaceRoot = new Deferred<string | undefined>();
3336

3437
configure(conf: yargs.Argv): void {
35-
conf.usage('$0 [workspace-directory] [options]');
38+
conf.usage('$0 [workspace-directories] [options]');
3639
conf.option('root-dir', {
3740
description: 'DEPRECATED: Sets the workspace directory.',
3841
});
3942
}
4043

41-
setArguments(args: yargs.Arguments): void {
42-
let wsPath: string | undefined = typeof args._[2] === 'undefined' ? undefined : String(args._[2]);
43-
if (!wsPath) {
44-
wsPath = args['root-dir'] as string;
45-
if (!wsPath) {
46-
this.workspaceRoot.resolve(undefined);
47-
return;
48-
}
44+
async setArguments(args: yargs.Arguments): Promise<void> {
45+
const workspaceArguments = args._.slice(2).map(probablyAlreadyString => String(probablyAlreadyString));
46+
if (workspaceArguments.length === 0 && args['root-dir']) {
47+
workspaceArguments.push(String(args['root-dir']));
4948
}
50-
if (!path.isAbsolute(wsPath)) {
51-
const cwd = process.cwd();
52-
wsPath = path.join(cwd, wsPath);
49+
if (workspaceArguments.length === 0) {
50+
this.workspaceRoot.resolve(undefined);
51+
} else if (workspaceArguments.length === 1) {
52+
this.workspaceRoot.resolve(this.normalizeWorkspaceArg(workspaceArguments[0]));
53+
} else {
54+
this.workspaceRoot.resolve(this.buildWorkspaceForMultipleArguments(workspaceArguments));
5355
}
54-
if (wsPath && wsPath.endsWith('/')) {
55-
wsPath = wsPath.slice(0, -1);
56+
}
57+
58+
protected normalizeWorkspaceArg(raw: string): string {
59+
return path.resolve(raw).replace(/\/$/, '');
60+
}
61+
62+
protected async buildWorkspaceForMultipleArguments(workspaceArguments: string[]): Promise<string | undefined> {
63+
try {
64+
const dirs = await Promise.all(workspaceArguments.map(async maybeDir => (await fs.stat(maybeDir).catch(() => undefined))?.isDirectory()));
65+
const folders = workspaceArguments.filter((_, index) => dirs[index]).map(dir => ({ path: this.normalizeWorkspaceArg(dir) }));
66+
if (folders.length < 2) {
67+
return folders[0]?.path;
68+
}
69+
const untitledWorkspaceUri = await this.workspaceUtils.getUntitledWorkspaceUri(
70+
new URI(await this.envVariablesServer.getConfigDirUri()),
71+
async uri => !await fs.pathExists(uri.path.fsPath()),
72+
);
73+
const untitledWorkspacePath = untitledWorkspaceUri.path.fsPath();
74+
75+
await fs.ensureDir(path.dirname(untitledWorkspacePath));
76+
await fs.writeFile(untitledWorkspacePath, JSON.stringify({ folders }, undefined, 4));
77+
return untitledWorkspacePath;
78+
} catch {
79+
return undefined;
5680
}
57-
this.workspaceRoot.resolve(wsPath);
5881
}
5982
}
6083

0 commit comments

Comments
 (0)