workspacesMainService.ts 9.9 KB
Newer Older
B
Benjamin Pasero 已提交
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

8
import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_EXTENSION, IWorkspaceSavedEvent, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
9
import { TPromise } from 'vs/base/common/winjs.base';
B
Benjamin Pasero 已提交
10 11
import { isParent } from 'vs/platform/files/common/files';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
12
import { extname, join, dirname, isAbsolute, resolve } from 'path';
B
Benjamin Pasero 已提交
13
import { mkdirp, writeFile, readFile } from 'vs/base/node/pfs';
14
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
15
import { isLinux, isMacintosh } from 'vs/base/common/platform';
16
import { delSync, readdirSync } from 'vs/base/node/extfs';
B
Benjamin Pasero 已提交
17 18
import Event, { Emitter } from 'vs/base/common/event';
import { ILogService } from 'vs/platform/log/common/log';
19
import { isEqual } from 'vs/base/common/paths';
B
Benjamin Pasero 已提交
20
import { coalesce } from 'vs/base/common/arrays';
21
import { createHash } from 'crypto';
22
import * as json from 'vs/base/common/json';
23 24
import * as jsonEdit from 'vs/base/common/jsonEdit';
import { applyEdit } from 'vs/base/common/jsonFormatter';
25
import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/workspaces';
26 27 28 29 30
import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace';

export interface IStoredWorkspace {
	folders: IStoredWorkspaceFolder[];
}
31

B
Benjamin Pasero 已提交
32 33 34 35
export class WorkspacesMainService implements IWorkspacesMainService {

	public _serviceBrand: any;

B
Benjamin Pasero 已提交
36
	protected workspacesHome: string;
B
Benjamin Pasero 已提交
37

38
	private _onWorkspaceSaved: Emitter<IWorkspaceSavedEvent>;
39
	private _onUntitledWorkspaceDeleted: Emitter<IWorkspaceIdentifier>;
40

41 42
	constructor(
		@IEnvironmentService private environmentService: IEnvironmentService,
43
		@ILogService private logService: ILogService
44
	) {
B
Benjamin Pasero 已提交
45
		this.workspacesHome = environmentService.workspacesHome;
46

47
		this._onWorkspaceSaved = new Emitter<IWorkspaceSavedEvent>();
48
		this._onUntitledWorkspaceDeleted = new Emitter<IWorkspaceIdentifier>();
49 50 51 52
	}

	public get onWorkspaceSaved(): Event<IWorkspaceSavedEvent> {
		return this._onWorkspaceSaved.event;
B
Benjamin Pasero 已提交
53 54
	}

55 56
	public get onUntitledWorkspaceDeleted(): Event<IWorkspaceIdentifier> {
		return this._onUntitledWorkspaceDeleted.event;
57 58
	}

B
Benjamin Pasero 已提交
59 60 61 62 63 64 65 66
	public resolveWorkspace(path: string): TPromise<IResolvedWorkspace> {
		if (!this.isWorkspacePath(path)) {
			return TPromise.as(null); // does not look like a valid workspace config file
		}

		return readFile(path).then(contents => this.doResolveWorkspace(path, contents.toString()));
	}

67
	public resolveWorkspaceSync(path: string): IResolvedWorkspace {
B
Benjamin Pasero 已提交
68
		if (!this.isWorkspacePath(path)) {
69 70 71
			return null; // does not look like a valid workspace config file
		}

B
Benjamin Pasero 已提交
72 73 74 75 76 77 78 79
		return this.doResolveWorkspace(path, readFileSync(path, 'utf8'));
	}

	private isWorkspacePath(path: string): boolean {
		return this.isInsideWorkspacesHome(path) || extname(path) === `.${WORKSPACE_EXTENSION}`;
	}

	private doResolveWorkspace(path: string, contents: string): IResolvedWorkspace {
80
		try {
81
			const workspace = this.doParseStoredWorkspace(path, contents);
82

83
			// relative paths get resolved against the workspace location
84
			workspace.folders.forEach(folder => {
85
				if (!isAbsolute(folder.path)) {
86
					folder.path = resolve(dirname(path), folder.path);
87 88 89 90 91 92
				}
			});

			return {
				id: this.getWorkspaceId(path),
				configPath: path,
93
				folders: toWorkspaceFolders(workspace.folders)
94
			};
95
		} catch (error) {
96 97 98 99 100 101 102
			this.logService.log(error.toString());
		}

		return null;
	}

