debugConfigurationManager.ts 12.8 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');
I
isidor 已提交
8
import { TPromise } from 'vs/base/common/winjs.base';
9
import objects = require('vs/base/common/objects');
10
import uri from 'vs/base/common/uri';
A
Alex Dima 已提交
11
import { Schemas } from 'vs/base/common/network';
12 13 14
import paths = require('vs/base/common/paths');
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import editor = require('vs/editor/common/editorCommon');
15
import extensionsRegistry = require('vs/platform/extensions/common/extensionsRegistry');
16
import platform = require('vs/platform/platform');
M
Martin Aeschlimann 已提交
17
import jsonContributionRegistry = require('vs/platform/jsonschemas/common/jsonContributionRegistry');
18 19 20
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IFileService } from 'vs/platform/files/common/files';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
21 22 23 24 25
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';
26
import { IQuickOpenService } from 'vs/workbench/services/quickopen/common/quickOpenService';
27

I
isidor 已提交
28
// debuggers extension point
29

30
export var debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<debug.IRawAdapter[]>('debuggers', {
31 32
	description: nls.localize('vscode.extension.contributes.debuggers', 'Contributes debug adapters.'),
	type: 'array',
33
	defaultSnippets: [{ body: [{ type: '', extensions: [] }] }],
34 35
	items: {
		type: 'object',
36
		defaultSnippets: [{ body: { type: '', program: '', runtime: '', enableBreakpointsFor: { languageIds: [ '' ] } } }],
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
		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'
			},
63 64 65 66
			args: {
				description: nls.localize('vscode.extension.contributes.debuggers.args', "Optional arguments to pass to the adapter."),
				type: 'array'
			},
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 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
			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'
			},
			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 已提交
117
// debug general schema
118

119
export var schemaId = 'vscode://schemas/launch';
I
isidor 已提交
120
const schema: IJSONSchema = {
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
	id: schemaId,
	type: 'object',
	title: nls.localize('app.launch.json.title', "Launch configuration"),
	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 已提交
139
};
140

I
isidor 已提交
141
const jsonRegistry = <jsonContributionRegistry.IJSONContributionRegistry>platform.Registry.as(jsonContributionRegistry.Extensions.JSONContribution);
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
jsonRegistry.registerSchema(schemaId, schema);
jsonRegistry.addSchemaFileAssociation('/.vscode/launch.json', schemaId);

export class ConfigurationManager {

	private configuration: debug.IConfig;
	private systemVariables: SystemVariables;
	private adapters: Adapter[];
	private allModeIdsForBreakpoints: { [key: string]: boolean };

	constructor(
		configName: string,
		@IWorkspaceContextService private contextService: IWorkspaceContextService,
		@IFileService private fileService: IFileService,
		@ITelemetryService private telemetryService: ITelemetryService,
		@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
		@IConfigurationService private configurationService: IConfigurationService,
159
		@IQuickOpenService private quickOpenService: IQuickOpenService
160
	) {
161
		this.systemVariables = this.contextService.getWorkspace() ? new SystemVariables(this.editorService, this.contextService) : null;
162
		this.setConfiguration(configName);
163 164 165 166 167 168 169 170 171 172 173 174 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 => {
					const adapter = new Adapter(rawAdapter, this.systemVariables, extension.description.extensionFolderPath);
					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);
								} else if (duplicate[attribute] && attribute !== 'type') {
I
isidor 已提交
185
									// give priority to the later registered extension.
186
									duplicate[attribute] = adapter[attribute];
187
									extension.collector.error(nls.localize('duplicateDebuggerType', "Debug type '{0}' is already registered and has attribute '{1}', ignoring attribute '{1}'.", adapter.type, attribute));
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
								} else {
									duplicate[attribute] = adapter[attribute];
								}
							}
						});
					} else {
						this.adapters.push(adapter);
					}

					adapter.enableBreakpointsFor.languageIds.forEach(modeId => {
						this.allModeIdsForBreakpoints[modeId] = true;
					});
				});
			});

