debugConfigurationManager.ts 16.5 KB
Newer Older
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.
 *--------------------------------------------------------------------------------------------*/

import path = require('path');
import nls = require('vs/nls');
8
import { sequence } from 'vs/base/common/async';
I
isidor 已提交
9
import { TPromise } from 'vs/base/common/winjs.base';
I
isidor 已提交
10
import strings = require('vs/base/common/strings');
11
import types = require('vs/base/common/types');
I
isidor 已提交
12
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
13
import Event, { Emitter } from 'vs/base/common/event';
14
import objects = require('vs/base/common/objects');
15
import uri from 'vs/base/common/uri';
A
Alex Dima 已提交
16
import { Schemas } from 'vs/base/common/network';
17 18 19
import paths = require('vs/base/common/paths');
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import editor = require('vs/editor/common/editorCommon');
20
import extensionsRegistry = require('vs/platform/extensions/common/extensionsRegistry');
21
import platform = require('vs/platform/platform');
M
Martin Aeschlimann 已提交
22
import jsonContributionRegistry = require('vs/platform/jsonschemas/common/jsonContributionRegistry');
23 24 25
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IFileService } from 'vs/platform/files/common/files';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
26
import { IKeybindingService } from 'vs/platform/keybinding/common/keybindingService';
27 28 29 30 31
import debug = require('vs/workbench/parts/debug/common/debug');
import { SystemVariables } from 'vs/workbench/parts/lib/node/systemVariables';
import { Adapter } from 'vs/workbench/parts/debug/node/debugAdapter';
import { IWorkspaceContextService } from 'vs/workbench/services/workspace/common/contextService';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
32
import { IQuickOpenService } from 'vs/workbench/services/quickopen/common/quickOpenService';
33

I
isidor 已提交
34
// debuggers extension point
35

36
export var debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<debug.IRawAdapter[]>('debuggers', {
37 38
	description: nls.localize('vscode.extension.contributes.debuggers', 'Contributes debug adapters.'),
	type: 'array',
39
	defaultSnippets: [{ body: [{ type: '', extensions: [] }] }],
40 41
	items: {
		type: 'object',
42
		defaultSnippets: [{ body: { type: '', program: '', runtime: '', enableBreakpointsFor: { languageIds: [ '' ] } } }],
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
		properties: {
			type: {
				description: nls.localize('vscode.extension.contributes.debuggers.type', "Unique identifier for this debug adapter."),
				type: 'string'
			},
			label: {
				description: nls.localize('vscode.extension.contributes.debuggers.label', "Display name for this debug adapter."),
				type: 'string'
			},
			enableBreakpointsFor: {
				description: nls.localize('vscode.extension.contributes.debuggers.enableBreakpointsFor', "Allow breakpoints for these languages."),
				type: 'object',
				properties: {
					languageIds : {
						description: nls.localize('vscode.extension.contributes.debuggers.enableBreakpointsFor.languageIds', "List of languages."),
						type: 'array',
						items: {
							type: 'string'
						}
					}
				}
			},
			program: {
				description: nls.localize('vscode.extension.contributes.debuggers.program', "Path to the debug adapter program. Path is either absolute or relative to the extension folder."),
				type: 'string'
			},
69 70 71 72
			args: {
				description: nls.localize('vscode.extension.contributes.debuggers.args', "Optional arguments to pass to the adapter."),
				type: 'array'
			},
73 74 75 76 77 78 79 80
			runtime : {
				description: nls.localize('vscode.extension.contributes.debuggers.runtime', "Optional runtime in case the program attribute is not an executable but requires a runtime."),
				type: 'string'
			},
			runtimeArgs : {
				description: nls.localize('vscode.extension.contributes.debuggers.runtimeArgs', "Optional runtime arguments."),
				type: 'array'
			},
81 82 83 84
			variables : {
				description: nls.localize('vscode.extension.contributes.debuggers.variables', "Mapping from interactive variables (e.g ${action.pickProcess}) in `launch.json` to a command."),
				type: 'object'
			},
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
			initialConfigurations: {
				description: nls.localize('vscode.extension.contributes.debuggers.initialConfigurations', "Configurations for generating the initial \'launch.json\'."),
				type: 'array',
			},
			configurationAttributes: {
				description: nls.localize('vscode.extension.contributes.debuggers.configurationAttributes', "JSON schema configurations for validating \'launch.json\'."),
				type: 'object'
			},
			windows: {
				description: nls.localize('vscode.extension.contributes.debuggers.windows', "Windows specific settings."),
				type: 'object',
				properties: {
					runtime : {
						description: nls.localize('vscode.extension.contributes.debuggers.windows.runtime', "Runtime used for Windows."),
						type: 'string'
					}
				}
			},
			osx: {
				description: nls.localize('vscode.extension.contributes.debuggers.osx', "OS X specific settings."),
				type: 'object',
				properties: {
					runtime : {
						description: nls.localize('vscode.extension.contributes.debuggers.osx.runtime', "Runtime used for OSX."),
						type: 'string'
					}
				}
			},
			linux: {
				description: nls.localize('vscode.extension.contributes.debuggers.linux', "Linux specific settings."),
				type: 'object',
				properties: {
					runtime : {
						description: nls.localize('vscode.extension.contributes.debuggers.linux.runtime', "Runtime used for Linux."),
						type: 'string'
					}
				}
			}
		}
	}
});

