Skip to content

Commit 5af1f51

Browse files
committed
save untitled workspace on renderer
1 parent 5360e03 commit 5af1f51

12 files changed

Lines changed: 209 additions & 240 deletions

File tree

src/vs/code/electron-main/windows.ts

Lines changed: 3 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ import product from 'vs/platform/product/node/product';
2525
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
2626
import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows';
2727
import { IHistoryMainService } from 'vs/platform/history/common/history';
28-
import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
28+
import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform';
2929
import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_FILTER, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces';
3030
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
3131
import { mnemonicButtonLabel } from 'vs/base/common/labels';
3232
import { Schemas } from 'vs/base/common/network';
3333
import { normalizeNFC } from 'vs/base/common/normalization';
3434
import { URI } from 'vs/base/common/uri';
35-
import { Queue, timeout } from 'vs/base/common/async';
35+
import { Queue } from 'vs/base/common/async';
3636
import { exists } from 'vs/base/node/pfs';
3737
import { getComparisonKey, isEqual, normalizePath, basename as resourcesBasename, originalFSPath, hasTrailingPathSeparator, removeTrailingPathSeparator } from 'vs/base/common/resources';
3838
import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts';
@@ -197,7 +197,7 @@ export class WindowsManager implements IWindowsMainService {
197197
}
198198

199199
this.dialogs = new Dialogs(environmentService, telemetryService, stateService, this);
200-
this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, environmentService, historyMainService, this);
200+
this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, this);
201201
}
202202

203203
ready(initialUserEnv: IProcessEnvironment): void {
@@ -1544,23 +1544,6 @@ export class WindowsManager implements IWindowsMainService {
15441544
this.workspacesMainService.deleteUntitledWorkspaceSync(workspace);
15451545
return;
15461546
}
1547-
1548-
if (windowClosing && !isMacintosh && this.getWindowCount() === 1) {
1549-
return; // Windows/Linux: quits when last window is closed, so do not ask then
1550-
}
1551-
1552-
// Handle untitled workspaces with prompt as needed
1553-
e.veto(this.workspacesManager.promptToSaveUntitledWorkspace(this.getWindowById(e.window.id), workspace).then((veto): boolean | Promise<boolean> => {
1554-
if (veto) {
1555-
return veto;
1556-
}
1557-
1558-
// Bug in electron: somehow we need this timeout so that the window closes properly. That
1559-
// might be related to the fact that the untitled workspace prompt shows up async and this
1560-
// code can execute before the dialog is fully closed which then blocks the window from closing.
1561-
// Issue: https://github.com/Microsoft/vscode/issues/41989
1562-
return timeout(0).then(() => veto);
1563-
}));
15641547
}
15651548

