@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.
Files changed (48) hide show
  1. package/lib/browser/untitled-workspace-exit-dialog.d.ts +36 -0
  2. package/lib/browser/untitled-workspace-exit-dialog.d.ts.map +1 -0
  3. package/lib/browser/untitled-workspace-exit-dialog.js +76 -0
  4. package/lib/browser/untitled-workspace-exit-dialog.js.map +1 -0
  5. package/lib/browser/workspace-commands.d.ts +1 -0
  6. package/lib/browser/workspace-commands.d.ts.map +1 -1
  7. package/lib/browser/workspace-commands.js +15 -9
  8. package/lib/browser/workspace-commands.js.map +1 -1
  9. package/lib/browser/workspace-delete-handler.d.ts +9 -0
  10. package/lib/browser/workspace-delete-handler.d.ts.map +1 -1
  11. package/lib/browser/workspace-delete-handler.js +34 -1
  12. package/lib/browser/workspace-delete-handler.js.map +1 -1
  13. package/lib/browser/workspace-frontend-contribution.d.ts +21 -8
  14. package/lib/browser/workspace-frontend-contribution.d.ts.map +1 -1
  15. package/lib/browser/workspace-frontend-contribution.js +95 -17
  16. package/lib/browser/workspace-frontend-contribution.js.map +1 -1
  17. package/lib/browser/workspace-frontend-module.d.ts.map +1 -1
  18. package/lib/browser/workspace-frontend-module.js +1 -0
  19. package/lib/browser/workspace-frontend-module.js.map +1 -1
  20. package/lib/browser/workspace-schema-updater.d.ts.map +1 -1
  21. package/lib/browser/workspace-schema-updater.js +2 -0
  22. package/lib/browser/workspace-schema-updater.js.map +1 -1
  23. package/lib/browser/workspace-service.d.ts +10 -3
  24. package/lib/browser/workspace-service.d.ts.map +1 -1
  25. package/lib/browser/workspace-service.js +49 -16
  26. package/lib/browser/workspace-service.js.map +1 -1
  27. package/lib/common/utils.d.ts +10 -0
  28. package/lib/common/utils.d.ts.map +1 -1
  29. package/lib/common/utils.js +27 -1
  30. package/lib/common/utils.js.map +1 -1
  31. package/lib/node/default-workspace-server.d.ts +15 -2
  32. package/lib/node/default-workspace-server.d.ts.map +1 -1
  33. package/lib/node/default-workspace-server.js +26 -1
  34. package/lib/node/default-workspace-server.js.map +1 -1
  35. package/lib/node/workspace-backend-module.d.ts.map +1 -1
  36. package/lib/node/workspace-backend-module.js +3 -0
  37. package/lib/node/workspace-backend-module.js.map +1 -1
  38. package/package.json +6 -6
  39. package/src/browser/untitled-workspace-exit-dialog.ts +70 -0
  40. package/src/browser/workspace-commands.ts +16 -10
  41. package/src/browser/workspace-delete-handler.ts +42 -1
  42. package/src/browser/workspace-frontend-contribution.ts +101 -20
  43. package/src/browser/workspace-frontend-module.ts +2 -1
  44. package/src/browser/workspace-schema-updater.ts +2 -0
  45. package/src/browser/workspace-service.ts +54 -19
  46. package/src/common/utils.ts +19 -0
  47. package/src/node/default-workspace-server.ts +29 -5
  48. 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 a folder after prompting the `Open Folder` dialog. Resolves to `undefined`, if
312
- * - the workspace root is not set,
313
- * - the folder to open does not exist, or
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 folder.
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 destinationFolderUri = await this.fileDialogService.showOpenDialog(props, rootStat);
326
- if (destinationFolderUri &&
327
- this.getCurrentWorkspaceUri()?.toString() !== destinationFolderUri.toString()) {
328
- const destinationFolder = await this.fileService.resolve(destinationFolderUri);
329
- if (destinationFolder.isDirectory) {
330
- this.workspaceService.open(destinationFolderUri);
331
- return destinationFolderUri;
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
- protected async saveWorkspaceAs(): Promise<void> {
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
- this.workspaceService.save(selected);
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 ? 'folder' : 'workspace';
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);
@@ -145,4 +145,6 @@ export const workspaceSchema: IJSONSchema = {
145
145
  }
146
146
  }
147
147
  },
148
+ allowComments: true,
149
+ allowTrailingCommas: true,
148
150
  };
@@ -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, VSCODE_EXT, getTemporaryWorkspaceFileUri } from '../common';
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 (!this._workspace.isDirectory &&
301
- (displayName.endsWith(`.${THEIA_EXT}`) || displayName.endsWith(`.${VSCODE_EXT}`))) {
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<void> {
357
- const rootUri = uri.toString();
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 file: ${uri}`;
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(rootUri);
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 location.');
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 not active workspace');
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
- return getTemporaryWorkspaceFileUri(this.envVariableServer);
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: 3, insertSpaces: true, eol: '' });
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
- window.location.reload(true);
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(fileStat: FileStat): boolean {
665
- return fileStat.resource.path.ext === `.${THEIA_EXT}` || fileStat.resource.path.ext === `.${VSCODE_EXT}`;
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
- folders.push(workspaceFile.resource.withScheme('file').parent.resolve(path).toString());
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
  }
@@ -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, () =>