	private doParseStoredWorkspace(path: string, contents: string): IStoredWorkspace {
103 104

		// Parse workspace file
105 106
		let storedWorkspace: IStoredWorkspace;
		try {
107
			storedWorkspace = json.parse(contents); // use fault tolerant parser
108 109 110
		} catch (error) {
			throw new Error(`${path} cannot be parsed as JSON file (${error}).`);
		}
111

112 113 114 115 116 117
		// Filter out folders which do not have a path set
		if (Array.isArray(storedWorkspace.folders)) {
			storedWorkspace.folders = storedWorkspace.folders.filter(folder => !!folder.path);
		}

		// Validate
118 119
		if (!Array.isArray(storedWorkspace.folders) || storedWorkspace.folders.length === 0) {
			throw new Error(`${path} looks like an invalid workspace file.`);
120
		}
121 122

		return storedWorkspace;
B
Benjamin Pasero 已提交
123 124
	}

125 126 127 128
	private isInsideWorkspacesHome(path: string): boolean {
		return isParent(path, this.environmentService.workspacesHome, !isLinux /* ignore case */);
	}

B
Benjamin Pasero 已提交
129
	public createWorkspace(folders: string[]): TPromise<IWorkspaceIdentifier> {
130 131 132 133 134 135 136 137 138 139 140 141
		const { workspace, configParent, storedWorkspace } = this.createUntitledWorkspace(folders);

		return mkdirp(configParent).then(() => {
			return writeFile(workspace.configPath, JSON.stringify(storedWorkspace, null, '\t')).then(() => workspace);
		});
	}

	public createWorkspaceSync(folders: string[]): IWorkspaceIdentifier {
		const { workspace, configParent, storedWorkspace } = this.createUntitledWorkspace(folders);

		if (!existsSync(this.workspacesHome)) {
			mkdirSync(this.workspacesHome);
B
Benjamin Pasero 已提交
142 143
		}

144 145 146 147 148 149 150 151
		mkdirSync(configParent);

		writeFileSync(workspace.configPath, JSON.stringify(storedWorkspace, null, '\t'));

		return workspace;
	}

	private createUntitledWorkspace(folders: string[]): { workspace: IWorkspaceIdentifier, configParent: string, storedWorkspace: IStoredWorkspace } {
152 153 154
		const randomId = (Date.now() + Math.round(Math.random() * 1000)).toString();
		const untitledWorkspaceConfigFolder = join(this.workspacesHome, randomId);
		const untitledWorkspaceConfigPath = join(untitledWorkspaceConfigFolder, UNTITLED_WORKSPACE_NAME);
B
Benjamin Pasero 已提交
155

156 157 158 159 160
		const storedWorkspace: IStoredWorkspace = {
			folders: folders.map(folder => ({
				path: folder
			}))
		};
B
Benjamin Pasero 已提交
161

162 163
		return {
			workspace: {
164 165
				id: this.getWorkspaceId(untitledWorkspaceConfigPath),
				configPath: untitledWorkspaceConfigPath
166 167 168 169
			},
			configParent: untitledWorkspaceConfigFolder,
			storedWorkspace
		};
B
Benjamin Pasero 已提交
170 171
	}

172 173 174 175 176 177
	public getWorkspaceId(workspaceConfigPath: string): string {
		if (!isLinux) {
			workspaceConfigPath = workspaceConfigPath.toLowerCase(); // sanitize for platform file system
		}

		return createHash('md5').update(workspaceConfigPath).digest('hex');
B
Benjamin Pasero 已提交
178
	}
B
Benjamin Pasero 已提交
179 180 181 182