15661549
focusLastActive(cli: ParsedArgs, context: OpenContext): ICodeWindow {
@@ -1994,8 +1977,6 @@ class WorkspacesManager {
19941977
constructor(
19951978
private readonly workspacesMainService: IWorkspacesMainService,
19961979
private readonly backupMainService: IBackupMainService,
1997-
private readonly environmentService: IEnvironmentService,
1998-
private readonly historyMainService: IHistoryMainService,
19991980
private readonly windowsMainService: IWindowsMainService,
20001981
) { }
20011982

@@ -2079,92 +2060,4 @@ class WorkspacesManager {
20792060
telemetryExtraData: options.telemetryExtraData
20802061
});
20812062
}
2082-
2083-
promptToSaveUntitledWorkspace(window: ICodeWindow | undefined, workspace: IWorkspaceIdentifier): Promise<boolean> {
2084-
enum ConfirmResult {
2085-
SAVE,
2086-
DONT_SAVE,
2087-
CANCEL
2088-
}
2089-
2090-
const save = { label: mnemonicButtonLabel(localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save")), result: ConfirmResult.SAVE };
2091-
const dontSave = { label: mnemonicButtonLabel(localize({ key: 'doNotSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save")), result: ConfirmResult.DONT_SAVE };
2092-
const cancel = { label: localize('cancel', "Cancel"), result: ConfirmResult.CANCEL };
2093-
2094-
const buttons: { label: string; result: ConfirmResult; }[] = [];
2095-
if (isWindows) {
2096-
buttons.push(save, dontSave, cancel);
2097-
} else if (isLinux) {
2098-
buttons.push(dontSave, cancel, save);
2099-
} else {
2100-
buttons.push(save, cancel, dontSave);
2101-
}
2102-
2103-
const options: Electron.MessageBoxOptions = {
2104-
title: this.environmentService.appNameLong,
2105-
message: localize('saveWorkspaceMessage', "Do you want to save your workspace configuration as a file?"),
2106-
detail: localize('saveWorkspaceDetail', "Save your workspace if you plan to open it again."),
2107-
noLink: true,
2108-
type: 'warning',
2109-
buttons: buttons.map(button => button.label),
2110-
cancelId: buttons.indexOf(cancel)
2111-
};
2112-
2113-
if (isLinux) {
2114-
options.defaultId = 2;
2115-
}
2116-
2117-
return this.windowsMainService.showMessageBox(options, window).then(res => {
2118-
switch (buttons[res.button].result) {
2119-
2120-
// Cancel: veto unload
2121-
case ConfirmResult.CANCEL:
2122-
return true;
2123-
2124-
// Don't Save: delete workspace
2125-
case ConfirmResult.DONT_SAVE:
2126-
this.workspacesMainService.deleteUntitledWorkspaceSync(workspace);
2127-
return false;
2128-
2129-
// Save: save workspace, but do not veto unload
2130-
case ConfirmResult.SAVE: {
2131-
return this.windowsMainService.showSaveDialog({
2132-
buttonLabel: mnemonicButtonLabel(localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save")),
2133-
title: localize('saveWorkspace', "Save Workspace"),
2134-
filters: WORKSPACE_FILTER,
2135-
defaultPath: this.getUntitledWorkspaceSaveDialogDefaultPath(workspace)
2136-
}, window).then(target => {
2137-
if (target) {
2138-
return this.workspacesMainService.saveWorkspaceAs(workspace, target).then(savedWorkspace => {
2139-
this.historyMainService.addRecentlyOpened([savedWorkspace], []);
2140-
this.workspacesMainService.deleteUntitledWorkspaceSync(workspace);
2141-
return false;
2142-
}, () => false);
2143-
}
2144-
2145-
return true; // keep veto if no target was provided
2146-
});
2147-
}
2148-
}
2149-
});
2150-
}
2151-
2152-
private getUntitledWorkspaceSaveDialogDefaultPath(workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string | undefined {
2153-
if (workspace) {
2154-
if (isSingleFolderWorkspaceIdentifier(workspace)) {
2155-
return workspace.scheme === Schemas.file ? dirname(workspace.fsPath) : undefined;
2156-
}
2157-
2158-
const resolvedWorkspace = workspace.configPath.scheme === Schemas.file && this.workspacesMainService.resolveLocalWorkspaceSync(workspace.configPath);
2159-
if (resolvedWorkspace && resolvedWorkspace.folders.length > 0) {
2160-
for (const folder of resolvedWorkspace.folders) {
2161-
if (folder.uri.scheme === Schemas.file) {
2162-
return dirname(folder.uri.fsPath);
2163-
}
2164-
}
2165-
}
2166-
}
2167-
2168-
return undefined;
2169-
}
21702063
}

src/vs/platform/windows/common/windows.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export interface IWindowsService {
117117
enterWorkspace(windowId: number, path: URI): Promise<IEnterWorkspaceResult | undefined>;
118118
toggleFullScreen(windowId: number): Promise<void>;
119119
setRepresentedFilename(windowId: number, fileName: string): Promise<void>;
120-
addRecentlyOpened(files: URI[]): Promise<void>;
120+
addRecentlyOpened(workspaces: URI[], folders: URI[], files: URI[]): Promise<void>;
121121
removeFromRecentlyOpened(paths: Array<IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | string>): Promise<void>;
122122
clearRecentlyOpened(): Promise<void>;
123123
getRecentlyOpened(windowId: number): Promise<IRecentlyOpened>;

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { IURLService, IURLHandler } from 'vs/platform/url/common/url';
1717
import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
1818
import { IWindowsMainService, ISharedProcess, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
1919
import { IHistoryMainService, IRecentlyOpened } from 'vs/platform/history/common/history';
20-
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
20+
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces';
2121
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
2222
import { Schemas } from 'vs/base/common/network';
2323
import { mnemonicButtonLabel } from 'vs/base/common/labels';
@@ -50,7 +50,8 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable
5050
@IURLService urlService: IURLService,
5151
@ILifecycleService private readonly lifecycleService: ILifecycleService,
5252
@IHistoryMainService private readonly historyService: IHistoryMainService,
53-
@ILogService private readonly logService: ILogService
53+
@ILogService private readonly logService: ILogService,
54+
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
5455
) {
5556
urlService.registerHandler(this);
5657

@@ -156,10 +157,11 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable
156157
return this.withWindow(windowId, codeWindow => codeWindow.setRepresentedFilename(fileName));
157158
}
158159

159-
async addRecentlyOpened(files: URI[]): Promise<void> {
160+
async addRecentlyOpened(workspaces: URI[], folders: URI[], files: URI[]): Promise<void> {
160161
this.logService.trace('windowsService#addRecentlyOpened');
161162

162-
this.historyService.addRecentlyOpened(undefined, files);
163+
const workspaceIdentifiers = workspaces.map(w => this.workspacesMainService.getWorkspaceIdentifier(w));
164+
this.historyService.addRecentlyOpened([...workspaceIdentifiers, ...folders], files);
163165
}
164166

165167
async removeFromRecentlyOpened(paths: Array<IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | string>): Promise<void> {

src/vs/platform/windows/node/windowsIpc.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class WindowsChannel implements IServerChannel {
5959
case 'enterWorkspace': return this.service.enterWorkspace(arg[0], URI.revive(arg[1]));
6060
case 'toggleFullScreen': return this.service.toggleFullScreen(arg);
6161
case 'setRepresentedFilename': return this.service.setRepresentedFilename(arg[0], arg[1]);
62-
case 'addRecentlyOpened': return this.service.addRecentlyOpened(arg.map(URI.revive));
62+
case 'addRecentlyOpened': return this.service.addRecentlyOpened(arg[0].map(URI.revive), arg[1].map(URI.revive), arg[2].map(URI.revive));
6363
case 'removeFromRecentlyOpened': {
6464
let paths: Array<IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | string> = arg;
6565
if (Array.isArray(paths)) {
@@ -178,8 +178,8 @@ export class WindowsChannelClient implements IWindowsService {
178178
return this.channel.call('setRepresentedFilename', [windowId, fileName]);
179179
}
180180

181-
addRecentlyOpened(files: URI[]): Promise<void> {
182-
return this.channel.call('addRecentlyOpened', files);
181+
addRecentlyOpened(workspaces: URI[], folders: URI[], files: URI[]): Promise<void> {
182+
return this.channel.call('addRecentlyOpened', [workspaces, folders, files]);
183183
}
184184

185185
removeFromRecentlyOpened(paths: Array<IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI>): Promise<void> {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,6 @@ export interface IWorkspacesMainService extends IWorkspacesService {
9696

9797
onUntitledWorkspaceDeleted: Event<IWorkspaceIdentifier>;
9898

99-
saveWorkspaceAs(workspace: IWorkspaceIdentifier, target: string): Promise<IWorkspaceIdentifier>;
100-
10199
createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier;
102100

103101
resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null;
@@ -115,6 +113,8 @@ export interface IWorkspacesService {
115113
_serviceBrand: any;
116114

117115
createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[]): Promise<IWorkspaceIdentifier>;
116+
117+
deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void>;
118118
}
119119

120120
export function isSingleFolderWorkspaceIdentifier(obj: any): obj is ISingleFolderWorkspaceIdentifier {

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

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

6-
import { IWorkspacesMainService, IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, rewriteWorkspaceFileForNewLocation, IUntitledWorkspaceInfo, getStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
6+
import { IWorkspacesMainService, IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
77
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
88
import { join, dirname } from 'vs/base/common/path';
9-
import { mkdirp, writeFile, readFile } from 'vs/base/node/pfs';
9+
import { mkdirp, writeFile } from 'vs/base/node/pfs';
1010
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
1111
import { isLinux } from 'vs/base/common/platform';
1212
import { delSync, readdirSync, writeFileAndFlushSync } from 'vs/base/node/extfs';
1313
import { Event, Emitter } from 'vs/base/common/event';
1414
import { ILogService } from 'vs/platform/log/common/log';
15-
import { isEqual } from 'vs/base/common/extpath';
1615
import { createHash } from 'crypto';
1716
import * as json from 'vs/base/common/json';
1817
import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace';
@@ -168,30 +167,6 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
168167
return this.isInsideWorkspacesHome(workspace.configPath);
169168
}
170169

171-
saveWorkspaceAs(workspace: IWorkspaceIdentifier, targetConfigPath: string): Promise<IWorkspaceIdentifier> {
172-
173-
if (workspace.configPath.scheme !== Schemas.file) {
174-
throw new Error('Only local workspaces can be saved with this API. Use WorkspaceEditingService.saveWorkspaceAs on the renderer instead.');
175-
}
176-
177-
const configPath = originalFSPath(workspace.configPath);
178-
179-
// Return early if target is same as source
180-
if (isEqual(configPath, targetConfigPath, !isLinux)) {
181-
return Promise.resolve(workspace);
182-
}
183-
184-
// Read the contents of the workspace file and resolve it
185-
return readFile(configPath).then(raw => {
186-
const targetConfigPathURI = URI.file(targetConfigPath);
187-
const newRawWorkspaceContents = rewriteWorkspaceFileForNewLocation(raw.toString(), workspace.configPath, targetConfigPathURI);
188-
189-
return writeFile(targetConfigPath, newRawWorkspaceContents).then(() => {
190-
return this.getWorkspaceIdentifier(targetConfigPathURI);
191-
});
192-
});
193-
}
194-
195170
deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void {
196171
if (!this.isUntitledWorkspace(workspace)) {
197172
return; // only supported for untitled workspaces
@@ -204,6 +179,11 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
204179
this._onUntitledWorkspaceDeleted.fire(workspace);
205180
}
206181

182+
deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void> {
183+
this.deleteUntitledWorkspaceSync(workspace);
184+
return Promise.resolve();
185+
}
186+
207187
private doDeleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void {
208188
const configPath = originalFSPath(workspace.configPath);
209189
try {

src/vs/platform/workspaces/node/workspacesIpc.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export class WorkspacesChannel implements IServerChannel {
3232

3333
return this.service.createUntitledWorkspace(folders);
3434
}
35+
case 'deleteUntitledWorkspace': {
36+
const w: IWorkspaceIdentifier = arg;
37+
return this.service.deleteUntitledWorkspace({ id: w.id, configPath: URI.revive(w.configPath) });
38+
}
3539
}
3640

3741
throw new Error(`Call not found: ${command}`);
@@ -47,4 +51,8 @@ export class WorkspacesChannelClient implements IWorkspacesService {
4751
createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[]): Promise<IWorkspaceIdentifier> {
4852
return this.channel.call('createUntitledWorkspace', folders).then(reviveWorkspaceIdentifier);
4953
}
54+
55+
deleteUntitledWorkspace(workspaceIdentifier: IWorkspaceIdentifier): Promise<void> {
56+
return this.channel.call('deleteUntitledWorkspace', workspaceIdentifier);
57+
}
5058
}

0 commit comments

Comments
 (0)