@theia/workspace 1.22.0-next.7 → 1.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/untitled-workspace-exit-dialog.d.ts +36 -0
- package/lib/browser/untitled-workspace-exit-dialog.d.ts.map +1 -0
- package/lib/browser/untitled-workspace-exit-dialog.js +76 -0
- package/lib/browser/untitled-workspace-exit-dialog.js.map +1 -0
- package/lib/browser/workspace-commands.d.ts +1 -0
- package/lib/browser/workspace-commands.d.ts.map +1 -1
- package/lib/browser/workspace-commands.js +15 -9
- package/lib/browser/workspace-commands.js.map +1 -1
- package/lib/browser/workspace-delete-handler.d.ts +9 -0
- package/lib/browser/workspace-delete-handler.d.ts.map +1 -1
- package/lib/browser/workspace-delete-handler.js +34 -1
- package/lib/browser/workspace-delete-handler.js.map +1 -1
- package/lib/browser/workspace-frontend-contribution.d.ts +21 -8
- package/lib/browser/workspace-frontend-contribution.d.ts.map +1 -1
- package/lib/browser/workspace-frontend-contribution.js +95 -17
- package/lib/browser/workspace-frontend-contribution.js.map +1 -1
- package/lib/browser/workspace-frontend-module.d.ts.map +1 -1
- package/lib/browser/workspace-frontend-module.js +1 -0
- package/lib/browser/workspace-frontend-module.js.map +1 -1
- package/lib/browser/workspace-schema-updater.d.ts.map +1 -1
- package/lib/browser/workspace-schema-updater.js +2 -0
- package/lib/browser/workspace-schema-updater.js.map +1 -1
- package/lib/browser/workspace-service.d.ts +10 -3
- package/lib/browser/workspace-service.d.ts.map +1 -1
- package/lib/browser/workspace-service.js +49 -16
- package/lib/browser/workspace-service.js.map +1 -1
- package/lib/common/utils.d.ts +10 -0
- package/lib/common/utils.d.ts.map +1 -1
- package/lib/common/utils.js +27 -1
- package/lib/common/utils.js.map +1 -1
- package/lib/node/default-workspace-server.d.ts +15 -2
- package/lib/node/default-workspace-server.d.ts.map +1 -1
- package/lib/node/default-workspace-server.js +26 -1
- package/lib/node/default-workspace-server.js.map +1 -1
- package/lib/node/workspace-backend-module.d.ts.map +1 -1
- package/lib/node/workspace-backend-module.js +3 -0
- package/lib/node/workspace-backend-module.js.map +1 -1
- package/package.json +6 -6
- package/src/browser/untitled-workspace-exit-dialog.ts +70 -0
- package/src/browser/workspace-commands.ts +16 -10
- package/src/browser/workspace-delete-handler.ts +42 -1
- package/src/browser/workspace-frontend-contribution.ts +101 -20
- package/src/browser/workspace-frontend-module.ts +2 -1
- package/src/browser/workspace-schema-updater.ts +2 -0
- package/src/browser/workspace-service.ts +54 -19
- package/src/common/utils.ts +19 -0
- package/src/node/default-workspace-server.ts +29 -5
- package/src/node/workspace-backend-module.ts +4 -1
|
@@ -15,17 +15,17 @@
|
|
|
15
15
|
********************************************************************************/
|
|
16
16
|
|
|
17
17
|
import { injectable, inject } from '@theia/core/shared/inversify';
|
|
18
|
-
import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry, SelectionService, MessageService, isWindows } from '@theia/core/lib/common';
|
|
18
|
+
import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry, SelectionService, MessageService, isWindows, MaybeArray } from '@theia/core/lib/common';
|
|
19
19
|
import { isOSX, environment, OS } from '@theia/core';
|
|
20
20
|
import {
|
|
21
21
|
open, OpenerService, CommonMenus, StorageService, LabelProvider, ConfirmDialog, KeybindingRegistry, KeybindingContribution,
|
|
22
|
-
CommonCommands, FrontendApplicationContribution, ApplicationShell, Saveable, SaveableSource, Widget, Navigatable, SHELL_TABBAR_CONTEXT_COPY
|
|
22
|
+
CommonCommands, FrontendApplicationContribution, ApplicationShell, Saveable, SaveableSource, Widget, Navigatable, SHELL_TABBAR_CONTEXT_COPY, OnWillStopAction
|
|
23
23
|
} from '@theia/core/lib/browser';
|
|
24
24
|
import { FileDialogService, OpenFileDialogProps, FileDialogTreeFilters } from '@theia/filesystem/lib/browser';
|
|
25
25
|
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
|
26
26
|
import { WorkspaceService } from './workspace-service';
|
|
27
27
|
import { THEIA_EXT, VSCODE_EXT } from '../common';
|
|
28
|
-
import { WorkspaceCommands } from './workspace-commands';
|
|
28
|
+
import { WorkspaceCommands, WorkspaceCommandContribution } from './workspace-commands';
|
|
29
29
|
import { QuickOpenWorkspace } from './quick-open-workspace';
|
|
30
30
|
import { WorkspacePreferences } from './workspace-preferences';
|
|
31
31
|
import URI from '@theia/core/lib/common/uri';
|
|
@@ -35,6 +35,9 @@ import { UTF8 } from '@theia/core/lib/common/encodings';
|
|
|
35
35
|
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
|
36
36
|
import { PreferenceConfigurations } from '@theia/core/lib/browser/preferences/preference-configurations';
|
|
37
37
|
import { nls } from '@theia/core/lib/common/nls';
|
|
38
|
+
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
|
|
39
|
+
import { FileStat } from '@theia/filesystem/lib/common/files';
|
|
40
|
+
import { UntitledWorkspaceExitDialog } from './untitled-workspace-exit-dialog';
|
|
38
41
|
|
|
39
42
|
export enum WorkspaceStates {
|
|
40
43
|
/**
|
|
@@ -51,6 +54,7 @@ export enum WorkspaceStates {
|
|
|
51
54
|
folder = 'folder',
|
|
52
55
|
};
|
|
53
56
|
export type WorkspaceState = keyof typeof WorkspaceStates;
|
|
57
|
+
export type WorkbenchState = keyof typeof WorkspaceStates;
|
|
54
58
|
|
|
55
59
|
@injectable()
|
|
56
60
|
export class WorkspaceFrontendContribution implements CommandContribution, KeybindingContribution, MenuContribution, FrontendApplicationContribution {
|
|
@@ -67,6 +71,7 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
|
|
|
67
71
|
@inject(WorkspacePreferences) protected preferences: WorkspacePreferences;
|
|
68
72
|
@inject(SelectionService) protected readonly selectionService: SelectionService;
|
|
69
73
|
@inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry;
|
|
74
|
+
@inject(WorkspaceCommandContribution) protected readonly workspaceCommands: WorkspaceCommandContribution;
|
|
70
75
|
|
|
71
76
|
@inject(ContextKeyService)
|
|
72
77
|
protected readonly contextKeyService: ContextKeyService;
|
|
@@ -90,11 +95,16 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
|
|
|
90
95
|
const updateWorkspaceStateKey = () => workspaceStateKey.set(this.updateWorkspaceStateKey());
|
|
91
96
|
updateWorkspaceStateKey();
|
|
92
97
|
|
|
98
|
+
const workbenchStateKey = this.contextKeyService.createKey<WorkbenchState>('workbenchState', 'empty');
|
|
99
|
+
const updateWorkbenchStateKey = () => workbenchStateKey.set(this.updateWorkbenchStateKey());
|
|
100
|
+
updateWorkbenchStateKey();
|
|
101
|
+
|
|
93
102
|
this.updateStyles();
|
|
94
103
|
this.workspaceService.onWorkspaceChanged(() => {
|
|
95
104
|
this.updateEncodingOverrides();
|
|
96
105
|
updateWorkspaceFolderCountKey();
|
|
97
106
|
updateWorkspaceStateKey();
|
|
107
|
+
updateWorkbenchStateKey();
|
|
98
108
|
this.updateStyles();
|
|
99
109
|
});
|
|
100
110
|
}
|
|
@@ -308,32 +318,64 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
|
|
|
308
318
|
}
|
|
309
319
|
|
|
310
320
|
/**
|
|
311
|
-
* Opens
|
|
312
|
-
* - the
|
|
313
|
-
* - the
|
|
314
|
-
* - it was not a directory, but a file resource.
|
|
321
|
+
* Opens one or more folders after prompting the `Open Folder` dialog. Resolves to `undefined`, if
|
|
322
|
+
* - the user's selection is empty or contains only files.
|
|
323
|
+
* - the new workspace is equal to the old workspace.
|
|
315
324
|
*
|
|
316
|
-
* Otherwise, resolves to the URI of the
|
|
325
|
+
* Otherwise, resolves to the URI of the new workspace:
|
|
326
|
+
* - a single folder if a single folder was selected.
|
|
327
|
+
* - a new, untitled workspace file if multiple folders were selected.
|
|
317
328
|
*/
|
|
318
329
|
protected async doOpenFolder(): Promise<URI | undefined> {
|
|
319
330
|
const props: OpenFileDialogProps = {
|
|
320
331
|
title: WorkspaceCommands.OPEN_FOLDER.dialogLabel,
|
|
321
332
|
canSelectFolders: true,
|
|
322
|
-
canSelectFiles: false
|
|
333
|
+
canSelectFiles: false,
|
|
334
|
+
canSelectMany: this.preferences['workspace.supportMultiRootWorkspace'],
|
|
323
335
|
};
|
|
324
336
|
const [rootStat] = await this.workspaceService.roots;
|
|
325
|
-
const
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
337
|
+
const targetFolders = await this.fileDialogService.showOpenDialog(props, rootStat);
|
|
338
|
+
if (targetFolders) {
|
|
339
|
+
const openableURI = await this.getOpenableWorkspaceUri(targetFolders);
|
|
340
|
+
if (openableURI) {
|
|
341
|
+
if (!this.workspaceService.workspace || !openableURI.isEqual(this.workspaceService.workspace.resource)) {
|
|
342
|
+
this.workspaceService.open(openableURI);
|
|
343
|
+
return openableURI;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
333
346
|
}
|
|
334
347
|
return undefined;
|
|
335
348
|
}
|
|
336
349
|
|
|
350
|
+
protected async getOpenableWorkspaceUri(uris: MaybeArray<URI>): Promise<URI | undefined> {
|
|
351
|
+
if (Array.isArray(uris)) {
|
|
352
|
+
if (uris.length < 2) {
|
|
353
|
+
return uris[0];
|
|
354
|
+
} else {
|
|
355
|
+
const foldersToOpen = (await Promise.all(uris.map(uri => this.fileService.resolve(uri))))
|
|
356
|
+
.filter(fileStat => !!fileStat?.isDirectory);
|
|
357
|
+
if (foldersToOpen.length === 1) {
|
|
358
|
+
return foldersToOpen[0].resource;
|
|
359
|
+
} else {
|
|
360
|
+
return this.createMultiRootWorkspace(foldersToOpen);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
return uris;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
protected async createMultiRootWorkspace(roots: FileStat[]): Promise<URI> {
|
|
369
|
+
const untitledWorkspace = await this.workspaceService.getUntitledWorkspace();
|
|
370
|
+
const folders = Array.from(new Set(roots.map(stat => stat.resource.path.toString())), path => ({ path }));
|
|
371
|
+
const workspaceStat = await this.fileService.createFile(
|
|
372
|
+
untitledWorkspace,
|
|
373
|
+
BinaryBuffer.fromString(JSON.stringify({ folders }, null, 4)), // eslint-disable-line no-null/no-null
|
|
374
|
+
{ overwrite: true }
|
|
375
|
+
);
|
|
376
|
+
return workspaceStat.resource;
|
|
377
|
+
}
|
|
378
|
+
|
|
337
379
|
/**
|
|
338
380
|
* Opens a workspace after raising the `Open Workspace` dialog. Resolves to the URI of the recently opened workspace,
|
|
339
381
|
* if it was successful. Otherwise, resolves to `undefined`.
|
|
@@ -396,7 +438,10 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
|
|
|
396
438
|
}
|
|
397
439
|
}
|
|
398
440
|
|
|
399
|
-
|
|
441
|
+
/**
|
|
442
|
+
* @returns whether the file was successfully saved.
|
|
443
|
+
*/
|
|
444
|
+
protected async saveWorkspaceAs(): Promise<boolean> {
|
|
400
445
|
let exist: boolean = false;
|
|
401
446
|
let overwrite: boolean = false;
|
|
402
447
|
let selected: URI | undefined;
|
|
@@ -418,8 +463,14 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
|
|
|
418
463
|
} while (selected && exist && !overwrite);
|
|
419
464
|
|
|
420
465
|
if (selected) {
|
|
421
|
-
|
|
466
|
+
try {
|
|
467
|
+
await this.workspaceService.save(selected);
|
|
468
|
+
return true;
|
|
469
|
+
} catch {
|
|
470
|
+
this.messageService.error(nls.localizeByDefault("Unable to save workspace '{0}'", selected.path.toString()));
|
|
471
|
+
}
|
|
422
472
|
}
|
|
473
|
+
return false;
|
|
423
474
|
}
|
|
424
475
|
|
|
425
476
|
/**
|
|
@@ -495,8 +546,16 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
|
|
|
495
546
|
}
|
|
496
547
|
|
|
497
548
|
protected updateWorkspaceStateKey(): WorkspaceState {
|
|
549
|
+
return this.doUpdateState();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
protected updateWorkbenchStateKey(): WorkbenchState {
|
|
553
|
+
return this.doUpdateState();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
protected doUpdateState(): WorkspaceState | WorkbenchState {
|
|
498
557
|
if (this.workspaceService.opened) {
|
|
499
|
-
return this.workspaceService.isMultiRootWorkspaceOpened ? '
|
|
558
|
+
return this.workspaceService.isMultiRootWorkspaceOpened ? 'workspace' : 'folder';
|
|
500
559
|
}
|
|
501
560
|
return 'empty';
|
|
502
561
|
}
|
|
@@ -527,6 +586,28 @@ export class WorkspaceFrontendContribution implements CommandContribution, Keybi
|
|
|
527
586
|
return this.workspaceService.workspace?.resource;
|
|
528
587
|
}
|
|
529
588
|
|
|
589
|
+
onWillStop(): OnWillStopAction | undefined {
|
|
590
|
+
const { workspace } = this.workspaceService;
|
|
591
|
+
if (workspace && this.workspaceService.isUntitledWorkspace(workspace.resource)) {
|
|
592
|
+
return {
|
|
593
|
+
action: async () => {
|
|
594
|
+
const shouldSaveFile = await new UntitledWorkspaceExitDialog({
|
|
595
|
+
title: nls.localizeByDefault('Do you want to save your workspace configuration as a file?')
|
|
596
|
+
}).open();
|
|
597
|
+
if (shouldSaveFile === "Don't Save") {
|
|
598
|
+
return true;
|
|
599
|
+
} else if (shouldSaveFile === 'Save') {
|
|
600
|
+
return this.saveWorkspaceAs();
|
|
601
|
+
}
|
|
602
|
+
return false; // If cancel, prevent exit.
|
|
603
|
+
|
|
604
|
+
},
|
|
605
|
+
reason: 'Untitled workspace.',
|
|
606
|
+
// Since deleting the workspace would hobble any future functionality, run this late.
|
|
607
|
+
priority: 100,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
}
|
|
530
611
|
}
|
|
531
612
|
|
|
532
613
|
export namespace WorkspaceFrontendContribution {
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
import { StorageService } from '@theia/core/lib/browser/storage-service';
|
|
31
31
|
import { LabelProviderContribution } from '@theia/core/lib/browser/label-provider';
|
|
32
32
|
import { VariableContribution } from '@theia/variable-resolver/lib/browser';
|
|
33
|
-
import { WorkspaceServer, workspacePath } from '../common';
|
|
33
|
+
import { WorkspaceServer, workspacePath, CommonWorkspaceUtils } from '../common';
|
|
34
34
|
import { WorkspaceFrontendContribution } from './workspace-frontend-contribution';
|
|
35
35
|
import { WorkspaceService } from './workspace-service';
|
|
36
36
|
import { WorkspaceCommandContribution, FileMenuContribution, EditMenuContribution } from './workspace-commands';
|
|
@@ -95,6 +95,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
|
|
|
95
95
|
bind(QuickOpenWorkspace).toSelf().inSingletonScope();
|
|
96
96
|
|
|
97
97
|
bind(WorkspaceUtils).toSelf().inSingletonScope();
|
|
98
|
+
bind(CommonWorkspaceUtils).toSelf().inSingletonScope();
|
|
98
99
|
|
|
99
100
|
bind(WorkspaceSchemaUpdater).toSelf().inSingletonScope();
|
|
100
101
|
bind(JsonSchemaContribution).toService(WorkspaceSchemaUpdater);
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
|
18
18
|
import URI from '@theia/core/lib/common/uri';
|
|
19
|
-
import { WorkspaceServer, THEIA_EXT,
|
|
19
|
+
import { WorkspaceServer, THEIA_EXT, CommonWorkspaceUtils } from '../common';
|
|
20
20
|
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
|
21
21
|
import { DEFAULT_WINDOW_HASH } from '@theia/core/lib/common/window';
|
|
22
22
|
import {
|
|
@@ -24,10 +24,10 @@ import {
|
|
|
24
24
|
} from '@theia/core/lib/browser';
|
|
25
25
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
26
26
|
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
|
27
|
-
import { ILogger, Disposable, DisposableCollection, Emitter, Event, MaybePromise, MessageService } from '@theia/core';
|
|
27
|
+
import { ILogger, Disposable, DisposableCollection, Emitter, Event, MaybePromise, MessageService, nls } from '@theia/core';
|
|
28
28
|
import { WorkspacePreferences } from './workspace-preferences';
|
|
29
29
|
import * as jsoncparser from 'jsonc-parser';
|
|
30
|
-
import * as Ajv from 'ajv';
|
|
30
|
+
import * as Ajv from '@theia/core/shared/ajv';
|
|
31
31
|
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
32
32
|
import { FileStat, BaseStat } from '@theia/filesystem/lib/common/files';
|
|
33
33
|
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
@@ -82,6 +82,9 @@ export class WorkspaceService implements FrontendApplicationContribution {
|
|
|
82
82
|
@inject(WorkspaceSchemaUpdater)
|
|
83
83
|
protected readonly schemaUpdater: WorkspaceSchemaUpdater;
|
|
84
84
|
|
|
85
|
+
@inject(CommonWorkspaceUtils)
|
|
86
|
+
protected readonly utils: CommonWorkspaceUtils;
|
|
87
|
+
|
|
85
88
|
protected applicationName: string;
|
|
86
89
|
|
|
87
90
|
protected _ready = new Deferred<void>();
|
|
@@ -297,9 +300,8 @@ export class WorkspaceService implements FrontendApplicationContribution {
|
|
|
297
300
|
let title: string | undefined;
|
|
298
301
|
if (this._workspace) {
|
|
299
302
|
const displayName = this._workspace.name;
|
|
300
|
-
if (
|
|
301
|
-
(
|
|
302
|
-
title = displayName.slice(0, displayName.lastIndexOf('.'));
|
|
303
|
+
if (this.isWorkspaceFile(this._workspace)) {
|
|
304
|
+
title = this.isUntitledWorkspace(this._workspace.resource) ? nls.localizeByDefault('Untitled (Workspace)') : displayName.slice(0, displayName.lastIndexOf('.'));
|
|
303
305
|
} else {
|
|
304
306
|
title = displayName;
|
|
305
307
|
}
|
|
@@ -353,12 +355,11 @@ export class WorkspaceService implements FrontendApplicationContribution {
|
|
|
353
355
|
this.doOpen(uri, options);
|
|
354
356
|
}
|
|
355
357
|
|
|
356
|
-
protected async doOpen(uri: URI, options?: WorkspaceInput): Promise<
|
|
357
|
-
const
|
|
358
|
-
const stat = await this.toFileStat(rootUri);
|
|
358
|
+
protected async doOpen(uri: URI, options?: WorkspaceInput): Promise<URI | undefined> {
|
|
359
|
+
const stat = await this.toFileStat(uri);
|
|
359
360
|
if (stat) {
|
|
360
361
|
if (!stat.isDirectory && !this.isWorkspaceFile(stat)) {
|
|
361
|
-
const message = `Not a valid workspace
|
|
362
|
+
const message = `Not a valid workspace: ${uri.path.toString()}`;
|
|
362
363
|
this.messageService.error(message);
|
|
363
364
|
throw new Error(message);
|
|
364
365
|
}
|
|
@@ -369,14 +370,14 @@ export class WorkspaceService implements FrontendApplicationContribution {
|
|
|
369
370
|
preserveWindow: this.preferences['workspace.preserveWindow'] || !this.opened,
|
|
370
371
|
...options
|
|
371
372
|
};
|
|
372
|
-
await this.server.setMostRecentlyUsedWorkspace(
|
|
373
|
+
await this.server.setMostRecentlyUsedWorkspace(uri.toString());
|
|
373
374
|
if (preserveWindow) {
|
|
374
375
|
this._workspace = stat;
|
|
375
376
|
}
|
|
376
377
|
this.openWindow(stat, { preserveWindow });
|
|
377
378
|
return;
|
|
378
379
|
}
|
|
379
|
-
throw new Error('Invalid workspace root URI. Expected an existing directory
|
|
380
|
+
throw new Error('Invalid workspace root URI. Expected an existing directory or workspace file.');
|
|
380
381
|
}
|
|
381
382
|
|
|
382
383
|
/**
|
|
@@ -409,7 +410,7 @@ export class WorkspaceService implements FrontendApplicationContribution {
|
|
|
409
410
|
|
|
410
411
|
async spliceRoots(start: number, deleteCount?: number, ...rootsToAdd: URI[]): Promise<URI[]> {
|
|
411
412
|
if (!this._workspace) {
|
|
412
|
-
throw new Error('There is
|
|
413
|
+
throw new Error('There is no active workspace');
|
|
413
414
|
}
|
|
414
415
|
const dedup = new Set<string>();
|
|
415
416
|
const roots = this._roots.map(root => (dedup.add(root.resource.toString()), root.resource.toString()));
|
|
@@ -437,13 +438,30 @@ export class WorkspaceService implements FrontendApplicationContribution {
|
|
|
437
438
|
}
|
|
438
439
|
|
|
439
440
|
async getUntitledWorkspace(): Promise<URI> {
|
|
440
|
-
|
|
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-service/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
459
|
}
|
|
442
460
|
|
|
443
461
|
protected async writeWorkspaceFile(workspaceFile: FileStat | undefined, workspaceData: WorkspaceData): Promise<FileStat | undefined> {
|
|
444
462
|
if (workspaceFile) {
|
|
445
463
|
const data = JSON.stringify(WorkspaceData.transformToRelative(workspaceData, workspaceFile));
|
|
446
|
-
const edits = jsoncparser.format(data, undefined, { tabSize:
|
|
464
|
+
const edits = jsoncparser.format(data, undefined, { tabSize: 2, insertSpaces: true, eol: '' });
|
|
447
465
|
const result = jsoncparser.applyEdits(data, edits);
|
|
448
466
|
await this.fileService.write(workspaceFile.resource, result);
|
|
449
467
|
return this.fileService.resolve(workspaceFile.resource);
|
|
@@ -515,7 +533,7 @@ export class WorkspaceService implements FrontendApplicationContribution {
|
|
|
515
533
|
this.setURLFragment('');
|
|
516
534
|
}
|
|
517
535
|
|
|
518
|
-
|
|
536
|
+
this.windowService.reload();
|
|
519
537
|
}
|
|
520
538
|
|
|
521
539
|
protected openNewWindow(workspacePath: string): void {
|
|
@@ -549,6 +567,11 @@ export class WorkspaceService implements FrontendApplicationContribution {
|
|
|
549
567
|
return false;
|
|
550
568
|
}
|
|
551
569
|
|
|
570
|
+
/**
|
|
571
|
+
* `true` if the current workspace is configured using a configuration file.
|
|
572
|
+
*
|
|
573
|
+
* `false` if there is no workspace or the workspace is simply a folder.
|
|
574
|
+
*/
|
|
552
575
|
get saved(): boolean {
|
|
553
576
|
return !!this._workspace && !this._workspace.isDirectory;
|
|
554
577
|
}
|
|
@@ -578,7 +601,12 @@ export class WorkspaceService implements FrontendApplicationContribution {
|
|
|
578
601
|
Object.assign(workspaceData, await this.getWorkspaceDataFromFile());
|
|
579
602
|
stat = await this.writeWorkspaceFile(stat, WorkspaceData.buildWorkspaceData(this._roots, workspaceData));
|
|
580
603
|
await this.server.setMostRecentlyUsedWorkspace(resource.toString());
|
|
604
|
+
// If saving a workspace based on an untitled workspace, delete the old file.
|
|
605
|
+
const toDelete = this.isUntitledWorkspace(this.workspace?.resource) && this.workspace!.resource;
|
|
581
606
|
await this.setWorkspace(stat);
|
|
607
|
+
if (toDelete && stat && !toDelete.isEqual(stat.resource)) {
|
|
608
|
+
await this.fileService.delete(toDelete).catch(() => { });
|
|
609
|
+
}
|
|
582
610
|
this.onWorkspaceLocationChangedEmitter.fire(stat);
|
|
583
611
|
}
|
|
584
612
|
|
|
@@ -661,8 +689,12 @@ export class WorkspaceService implements FrontendApplicationContribution {
|
|
|
661
689
|
*
|
|
662
690
|
* Example: We should not try to read the contents of an .exe file.
|
|
663
691
|
*/
|
|
664
|
-
protected isWorkspaceFile(
|
|
665
|
-
return
|
|
692
|
+
protected isWorkspaceFile(candidate: FileStat | URI): boolean {
|
|
693
|
+
return this.utils.isWorkspaceFile(candidate);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
isUntitledWorkspace(candidate?: URI): boolean {
|
|
697
|
+
return this.utils.isUntitledWorkspace(candidate);
|
|
666
698
|
}
|
|
667
699
|
|
|
668
700
|
/**
|
|
@@ -741,7 +773,10 @@ export namespace WorkspaceData {
|
|
|
741
773
|
if (path.startsWith('file:///')) {
|
|
742
774
|
folders.push(path);
|
|
743
775
|
} else {
|
|
744
|
-
|
|
776
|
+
const absolutePath = workspaceFile.resource.withScheme('file').parent.resolveToAbsolute(path)?.toString();
|
|
777
|
+
if (absolutePath) {
|
|
778
|
+
folders.push(absolutePath.toString());
|
|
779
|
+
}
|
|
745
780
|
}
|
|
746
781
|
|
|
747
782
|
}
|
package/src/common/utils.ts
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
import URI from '@theia/core/lib/common/uri';
|
|
20
20
|
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
|
21
|
+
import { injectable } from '@theia/core/shared/inversify';
|
|
22
|
+
import { FileStat } from '@theia/filesystem/lib/common/files';
|
|
21
23
|
|
|
22
24
|
export const THEIA_EXT = 'theia-workspace';
|
|
23
25
|
export const VSCODE_EXT = 'code-workspace';
|
|
@@ -29,3 +31,20 @@ export async function getTemporaryWorkspaceFileUri(envVariableServer: EnvVariabl
|
|
|
29
31
|
const configDirUri = await envVariableServer.getConfigDirUri();
|
|
30
32
|
return new URI(configDirUri).resolve(`Untitled.${THEIA_EXT}`);
|
|
31
33
|
}
|
|
34
|
+
|
|
35
|
+
@injectable()
|
|
36
|
+
export class CommonWorkspaceUtils {
|
|
37
|
+
/**
|
|
38
|
+
* Check if the file should be considered as a workspace file.
|
|
39
|
+
*
|
|
40
|
+
* Example: We should not try to read the contents of an .exe file.
|
|
41
|
+
*/
|
|
42
|
+
isWorkspaceFile(candidate: FileStat | URI): boolean {
|
|
43
|
+
const uri = FileStat.is(candidate) ? candidate.resource : candidate;
|
|
44
|
+
return uri.path.ext === `.${THEIA_EXT}` || uri.path.ext === `.${VSCODE_EXT}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
isUntitledWorkspace(candidate?: URI): boolean {
|
|
48
|
+
return !!candidate && this.isWorkspaceFile(candidate) && candidate.path.base.startsWith('Untitled');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -18,12 +18,11 @@ import * as path from 'path';
|
|
|
18
18
|
import * as yargs from '@theia/core/shared/yargs';
|
|
19
19
|
import * as fs from '@theia/core/shared/fs-extra';
|
|
20
20
|
import * as jsoncparser from 'jsonc-parser';
|
|
21
|
-
|
|
22
21
|
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
|
23
|
-
import { FileUri } from '@theia/core/lib/node';
|
|
22
|
+
import { FileUri, BackendApplicationContribution } from '@theia/core/lib/node';
|
|
24
23
|
import { CliContribution } from '@theia/core/lib/node/cli';
|
|
25
24
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
26
|
-
import { WorkspaceServer } from '../common';
|
|
25
|
+
import { WorkspaceServer, CommonWorkspaceUtils } from '../common';
|
|
27
26
|
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
|
28
27
|
|
|
29
28
|
@injectable()
|
|
@@ -59,9 +58,14 @@ export class WorkspaceCliContribution implements CliContribution {
|
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
@injectable()
|
|
62
|
-
export class DefaultWorkspaceServer implements WorkspaceServer {
|
|
61
|
+
export class DefaultWorkspaceServer implements WorkspaceServer, BackendApplicationContribution {
|
|
63
62
|
|
|
64
63
|
protected root: Deferred<string | undefined> = new Deferred();
|
|
64
|
+
/**
|
|
65
|
+
* Untitled workspaces that are not among the most recent N workspaces will be deleted on start. Increase this number to keep older files,
|
|
66
|
+
* lower it to delete stale untitled workspaces more aggressively.
|
|
67
|
+
*/
|
|
68
|
+
protected untitledWorkspaceStaleThreshhold = 10;
|
|
65
69
|
|
|
66
70
|
@inject(WorkspaceCliContribution)
|
|
67
71
|
protected readonly cliParams: WorkspaceCliContribution;
|
|
@@ -69,12 +73,19 @@ export class DefaultWorkspaceServer implements WorkspaceServer {
|
|
|
69
73
|
@inject(EnvVariablesServer)
|
|
70
74
|
protected readonly envServer: EnvVariablesServer;
|
|
71
75
|
|
|
76
|
+
@inject(CommonWorkspaceUtils)
|
|
77
|
+
protected readonly utils: CommonWorkspaceUtils;
|
|
78
|
+
|
|
72
79
|
@postConstruct()
|
|
73
80
|
protected async init(): Promise<void> {
|
|
74
81
|
const root = await this.getRoot();
|
|
75
82
|
this.root.resolve(root);
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
async onStart(): Promise<void> {
|
|
86
|
+
await this.removeOldUntitledWorkspaces();
|
|
87
|
+
}
|
|
88
|
+
|
|
78
89
|
protected async getRoot(): Promise<string | undefined> {
|
|
79
90
|
let root = await this.getWorkspaceURIFromCli();
|
|
80
91
|
if (!root) {
|
|
@@ -115,7 +126,7 @@ export class DefaultWorkspaceServer implements WorkspaceServer {
|
|
|
115
126
|
data.recentRoots.forEach(element => {
|
|
116
127
|
if (element.length > 0) {
|
|
117
128
|
if (this.workspaceStillExist(element)) {
|
|
118
|
-
listUri.push(element);
|
|
129
|
+
listUri.push(FileUri.fsPath(element));
|
|
119
130
|
}
|
|
120
131
|
}
|
|
121
132
|
});
|
|
@@ -169,6 +180,19 @@ export class DefaultWorkspaceServer implements WorkspaceServer {
|
|
|
169
180
|
const configDirUri = await this.envServer.getConfigDirUri();
|
|
170
181
|
return path.resolve(FileUri.fsPath(configDirUri), 'recentworkspace.json');
|
|
171
182
|
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Removes untitled workspaces that are not among the most recently used workspaces.
|
|
186
|
+
* Use the `untitledWorkspaceStaleThreshhold` to configure when to delete workspaces.
|
|
187
|
+
*/
|
|
188
|
+
protected async removeOldUntitledWorkspaces(): Promise<void> {
|
|
189
|
+
const recents = (await this.getRecentWorkspaces()).map(FileUri.fsPath);
|
|
190
|
+
const olderUntitledWorkspaces = recents.slice(this.untitledWorkspaceStaleThreshhold).filter(workspace => this.utils.isUntitledWorkspace(FileUri.create(workspace)));
|
|
191
|
+
await Promise.all(olderUntitledWorkspaces.map(workspace => fs.promises.unlink(FileUri.fsPath(workspace)).catch(() => { })));
|
|
192
|
+
if (olderUntitledWorkspaces.length > 0) {
|
|
193
|
+
await this.writeToUserHome({ recentRoots: await this.getRecentWorkspaces() });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
172
196
|
}
|
|
173
197
|
|
|
174
198
|
interface RecentWorkspacePathsData {
|
|
@@ -16,15 +16,18 @@
|
|
|
16
16
|
|
|
17
17
|
import { ContainerModule } from '@theia/core/shared/inversify';
|
|
18
18
|
import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core/lib/common';
|
|
19
|
-
import { WorkspaceServer, workspacePath } from '../common';
|
|
19
|
+
import { WorkspaceServer, workspacePath, CommonWorkspaceUtils } from '../common';
|
|
20
20
|
import { DefaultWorkspaceServer, WorkspaceCliContribution } from './default-workspace-server';
|
|
21
21
|
import { CliContribution } from '@theia/core/lib/node/cli';
|
|
22
|
+
import { BackendApplicationContribution } from '@theia/core/lib/node';
|
|
22
23
|
|
|
23
24
|
export default new ContainerModule(bind => {
|
|
24
25
|
bind(WorkspaceCliContribution).toSelf().inSingletonScope();
|
|
25
26
|
bind(CliContribution).toService(WorkspaceCliContribution);
|
|
26
27
|
bind(DefaultWorkspaceServer).toSelf().inSingletonScope();
|
|
27
28
|
bind(WorkspaceServer).toService(DefaultWorkspaceServer);
|
|
29
|
+
bind(BackendApplicationContribution).toService(WorkspaceServer);
|
|
30
|
+
bind(CommonWorkspaceUtils).toSelf().inSingletonScope();
|
|
28
31
|
|
|
29
32
|
bind(ConnectionHandler).toDynamicValue(ctx =>
|
|
30
33
|
new JsonRpcConnectionHandler(workspacePath, () =>
|