I
isidor 已提交
127
// debug general schema
128

129
export var schemaId = 'vscode://schemas/launch';
I
isidor 已提交
130
const schema: IJSONSchema = {
131 132
	id: schemaId,
	type: 'object',
133
	title: nls.localize('app.launch.json.title', "Launch"),
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
	required: ['version', 'configurations'],
	properties: {
		version: {
			type: 'string',
			description: nls.localize('app.launch.json.version', "Version of this file format."),
			default: '0.2.0'
		},
		configurations: {
			type: 'array',
			description: nls.localize('app.launch.json.configurations', "List of configurations. Add new configurations or edit existing ones."),
			items: {
				oneOf: []
			}
		}
	}
I
isidor 已提交
149
};
150

I
isidor 已提交
151
const jsonRegistry = <jsonContributionRegistry.IJSONContributionRegistry>platform.Registry.as(jsonContributionRegistry.Extensions.JSONContribution);
152 153
jsonRegistry.registerSchema(schemaId, schema);

154
export class ConfigurationManager implements debug.IConfigurationManager {
155

156
	public configuration: debug.IConfig;
157 158 159
	private systemVariables: SystemVariables;
	private adapters: Adapter[];
	private allModeIdsForBreakpoints: { [key: string]: boolean };
160
	private _onDidConfigurationChange: Emitter<string>;
161 162 163 164 165 166 167 168

	constructor(
		configName: string,
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
		@IFileService private fileService: IFileService,
		@ITelemetryService private telemetryService: ITelemetryService,
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
		@IConfigurationService private configurationService: IConfigurationService,
169
		@IQuickOpenService private quickOpenService: IQuickOpenService,
170
		@IKeybindingService private keybindingService: IKeybindingService
171
	) {
172
		this._onDidConfigurationChange = new Emitter<string>();
173
		this.systemVariables = this.contextService.getWorkspace() ? new SystemVariables(this.editorService, this.contextService) : null;
174
		this.setConfiguration(configName);
175 176 177 178 179 180 181 182 183 184
		this.adapters = [];
		this.registerListeners();
		this.allModeIdsForBreakpoints = {};
	}

