workspacesMainService.ts 9.8 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';

J
Johannes Rieken 已提交
8
import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_EXTENSION, IWorkspaceSavedEvent, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder, isRawFileWorkspaceFolder, isStoredWorkspaceFolder } 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
import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace';
J
Johannes Rieken 已提交
27
import URI from 'vs/base/common/uri';
28 29 30 31

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

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

	public _serviceBrand: any;

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

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

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

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

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

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

B
Benjamin Pasero 已提交
60 61 62 63 64 65 66 67
	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()));
	}

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

B
Benjamin Pasero 已提交
73 74 75 76 77 78 79 80
		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 {
81
		try {
82
			const workspace = this.doParseStoredWorkspace(path, contents);
83

84 85 86
			return {
				id: this.getWorkspaceId(path),
				configPath: path,
J
Johannes Rieken 已提交
87
				folders: toWorkspaceFolders(workspace.folders, URI.file(dirname(path)))
88
			};
89
		} catch (error) {
90 91 92 93 94 95 96
			this.logService.log(error.toString());
		}

		return null;
	}

	private doParseStoredWorkspace(path: string, contents: string): IStoredWorkspace {
97 98

		// Parse workspace file
99 100
		let storedWorkspace: IStoredWorkspace;
		try {
101
			storedWorkspace = json.parse(contents); // use fault tolerant parser
102 103 104
		} catch (error) {
			throw new Error(`${path} cannot be parsed as JSON file (${error}).`);
		}
105

106
		// Filter out folders which do not have a path or uri set
107
		if (Array.isArray(storedWorkspace.folders)) {
J
Johannes Rieken 已提交
108
			storedWorkspace.folders = storedWorkspace.folders.filter(folder => isStoredWorkspaceFolder(folder));
109 110 111
		}

		// Validate
112
		if (!Array.isArray(storedWorkspace.folders)) {
113
			throw new Error(`${path} looks like an invalid workspace file.`);
114
		}
115 116

		return storedWorkspace;
B
Benjamin Pasero 已提交
117 118
	}

119 120 121 122
	private isInsideWorkspacesHome(path: string): boolean {
		return isParent(path, this.environmentService.workspacesHome, !isLinux /* ignore case */);
	}

B
Benjamin Pasero 已提交
123
	public createWorkspace(folders: string[]): TPromise<IWorkspaceIdentifier> {
124 125 126 127 128 129 130 131 132 133 134 135
		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 已提交
136 137
		}

138 139 140 141 142 143 144 145
		mkdirSync(configParent);

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

		return workspace;
	}

	private createUntitledWorkspace(folders: string[]): { workspace: IWorkspaceIdentifier, configParent: string, storedWorkspace: IStoredWorkspace } {
146 147 148
		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 已提交
149

150 151 152 153 154
		const storedWorkspace: IStoredWorkspace = {
			folders: folders.map(folder => ({
				path: folder
			}))
		};
B
Benjamin Pasero 已提交
155

156 157
		return {
			workspace: {
158 159
				id: this.getWorkspaceId(untitledWorkspaceConfigPath),
				configPath: untitledWorkspaceConfigPath
160 161 162 163
			},
			configParent: untitledWorkspaceConfigFolder,
			storedWorkspace
		};
B
Benjamin Pasero 已提交
164 165
	}

166 167 168 169 170 171
	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 已提交
172
	}
B
Benjamin Pasero 已提交
173 174 175 176

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

178
	public saveWorkspace(workspace: IWorkspaceIdentifier, targetConfigPath: string): TPromise<IWorkspaceIdentifier> {
B
Benjamin Pasero 已提交
179 180

		// Return early if target is same as source
181
		if (isEqual(workspace.configPath, targetConfigPath, !isLinux)) {
B
Benjamin Pasero 已提交
182 183 184
			return TPromise.as(workspace);
		}

185
		// Read the contents of the workspace file and resolve it
186 187
		return readFile(workspace.configPath).then(raw => {
			const rawWorkspaceContents = raw.toString();
188 189
			let storedWorkspace: IStoredWorkspace;
			try {
190
				storedWorkspace = this.doParseStoredWorkspace(workspace.configPath, rawWorkspaceContents);
191 192 193 194 195 196 197 198 199 200 201
			} 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 => {
J
Johannes Rieken 已提交
202 203 204 205 206
				if (isRawFileWorkspaceFolder(folder)) {
					if (!isAbsolute(folder.path)) {
						folder.path = resolve(sourceConfigFolder, folder.path); // relative paths get resolved against the workspace location
					}
					folder.path = massageFolderPathForWorkspace(folder.path, targetConfigFolder, storedWorkspace.folders);
207 208
				}

209 210 211 212 213 214 215 216
			});

			// 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);
217
			});
218

219
			return writeFile(targetConfigPath, newRawWorkspaceContents).then(() => {
220
				const savedWorkspaceIdentifier = { id: this.getWorkspaceId(targetConfigPath), configPath: targetConfigPath };
221

222 223
				// Event
				this._onWorkspaceSaved.fire({ workspace: savedWorkspaceIdentifier, oldConfigPath: workspace.configPath });
224

225 226 227 228 229
				// Delete untitled workspace
				this.deleteUntitledWorkspaceSync(workspace);

				return savedWorkspaceIdentifier;
			});
230 231
		});
	}
232

233
	public deleteUntitledWorkspaceSync(workspace: IWorkspaceIdentifier): void {
234
		if (!this.isUntitledWorkspace(workspace)) {
235
			return; // only supported for untitled workspaces
236 237
		}

238
		// Delete from disk
239
		this.doDeleteUntitledWorkspaceSync(workspace.configPath);
240 241

		// Event
242
		this._onUntitledWorkspaceDeleted.fire(workspace);
243
	}
244 245 246 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

	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 已提交
274
}