未验证 提交 c7622aa8 编写于 作者: M Martin Aeschlimann 提交者: GitHub

Merge pull request #69532 from Microsoft/aeschli/saveUntitledWorkspaceOnRenderer

save untitled workspace on renderer
......@@ -15,7 +15,7 @@ import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window';
import { hasArgs, asArray } from 'vs/platform/environment/node/argv';
import { ipcMain as ipc, screen, BrowserWindow, dialog, systemPreferences } from 'electron';
import { parseLineAndColumnAware } from 'vs/code/node/paths';
import { ILifecycleService, UnloadReason, IWindowUnloadEvent, LifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
import { ILifecycleService, UnloadReason, LifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ILogService } from 'vs/platform/log/common/log';
import { IWindowSettings, OpenContext, IPath, IWindowConfiguration, INativeOpenDialogOptions, IPathsToWaitFor, IEnterWorkspaceResult, IMessageBoxResult, INewWindowOptions, IURIToOpen, URIType, OpenDialogOptions } from 'vs/platform/windows/common/windows';
......@@ -25,14 +25,14 @@ import product from 'vs/platform/product/node/product';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows';
import { IHistoryMainService } from 'vs/platform/history/common/history';
import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform';
import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_FILTER, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { Schemas } from 'vs/base/common/network';
import { normalizeNFC } from 'vs/base/common/normalization';
import { URI } from 'vs/base/common/uri';
import { Queue, timeout } from 'vs/base/common/async';
import { Queue } from 'vs/base/common/async';
import { exists } from 'vs/base/node/pfs';
import { getComparisonKey, isEqual, normalizePath, basename as resourcesBasename, originalFSPath, hasTrailingPathSeparator, removeTrailingPathSeparator } from 'vs/base/common/resources';
import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts';
......@@ -197,7 +197,7 @@ export class WindowsManager implements IWindowsMainService {
}
this.dialogs = new Dialogs(environmentService, telemetryService, stateService, this);
this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, environmentService, historyMainService, this);
this.workspacesManager = new WorkspacesManager(workspacesMainService, backupMainService, this);
}
ready(initialUserEnv: IProcessEnvironment): void {
......@@ -233,7 +233,6 @@ export class WindowsManager implements IWindowsMainService {
}
// Handle various lifecycle events around windows
this.lifecycleService.onBeforeWindowUnload(e => this.onBeforeWindowUnload(e));
this.lifecycleService.onBeforeWindowClose(window => this.onBeforeWindowClose(window));
this.lifecycleService.onBeforeShutdown(() => this.onBeforeShutdown());
this.onWindowsCountChanged(e => {
......@@ -1526,43 +1525,6 @@ export class WindowsManager implements IWindowsMainService {
this.workspacesManager.pickWorkspaceAndOpen(options);
}
private onBeforeWindowUnload(e: IWindowUnloadEvent): void {
const windowClosing = (e.reason === UnloadReason.CLOSE);
const windowLoading = (e.reason === UnloadReason.LOAD);
if (!windowClosing && !windowLoading) {
return; // only interested when window is closing or loading
}
const workspace = e.window.openedWorkspace;
if (!workspace || !this.workspacesMainService.isUntitledWorkspace(workspace)) {
return; // only care about untitled workspaces to ask for saving
}
if (e.window.config && !!e.window.config.extensionDevelopmentPath) {
// do not ask to save workspace when doing extension development
// but still delete it.
this.workspacesMainService.deleteUntitledWorkspaceSync(workspace);
return;
}
if (windowClosing && !isMacintosh && this.getWindowCount() === 1) {
return; // Windows/Linux: quits when last window is closed, so do not ask then
}
// Handle untitled workspaces with prompt as needed
e.veto(this.workspacesManager.promptToSaveUntitledWorkspace(this.getWindowById(e.window.id), workspace).then((veto): boolean | Promise<boolean> => {
if (veto) {
return veto;
}
// Bug in electron: somehow we need this timeout so that the window closes properly. That
// might be related to the fact that the untitled workspace prompt shows up async and this
// code can execute before the dialog is fully closed which then blocks the window from closing.
// Issue: https://github.com/Microsoft/vscode/issues/41989
return timeout(0).then(() => veto);
}));
}
focusLastActive(cli: ParsedArgs, context: OpenContext): ICodeWindow {
const lastActive = this.getLastActiveWindow();
if (lastActive) {
......@@ -1994,8 +1956,6 @@ class WorkspacesManager {
constructor(
private readonly workspacesMainService: IWorkspacesMainService,
private readonly backupMainService: IBackupMainService,
private readonly environmentService: IEnvironmentService,
private readonly historyMainService: IHistoryMainService,
private readonly windowsMainService: IWindowsMainService,
) { }
......@@ -2079,92 +2039,4 @@ class WorkspacesManager {
telemetryExtraData: options.telemetryExtraData
});
}
promptToSaveUntitledWorkspace(window: ICodeWindow | undefined, workspace: IWorkspaceIdentifier): Promise<boolean> {
enum ConfirmResult {
SAVE,
DONT_SAVE,
CANCEL
}
const save = { label: mnemonicButtonLabel(localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save")), result: ConfirmResult.SAVE };
const dontSave = { label: mnemonicButtonLabel(localize({ key: 'doNotSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save")), result: ConfirmResult.DONT_SAVE };
const cancel = { label: localize('cancel', "Cancel"), result: ConfirmResult.CANCEL };
const buttons: { label: string; result: ConfirmResult; }[] = [];
if (isWindows) {
buttons.push(save, dontSave, cancel);
} else if (isLinux) {
buttons.push(dontSave, cancel, save);
} else {
buttons.push(save, cancel, dontSave);
}
const options: Electron.MessageBoxOptions = {
title: this.environmentService.appNameLong,
message: localize('saveWorkspaceMessage', "Do you want to save your workspace configuration as a file?"),
detail: localize('saveWorkspaceDetail', "Save your workspace if you plan to open it again."),
noLink: true,
type: 'warning',
buttons: buttons.map(button => button.label),
cancelId: buttons.indexOf(cancel)
};
if (isLinux) {
options.defaultId = 2;
}
return this.windowsMainService.showMessageBox(options, window).then(res => {
switch (buttons[res.button].result) {
// Cancel: veto unload
case ConfirmResult.CANCEL:
return true;
// Don't Save: delete workspace
case ConfirmResult.DONT_SAVE:
this.workspacesMainService.deleteUntitledWorkspaceSync(workspace);
return false;
// Save: save workspace, but do not veto unload
case ConfirmResult.SAVE: {
return this.windowsMainService.showSaveDialog({
buttonLabel: mnemonicButtonLabel(localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save")),
title: localize('saveWorkspace', "Save Workspace"),
filters: WORKSPACE_FILTER,
defaultPath: this.getUntitledWorkspaceSaveDialogDefaultPath(workspace)
}, window).then(target => {
if (target) {
return this.workspacesMainService.saveWorkspaceAs(workspace, target).then(savedWorkspace => {
this.historyMainService.addRecentlyOpened([savedWorkspace], []);
this.workspacesMainService.deleteUntitledWorkspaceSync(workspace);
return false;
}, () => false);
}
return true; // keep veto if no target was provided
});
}
}
});
}
private getUntitledWorkspaceSaveDialogDefaultPath(workspace?: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier): string | undefined {
if (workspace) {
if (isSingleFolderWorkspaceIdentifier(workspace)) {
return workspace.scheme === Schemas.file ? dirname(workspace.fsPath) : undefined;
}
const resolvedWorkspace = workspace.configPath.scheme === Schemas.file && this.workspacesMainService.resolveLocalWorkspaceSync(workspace.configPath);
if (resolvedWorkspace && resolvedWorkspace.folders.length > 0) {
for (const folder of resolvedWorkspace.folders) {
if (folder.uri.scheme === Schemas.file) {
return dirname(folder.uri.fsPath);
}
}
}
}
return undefined;
}
}
......@@ -117,7 +117,7 @@ export interface IWindowsService {
enterWorkspace(windowId: number, path: URI): Promise<IEnterWorkspaceResult | undefined>;
toggleFullScreen(windowId: number): Promise<void>;
setRepresentedFilename(windowId: number, fileName: string): Promise<void>;
addRecentlyOpened(files: URI[]): Promise<void>;
addRecentlyOpened(workspaces: URI[], folders: URI[], files: URI[]): Promise<void>;
removeFromRecentlyOpened(paths: Array<IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | string>): Promise<void>;
clearRecentlyOpened(): Promise<void>;
getRecentlyOpened(windowId: number): Promise<IRecentlyOpened>;
......
......@@ -17,7 +17,7 @@ import { IURLService, IURLHandler } from 'vs/platform/url/common/url';
import { ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain';
import { IWindowsMainService, ISharedProcess, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { IHistoryMainService, IRecentlyOpened } from 'vs/platform/history/common/history';
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces';
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
import { Schemas } from 'vs/base/common/network';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
......@@ -50,7 +50,8 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable
@IURLService urlService: IURLService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@IHistoryMainService private readonly historyService: IHistoryMainService,
@ILogService private readonly logService: ILogService
@ILogService private readonly logService: ILogService,
@IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService,
) {
urlService.registerHandler(this);
......@@ -156,10 +157,11 @@ export class WindowsService implements IWindowsService, IURLHandler, IDisposable
return this.withWindow(windowId, codeWindow => codeWindow.setRepresentedFilename(fileName));
}
async addRecentlyOpened(files: URI[]): Promise<void> {
async addRecentlyOpened(workspaces: URI[], folders: URI[], files: URI[]): Promise<void> {
this.logService.trace('windowsService#addRecentlyOpened');
this.historyService.addRecentlyOpened(undefined, files);
const workspaceIdentifiers = workspaces.map(w => this.workspacesMainService.getWorkspaceIdentifier(w));
this.historyService.addRecentlyOpened([...workspaceIdentifiers, ...folders], files);
}
async removeFromRecentlyOpened(paths: Array<IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | string>): Promise<void> {
......
......@@ -59,7 +59,7 @@ export class WindowsChannel implements IServerChannel {
case 'enterWorkspace': return this.service.enterWorkspace(arg[0], URI.revive(arg[1]));
case 'toggleFullScreen': return this.service.toggleFullScreen(arg);
case 'setRepresentedFilename': return this.service.setRepresentedFilename(arg[0], arg[1]);
case 'addRecentlyOpened': return this.service.addRecentlyOpened(arg.map(URI.revive));
case 'addRecentlyOpened': return this.service.addRecentlyOpened(arg[0].map(URI.revive), arg[1].map(URI.revive), arg[2].map(URI.revive));
case 'removeFromRecentlyOpened': {
let paths: Array<IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI | string> = arg;
if (Array.isArray(paths)) {
......@@ -178,8 +178,8 @@ export class WindowsChannelClient implements IWindowsService {
return this.channel.call('setRepresentedFilename', [windowId, fileName]);
}
addRecentlyOpened(files: URI[]): Promise<void> {
return this.channel.call('addRecentlyOpened', files);
addRecentlyOpened(workspaces: URI[], folders: URI[], files: URI[]): Promise<void> {
return this.channel.call('addRecentlyOpened', [workspaces, folders, files]);
}
removeFromRecentlyOpened(paths: Array<IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | URI>): Promise<void> {
......
......@@ -97,8 +97,6 @@ export interface IWorkspacesMainService extends IWorkspacesService {
onUntitledWorkspaceDeleted: Event<IWorkspaceIdentifier>;
saveWorkspaceAs(workspace: IWorkspaceIdentifier, target: string): Promise<IWorkspaceIdentifier>;
createUntitledWorkspaceSync(folders?: IWorkspaceFolderCreationData[]): IWorkspaceIdentifier;
resolveLocalWorkspaceSync(path: URI): IResolvedWorkspace | null;
......@@ -116,6 +114,8 @@ export interface IWorkspacesService {
_serviceBrand: any;
createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise<IWorkspaceIdentifier>;
deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void>;
}
export function isSingleFolderWorkspaceIdentifier(obj: any): obj is ISingleFolderWorkspaceIdentifier {
......
......@@ -3,16 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IWorkspacesMainService, IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, rewriteWorkspaceFileForNewLocation, IUntitledWorkspaceInfo, getStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspacesMainService, IWorkspaceIdentifier, hasWorkspaceFileExtension, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IUntitledWorkspaceInfo, getStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { join, dirname } from 'vs/base/common/path';
import { mkdirp, writeFile, readFile } from 'vs/base/node/pfs';
import { mkdirp, writeFile } from 'vs/base/node/pfs';
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
import { isLinux } from 'vs/base/common/platform';
import { delSync, readdirSync, writeFileAndFlushSync } from 'vs/base/node/extfs';
import { Event, Emitter } from 'vs/base/common/event';
import { ILogService } from 'vs/platform/log/common/log';
import { isEqual } from 'vs/base/common/extpath';
import { createHash } from 'crypto';
import * as json from 'vs/base/common/json';
import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace';
......@@ -169,30 +168,6 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
return this.isInsideWorkspacesHome(workspace.configPath);
}
saveWorkspaceAs(workspace: IWorkspaceIdentifier, targetConfigPath: string): Promise<IWorkspaceIdentifier> {
if (workspace.configPath.scheme !== Schemas.file) {
throw new Error('Only local workspaces can be saved with this API. Use WorkspaceEditingService.saveWorkspaceAs on the renderer instead.');
}
const configPath = originalFSPath(workspace.configPath);
// Return early if target is same as source
if (isEqual(configPath, targetConfigPath, !isLinux)) {
return Promise.resolve(workspace);
}
// Read the contents of the workspace file and resolve it
return readFile(configPath).then(raw => {
const targetConfigPathURI = URI.file(targetConfigPath);
const newRawWorkspaceContents = rewriteWorkspaceFileForNewLocation(raw.toString(), workspace.configPath, targetConfigPathURI);
return writeFile(targetConfigPath, newRawWorkspaceContents).then(() => {
return this.getWorkspaceIdentifier(targetConfigPathURI);
});
});
}
deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void {
if (!this.isUntitledWorkspace(workspace)) {
return; // only supported for untitled workspaces
......@@ -205,6 +180,11 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain
this._onUntitledWorkspaceDeleted.fire(workspace);
}
deleteUntitledWorkspace(workspace: IWorkspaceIdentifier): Promise<void> {
this.deleteUntitledWorkspaceSync(workspace);
return Promise.resolve();
}
private doDeleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void {
const configPath = originalFSPath(workspace.configPath);
try {
......
......@@ -33,6 +33,10 @@ export class WorkspacesChannel implements IServerChannel {
return this.service.createUntitledWorkspace(folders, remoteAuthority);
}
case 'deleteUntitledWorkspace': {
const w: IWorkspaceIdentifier = arg;
return this.service.deleteUntitledWorkspace({ id: w.id, configPath: URI.revive(w.configPath) });
}
}
throw new Error(`Call not found: ${command}`);
......@@ -48,4 +52,8 @@ export class WorkspacesChannelClient implements IWorkspacesService {
createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise<IWorkspaceIdentifier> {
return this.channel.call('createUntitledWorkspace', [folders, remoteAuthority]).then(reviveWorkspaceIdentifier);
}
deleteUntitledWorkspace(workspaceIdentifier: IWorkspaceIdentifier): Promise<void> {
return this.channel.call('deleteUntitledWorkspace', workspaceIdentifier);
}
}
......@@ -7,12 +7,11 @@ import * as assert from 'assert';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'vs/base/common/path';
import * as extfs from 'vs/base/node/extfs';
import * as pfs from 'vs/base/node/pfs';
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
import { parseArgs } from 'vs/platform/environment/node/argv';
import { WorkspacesMainService, IStoredWorkspace } from 'vs/platform/workspaces/electron-main/workspacesMainService';
import { WORKSPACE_EXTENSION, IWorkspaceIdentifier, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
import { WORKSPACE_EXTENSION, IWorkspaceIdentifier, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder, rewriteWorkspaceFileForNewLocation } from 'vs/platform/workspaces/common/workspaces';
import { NullLogService } from 'vs/platform/log/common/log';
import { URI } from 'vs/base/common/uri';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
......@@ -235,95 +234,85 @@ suite('WorkspacesMainService', () => {
});
});
test('saveWorkspace (untitled)', () => {
return createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]).then(workspace => {
const workspaceConfigPath = path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`);
return service.saveWorkspaceAs(workspace, workspaceConfigPath).then(savedWorkspace => {
assert.ok(savedWorkspace.id);
assert.notEqual(savedWorkspace.id, workspace.id);
assertPathEquals(savedWorkspace.configPath.fsPath, workspaceConfigPath);
const ws = JSON.parse(fs.readFileSync(savedWorkspace.configPath.fsPath).toString()) as IStoredWorkspace;
assert.equal(ws.folders.length, 3);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, process.cwd()); // absolute
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, '.'); // relative
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[2]).path, path.relative(path.dirname(workspaceConfigPath), path.join(os.tmpdir(), 'somefolder'))); // relative
extfs.delSync(workspaceConfigPath);
});
});
});
test('rewriteWorkspaceFileForNewLocation', () => {
const folder1 = process.cwd(); // absolute path because outside of tmpDir
const tmpDir = os.tmpdir();
const tmpInsideDir = path.join(os.tmpdir(), 'inside');
return createWorkspace([folder1, tmpInsideDir, path.join(tmpInsideDir, 'somefolder')]).then(workspace => {
const origContent = fs.readFileSync(workspace.configPath.fsPath).toString();
let origConfigPath = workspace.configPath;
let workspaceConfigPath = URI.file(path.join(tmpDir, 'inside', 'myworkspace1.code-workspace'));
let newContent = rewriteWorkspaceFileForNewLocation(origContent, origConfigPath, workspaceConfigPath);
let ws = JSON.parse(newContent) as IStoredWorkspace;
assert.equal(ws.folders.length, 3);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1); // absolute path because outside of tmpdir
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, '.');
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[2]).path, 'somefolder');
origConfigPath = workspaceConfigPath;
workspaceConfigPath = URI.file(path.join(tmpDir, 'myworkspace2.code-workspace'));
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath);
ws = JSON.parse(newContent) as IStoredWorkspace;
assert.equal(ws.folders.length, 3);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, 'inside');
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[2]).path, isWindows ? 'inside\\somefolder' : 'inside/somefolder');
origConfigPath = workspaceConfigPath;
workspaceConfigPath = URI.file(path.join(tmpDir, 'other', 'myworkspace2.code-workspace'));
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath);
ws = JSON.parse(newContent) as IStoredWorkspace;
assert.equal(ws.folders.length, 3);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, folder1);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, tmpInsideDir);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[2]).path, path.join(tmpInsideDir, 'somefolder'));
origConfigPath = workspaceConfigPath;
workspaceConfigPath = URI.parse('foo://foo/bar/myworkspace2.code-workspace');
newContent = rewriteWorkspaceFileForNewLocation(newContent, origConfigPath, workspaceConfigPath);
ws = JSON.parse(newContent) as IStoredWorkspace;
assert.equal(ws.folders.length, 3);
assert.equal((<IRawUriWorkspaceFolder>ws.folders[0]).uri, URI.file(folder1).toString(true));
assert.equal((<IRawUriWorkspaceFolder>ws.folders[1]).uri, URI.file(tmpInsideDir).toString(true));
assert.equal((<IRawUriWorkspaceFolder>ws.folders[2]).uri, URI.file(path.join(tmpInsideDir, 'somefolder')).toString(true));
test('saveWorkspace (saved workspace)', () => {
return createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]).then(workspace => {
const workspaceConfigPath = path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`);
const newWorkspaceConfigPath = path.join(os.tmpdir(), `mySavedWorkspace.${Date.now()}.${WORKSPACE_EXTENSION}`);
return service.saveWorkspaceAs(workspace, workspaceConfigPath).then(savedWorkspace => {
return service.saveWorkspaceAs(savedWorkspace, newWorkspaceConfigPath).then(newSavedWorkspace => {
assert.ok(newSavedWorkspace.id);
assert.notEqual(newSavedWorkspace.id, workspace.id);
assertPathEquals(newSavedWorkspace.configPath.fsPath, newWorkspaceConfigPath);
const ws = JSON.parse(fs.readFileSync(newSavedWorkspace.configPath.fsPath).toString()) as IStoredWorkspace;
assert.equal(ws.folders.length, 3);
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[0]).path, process.cwd()); // absolute path because outside of tmpdir
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[1]).path, '.'); // relative path because inside of tmpdir
assertPathEquals((<IRawFileWorkspaceFolder>ws.folders[2]).path, 'somefolder'); // relative
extfs.delSync(workspaceConfigPath);
extfs.delSync(newWorkspaceConfigPath);
});
});
service.deleteUntitledWorkspaceSync(workspace);
});
});
test('saveWorkspace (saved workspace, preserves comments)', () => {
test('rewriteWorkspaceFileForNewLocation (preserves comments)', () => {
return createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]).then(workspace => {
const workspaceConfigPath = path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`);
const newWorkspaceConfigPath = path.join(os.tmpdir(), `mySavedWorkspace.${Date.now()}.${WORKSPACE_EXTENSION}`);
const workspaceConfigPath = URI.file(path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`));
return service.saveWorkspaceAs(workspace, workspaceConfigPath).then(savedWorkspace => {
const contents = fs.readFileSync(savedWorkspace.configPath.fsPath).toString();
fs.writeFileSync(savedWorkspace.configPath.fsPath, `// this is a comment\n${contents}`);
let origContent = fs.readFileSync(workspace.configPath.fsPath).toString();
origContent = `// this is a comment\n${origContent}`;
return service.saveWorkspaceAs(savedWorkspace, newWorkspaceConfigPath).then(newSavedWorkspace => {
assert.ok(newSavedWorkspace.id);
assert.notEqual(newSavedWorkspace.id, workspace.id);
assertPathEquals(newSavedWorkspace.configPath.fsPath, newWorkspaceConfigPath);
let newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath);
const savedContents = fs.readFileSync(newSavedWorkspace.configPath.fsPath).toString();
assert.equal(0, savedContents.indexOf('// this is a comment'));
assert.equal(0, newContent.indexOf('// this is a comment'));
extfs.delSync(workspaceConfigPath);
extfs.delSync(newWorkspaceConfigPath);
});
});
service.deleteUntitledWorkspaceSync(workspace);
});
});
test('saveWorkspace (saved workspace, preserves forward slashes)', () => {
test('rewriteWorkspaceFileForNewLocation (preserves forward slashes)', () => {
return createWorkspace([process.cwd(), os.tmpdir(), path.join(os.tmpdir(), 'somefolder')]).then(workspace => {
const workspaceConfigPath = path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`);
const newWorkspaceConfigPath = path.join(os.tmpdir(), `mySavedWorkspace.${Date.now()}.${WORKSPACE_EXTENSION}`);
return service.saveWorkspaceAs(workspace, workspaceConfigPath).then(savedWorkspace => {
const contents = fs.readFileSync(savedWorkspace.configPath.fsPath).toString();
fs.writeFileSync(savedWorkspace.configPath.fsPath, contents.replace(/[\\]/g, '/')); // convert backslash to slash
const workspaceConfigPath = URI.file(path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`));
let origContent = fs.readFileSync(workspace.configPath.fsPath).toString();
origContent = origContent.replace(/[\\]/g, '/'); // convert backslash to slash
return service.saveWorkspaceAs(savedWorkspace, newWorkspaceConfigPath).then(newSavedWorkspace => {
assert.ok(newSavedWorkspace.id);
assert.notEqual(newSavedWorkspace.id, workspace.id);
assertPathEquals(newSavedWorkspace.configPath.fsPath, newWorkspaceConfigPath);
const newContent = rewriteWorkspaceFileForNewLocation(origContent, workspace.configPath, workspaceConfigPath);
const ws = JSON.parse(fs.readFileSync(newSavedWorkspace.configPath.fsPath).toString()) as IStoredWorkspace;
assert.ok(ws.folders.every(f => (<IRawFileWorkspaceFolder>f).path.indexOf('\\') < 0));
const ws = JSON.parse(newContent) as IStoredWorkspace;
assert.ok(ws.folders.every(f => (<IRawFileWorkspaceFolder>f).path.indexOf('\\') < 0));
extfs.delSync(workspaceConfigPath);
extfs.delSync(newWorkspaceConfigPath);
});
});
service.deleteUntitledWorkspaceSync(workspace);
});
});
......@@ -339,15 +328,7 @@ suite('WorkspacesMainService', () => {
test('deleteUntitledWorkspaceSync (saved)', () => {
return createWorkspace([process.cwd(), os.tmpdir()]).then(workspace => {
const workspaceConfigPath = path.join(os.tmpdir(), `myworkspace.${Date.now()}.${WORKSPACE_EXTENSION}`);
return service.saveWorkspaceAs(workspace, workspaceConfigPath).then(savedWorkspace => {
assert.ok(fs.existsSync(savedWorkspace.configPath.fsPath));
service.deleteUntitledWorkspaceSync(savedWorkspace);
assert.ok(fs.existsSync(savedWorkspace.configPath.fsPath));
});
service.deleteUntitledWorkspaceSync(workspace);
});
});
......
......@@ -9,12 +9,10 @@ import { IWindowService } from 'vs/platform/windows/common/windows';
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing';
import { WORKSPACE_FILTER, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL, PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands';
import { URI } from 'vs/base/common/uri';
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { INotificationService } from 'vs/platform/notification/common/notification';
......@@ -132,15 +130,14 @@ export class SaveWorkspaceAsAction extends Action {
id: string,
label: string,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService,
@IFileDialogService private readonly dialogService: IFileDialogService
@IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService
) {
super(id, label);
}
run(): Promise<any> {
return this.getNewWorkspaceConfigPath().then((configPathUri): Promise<void> | void => {
return this.workspaceEditingService.pickNewWorkspacePath().then((configPathUri): Promise<void> | void => {
if (configPathUri) {
switch (this.contextService.getWorkbenchState()) {
case WorkbenchState.EMPTY:
......@@ -154,15 +151,6 @@ export class SaveWorkspaceAsAction extends Action {
}
});
}
private getNewWorkspaceConfigPath(): Promise<URI | undefined> {
return this.dialogService.showSaveDialog({
saveLabel: mnemonicButtonLabel(nls.localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save")),
title: nls.localize('saveWorkspace', "Save Workspace"),
filters: WORKSPACE_FILTER,
defaultUri: this.dialogService.defaultWorkspacePath()
});
}
}
export class OpenWorkspaceAction extends Action {
......
......@@ -180,7 +180,7 @@ export class ResourcesDropHandler {
// Add external ones to recently open list unless dropped resource is a workspace
const filesToAddToHistory = untitledOrFileResources.filter(d => d.isExternal && d.resource.scheme === Schemas.file).map(d => d.resource);
if (filesToAddToHistory.length) {
this.windowsService.addRecentlyOpened(filesToAddToHistory);
this.windowsService.addRecentlyOpened([], [], filesToAddToHistory);
}
const editors: IResourceEditor[] = untitledOrFileResources.map(untitledOrFileResource => ({
......
......@@ -51,4 +51,9 @@ export interface IWorkspaceEditingService {
* copies current workspace settings to the target workspace.
*/
copyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): Promise<void>;
/**
* picks a new workspace path
*/
pickNewWorkspacePath(): Promise<URI | undefined>;
}
\ No newline at end of file
......@@ -9,7 +9,7 @@ import * as nls from 'vs/nls';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IWindowService, MessageBoxOptions, IWindowsService } from 'vs/platform/windows/common/windows';
import { IJSONEditingService, JSONEditingError, JSONEditingErrorCode } from 'vs/workbench/services/configuration/common/jsonEditing';
import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, isWorkspaceIdentifier, toWorkspaceIdentifier, IWorkspacesService, rewriteWorkspaceFileForNewLocation } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspacesService, rewriteWorkspaceFileForNewLocation, WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces';
import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { StorageService } from 'vs/platform/storage/node/storageService';
......@@ -20,13 +20,16 @@ import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { BackupFileService } from 'vs/workbench/services/backup/node/backupFileService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { distinct } from 'vs/base/common/arrays';
import { isLinux } from 'vs/base/common/platform';
import { isEqual, basename } from 'vs/base/common/resources';
import { isLinux, isWindows, isMacintosh } from 'vs/base/common/platform';
import { isEqual, basename, isEqualOrParent } from 'vs/base/common/resources';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IFileService } from 'vs/platform/files/common/files';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILifecycleService, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle';
import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class WorkspaceEditingService implements IWorkspaceEditingService {
......@@ -45,8 +48,94 @@ export class WorkspaceEditingService implements IWorkspaceEditingService {
@IFileService private readonly fileSystemService: IFileService,
@IWindowsService private readonly windowsService: IWindowsService,
@IWorkspacesService private readonly workspaceService: IWorkspacesService,
@IEnvironmentService private readonly environmentService: IEnvironmentService
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@IDialogService private readonly dialogService: IDialogService,
@ILifecycleService readonly lifecycleService: ILifecycleService
) {
lifecycleService.onBeforeShutdown(async e => {
const saveOperation = this.saveUntitedBeforeShutdown(e.reason);
if (saveOperation) {
e.veto(saveOperation);
}
});
}
private saveUntitedBeforeShutdown(reason: ShutdownReason): Promise<boolean> | undefined {
if (reason !== ShutdownReason.LOAD && reason !== ShutdownReason.CLOSE) {
return undefined; // only interested when window is closing or loading
}
const workspaceIdentifier = this.getCurrentWorkspaceIdentifier();
if (!workspaceIdentifier || !isEqualOrParent(workspaceIdentifier.configPath, this.environmentService.untitledWorkspacesHome)) {
return undefined; // only care about untitled workspaces to ask for saving
}
return this.windowsService.getWindowCount().then(windowCount => {
if (reason === ShutdownReason.CLOSE && !isMacintosh && windowCount === 1) {
return false; // Windows/Linux: quits when last window is closed, so do not ask then
}
enum ConfirmResult {
SAVE,
DONT_SAVE,
CANCEL
}
const save = { label: mnemonicButtonLabel(nls.localize('save', "Save")), result: ConfirmResult.SAVE };
const dontSave = { label: mnemonicButtonLabel(nls.localize('doNotSave', "Don't Save")), result: ConfirmResult.DONT_SAVE };
const cancel = { label: nls.localize('cancel', "Cancel"), result: ConfirmResult.CANCEL };
const buttons: { label: string; result: ConfirmResult; }[] = [];
if (isWindows) {
buttons.push(save, dontSave, cancel);
} else if (isLinux) {
buttons.push(dontSave, cancel, save);
} else {
buttons.push(save, cancel, dontSave);
}
const message = nls.localize('saveWorkspaceMessage', "Do you want to save your workspace configuration as a file?");
const detail = nls.localize('saveWorkspaceDetail', "Save your workspace if you plan to open it again.");
const cancelId = buttons.indexOf(cancel);
return this.dialogService.show(Severity.Warning, message, buttons.map(button => button.label), { detail, cancelId }).then(res => {
switch (buttons[res].result) {
// Cancel: veto unload
case ConfirmResult.CANCEL:
return true;
// Don't Save: delete workspace
case ConfirmResult.DONT_SAVE:
this.workspaceService.deleteUntitledWorkspace(workspaceIdentifier);
return false;
// Save: save workspace, but do not veto unload
case ConfirmResult.SAVE: {
return this.pickNewWorkspacePath().then(newWorkspacePath => {
if (newWorkspacePath) {
return this.saveWorkspaceAs(workspaceIdentifier, newWorkspacePath).then(_ => {
this.windowsService.addRecentlyOpened([newWorkspacePath], [], []);
this.workspaceService.deleteUntitledWorkspace(workspaceIdentifier);
return false;
}, () => false);
}
return true; // keep veto if no target was provided
});
}
}
});
});
}
pickNewWorkspacePath(): Promise<URI | undefined> {
return this.fileDialogService.showSaveDialog({
saveLabel: mnemonicButtonLabel(nls.localize('save', "Save")),
title: nls.localize('saveWorkspace', "Save Workspace"),
filters: WORKSPACE_FILTER,
defaultUri: this.fileDialogService.defaultWorkspacePath()
});
}
updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): Promise<void> {
......@@ -165,11 +254,11 @@ export class WorkspaceEditingService implements IWorkspaceEditingService {
if (!this.isValidTargetWorkspacePath(path)) {
return Promise.reject(null);
}
const currentWorkspaceIdentifier = toWorkspaceIdentifier(this.contextService.getWorkspace());
if (!isWorkspaceIdentifier(currentWorkspaceIdentifier)) {
const workspaceIdentifier = this.getCurrentWorkspaceIdentifier();
if (!workspaceIdentifier) {
return Promise.reject(null);
}
await this.saveWorkspaceAs(currentWorkspaceIdentifier, path);
await this.saveWorkspaceAs(workspaceIdentifier, path);
return this.enterWorkspace(path);
}
......@@ -329,6 +418,14 @@ export class WorkspaceEditingService implements IWorkspaceEditingService {
return this.jsonEditingService.write(toWorkspace.configPath, { key: 'settings', value: targetWorkspaceConfiguration }, true);
}
private getCurrentWorkspaceIdentifier(): IWorkspaceIdentifier | undefined {
const workspace = this.contextService.getWorkspace();
if (workspace && workspace.configuration) {
return { id: workspace.id, configPath: workspace.configuration };
}
return undefined;
}
}
registerSingleton(IWorkspaceEditingService, WorkspaceEditingService, true);
\ No newline at end of file
......@@ -1240,7 +1240,7 @@ export class TestWindowsService implements IWindowsService {
return Promise.resolve();
}
addRecentlyOpened(_files: URI[]): Promise<void> {
addRecentlyOpened(_workspaces: URI[], _folders: URI[], _files: URI[]): Promise<void> {
return Promise.resolve();
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册