I
isidor 已提交
203
			// update the schema to include all attributes and types from extensions.
204 205 206 207
			// debug.schema.properties['configurations'].items.properties.type.enum = this.adapters.map(adapter => adapter.type);
			this.adapters.forEach(adapter => {
				const schemaAttributes = adapter.getSchemaAttributes();
				if (schemaAttributes) {
208
					(<IJSONSchema> schema.properties['configurations'].items).oneOf.push(...schemaAttributes);
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
				}
			});
		});
	}

	public getConfiguration(): debug.IConfig {
		return this.configuration;
	}

	public getConfigurationName(): string {
		return this.configuration ? this.configuration.name : null;
	}

	public getAdapter(): Adapter {
		return this.adapters.filter(adapter => adapter.type === this.configuration.type).pop();
	}

I
isidor 已提交
226
	public setConfiguration(name: string): TPromise<void> {
227 228 229 230 231 232
		return this.loadLaunchConfig().then(config => {
			if (!config || !config.configurations) {
				this.configuration = null;
				return;
			}

I
isidor 已提交
233
			// if the configuration name is not set yet, take the first launch config (can happen if debug viewlet has not been opened yet).
234 235
			const filtered = name ? config.configurations.filter(cfg => cfg.name === name) : [config.configurations[0]];

I
isidor 已提交
236
			// massage configuration attributes - append workspace path to relatvie paths, substitute variables in paths.
237
			this.configuration = filtered.length === 1 ? objects.deepClone(filtered[0]) : null;
238 239 240
			if (this.configuration) {
				if (this.systemVariables) {
					Object.keys(this.configuration).forEach(key => {
I
isidor 已提交
241
						this.configuration[key] = this.systemVariables.resolveAny(this.configuration[key]);
242 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 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
				this.configuration.debugServer = config.debugServer;
			}
		});
	}

	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;
				}

				return this.fileService.updateContent(resource, content).then(() => true);
			}
		)).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> {
		return this.quickOpenService.pick(this.adapters, { placeHolder: nls.localize('selectDebug', "Select Debug Environment") })
		.then(adapter => {
			if (!adapter) {
				return null;
			}

			return this.massageInitialConfigurations(adapter).then(() =>
				JSON.stringify({
					version: '0.2.0',
					configurations: adapter.initialConfigurations ? adapter.initialConfigurations : []
				}, null, '\t')
I
isidor 已提交
289
			);
290 291 292
		});
	}

I
isidor 已提交
293
	private massageInitialConfigurations(adapter: Adapter): TPromise<void> {
294
		if (!adapter || !adapter.initialConfigurations || adapter.type !== 'node') {
I
isidor 已提交
295
			return TPromise.as(undefined);
296 297
		}

I
isidor 已提交
298
		// check package.json for 'main' or 'scripts' so we generate a more pecise 'program' attribute in launch.json.
299 300 301 302 303 304 305 306 307 308 309 310 311
		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;
312
		}, err => null).then((program: string) => {
313
			adapter.initialConfigurations.forEach(config => {
I
isidor 已提交
314
				if (program && config.program) {
315
					if (!path.isAbsolute(program)) {
I
isidor 已提交
316
						program = path.join('${workspaceRoot}', program);
317 318
					}

I
isidor 已提交
319
					config.program = program;
320 321 322 323 324
				}
			});
		});
	}

I
isidor 已提交
325
	public canSetBreakpointsIn(model: editor.IModel): boolean {
A
Alex Dima 已提交
326
		if (model.getAssociatedResource().scheme === Schemas.inMemory) {
327 328 329
			return false;
		}

I
isidor 已提交
330 331
		const mode = model ? model.getMode() : null;
		const modeId = mode ? mode.getId() : null;
332 333 334 335 336 337 338 339

		return !!this.allModeIdsForBreakpoints[modeId];
	}

	public loadLaunchConfig(): TPromise<debug.IGlobalConfig> {
		return this.configurationService.loadConfiguration('launch');
	}
}