	private registerListeners(): void {
		debuggersExtPoint.setHandler((extensions) => {

			extensions.forEach(extension => {
				extension.value.forEach(rawAdapter => {
185
					const adapter = new Adapter(rawAdapter, this.systemVariables, extension.description);
186 187 188 189 190 191 192 193 194 195
					const duplicate = this.adapters.filter(a => a.type === adapter.type)[0];
					if (!rawAdapter.type || (typeof rawAdapter.type !== 'string')) {
						extension.collector.error(nls.localize('debugNoType', "Debug adapter 'type' can not be omitted and must be of type 'string'."));
					}

					if (duplicate) {
						Object.keys(adapter).forEach(attribute => {
							if (adapter[attribute]) {
								if (attribute === 'enableBreakpointsFor') {
									Object.keys(adapter.enableBreakpointsFor).forEach(languageId => duplicate.enableBreakpointsFor[languageId] = true);
196
								} else if (duplicate[attribute] && attribute !== 'type' && attribute !== 'extensionDescription') {
I
isidor 已提交
197
									// give priority to the later registered extension.
198
									duplicate[attribute] = adapter[attribute];
199
									extension.collector.error(nls.localize('duplicateDebuggerType', "Debug type '{0}' is already registered and has attribute '{1}', ignoring attribute '{1}'.", adapter.type, attribute));
200 201 202 203 204 205 206 207 208
								} else {
									duplicate[attribute] = adapter[attribute];
								}
							}
						});
					} else {
						this.adapters.push(adapter);
					}

I
isidor 已提交
209 210 211 212 213
					if (adapter.enableBreakpointsFor) {
						adapter.enableBreakpointsFor.languageIds.forEach(modeId => {
							this.allModeIdsForBreakpoints[modeId] = true;
						});
					}
214 215 216
				});
			});

I
isidor 已提交
217
			// update the schema to include all attributes and types from extensions.
218 219 220 221
			// debug.schema.properties['configurations'].items.properties.type.enum = this.adapters.map(adapter => adapter.type);
			this.adapters.forEach(adapter => {
				const schemaAttributes = adapter.getSchemaAttributes();
				if (schemaAttributes) {
222
					(<IJSONSchema> schema.properties['configurations'].items).oneOf.push(...schemaAttributes);
223 224 225 226 227
				}
			});
		});
	}

228 229
	public get onDidConfigurationChange(): Event<string> {
		return this._onDidConfigurationChange.event;
230 231
	}

232
	public get configurationName(): string {
233 234 235
		return this.configuration ? this.configuration.name : null;
	}

236
	public get adapter(): Adapter {
I
isidor 已提交
237 238 239 240
		if (!this.configuration || !this.configuration.type) {
			return null;
		}

I
isidor 已提交
241
		return this.adapters.filter(adapter => strings.equalsIgnoreCase(adapter.type, this.configuration.type)).pop();
242 243
	}

244
	/**
I
isidor 已提交
245
	 * Resolve all interactive variables in configuration #6569
246 247
	 */
	public resolveInteractiveVariables(): TPromise<debug.IConfig>  {
I
isidor 已提交
248 249 250 251
		if (!this.configuration) {
			return TPromise.as(null);
		}

252
		// We need a map from interactive variables to keys because we only want to trigger an command once per key -
253 254 255 256
		// even though it might occure multiple times in configuration #7026.
		const interactiveVariablesToKeys: { [key: string]: string[] } = {};
		Object.keys(this.configuration).forEach(key => {
			if (typeof this.configuration[key] === 'string') {
257
				const matches = /\${command.(.+)}/.exec(this.configuration[key]);
258 259 260 261
				if (matches && matches.length === 2) {
					const interactiveVariable = matches[1];
					if (!interactiveVariablesToKeys[interactiveVariable]) {
						interactiveVariablesToKeys[interactiveVariable] = [];
262
					}
263
					interactiveVariablesToKeys[interactiveVariable].push(key);
264
				}
265 266
			}
		});
267

268 269 270 271 272 273 274 275 276 277 278 279 280
		const factory: { (): TPromise<any> }[] = Object.keys(interactiveVariablesToKeys).map(interactiveVariable => {
			return () => {
				const commandId = this.adapter.variables ? this.adapter.variables[interactiveVariable] : null;
				if (!commandId) {
					return TPromise.wrapError(nls.localize('interactiveVariableNotFound', "Adapter {0} does not contribute variable {1} that is specified in launch configuration.", this.adapter.type, interactiveVariable));
				} else {
					return this.keybindingService.executeCommand<string>(commandId, this.configuration).then(result => {
						if (!result) {
							this.configuration.silentlyAbort = true;
						}
						interactiveVariablesToKeys[interactiveVariable].forEach(key => this.configuration[key] = result);
					});
				}
281
			};
282 283
		});

284
		return sequence(factory).then(() => this.configuration);
285 286
	}

287
	public setConfiguration(nameOrConfig: string|debug.IConfig): TPromise<void> {
288
		return this.loadLaunchConfig().then(config => {
289 290 291 292 293 294 295
			if (types.isObject(nameOrConfig)) {
				this.configuration = objects.deepClone(nameOrConfig) as debug.IConfig;
			} else {
				if (!config || !config.configurations) {
					this.configuration = null;
					return;
				}
296 297 298 299 300 301 302 303
				// if the configuration name is not set yet, take the first launch config (can happen if debug viewlet has not been opened yet).
				const filtered = nameOrConfig ? config.configurations.filter(cfg => cfg.name === nameOrConfig) : [config.configurations[0]];

				this.configuration = filtered.length === 1 ? objects.deepClone(filtered[0]) : null;
				if (config && this.configuration) {
					this.configuration.debugServer = config.debugServer;
				}
			}
304

I
isidor 已提交
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
			if (this.configuration) {
				// Set operating system specific properties #1873
				if (isWindows && this.configuration.windows) {
					Object.keys(this.configuration.windows).forEach(key => {
						this.configuration[key] = this.configuration.windows[key];
					});
				}
				if (isMacintosh && this.configuration.osx) {
					Object.keys(this.configuration.osx).forEach(key => {
						this.configuration[key] = this.configuration.osx[key];
					});
				}
				if (isLinux && this.configuration.linux) {
					Object.keys(this.configuration.linux).forEach(key => {
						this.configuration[key] = this.configuration.linux[key];
					});
				}
322

I
isidor 已提交
323 324 325 326 327 328
				// massage configuration attributes - append workspace path to relatvie paths, substitute variables in paths.
				if (this.systemVariables) {
					Object.keys(this.configuration).forEach(key => {
						this.configuration[key] = this.systemVariables.resolveAny(this.configuration[key]);
					});
				}
329
			}
330
		}).then(() => this._onDidConfigurationChange.fire(this.configurationName));
331 332 333 334 335 336 337 338 339 340 341
	}