	public isUntitledWorkspace(workspace: IWorkspaceIdentifier): boolean {
		return this.isInsideWorkspacesHome(workspace.configPath);
	}
183

184
	public saveWorkspace(workspace: IWorkspaceIdentifier, targetConfigPath: string): TPromise<IWorkspaceIdentifier> {
B
Benjamin Pasero 已提交
185 186

		// Return early if target is same as source
187
		if (isEqual(workspace.configPath, targetConfigPath, !isLinux)) {
B
Benjamin Pasero 已提交
188 189 190
			return TPromise.as(workspace);
		}

191
		// Read the contents of the workspace file and resolve it
192 193
		return readFile(workspace.configPath).then(raw => {
			const rawWorkspaceContents = raw.toString();
194 195
			let storedWorkspace: IStoredWorkspace;
			try {
196
				storedWorkspace = this.doParseStoredWorkspace(workspace.configPath, rawWorkspaceContents);
197 198 199 200 201 202 203 204 205 206 207
			} catch (error) {
				return TPromise.wrapError(error);
			}

			const sourceConfigFolder = dirname(workspace.configPath);
			const targetConfigFolder = dirname(targetConfigPath);

			// Rewrite absolute paths to relative paths if the target workspace folder
			// is a parent of the location of the workspace file itself. Otherwise keep
			// using absolute paths.
			storedWorkspace.folders.forEach(folder => {
208 209 210 211
				if (!isAbsolute(folder.path)) {
					folder.path = resolve(sourceConfigFolder, folder.path); // relative paths get resolved against the workspace location
				}

212
				folder.path = massageFolderPathForWorkspace(folder.path, targetConfigFolder, storedWorkspace.folders);
213 214 215 216 217 218 219 220
			});

			// Preserve as much of the existing workspace as possible by using jsonEdit
			// and only changing the folders portion.
			let newRawWorkspaceContents = rawWorkspaceContents;
			const edits = jsonEdit.setProperty(rawWorkspaceContents, ['folders'], storedWorkspace.folders, { insertSpaces: false, tabSize: 4, eol: (isLinux || isMacintosh) ? '\n' : '\r\n' });
			edits.forEach(edit => {
				newRawWorkspaceContents = applyEdit(rawWorkspaceContents, edit);
221
			});
222

223
			return writeFile(targetConfigPath, newRawWorkspaceContents).then(() => {
224
				const savedWorkspaceIdentifier = { id: this.getWorkspaceId(targetConfigPath), configPath: targetConfigPath };
225

226 227
				// Event
				this._onWorkspaceSaved.fire({ workspace: savedWorkspaceIdentifier, oldConfigPath: workspace.configPath });
228

229 230 231 232 233
				// Delete untitled workspace
				this.deleteUntitledWorkspaceSync(workspace);

				return savedWorkspaceIdentifier;
			});
234 235
		});
	}
236

237
	public deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void {
238
		if (!this.isUntitledWorkspace(workspace)) {
239
			return; // only supported for untitled workspaces
240 241
		}

242
		// Delete from disk
243
		this.doDeleteUntitledWorkspaceSync(workspace.configPath);
244 245

		// Event
246
		this._onUntitledWorkspaceDeleted.fire(workspace);
247
	}
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277

	private doDeleteUntitledWorkspaceSync(configPath: string): void {
		try {
			delSync(dirname(configPath));
		} catch (error) {
			this.logService.log(`Unable to delete untitled workspace ${configPath} (${error}).`);
		}
	}

	public getUntitledWorkspacesSync(): IWorkspaceIdentifier[] {
		let untitledWorkspacePaths: string[] = [];
		try {
			untitledWorkspacePaths = readdirSync(this.workspacesHome).map(folder => join(this.workspacesHome, folder, UNTITLED_WORKSPACE_NAME));
		} catch (error) {
			this.logService.log(`Unable to read folders in ${this.workspacesHome} (${error}).`);
		}

		const untitledWorkspaces: IWorkspaceIdentifier[] = coalesce(untitledWorkspacePaths.map(untitledWorkspacePath => {
			const workspace = this.resolveWorkspaceSync(untitledWorkspacePath);
			if (!workspace) {
				this.doDeleteUntitledWorkspaceSync(untitledWorkspacePath);

				return null; // invalid workspace
			}

			return { id: workspace.id, configPath: untitledWorkspacePath };
		}));

		return untitledWorkspaces;
	}
B
Benjamin Pasero 已提交
278
}