Skip to content

Commit f7dde7f

Browse files
author
Benjamin Pasero
authored
Indicate dirty workspaces in recently opened (microsoft#93723)
* do not restore folders/workspaces with backups * add method to getDirtyWorkspaces * 💄 getRecentlyOpened * identify dirty workspaces * show a dialog * change to one method * fix actions * 💄
1 parent 95343b6 commit f7dde7f

13 files changed

Lines changed: 276 additions & 79 deletions

File tree

src/vs/platform/backup/electron-main/backup.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
7-
import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
7+
import { IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
88
import { URI } from 'vs/base/common/uri';
99
import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
1010

@@ -15,6 +15,12 @@ export interface IWorkspaceBackupInfo {
1515
remoteAuthority?: string;
1616
}
1717

18+
export function isWorkspaceBackupInfo(obj: unknown): obj is IWorkspaceBackupInfo {
19+
const candidate = obj as IWorkspaceBackupInfo;
20+
21+
return candidate && isWorkspaceIdentifier(candidate.workspace);
22+
}
23+
1824
export interface IBackupMainService {
1925
_serviceBrand: undefined;
2026

@@ -31,4 +37,12 @@ export interface IBackupMainService {
3137
unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void;
3238
unregisterFolderBackupSync(folderUri: URI): void;
3339
unregisterEmptyWindowBackupSync(backupFolder: string): void;
40+
41+
/**
42+
* All folders or workspaces that are known to have
43+
* backups stored. This call is long running because
44+
* it checks for each backup location if any backups
45+
* are stored.
46+
*/
47+
getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>>;
3448
}

src/vs/platform/backup/electron-main/backupMainService.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as path from 'vs/base/common/path';
99
import * as platform from 'vs/base/common/platform';
1010
import { writeFileSync, writeFile, readFile, readdir, exists, rimraf, rename, RimRafMode } from 'vs/base/node/pfs';
1111
import * as arrays from 'vs/base/common/arrays';
12-
import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
12+
import { IBackupMainService, IWorkspaceBackupInfo, isWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup';
1313
import { IBackupWorkspacesFormat, IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup';
1414
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
1515
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -255,7 +255,7 @@ export class BackupMainService implements IBackupMainService {
255255
seenIds.add(workspace.id);
256256

257257
const backupPath = this.getBackupPath(workspace.id);
258-
const hasBackups = await this.hasBackups(backupPath);
258+
const hasBackups = await this.doHasBackups(backupPath);
259259

260260
// If the workspace has no backups, ignore it
261261
if (hasBackups) {
@@ -287,7 +287,7 @@ export class BackupMainService implements IBackupMainService {
287287
seenIds.add(key);
288288

289289
const backupPath = this.getBackupPath(this.getFolderHash(folderURI));
290-
const hasBackups = await this.hasBackups(backupPath);
290+
const hasBackups = await this.doHasBackups(backupPath);
291291

292292
// If the folder has no backups, ignore it
293293
if (hasBackups) {
@@ -325,7 +325,7 @@ export class BackupMainService implements IBackupMainService {
325325
seenIds.add(backupFolder);
326326

327327
const backupPath = this.getBackupPath(backupFolder);
328-
if (await this.hasBackups(backupPath)) {
328+
if (await this.doHasBackups(backupPath)) {
329329
result.push(backupInfo);
330330
} else {
331331
await this.deleteStaleBackup(backupPath);
@@ -388,7 +388,48 @@ export class BackupMainService implements IBackupMainService {
388388
return true;
389389
}
390390

391-
private async hasBackups(backupPath: string): Promise<boolean> {
391+
async getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>> {
392+
const dirtyWorkspaces: Array<IWorkspaceIdentifier | URI> = [];
393+
394+
// Workspaces with backups
395+
for (const workspace of this.rootWorkspaces) {
396+
if ((await this.hasBackups(workspace))) {
397+
dirtyWorkspaces.push(workspace.workspace);
398+
}
399+
}
400+
401+
// Folders with backups
402+
for (const folder of this.folderWorkspaces) {
403+
if ((await this.hasBackups(folder))) {
404+
dirtyWorkspaces.push(folder);
405+
}
406+
}
407+
408+
return dirtyWorkspaces;
409+
}
410+
411+
private hasBackups(backupLocation: IWorkspaceBackupInfo | IEmptyWindowBackupInfo | URI): Promise<boolean> {
412+
let backupPath: string;
413+
414+
// Folder
415+
if (URI.isUri(backupLocation)) {
416+
backupPath = this.getBackupPath(this.getFolderHash(backupLocation));
417+
}
418+
419+
// Workspace
420+
else if (isWorkspaceBackupInfo(backupLocation)) {
421+
backupPath = this.getBackupPath(backupLocation.workspace.id);
422+
}
423+
424+
// Empty
425+
else {
426+
backupPath = backupLocation.backupFolder;
427+
}
428+
429+
return this.doHasBackups(backupPath);
430+
}
431+
432+
private async doHasBackups(backupPath: string): Promise<boolean> {
392433
try {
393434
const backupSchemas = await readdir(backupPath);
394435

src/vs/platform/backup/test/electron-main/backupMainService.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
2222
import { createHash } from 'crypto';
2323
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
2424
import { Schemas } from 'vs/base/common/network';
25+
import { isEqual } from 'vs/base/common/resources';
2526

2627
suite('BackupMainService', () => {
2728

@@ -731,4 +732,45 @@ suite('BackupMainService', () => {
731732
}
732733
});
733734
});
735+
736+
suite('getDirtyWorkspaces', () => {
737+
test('should report if a workspace or folder has backups', async () => {
738+
const folderBackupPath = service.registerFolderBackupSync(fooFile);
739+
740+
const backupWorkspaceInfo = toWorkspaceBackupInfo(fooFile.fsPath);
741+
const workspaceBackupPath = service.registerWorkspaceBackupSync(backupWorkspaceInfo);
742+
743+
assert.equal(((await service.getDirtyWorkspaces()).length), 0);
744+
745+
try {
746+
await pfs.mkdirp(path.join(folderBackupPath, Schemas.file));
747+
await pfs.mkdirp(path.join(workspaceBackupPath, Schemas.untitled));
748+
} catch (error) {
749+
// ignore - folder might exist already
750+
}
751+
752+
assert.equal(((await service.getDirtyWorkspaces()).length), 0);
753+
754+
fs.writeFileSync(path.join(folderBackupPath, Schemas.file, '594a4a9d82a277a899d4713a5b08f504'), '');
755+
fs.writeFileSync(path.join(workspaceBackupPath, Schemas.untitled, '594a4a9d82a277a899d4713a5b08f504'), '');
756+
757+
const dirtyWorkspaces = await service.getDirtyWorkspaces();
758+
assert.equal(dirtyWorkspaces.length, 2);
759+
760+
let found = 0;
761+
for (const dirtyWorkpspace of dirtyWorkspaces) {
762+
if (URI.isUri(dirtyWorkpspace)) {
763+
if (isEqual(fooFile, dirtyWorkpspace)) {
764+
found++;
765+
}
766+
} else {
767+
if (isEqual(backupWorkspaceInfo.workspace.configPath, dirtyWorkpspace.configPath)) {
768+
found++;
769+
}
770+
}
771+
}
772+
773+
assert.equal(found, 2);
774+
});
775+
});
734776
});

src/vs/platform/windows/electron-main/windowsMainService.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,14 +452,16 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
452452
}
453453

454454
//
455-
// These are windows to restore because of hot-exit or from previous session that would otherwise be lost (only performed once on startup!)
455+
// These are windows to restore because of hot-exit or from previous session (only performed once on startup!)
456456
//
457457
let workspacesToRestore: IWorkspacePathToOpen[] = [];
458458
if (openConfig.initialStartup && !openConfig.cli.extensionDevelopmentPath && !openConfig.cli['disable-restore-windows']) {
459-
// collect from workspaces with hot-exit backups and from previous window session
459+
460+
// Untitled workspaces are always restored
460461
workspacesToRestore = this.workspacesMainService.getUntitledWorkspacesSync();
461462
workspacesToOpen.push(...workspacesToRestore);
462463

464+
// Empty windows with backups are always restored
463465
emptyToRestore.push(...this.backupMainService.getEmptyWindowBackupPaths());
464466
} else {
465467
emptyToRestore.length = 0;

src/vs/platform/workspaces/common/workspaces.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { toSlashes } from 'vs/base/common/extpath';
1818
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
1919
import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts';
2020
import { ILogService } from 'vs/platform/log/common/log';
21-
import { Event as CommonEvent } from 'vs/base/common/event';
21+
import { Event } from 'vs/base/common/event';
2222
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
2323

2424
export const WORKSPACE_EXTENSION = 'code-workspace';
@@ -31,18 +31,21 @@ export interface IWorkspacesService {
3131

3232
_serviceBrand: undefined;
3333

34-
// Management
34+
// Workspaces Management
3535
enterWorkspace(path: URI): Promise<IEnterWorkspaceResult | null>;
3636
createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise<IWorkspaceIdentifier>;
3737
deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void>;
3838
getWorkspaceIdentifier(workspacePath: URI): Promise<IWorkspaceIdentifier>;
3939

40-
// History
41-
readonly onRecentlyOpenedChange: CommonEvent<void>;
40+
// Workspaces History
41+
readonly onRecentlyOpenedChange: Event<void>;
4242
addRecentlyOpened(recents: IRecent[]): Promise<void>;
4343
removeRecentlyOpened(workspaces: URI[]): Promise<void>;
4444
clearRecentlyOpened(): Promise<void>;
4545
getRecentlyOpened(): Promise<IRecentlyOpened>;
46+
47+
// Dirty Workspaces
48+
getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>>;
4649
}
4750

4851
export interface IRecentlyOpened {

src/vs/platform/workspaces/electron-main/workspacesHistoryMainService.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { IStateService } from 'vs/platform/state/node/state';
99
import { app, JumpListCategory } from 'electron';
1010
import { ILogService } from 'vs/platform/log/common/log';
1111
import { getBaseLabel, getPathLabel, splitName } from 'vs/base/common/labels';
12-
import { IPath } from 'vs/platform/windows/common/windows';
1312
import { Event as CommonEvent, Emitter } from 'vs/base/common/event';
1413
import { isWindows, isMacintosh } from 'vs/base/common/platform';
1514
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IRecentlyOpened, isRecentWorkspace, isRecentFolder, IRecent, isRecentFile, IRecentFolder, IRecentWorkspace, IRecentFile, toStoreData, restoreRecentlyOpened, RecentlyOpenedStorageData } from 'vs/platform/workspaces/common/workspaces';
@@ -24,6 +23,7 @@ import { exists } from 'vs/base/node/pfs';
2423
import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
2524
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
2625
import { Disposable } from 'vs/base/common/lifecycle';
26+
import { ICodeWindow } from 'vs/platform/windows/electron-main/windows';
2727

2828
export const IWorkspacesHistoryMainService = createDecorator<IWorkspacesHistoryMainService>('workspacesHistoryMainService');
2929

@@ -34,7 +34,7 @@ export interface IWorkspacesHistoryMainService {
3434
readonly onRecentlyOpenedChange: CommonEvent<void>;
3535

3636
addRecentlyOpened(recents: IRecent[]): void;
37-
getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened;
37+
getRecentlyOpened(include?: ICodeWindow): IRecentlyOpened;
3838
removeRecentlyOpened(paths: URI[]): void;
3939
clearRecentlyOpened(): void;
4040

@@ -241,20 +241,23 @@ export class WorkspacesHistoryMainService extends Disposable implements IWorkspa
241241
this._onRecentlyOpenedChange.fire();
242242
}
243243

244-
getRecentlyOpened(currentWorkspace?: IWorkspaceIdentifier, currentFolder?: ISingleFolderWorkspaceIdentifier, currentFiles?: IPath[]): IRecentlyOpened {
244+
getRecentlyOpened(include?: ICodeWindow): IRecentlyOpened {
245245
const workspaces: Array<IRecentFolder | IRecentWorkspace> = [];
246246
const files: IRecentFile[] = [];
247247

248248
// Add current workspace to beginning if set
249+
const currentWorkspace = include?.config?.workspace;
249250
if (currentWorkspace && !this.workspacesMainService.isUntitledWorkspace(currentWorkspace)) {
250251
workspaces.push({ workspace: currentWorkspace });
251252
}
252253

254+
const currentFolder = include?.config?.folderUri;
253255
if (currentFolder) {
254256
workspaces.push({ folderUri: currentFolder });
255257
}
256258

257259
// Add currently files to open to the beginning if any
260+
const currentFiles = include?.config?.filesToOpenOrCreate;
258261
if (currentFiles) {
259262
for (let currentFile of currentFiles) {
260263
const fileUri = currentFile.fileUri;

src/vs/platform/workspaces/electron-main/workspacesService.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { URI } from 'vs/base/common/uri';
99
import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService';
1010
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
1111
import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService';
12+
import { IBackupMainService } from 'vs/platform/backup/electron-main/backup';
1213

1314
export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspacesService, Promise<unknown> /* only methods, not events */, number /* window ID */> {
1415

@@ -17,7 +18,8 @@ export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspac
1718
constructor(
1819
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
1920
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
20-
@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService
21+
@IWorkspacesHistoryMainService private readonly workspacesHistoryMainService: IWorkspacesHistoryMainService,
22+
@IBackupMainService private readonly backupMainService: IBackupMainService
2123
) {
2224
}
2325

@@ -51,12 +53,7 @@ export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspac
5153
readonly onRecentlyOpenedChange = this.workspacesHistoryMainService.onRecentlyOpenedChange;
5254

5355
async getRecentlyOpened(windowId: number): Promise<IRecentlyOpened> {
54-
const window = this.windowsMainService.getWindowById(windowId);
55-
if (window?.config) {
56-
return this.workspacesHistoryMainService.getRecentlyOpened(window.config.workspace, window.config.folderUri, window.config.filesToOpenOrCreate);
57-
}
58-
59-
return this.workspacesHistoryMainService.getRecentlyOpened();
56+
return this.workspacesHistoryMainService.getRecentlyOpened(this.windowsMainService.getWindowById(windowId));
6057
}
6158

6259
async addRecentlyOpened(windowId: number, recents: IRecent[]): Promise<void> {
@@ -72,4 +69,13 @@ export class WorkspacesService implements AddFirstParameterToFunctions<IWorkspac
7269
}
7370

7471
//#endregion
72+
73+
74+
//#region Dirty Workspaces
75+
76+
async getDirtyWorkspaces(): Promise<Array<IWorkspaceIdentifier | URI>> {
77+
return this.backupMainService.getDirtyWorkspaces();
78+
}
79+
80+
//#endregion
7581
}

src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export class TestDialogMainService implements IDialogMainService {
5656
}
5757

5858
export class TestBackupMainService implements IBackupMainService {
59+
5960
_serviceBrand: undefined;
6061

6162
isHotExitEnabled(): boolean {
@@ -97,6 +98,10 @@ export class TestBackupMainService implements IBackupMainService {
9798
unregisterEmptyWindowBackupSync(backupFolder: string): void {
9899
throw new Error('Method not implemented.');
99100
}
101+
102+
async getDirtyWorkspaces(): Promise<(IWorkspaceIdentifier | URI)[]> {
103+
return [];
104+
}
100105
}
101106

102107
suite('WorkspacesMainService', () => {

src/vs/workbench/browser/actions/developerActions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import 'vs/css!./media/screencast';
6+
import 'vs/css!./media/actions';
77

88
import { Action } from 'vs/base/common/actions';
99
import * as nls from 'vs/nls';

src/vs/workbench/browser/actions/media/screencast.css renamed to src/vs/workbench/browser/actions/media/actions.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
.quick-input-list .quick-input-list-entry.has-actions:hover .quick-input-list-entry-action-bar .action-label.dirty-workspace::before {
7+
content: "\ea76"; /* Close icon flips between black dot and "X" for dirty workspaces */
8+
}
9+
610
.monaco-workbench .screencast-mouse {
711
position: absolute;
812
border: 2px solid red;

0 commit comments

Comments
 (0)