	public openConfigFile(sideBySide: boolean): TPromise<boolean> {
		const resource = uri.file(paths.join(this.contextService.getWorkspace().resource.fsPath, '/.vscode/launch.json'));

		return this.fileService.resolveContent(resource).then(content => true, err =>
			this.getInitialConfigFileContent().then(content => {
				if (!content) {
					return false;
				}

342
				return this.fileService.updateContent(resource, content).then(() => true);
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
			}
		)).then(configFileCreated => {
			if (!configFileCreated) {
				return false;
			}
			this.telemetryService.publicLog('debugConfigure');

			return this.editorService.openEditor({
				resource: resource,
				options: {
					forceOpen: true
				}
			}, sideBySide).then(() => true);
		}, (error) => {
			throw new Error(nls.localize('DebugConfig.failed', "Unable to create 'launch.json' file inside the '.vscode' folder ({0}).", error));
		});
	}

	private getInitialConfigFileContent(): TPromise<string> {
I
isidor 已提交
362
		return this.quickOpenService.pick(this.adapters, { placeHolder: nls.localize('selectDebug', "Select Environment") })
363 364 365 366 367
		.then(adapter => {
			if (!adapter) {
				return null;
			}

368 369 370 371 372 373 374 375
			return this.massageInitialConfigurations(adapter).then(() => {
				let editorConfig = this.configurationService.getConfiguration<any>();
				return JSON.stringify(
					{
						version: '0.2.0',
						configurations: adapter.initialConfigurations ? adapter.initialConfigurations : []
					},
					null,
376
					editorConfig.editor.insertSpaces ? strings.repeat(' ', editorConfig.editor.tabSize) : '\t');
377
			});
378 379 380
		});
	}

I
isidor 已提交
381
	private massageInitialConfigurations(adapter: Adapter): TPromise<void> {
382
		if (!adapter || !adapter.initialConfigurations || adapter.type !== 'node') {
I
isidor 已提交
383
			return TPromise.as(undefined);
384 385
		}

I
isidor 已提交
386
		// check package.json for 'main' or 'scripts' so we generate a more pecise 'program' attribute in launch.json.
387 388 389 390 391 392 393 394 395 396 397 398 399
		const packageJsonUri = uri.file(paths.join(this.contextService.getWorkspace().resource.fsPath, '/package.json'));
		return this.fileService.resolveContent(packageJsonUri).then(jsonContent => {
			try {
				const jsonObject = JSON.parse(jsonContent.value);
				if (jsonObject.main) {
					return jsonObject.main;
				} else if (jsonObject.scripts && typeof jsonObject.scripts.start === 'string') {
					return (<string>jsonObject.scripts.start).split(' ').pop();
				}

			} catch (error) { }

			return null;
400
		}, err => null).then((program: string) => {
401
			adapter.initialConfigurations.forEach(config => {
I
isidor 已提交
402
				if (program && config.program) {
403
					if (!path.isAbsolute(program)) {
I
isidor 已提交
404
						program = paths.join('${workspaceRoot}', program);
405 406
					}

I
isidor 已提交
407
					config.program = program;
408 409 410 411 412
				}
			});
		});
	}

I
isidor 已提交
413
	public canSetBreakpointsIn(model: editor.IModel): boolean {
414
		if (model.uri.scheme === Schemas.inMemory) {
415 416 417
			return false;
		}

I
isidor 已提交
418 419
		const mode = model ? model.getMode() : null;
		const modeId = mode ? mode.getId() : null;
420 421 422 423 424

		return !!this.allModeIdsForBreakpoints[modeId];
	}

	public loadLaunchConfig(): TPromise<debug.IGlobalConfig> {
425
		return TPromise.as(this.configurationService.getConfiguration<debug.IGlobalConfig>('launch'));
426 427
	}
}