issueReporterMain.ts 39.0 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

B
Benjamin Pasero 已提交
6
import 'vs/css!./media/issueReporter';
7
import { shell, ipcRenderer, webFrame, clipboard } from 'electron';
8
import { localize } from 'vs/nls';
9
import { $ } from 'vs/base/browser/dom';
10
import * as collections from 'vs/base/common/collections';
11
import * as browser from 'vs/base/browser/browser';
12
import { escape } from 'vs/base/common/strings';
13 14
import product from 'vs/platform/product/node/product';
import pkg from 'vs/platform/product/node/package';
15
import * as os from 'os';
16 17
import { debounce } from 'vs/base/common/decorators';
import * as platform from 'vs/base/common/platform';
18
import { Disposable } from 'vs/base/common/lifecycle';
J
Joao Moreno 已提交
19
import { getDelayedChannel } from 'vs/base/parts/ipc/node/ipc';
20 21 22
import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IWindowConfiguration, IWindowsService } from 'vs/platform/windows/common/windows';
23
import { NullTelemetryService, combinedAppender, LogAppender } from 'vs/platform/telemetry/common/telemetryUtils';
24 25
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
J
Joao Moreno 已提交
26
import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc';
27 28 29
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties';
30 31
import { WindowsService } from 'vs/platform/windows/electron-browser/windowsService';
import { MainProcessService, IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
32
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
33
import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from 'vs/code/electron-browser/issue/issueReporterModel';
34
import { IssueReporterData, IssueReporterStyles, IssueType, ISettingsSearchIssueReporterData, IssueReporterFeatures, IssueReporterExtensionData } from 'vs/platform/issue/common/issue';
35
import BaseHtml from 'vs/code/electron-browser/issue/issueReporterPage';
36
import { createSpdLogService } from 'vs/platform/log/node/spdlogService';
J
Joao Moreno 已提交
37
import { LogLevelSetterChannelClient, FollowerLogService } from 'vs/platform/log/node/logIpc';
38
import { ILogService, getLogLevel } from 'vs/platform/log/common/log';
39
import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel';
40
import { normalizeGitHubUrl } from 'vs/code/electron-browser/issue/issueReporterUtil';
41
import { Button } from 'vs/base/browser/ui/button/button';
42
import { withUndefinedAsNull } from 'vs/base/common/types';
43

44
const MAX_URL_LENGTH = platform.isWindows ? 2081 : 5400;
45

46 47 48
interface SearchResult {
	html_url: string;
	title: string;
49
	state?: string;
50 51
}

52 53
export interface IssueReporterConfiguration extends IWindowConfiguration {
	data: IssueReporterData;
54
	features: IssueReporterFeatures;
55 56 57
}

export function startup(configuration: IssueReporterConfiguration) {
58
	document.body.innerHTML = BaseHtml();
59
	const issueReporter = new IssueReporter(configuration);
60 61
	issueReporter.render();
	document.body.style.display = 'block';
62 63
}

64
export class IssueReporter extends Disposable {
65 66
	private environmentService: IEnvironmentService;
	private telemetryService: ITelemetryService;
67
	private logService: ILogService;
68
	private readonly issueReporterModel: IssueReporterModel;
69
	private numberOfSearchResultsDisplayed = 0;
70 71
	private receivedSystemInfo = false;
	private receivedPerformanceInfo = false;
72
	private shouldQueueSearch = false;
73
	private hasBeenSubmitted = false;
74

75
	private readonly previewButton: Button;
76

77
	constructor(configuration: IssueReporterConfiguration) {
78
		super();
R
Rachel Macfarlane 已提交
79

80 81
		this.initServices(configuration);

R
Rachel Macfarlane 已提交
82
		this.issueReporterModel = new IssueReporterModel({
83
			issueType: configuration.data.issueType || IssueType.Bug,
84 85 86
			versionInfo: {
				vscodeVersion: `${pkg.name} ${pkg.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`,
				os: `${os.type()} ${os.arch()} ${os.release()}`
87
			},
S
Sandeep Somavarapu 已提交
88
			extensionsDisabled: !!this.environmentService.disableExtensions,
R
Rachel Macfarlane 已提交
89
		});
90

91 92 93 94
		const issueReporterElement = this.getElementById('issue-reporter');
		if (issueReporterElement) {
			this.previewButton = new Button(issueReporterElement);
		}
95

96
		ipcRenderer.on('vscode:issuePerformanceInfoResponse', (_: unknown, info: Partial<IssueReporterData>) => {
97
			this.logService.trace('issueReporter: Received performance data');
R
Rachel Macfarlane 已提交
98
			this.issueReporterModel.update(info);
99
			this.receivedPerformanceInfo = true;
100

101 102 103 104 105
			const state = this.issueReporterModel.getData();
			this.updateProcessInfo(state);
			this.updateWorkspaceInfo(state);
			this.updatePreviewButtonState();
		});
106

107
		ipcRenderer.on('vscode:issueSystemInfoResponse', (_: unknown, info: any) => {
108
			this.logService.trace('issueReporter: Received system data');
109 110 111 112 113
			this.issueReporterModel.update({ systemInfo: info });
			this.receivedSystemInfo = true;

			this.updateSystemInfo(this.issueReporterModel.getData());
			this.updatePreviewButtonState();
114
		});
115

116
		ipcRenderer.send('vscode:issueSystemInfoRequest');
117
		if (configuration.data.issueType === IssueType.PerformanceIssue) {
118
			ipcRenderer.send('vscode:issuePerformanceInfoRequest');
119
		}
120
		this.logService.trace('issueReporter: Sent data requests');
121

122
		if (window.document.documentElement.lang !== 'en') {
123
			show(this.getElementById('english'));
124
		}
125

126
		this.setUpTypes();
127
		this.setEventHandlers();
128 129 130
		this.applyZoom(configuration.data.zoomLevel);
		this.applyStyles(configuration.data.styles);
		this.handleExtensionData(configuration.data.enabledExtensions);
131 132 133 134

		if (configuration.data.issueType === IssueType.SettingsSearchIssue) {
			this.handleSettingsSearchData(<ISettingsSearchIssueReporterData>configuration.data);
		}
135 136 137 138 139 140
	}

	render(): void {
		this.renderBlocks();
	}

141 142 143 144 145 146 147 148 149
	private applyZoom(zoomLevel: number) {
		webFrame.setZoomLevel(zoomLevel);
		browser.setZoomFactor(webFrame.getZoomFactor());
		// See https://github.com/Microsoft/vscode/issues/26151
		// Cannot be trusted because the webFrame might take some time
		// until it really applies the new zoom level
		browser.setZoomLevel(webFrame.getZoomLevel(), /*isTrusted*/false);
	}

150 151 152 153 154
	private applyStyles(styles: IssueReporterStyles) {
		const styleTag = document.createElement('style');
		const content: string[] = [];

		if (styles.inputBackground) {
155
			content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state, .block-info { background-color: ${styles.inputBackground}; }`);
156 157 158
		}

		if (styles.inputBorder) {
R
Rachel Macfarlane 已提交
159
			content.push(`input[type="text"], textarea, select { border: 1px solid ${styles.inputBorder}; }`);
160
		} else {
R
Rachel Macfarlane 已提交
161
			content.push(`input[type="text"], textarea, select { border: 1px solid transparent; }`);
162 163 164
		}

		if (styles.inputForeground) {
165
			content.push(`input[type="text"], textarea, select, .issues-container > .issue > .issue-state, .block-info { color: ${styles.inputForeground}; }`);
166 167 168
		}

		if (styles.inputErrorBorder) {
169
			content.push(`.invalid-input, .invalid-input:focus { border: 1px solid ${styles.inputErrorBorder} !important; }`);
170
			content.push(`.validation-error, .required-input { color: ${styles.inputErrorBorder}; }`);
171 172 173
		}

		if (styles.inputActiveBorder) {
174
			content.push(`input[type='text']:focus, textarea:focus, select:focus, summary:focus, button:focus, a:focus, .workbenchCommand:focus  { border: 1px solid ${styles.inputActiveBorder}; outline-style: none; }`);
175 176 177
		}

		if (styles.textLinkColor) {
178
			content.push(`a, .workbenchCommand { color: ${styles.textLinkColor}; }`);
179 180 181 182 183 184
		}

		if (styles.textLinkColor) {
			content.push(`a { color: ${styles.textLinkColor}; }`);
		}

185 186 187 188
		if (styles.textLinkActiveForeground) {
			content.push(`a:hover, .workbenchCommand:hover { color: ${styles.textLinkActiveForeground}; }`);
		}

189
		if (styles.sliderBackgroundColor) {
E
EbXpJ6bp 已提交
190
			content.push(`::-webkit-scrollbar-thumb { background-color: ${styles.sliderBackgroundColor}; }`);
191 192 193
		}

		if (styles.sliderActiveColor) {
E
EbXpJ6bp 已提交
194
			content.push(`::-webkit-scrollbar-thumb:active { background-color: ${styles.sliderActiveColor}; }`);
195 196 197
		}

		if (styles.sliderHoverColor) {
E
EbXpJ6bp 已提交
198
			content.push(`::--webkit-scrollbar-thumb:hover { background-color: ${styles.sliderHoverColor}; }`);
199 200
		}

201 202 203 204 205 206 207 208 209
		if (styles.buttonBackground) {
			content.push(`.monaco-text-button { background-color: ${styles.buttonBackground} !important; }`);
		}

		if (styles.buttonForeground) {
			content.push(`.monaco-text-button { color: ${styles.buttonForeground} !important; }`);
		}

		if (styles.buttonHoverBackground) {
210
			content.push(`.monaco-text-button:hover, .monaco-text-button:focus { background-color: ${styles.buttonHoverBackground} !important; }`);
211 212
		}

213 214
		styleTag.innerHTML = content.join('\n');
		document.head.appendChild(styleTag);
215
		document.body.style.color = withUndefinedAsNull(styles.color);
216 217
	}

218
	private handleExtensionData(extensions: IssueReporterExtensionData[]) {
219
		const { nonThemes, themes } = collections.groupBy(extensions, ext => {
220
			return ext.isTheme ? 'themes' : 'nonThemes';
221 222 223
		});

		const numberOfThemeExtesions = themes && themes.length;
224
		this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: extensions });
225
		this.updateExtensionTable(nonThemes, numberOfThemeExtesions);
226 227

		if (this.environmentService.disableExtensions || extensions.length === 0) {
228
			(<HTMLButtonElement>this.getElementById('disableExtensions')).disabled = true;
229
		}
230 231

		this.updateExtensionSelector(extensions);
232 233
	}

234 235 236 237 238 239 240 241 242 243 244 245
	private handleSettingsSearchData(data: ISettingsSearchIssueReporterData): void {
		this.issueReporterModel.update({
			actualSearchResults: data.actualSearchResults,
			query: data.query,
			filterResultCount: data.filterResultCount
		});
		this.updateSearchedExtensionTable(data.enabledExtensions);
		this.updateSettingsSearchDetails(data);
	}

	private updateSettingsSearchDetails(data: ISettingsSearchIssueReporterData): void {
		const target = document.querySelector('.block-settingsSearchResults .block-info');
246 247
		if (target) {
			const details = `
248 249 250 251
			<div class='block-settingsSearchResults-details'>
				<div>Query: "${data.query}"</div>
				<div>Literal match count: ${data.filterResultCount}</div>
			</div>
252
			`;
253

254 255 256 257 258 259
			let table = `
				<tr>
					<th>Setting</th>
					<th>Extension</th>
					<th>Score</th>
				</tr>`;
260

261 262 263 264 265 266 267 268 269
			data.actualSearchResults
				.forEach(setting => {
					table += `
						<tr>
							<td>${setting.key}</td>
							<td>${setting.extensionId}</td>
							<td>${String(setting.score).slice(0, 5)}</td>
						</tr>`;
				});
270

271 272
			target.innerHTML = `${details}<table>${table}</table>`;
		}
273 274
	}

275
	private initServices(configuration: IWindowConfiguration): void {
276
		const serviceCollection = new ServiceCollection();
277 278
		const mainProcessService = new MainProcessService(configuration.windowId);
		serviceCollection.set(IMainProcessService, mainProcessService);
279

280
		serviceCollection.set(IWindowsService, new WindowsService(mainProcessService));
281 282
		this.environmentService = new EnvironmentService(configuration, configuration.execPath);

283
		const logService = createSpdLogService(`issuereporter${configuration.windowId}`, getLogLevel(this.environmentService), this.environmentService.logsPath);
284
		const logLevelClient = new LogLevelSetterChannelClient(mainProcessService.getChannel('loglevel'));
285 286
		this.logService = new FollowerLogService(logLevelClient, logService);

287 288 289 290
		const sharedProcess = (<IWindowsService>serviceCollection.get(IWindowsService)).whenSharedProcessReady()
			.then(() => connectNet(this.environmentService.sharedIPCHandle, `window:${configuration.windowId}`));

		const instantiationService = new InstantiationService(serviceCollection, true);
291
		if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) {
J
Joao Moreno 已提交
292
			const channel = getDelayedChannel(sharedProcess.then(c => c.getChannel('telemetryAppender')));
293
			const appender = combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(logService));
294
			const commonProperties = resolveCommonProperties(product.commit || 'Commit unknown', pkg.version, configuration.machineId, this.environmentService.installSourcePath);
295 296 297 298
			const piiPaths = [this.environmentService.appRoot, this.environmentService.extensionsPath];
			const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths };

			const telemetryService = instantiationService.createInstance(TelemetryService, config);
299 300
			this._register(telemetryService);

301 302 303 304
			this.telemetryService = telemetryService;
		} else {
			this.telemetryService = NullTelemetryService;
		}
305 306
	}

307
	private setEventHandlers(): void {
R
Rachel Macfarlane 已提交
308
		this.addEventListener('issue-type', 'change', (event: Event) => {
309 310 311
			const issueType = parseInt((<HTMLInputElement>event.target).value);
			this.issueReporterModel.update({ issueType: issueType });
			if (issueType === IssueType.PerformanceIssue && !this.receivedPerformanceInfo) {
312
				ipcRenderer.send('vscode:issuePerformanceInfoRequest');
313
			}
314
			this.updatePreviewButtonState();
R
Fixes  
Rachel Macfarlane 已提交
315
			this.setSourceOptions();
316 317 318
			this.render();
		});

319
		['includeSystemInfo', 'includeProcessInfo', 'includeWorkspaceInfo', 'includeExtensions', 'includeSearchedExtensions', 'includeSettingsSearchDetails'].forEach(elementId => {
R
Rachel Macfarlane 已提交
320
			this.addEventListener(elementId, 'click', (event: Event) => {
321 322 323
				event.stopPropagation();
				this.issueReporterModel.update({ [elementId]: !this.issueReporterModel.getData()[elementId] });
			});
R
Rachel Macfarlane 已提交
324 325
		});

326 327 328
		const showInfoElements = document.getElementsByClassName('showInfo');
		for (let i = 0; i < showInfoElements.length; i++) {
			const showInfo = showInfoElements.item(i);
329
			showInfo!.addEventListener('click', (e) => {
330
				e.preventDefault();
331
				const label = (<HTMLDivElement>e.target);
332 333 334 335 336 337 338 339 340 341
				if (label) {
					const containingElement = label.parentElement && label.parentElement.parentElement;
					const info = containingElement && containingElement.lastElementChild;
					if (info && info.classList.contains('hidden')) {
						show(info);
						label.textContent = localize('hide', "hide");
					} else {
						hide(info);
						label.textContent = localize('show', "show");
					}
342 343 344 345
				}
			});
		}

346
		this.addEventListener('issue-source', 'change', (e: Event) => {
R
Fixes  
Rachel Macfarlane 已提交
347 348 349
			const value = (<HTMLInputElement>e.target).value;
			const problemSourceHelpText = this.getElementById('problem-source-help-text')!;
			if (value === '') {
350
				this.issueReporterModel.update({ fileOnExtension: undefined });
R
Fixes  
Rachel Macfarlane 已提交
351 352 353 354 355 356 357 358 359
				show(problemSourceHelpText);
				this.clearSearchResults();
				this.render();
				return;
			} else {
				hide(problemSourceHelpText);
			}

			const fileOnExtension = JSON.parse(value);
360
			this.issueReporterModel.update({ fileOnExtension: fileOnExtension });
361
			this.render();
362

363
			const title = (<HTMLInputElement>this.getElementById('issue-title')).value;
364 365 366 367 368 369
			if (fileOnExtension) {
				this.searchExtensionIssues(title);
			} else {
				const description = this.issueReporterModel.getData().issueDescription;
				this.searchVSCodeIssues(title, description);
			}
R
Rachel Macfarlane 已提交
370 371
		});

372 373
		this.addEventListener('description', 'input', (e: Event) => {
			const issueDescription = (<HTMLInputElement>e.target).value;
374
			this.issueReporterModel.update({ issueDescription });
375 376

			// Only search for extension issues on title change
R
Fixes  
Rachel Macfarlane 已提交
377
			if (this.issueReporterModel.fileOnExtension() === false) {
378
				const title = (<HTMLInputElement>this.getElementById('issue-title')).value;
379 380
				this.searchVSCodeIssues(title, issueDescription);
			}
381 382
		});

383 384 385
		this.addEventListener('issue-title', 'input', (e: Event) => {
			const title = (<HTMLInputElement>e.target).value;
			const lengthValidationMessage = this.getElementById('issue-title-length-validation-error');
386 387 388 389 390 391
			if (title && this.getIssueUrlWithTitle(title).length > MAX_URL_LENGTH) {
				show(lengthValidationMessage);
			} else {
				hide(lengthValidationMessage);
			}

R
Fixes  
Rachel Macfarlane 已提交
392 393 394 395 396 397
			const fileOnExtension = this.issueReporterModel.fileOnExtension();
			if (fileOnExtension === undefined) {
				return;
			}

			if (fileOnExtension) {
398 399 400 401 402
				this.searchExtensionIssues(title);
			} else {
				const description = this.issueReporterModel.getData().issueDescription;
				this.searchVSCodeIssues(title, description);
			}
403
		});
404

405
		this.previewButton.onDidClick(() => this.createIssue());
406

407 408 409 410
		function sendWorkbenchCommand(commandId: string) {
			ipcRenderer.send('vscode:workbenchCommand', { id: commandId, from: 'issueReporter' });
		}

R
Rachel Macfarlane 已提交
411
		this.addEventListener('disableExtensions', 'click', () => {
412
			sendWorkbenchCommand('workbench.action.reloadWindowWithExtensionsDisabled');
413 414
		});

R
Rachel Macfarlane 已提交
415
		this.addEventListener('disableExtensions', 'keydown', (e: KeyboardEvent) => {
416
			e.stopPropagation();
417
			if (e.keyCode === 13 || e.keyCode === 32) {
418 419
				sendWorkbenchCommand('workbench.extensions.action.disableAll');
				sendWorkbenchCommand('workbench.action.reloadWindow');
420 421 422
			}
		});

423 424 425 426 427
		document.onkeydown = (e: KeyboardEvent) => {
			const cmdOrCtrlKey = platform.isMacintosh ? e.metaKey : e.ctrlKey;
			// Cmd/Ctrl+Enter previews issue and closes window
			if (cmdOrCtrlKey && e.keyCode === 13) {
				if (this.createIssue()) {
428
					ipcRenderer.send('vscode:closeIssueReporter');
429
				}
430
			}
431

432 433 434 435
			// Cmd/Ctrl + w closes issue window
			if (cmdOrCtrlKey && e.keyCode === 87) {
				e.stopPropagation();
				e.preventDefault();
436

437
				const issueTitle = (<HTMLInputElement>this.getElementById('issue-title'))!.value;
438 439 440 441 442 443
				const { issueDescription } = this.issueReporterModel.getData();
				if (!this.hasBeenSubmitted && (issueTitle || issueDescription)) {
					ipcRenderer.send('vscode:issueReporterConfirmClose');
				} else {
					ipcRenderer.send('vscode:closeIssueReporter');
				}
444 445
			}

446 447 448 449 450 451 452 453 454
			// Cmd/Ctrl + zooms in
			if (cmdOrCtrlKey && e.keyCode === 187) {
				this.applyZoom(webFrame.getZoomLevel() + 1);
			}

			// Cmd/Ctrl - zooms out
			if (cmdOrCtrlKey && e.keyCode === 189) {
				this.applyZoom(webFrame.getZoomLevel() - 1);
			}
455 456 457 458 459 460 461 462 463 464

			// With latest electron upgrade, cmd+a is no longer propagating correctly for inputs in this window on mac
			// Manually perform the selection
			if (platform.isMacintosh) {
				if (cmdOrCtrlKey && e.keyCode === 65 && e.target) {
					if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
						(<HTMLInputElement>e.target).select();
					}
				}
			}
465
		};
466 467
	}

468 469
	private updatePreviewButtonState() {
		if (this.isPreviewEnabled()) {
470 471
			this.previewButton.label = localize('previewOnGitHub', "Preview on GitHub");
			this.previewButton.enabled = true;
472
		} else {
473 474
			this.previewButton.enabled = false;
			this.previewButton.label = localize('loadingData', "Loading data...");
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491
		}
	}

	private isPreviewEnabled() {
		const issueType = this.issueReporterModel.getData().issueType;
		if (issueType === IssueType.Bug && this.receivedSystemInfo) {
			return true;
		}

		if (issueType === IssueType.PerformanceIssue && this.receivedSystemInfo && this.receivedPerformanceInfo) {
			return true;
		}

		if (issueType === IssueType.FeatureRequest) {
			return true;
		}

492 493 494 495
		if (issueType === IssueType.SettingsSearchIssue) {
			return true;
		}

496 497 498
		return false;
	}

499
	private getExtensionRepositoryUrl(): string | undefined {
500
		const selectedExtension = this.issueReporterModel.getData().selectedExtension;
501
		return selectedExtension && selectedExtension.repositoryUrl;
502
	}
503

504
	private getExtensionBugsUrl(): string | undefined {
505
		const selectedExtension = this.issueReporterModel.getData().selectedExtension;
506
		return selectedExtension && selectedExtension.bugsUrl;
507 508
	}

509
	private searchVSCodeIssues(title: string, issueDescription?: string): void {
R
Rachel Macfarlane 已提交
510
		if (title) {
511 512 513 514 515 516
			this.searchDuplicates(title, issueDescription);
		} else {
			this.clearSearchResults();
		}
	}

517
	private searchExtensionIssues(title: string): void {
518
		const url = this.getExtensionGitHubUrl();
519
		if (title) {
520
			const matches = /^https?:\/\/github\.com\/(.*)/.exec(url);
521 522 523 524
			if (matches && matches.length) {
				const repo = matches[1];
				return this.searchGitHub(repo, title);
			}
525 526 527 528 529 530 531

			// If the extension has no repository, display empty search results
			if (this.issueReporterModel.getData().selectedExtension) {
				this.clearSearchResults();
				return this.displaySearchResults([]);

			}
532 533 534 535 536
		}

		this.clearSearchResults();
	}

537
	private clearSearchResults(): void {
538
		const similarIssues = this.getElementById('similar-issues')!;
539
		similarIssues.innerHTML = '';
540
		this.numberOfSearchResultsDisplayed = 0;
541 542
	}

543 544 545
	@debounce(300)
	private searchGitHub(repo: string, title: string): void {
		const query = `is:issue+repo:${repo}+${title}`;
546
		const similarIssues = this.getElementById('similar-issues')!;
547 548 549 550 551 552 553 554 555 556 557 558 559

		window.fetch(`https://api.github.com/search/issues?q=${query}`).then((response) => {
			response.json().then(result => {
				similarIssues.innerHTML = '';
				if (result && result.items) {
					this.displaySearchResults(result.items);
				} else {
					// If the items property isn't present, the rate limit has been hit
					const message = $('div.list-title');
					message.textContent = localize('rateLimited', "GitHub query limit exceeded. Please wait.");
					similarIssues.appendChild(message);

					const resetTime = response.headers.get('X-RateLimit-Reset');
560
					const timeToWait = resetTime ? parseInt(resetTime) - Math.floor(Date.now() / 1000) : 1;
561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576
					if (this.shouldQueueSearch) {
						this.shouldQueueSearch = false;
						setTimeout(() => {
							this.searchGitHub(repo, title);
							this.shouldQueueSearch = true;
						}, timeToWait * 1000);
					}
				}
			}).catch(e => {
				this.logSearchError(e);
			});
		}).catch(e => {
			this.logSearchError(e);
		});
	}

577
	@debounce(300)
578
	private searchDuplicates(title: string, body?: string): void {
579
		const url = 'https://vscode-probot.westus.cloudapp.azure.com:7890/duplicate_candidates';
580 581 582
		const init = {
			method: 'POST',
			body: JSON.stringify({
583 584
				title,
				body
585 586 587 588 589 590 591 592 593 594 595
			}),
			headers: new Headers({
				'Content-Type': 'application/json'
			})
		};

		window.fetch(url, init).then((response) => {
			response.json().then(result => {
				this.clearSearchResults();

				if (result && result.candidates) {
596
					this.displaySearchResults(result.candidates);
597
				} else {
598
					throw new Error('Unexpected response, no candidates property');
599 600 601 602 603 604 605 606 607 608
				}
			}).catch((error) => {
				this.logSearchError(error);
			});
		}).catch((error) => {
			this.logSearchError(error);
		});
	}

	private displaySearchResults(results: SearchResult[]) {
609
		const similarIssues = this.getElementById('similar-issues')!;
610
		if (results.length) {
611
			const issues = $('div.issues-container');
612 613 614
			const issuesText = $('div.list-title');
			issuesText.textContent = localize('similarIssues', "Similar issues");

615 616
			this.numberOfSearchResultsDisplayed = results.length < 5 ? results.length : 5;
			for (let i = 0; i < this.numberOfSearchResultsDisplayed; i++) {
617
				const issue = results[i];
618
				const link = $('a.issue-link', { href: issue.html_url });
619 620
				link.textContent = issue.title;
				link.title = issue.title;
621 622 623
				link.addEventListener('click', (e) => this.openLink(e));
				link.addEventListener('auxclick', (e) => this.openLink(<MouseEvent>e));

624
				let issueState: HTMLElement;
625
				let item: HTMLElement;
626 627 628 629 630 631 632 633 634 635 636 637 638
				if (issue.state) {
					issueState = $('span.issue-state');

					const issueIcon = $('span.issue-icon');
					const octicon = new OcticonLabel(issueIcon);
					octicon.text = issue.state === 'open' ? '$(issue-opened)' : '$(issue-closed)';

					const issueStateLabel = $('span.issue-state.label');
					issueStateLabel.textContent = issue.state === 'open' ? localize('open', "Open") : localize('closed', "Closed");

					issueState.title = issue.state === 'open' ? localize('open', "Open") : localize('closed', "Closed");
					issueState.appendChild(issueIcon);
					issueState.appendChild(issueStateLabel);
639 640 641 642

					item = $('div.issue', {}, issueState, link);
				} else {
					item = $('div.issue', {}, link);
643 644
				}

645 646 647 648 649
				issues.appendChild(item);
			}

			similarIssues.appendChild(issuesText);
			similarIssues.appendChild(issues);
650
		} else {
651
			const message = $('div.list-title');
R
Rachel Macfarlane 已提交
652
			message.textContent = localize('noSimilarIssues', "No similar issues found");
653
			similarIssues.appendChild(message);
654 655 656
		}
	}

657
	private logSearchError(error: Error) {
658
		this.logService.warn('issueReporter#search ', error.message);
659 660
		/* __GDPR__
		"issueReporterSearchError" : {
661
				"message" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }
662 663 664 665 666
			}
		*/
		this.telemetryService.publicLog('issueReporterSearchError', { message: error.message });
	}

667 668 669
	private setUpTypes(): void {
		const makeOption = (issueType: IssueType, description: string) => `<option value="${issueType.valueOf()}">${escape(description)}</option>`;

670
		const typeSelect = this.getElementById('issue-type')! as HTMLSelectElement;
671 672 673 674 675 676 677
		const { issueType } = this.issueReporterModel.getData();
		if (issueType === IssueType.SettingsSearchIssue) {
			typeSelect.innerHTML = makeOption(IssueType.SettingsSearchIssue, localize('settingsSearchIssue', "Settings Search Issue"));
			typeSelect.disabled = true;
		} else {
			typeSelect.innerHTML = [
				makeOption(IssueType.Bug, localize('bugReporter', "Bug Report")),
678 679
				makeOption(IssueType.FeatureRequest, localize('featureRequest', "Feature Request")),
				makeOption(IssueType.PerformanceIssue, localize('performanceIssue', "Performance Issue"))
680 681 682 683
			].join('\n');
		}

		typeSelect.value = issueType.toString();
R
Fixes  
Rachel Macfarlane 已提交
684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722

		this.setSourceOptions();
	}

	private makeOption(value: string, description: string, disabled: boolean): HTMLOptionElement {
		const option: HTMLOptionElement = document.createElement('option');
		option.disabled = disabled;
		option.value = value;
		option.textContent = description;

		return option;
	}

	private setSourceOptions(): void {
		const sourceSelect = this.getElementById('issue-source')! as HTMLSelectElement;
		const selected = sourceSelect.selectedIndex;
		sourceSelect.innerHTML = '';
		const { issueType } = this.issueReporterModel.getData();
		if (issueType === IssueType.FeatureRequest) {
			sourceSelect.append(...[
				this.makeOption('', localize('selectSource', "Select source"), true),
				this.makeOption('false', localize('vscode', "Visual Studio Code"), false),
				this.makeOption('true', localize('extension', "An extension"), false)
			]);
		} else {
			sourceSelect.append(...[
				this.makeOption('', localize('selectSource', "Select source"), true),
				this.makeOption('false', localize('vscode', "Visual Studio Code"), false),
				this.makeOption('true', localize('extension', "An extension"), false),
				this.makeOption('', localize('unknown', "Don't Know"), false)
			]);
		}

		if (selected !== -1 && selected < sourceSelect.options.length) {
			sourceSelect.selectedIndex = selected;
		} else {
			sourceSelect.selectedIndex = 0;
			hide(this.getElementById('problem-source-help-text'));
		}
723 724
	}

725
	private renderBlocks(): void {
726
		// Depending on Issue Type, we render different blocks and text
727
		const { issueType, fileOnExtension } = this.issueReporterModel.getData();
728
		const blockContainer = this.getElementById('block-container');
729 730 731
		const systemBlock = document.querySelector('.block-system');
		const processBlock = document.querySelector('.block-process');
		const workspaceBlock = document.querySelector('.block-workspace');
732
		const extensionsBlock = document.querySelector('.block-extensions');
733 734
		const searchedExtensionsBlock = document.querySelector('.block-searchedExtensions');
		const settingsSearchResultsBlock = document.querySelector('.block-settingsSearchResults');
735

736 737 738 739
		const problemSource = this.getElementById('problem-source')!;
		const descriptionTitle = this.getElementById('issue-description-label')!;
		const descriptionSubtitle = this.getElementById('issue-description-subtitle')!;
		const extensionSelector = this.getElementById('extension-selection')!;
740

741
		// Hide all by default
742
		hide(blockContainer);
743 744 745 746 747 748
		hide(systemBlock);
		hide(processBlock);
		hide(workspaceBlock);
		hide(extensionsBlock);
		hide(searchedExtensionsBlock);
		hide(settingsSearchResultsBlock);
749
		hide(problemSource);
750
		hide(extensionSelector);
751

752
		if (issueType === IssueType.Bug) {
753
			show(blockContainer);
754
			show(systemBlock);
755 756 757 758 759 760 761
			show(problemSource);

			if (fileOnExtension) {
				show(extensionSelector);
			} else {
				show(extensionsBlock);
			}
762

763
			descriptionTitle.innerHTML = `${localize('stepsToReproduce', "Steps to Reproduce")} <span class="required-input">*</span>`;
764
			descriptionSubtitle.innerHTML = localize('bugDescription', "Share the steps needed to reliably reproduce the problem. Please include actual and expected results. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.");
765
		} else if (issueType === IssueType.PerformanceIssue) {
766
			show(blockContainer);
767 768 769
			show(systemBlock);
			show(processBlock);
			show(workspaceBlock);
770 771 772 773 774 775 776
			show(problemSource);

			if (fileOnExtension) {
				show(extensionSelector);
			} else {
				show(extensionsBlock);
			}
777

778
			descriptionTitle.innerHTML = `${localize('stepsToReproduce', "Steps to Reproduce")} <span class="required-input">*</span>`;
779
			descriptionSubtitle.innerHTML = localize('performanceIssueDesciption', "When did this performance issue happen? Does it occur on startup or after a specific series of actions? We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.");
780
		} else if (issueType === IssueType.FeatureRequest) {
781
			descriptionTitle.innerHTML = `${localize('description', "Description")} <span class="required-input">*</span>`;
782
			descriptionSubtitle.innerHTML = localize('featureRequestDescription', "Please describe the feature you would like to see. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.");
783 784 785 786 787
			show(problemSource);

			if (fileOnExtension) {
				show(extensionSelector);
			}
788
		} else if (issueType === IssueType.SettingsSearchIssue) {
789
			show(blockContainer);
790 791 792 793 794
			show(searchedExtensionsBlock);
			show(settingsSearchResultsBlock);

			descriptionTitle.innerHTML = `${localize('expectedResults', "Expected Results")} <span class="required-input">*</span>`;
			descriptionSubtitle.innerHTML = localize('settingsSearchResultsDescription', "Please list the results that you were expecting to see when you searched with this query. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.");
795 796 797
		}
	}

798
	private validateInput(inputId: string): boolean {
799
		const inputElement = (<HTMLInputElement>this.getElementById(inputId));
R
Rachel Macfarlane 已提交
800 801
		if (!inputElement.value) {
			inputElement.classList.add('invalid-input');
802 803
			return false;
		} else {
R
Rachel Macfarlane 已提交
804
			inputElement.classList.remove('invalid-input');
805
			return true;
806
		}
807
	}
808

809
	private validateInputs(): boolean {
R
Rachel Macfarlane 已提交
810
		let isValid = true;
R
Rachel Macfarlane 已提交
811
		['issue-title', 'description', 'issue-source'].forEach(elementId => {
R
Rachel Macfarlane 已提交
812
			isValid = this.validateInput(elementId) && isValid;
R
Rachel Macfarlane 已提交
813 814
		});

815
		if (this.issueReporterModel.fileOnExtension()) {
R
Rachel Macfarlane 已提交
816 817 818
			isValid = this.validateInput('extension-selector') && isValid;
		}

R
Rachel Macfarlane 已提交
819
		return isValid;
820 821
	}

822
	private createIssue(): boolean {
823
		if (!this.validateInputs()) {
824 825
			// If inputs are invalid, set focus to the first one and add listeners on them
			// to detect further changes
826 827 828 829
			const invalidInput = document.getElementsByClassName('invalid-input');
			if (invalidInput.length) {
				(<HTMLInputElement>invalidInput[0]).focus();
			}
830

831
			this.addEventListener('issue-title', 'input', _ => {
832 833 834
				this.validateInput('issue-title');
			});

835
			this.addEventListener('description', 'input', _ => {
836 837
				this.validateInput('description');
			});
838

R
Fixes  
Rachel Macfarlane 已提交
839 840 841 842
			this.addEventListener('issue-source', 'change', _ => {
				this.validateInput('issue-source');
			});

843
			if (this.issueReporterModel.fileOnExtension()) {
844
				this.addEventListener('extension-selector', 'change', _ => {
845 846 847 848
					this.validateInput('extension-selector');
				});
			}

849
			return false;
850 851
		}

852 853
		/* __GDPR__
			"issueReporterSubmit" : {
K
kieferrm 已提交
854 855
				"issueType" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
				"numSimilarIssuesDisplayed" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
856 857 858
			}
		*/
		this.telemetryService.publicLog('issueReporterSubmit', { issueType: this.issueReporterModel.getData().issueType, numSimilarIssuesDisplayed: this.numberOfSearchResultsDisplayed });
859
		this.hasBeenSubmitted = true;
860

861
		const baseUrl = this.getIssueUrlWithTitle((<HTMLInputElement>this.getElementById('issue-title')).value);
R
Rachel Macfarlane 已提交
862
		const issueBody = this.issueReporterModel.serialize();
863
		let url = baseUrl + `&body=${encodeURIComponent(issueBody)}`;
864

865
		if (url.length > MAX_URL_LENGTH) {
866 867
			clipboard.writeText(issueBody);
			url = baseUrl + `&body=${encodeURIComponent(localize('pasteData', "We have written the needed data into your clipboard because it was too large to send. Please paste."))}`;
868 869
		}

870
		ipcRenderer.send('vscode:openExternal', url);
871
		return true;
872 873
	}

874 875 876 877 878 879 880 881 882 883 884 885 886 887
	private getExtensionGitHubUrl(): string {
		let repositoryUrl = '';
		const bugsUrl = this.getExtensionBugsUrl();
		const extensionUrl = this.getExtensionRepositoryUrl();
		// If given, try to match the extension's bug url
		if (bugsUrl && bugsUrl.match(/^https?:\/\/github\.com\/(.*)/)) {
			repositoryUrl = normalizeGitHubUrl(bugsUrl);
		} else if (extensionUrl && extensionUrl.match(/^https?:\/\/github\.com\/(.*)/)) {
			repositoryUrl = normalizeGitHubUrl(extensionUrl);
		}

		return repositoryUrl;
	}

888 889
	private getIssueUrlWithTitle(issueTitle: string): string {
		let repositoryUrl = product.reportIssueUrl;
890
		if (this.issueReporterModel.fileOnExtension()) {
891 892 893
			const extensionGitHubUrl = this.getExtensionGitHubUrl();
			if (extensionGitHubUrl) {
				repositoryUrl = extensionGitHubUrl + '/issues/new';
894 895 896
			}
		}

897
		const queryStringPrefix = product.reportIssueUrl.indexOf('?') === -1 ? '?' : '&';
898
		return `${repositoryUrl}${queryStringPrefix}title=${encodeURIComponent(issueTitle)}`;
899 900
	}

901
	private updateSystemInfo(state: IssueReporterModelData) {
902
		const target = document.querySelector('.block-system .block-info');
903 904 905 906 907 908 909 910 911 912 913 914 915 916 917
		if (target) {
			let tableHtml = '';
			Object.keys(state.systemInfo).forEach(k => {
				const data = typeof state.systemInfo[k] === 'object'
					? Object.keys(state.systemInfo[k]).map(key => `${key}: ${state.systemInfo[k][key]}`).join('<br>')
					: state.systemInfo[k];

				tableHtml += `
					<tr>
						<td>${k}</td>
						<td>${data}</td>
					</tr>`;
			});
			target.innerHTML = `<table>${tableHtml}</table>`;
		}
918
	}
919

920
	private updateExtensionSelector(extensions: IssueReporterExtensionData[]): void {
921 922 923 924 925 926 927
		interface IOption {
			name: string;
			id: string;
		}

		const extensionOptions: IOption[] = extensions.map(extension => {
			return {
928 929
				name: extension.displayName || extension.name || '',
				id: extension.id
930 931 932 933 934
			};
		});

		// Sort extensions by name
		extensionOptions.sort((a, b) => {
935 936 937
			const aName = a.name.toLowerCase();
			const bName = b.name.toLowerCase();
			if (aName > bName) {
938 939 940
				return 1;
			}

941
			if (aName < bName) {
942 943 944 945 946 947 948
				return -1;
			}

			return 0;
		});

		const makeOption = (extension: IOption) => `<option value="${extension.id}">${escape(extension.name)}</option>`;
949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967
		const extensionsSelector = this.getElementById('extension-selector');
		if (extensionsSelector) {
			extensionsSelector.innerHTML = '<option></option>' + extensionOptions.map(makeOption).join('\n');

			this.addEventListener('extension-selector', 'change', (e: Event) => {
				const selectedExtensionId = (<HTMLInputElement>e.target).value;
				const extensions = this.issueReporterModel.getData().allExtensions;
				const matches = extensions.filter(extension => extension.id === selectedExtensionId);
				if (matches.length) {
					this.issueReporterModel.update({ selectedExtension: matches[0] });

					const title = (<HTMLInputElement>this.getElementById('issue-title')).value;
					this.searchExtensionIssues(title);
				} else {
					this.issueReporterModel.update({ selectedExtension: undefined });
					this.clearSearchResults();
				}
			});
		}
968 969
	}

970
	private updateProcessInfo(state: IssueReporterModelData) {
971
		const target = document.querySelector('.block-process .block-info');
972 973 974
		if (target) {
			target.innerHTML = `<code>${state.processInfo}</code>`;
		}
975 976
	}

977
	private updateWorkspaceInfo(state: IssueReporterModelData) {
978
		document.querySelector('.block-workspace .block-info code')!.textContent = '\n' + state.workspaceInfo;
979
	}
980

981
	private updateExtensionTable(extensions: IssueReporterExtensionData[], numThemeExtensions: number): void {
982
		const target = document.querySelector('.block-extensions .block-info');
983 984 985 986 987
		if (target) {
			if (this.environmentService.disableExtensions) {
				target.innerHTML = localize('disabledExtensions', "Extensions are disabled");
				return;
			}
988

989 990
			const themeExclusionStr = numThemeExtensions ? `\n(${numThemeExtensions} theme extensions excluded)` : '';
			extensions = extensions || [];
991

992 993 994 995
			if (!extensions.length) {
				target.innerHTML = 'Extensions: none' + themeExclusionStr;
				return;
			}
996

997 998
			const table = this.getExtensionTableHtml(extensions);
			target.innerHTML = `<table>${table}</table>${themeExclusionStr}`;
999
		}
1000 1001
	}

1002
	private updateSearchedExtensionTable(extensions: IssueReporterExtensionData[]): void {
1003
		const target = document.querySelector('.block-searchedExtensions .block-info');
1004 1005 1006 1007 1008
		if (target) {
			if (!extensions.length) {
				target.innerHTML = 'Extensions: none';
				return;
			}
1009

1010 1011
			const table = this.getExtensionTableHtml(extensions);
			target.innerHTML = `<table>${table}</table>`;
1012 1013 1014
		}
	}

1015
	private getExtensionTableHtml(extensions: IssueReporterExtensionData[]): string {
1016 1017 1018 1019 1020 1021 1022
		let table = `
			<tr>
				<th>Extension</th>
				<th>Author (truncated)</th>
				<th>Version</th>
			</tr>`;

1023 1024
		table += extensions.map(extension => {
			return `
1025
				<tr>
1026 1027 1028
					<td>${extension.name}</td>
					<td>${extension.publisher.substr(0, 3)}</td>
					<td>${extension.version}</td>
1029
				</tr>`;
1030
		}).join('');
1031

1032
		return table;
1033
	}
1034 1035 1036 1037 1038 1039 1040 1041 1042

	private openLink(event: MouseEvent): void {
		event.preventDefault();
		event.stopPropagation();
		// Exclude right click
		if (event.which < 3) {
			shell.openExternal((<HTMLAnchorElement>event.target).href);

			/* __GDPR__
1043
				"issueReporterViewSimilarIssue" : { }
1044
			*/
1045
			this.telemetryService.publicLog('issueReporterViewSimilarIssue');
1046 1047
		}
	}
R
Rachel Macfarlane 已提交
1048

1049
	private getElementById(elementId: string): HTMLElement | undefined {
R
Rachel Macfarlane 已提交
1050 1051
		const element = document.getElementById(elementId);
		if (element) {
1052
			return element;
R
Rachel Macfarlane 已提交
1053 1054 1055 1056
		} else {
			const error = new Error(`${elementId} not found.`);
			this.logService.error(error);
			/* __GDPR__
1057
				"issueReporterGetElementError" : {
R
Rachel Macfarlane 已提交
1058 1059 1060
						"message" : { "classification": "CallstackOrException", "purpose": "PerformanceAndHealth" }
					}
				*/
1061 1062 1063 1064 1065 1066 1067 1068 1069 1070
			this.telemetryService.publicLog('issueReporterGetElementError', { message: error.message });

			return undefined;
		}
	}

	private addEventListener(elementId: string, eventType: string, handler: (event: Event) => void): void {
		const element = this.getElementById(elementId);
		if (element) {
			element.addEventListener(eventType, handler);
R
Rachel Macfarlane 已提交
1071 1072
		}
	}
1073 1074
}

1075 1076
// helper functions

1077 1078 1079 1080
function hide(el: Element | undefined | null) {
	if (el) {
		el.classList.add('hidden');
	}
1081
}
1082 1083 1084 1085
function show(el: Element | undefined | null) {
	if (el) {
		el.classList.remove('hidden');
	}
1086
}