Path: blob/main/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts
5250 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import { $, isHTMLInputElement, isHTMLTextAreaElement, reset } from '../../../../base/browser/dom.js';5import { createStyleSheet } from '../../../../base/browser/domStylesheets.js';6import { Button, ButtonWithDropdown, unthemedButtonStyles } from '../../../../base/browser/ui/button/button.js';7import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';8import { Delayer, RunOnceScheduler } from '../../../../base/common/async.js';9import { VSBuffer } from '../../../../base/common/buffer.js';10import { Codicon } from '../../../../base/common/codicons.js';11import { groupBy } from '../../../../base/common/collections.js';12import { debounce } from '../../../../base/common/decorators.js';13import { CancellationError } from '../../../../base/common/errors.js';14import { Disposable } from '../../../../base/common/lifecycle.js';15import { Schemas } from '../../../../base/common/network.js';16import { isLinuxSnap, isMacintosh } from '../../../../base/common/platform.js';17import { IProductConfiguration } from '../../../../base/common/product.js';18import { joinPath } from '../../../../base/common/resources.js';19import { escape } from '../../../../base/common/strings.js';20import { ThemeIcon } from '../../../../base/common/themables.js';21import { URI } from '../../../../base/common/uri.js';22import { Action } from '../../../../base/common/actions.js';23import { localize } from '../../../../nls.js';24import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';25import { IFileService } from '../../../../platform/files/common/files.js';26import { IOpenerService } from '../../../../platform/opener/common/opener.js';27import { getIconsStyleSheet } from '../../../../platform/theme/browser/iconsStyleSheet.js';28import { IThemeService } from '../../../../platform/theme/common/themeService.js';29import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';30import { IIssueFormService, IssueReporterData, IssueReporterExtensionData, IssueType } from '../common/issue.js';31import { normalizeGitHubUrl } from '../common/issueReporterUtil.js';32import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from './issueReporterModel.js';33import { IAuthenticationService } from '../../../services/authentication/common/authentication.js';3435const MAX_URL_LENGTH = 7500;3637// Github API and issues on web has a limit of 65536. If extension data is too large, we will allow users to downlaod and attach it as a file.38// We round down to be safe.39// ref https://github.com/github/issues/issues/128584041const MAX_EXTENSION_DATA_LENGTH = 60000;4243interface SearchResult {44html_url: string;45title: string;46state?: string;47}4849enum IssueSource {50VSCode = 'vscode',51Extension = 'extension',52Marketplace = 'marketplace',53Unknown = 'unknown'54}555657export class BaseIssueReporterService extends Disposable {58public issueReporterModel: IssueReporterModel;59public receivedSystemInfo = false;60public numberOfSearchResultsDisplayed = 0;61public receivedPerformanceInfo = false;62public shouldQueueSearch = false;63public hasBeenSubmitted = false;64public openReporter = false;65public loadingExtensionData = false;66public selectedExtension = '';67public delayedSubmit = new Delayer<void>(300);68public publicGithubButton!: Button | ButtonWithDropdown;69public internalGithubButton!: Button | ButtonWithDropdown;70public nonGitHubIssueUrl = false;71public needsUpdate = false;72public acknowledged = false;73private createAction: Action;74private previewAction: Action;75private privateAction: Action;7677constructor(78public disableExtensions: boolean,79public data: IssueReporterData,80public os: {81type: string;82arch: string;83release: string;84},85public product: IProductConfiguration,86public readonly window: Window,87public readonly isWeb: boolean,88@IIssueFormService public readonly issueFormService: IIssueFormService,89@IThemeService public readonly themeService: IThemeService,90@IFileService public readonly fileService: IFileService,91@IFileDialogService public readonly fileDialogService: IFileDialogService,92@IContextMenuService public readonly contextMenuService: IContextMenuService,93@IAuthenticationService public readonly authenticationService: IAuthenticationService,94@IOpenerService public readonly openerService: IOpenerService95) {96super();97const targetExtension = data.extensionId ? data.enabledExtensions.find(extension => extension.id.toLocaleLowerCase() === data.extensionId?.toLocaleLowerCase()) : undefined;98this.issueReporterModel = new IssueReporterModel({99...data,100issueType: data.issueType || IssueType.Bug,101versionInfo: {102vscodeVersion: `${product.nameShort} ${!!product.darwinUniversalAssetId ? `${product.version} (Universal)` : product.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`,103os: `${this.os.type} ${this.os.arch} ${this.os.release}${isLinuxSnap ? ' snap' : ''}`104},105extensionsDisabled: !!this.disableExtensions,106fileOnExtension: data.extensionId ? !targetExtension?.isBuiltin : undefined,107selectedExtension: targetExtension108});109110this._register(this.authenticationService.onDidChangeSessions(async () => {111const previousAuthState = !!this.data.githubAccessToken;112113let githubAccessToken = '';114try {115const githubSessions = await this.authenticationService.getSessions('github');116const potentialSessions = githubSessions.filter(session => session.scopes.includes('repo'));117githubAccessToken = potentialSessions[0]?.accessToken;118} catch (e) {119// Ignore120}121122this.data.githubAccessToken = githubAccessToken;123124const currentAuthState = !!githubAccessToken;125if (previousAuthState !== currentAuthState) {126this.updateButtonStates();127}128}));129130const fileOnMarketplace = data.issueSource === IssueSource.Marketplace;131const fileOnProduct = data.issueSource === IssueSource.VSCode;132this.issueReporterModel.update({ fileOnMarketplace, fileOnProduct });133134this.createAction = this._register(new Action('issueReporter.create', localize('create', "Create on GitHub"), undefined, true, async () => {135this.delayedSubmit.trigger(async () => {136this.createIssue(true); // create issue137});138}));139this.previewAction = this._register(new Action('issueReporter.preview', localize('preview', "Preview on GitHub"), undefined, true, async () => {140this.delayedSubmit.trigger(async () => {141this.createIssue(false); // preview issue142});143}));144this.privateAction = this._register(new Action('issueReporter.privateCreate', localize('privateCreate', "Create Internally"), undefined, true, async () => {145this.delayedSubmit.trigger(async () => {146this.createIssue(true, true); // create private issue147});148}));149150const issueTitle = data.issueTitle;151if (issueTitle) {152// eslint-disable-next-line no-restricted-syntax153const issueTitleElement = this.getElementById<HTMLInputElement>('issue-title');154if (issueTitleElement) {155issueTitleElement.value = issueTitle;156}157}158159const issueBody = data.issueBody;160if (issueBody) {161// eslint-disable-next-line no-restricted-syntax162const description = this.getElementById<HTMLTextAreaElement>('description');163if (description) {164description.value = issueBody;165this.issueReporterModel.update({ issueDescription: issueBody });166}167}168169if (this.window.document.documentElement.lang !== 'en') {170// eslint-disable-next-line no-restricted-syntax171show(this.getElementById('english'));172}173174const codiconStyleSheet = createStyleSheet();175codiconStyleSheet.id = 'codiconStyles';176177const iconsStyleSheet = this._register(getIconsStyleSheet(this.themeService));178function updateAll() {179codiconStyleSheet.textContent = iconsStyleSheet.getCSS();180}181182const delayer = new RunOnceScheduler(updateAll, 0);183this._register(iconsStyleSheet.onDidChange(() => delayer.schedule()));184delayer.schedule();185186this.handleExtensionData(data.enabledExtensions);187this.setUpTypes();188189// Handle case where extension is pre-selected through the command190if ((data.data || data.uri) && targetExtension) {191this.updateExtensionStatus(targetExtension);192}193194// initialize the reporting button(s)195// eslint-disable-next-line no-restricted-syntax196const issueReporterElement = this.getElementById('issue-reporter');197if (issueReporterElement) {198this.updateButtonStates();199}200}201202render(): void {203this.renderBlocks();204}205206setInitialFocus() {207const { fileOnExtension } = this.issueReporterModel.getData();208if (fileOnExtension) {209// eslint-disable-next-line no-restricted-syntax210const issueTitle = this.window.document.getElementById('issue-title');211issueTitle?.focus();212} else {213// eslint-disable-next-line no-restricted-syntax214const issueType = this.window.document.getElementById('issue-type');215issueType?.focus();216}217}218219public updateButtonStates() {220// eslint-disable-next-line no-restricted-syntax221const issueReporterElement = this.getElementById('issue-reporter');222if (!issueReporterElement) {223// shouldn't occur -- throw?224return;225}226227228// public elements section229// eslint-disable-next-line no-restricted-syntax230let publicElements = this.getElementById('public-elements');231if (!publicElements) {232publicElements = document.createElement('div');233publicElements.id = 'public-elements';234publicElements.classList.add('public-elements');235issueReporterElement.appendChild(publicElements);236}237this.updatePublicGithubButton(publicElements);238this.updatePublicRepoLink(publicElements);239240241// private filing section242// eslint-disable-next-line no-restricted-syntax243let internalElements = this.getElementById('internal-elements');244if (!internalElements) {245internalElements = document.createElement('div');246internalElements.id = 'internal-elements';247internalElements.classList.add('internal-elements');248internalElements.classList.add('hidden');249issueReporterElement.appendChild(internalElements);250}251// eslint-disable-next-line no-restricted-syntax252let filingRow = this.getElementById('internal-top-row');253if (!filingRow) {254filingRow = document.createElement('div');255filingRow.id = 'internal-top-row';256filingRow.classList.add('internal-top-row');257internalElements.appendChild(filingRow);258}259this.updateInternalFilingNote(filingRow);260this.updateInternalGithubButton(filingRow);261this.updateInternalElementsVisibility();262}263264private updateInternalFilingNote(container: HTMLElement) {265// eslint-disable-next-line no-restricted-syntax266let filingNote = this.getElementById('internal-preview-message');267if (!filingNote) {268filingNote = document.createElement('span');269filingNote.id = 'internal-preview-message';270filingNote.classList.add('internal-preview-message');271container.appendChild(filingNote);272}273274filingNote.textContent = escape(localize('internalPreviewMessage', 'If your copilot debug logs contain private information:'));275}276277private updatePublicGithubButton(container: HTMLElement): void {278// eslint-disable-next-line no-restricted-syntax279const issueReporterElement = this.getElementById('issue-reporter');280if (!issueReporterElement) {281return;282}283284// Dispose of the existing button285if (this.publicGithubButton) {286this.publicGithubButton.dispose();287}288289// setup button + dropdown if applicable290if (!this.acknowledged && this.needsUpdate) { // * old version and hasn't ack'd291this.publicGithubButton = this._register(new Button(container, unthemedButtonStyles));292this.publicGithubButton.label = localize('acknowledge', "Confirm Version Acknowledgement");293this.publicGithubButton.enabled = false;294} else if (this.data.githubAccessToken && this.isPreviewEnabled()) { // * has access token, create by default, preview dropdown295this.publicGithubButton = this._register(new ButtonWithDropdown(container, {296contextMenuProvider: this.contextMenuService,297actions: [this.previewAction],298addPrimaryActionToDropdown: false,299...unthemedButtonStyles300}));301this._register(this.publicGithubButton.onDidClick(() => {302this.createAction.run();303}));304this.publicGithubButton.label = localize('createOnGitHub', "Create on GitHub");305this.publicGithubButton.enabled = true;306} else if (this.data.githubAccessToken && !this.isPreviewEnabled()) { // * Access token but invalid preview state: simple Button (create only)307this.publicGithubButton = this._register(new Button(container, unthemedButtonStyles));308this._register(this.publicGithubButton.onDidClick(() => {309this.createAction.run();310}));311this.publicGithubButton.label = localize('createOnGitHub', "Create on GitHub");312this.publicGithubButton.enabled = true;313} else { // * No access token: simple Button (preview only)314this.publicGithubButton = this._register(new Button(container, unthemedButtonStyles));315this._register(this.publicGithubButton.onDidClick(() => {316this.previewAction.run();317}));318this.publicGithubButton.label = localize('previewOnGitHub', "Preview on GitHub");319this.publicGithubButton.enabled = true;320}321322// make sure that the repo link is after the button323// eslint-disable-next-line no-restricted-syntax324const repoLink = this.getElementById('show-repo-name');325if (repoLink) {326container.insertBefore(this.publicGithubButton.element, repoLink);327}328}329330private updatePublicRepoLink(container: HTMLElement): void {331// eslint-disable-next-line no-restricted-syntax332let issueRepoName = this.getElementById('show-repo-name') as HTMLAnchorElement;333if (!issueRepoName) {334issueRepoName = document.createElement('a');335issueRepoName.id = 'show-repo-name';336issueRepoName.classList.add('hidden');337container.appendChild(issueRepoName);338}339340341const selectedExtension = this.issueReporterModel.getData().selectedExtension;342if (selectedExtension && selectedExtension.uri) {343const urlString = URI.revive(selectedExtension.uri).toString();344issueRepoName.href = urlString;345issueRepoName.addEventListener('click', (e) => this.openLink(e));346issueRepoName.addEventListener('auxclick', (e) => this.openLink(<MouseEvent>e));347const gitHubInfo = this.parseGitHubUrl(urlString);348issueRepoName.textContent = gitHubInfo ? gitHubInfo.owner + '/' + gitHubInfo.repositoryName : urlString;349Object.assign(issueRepoName.style, {350alignSelf: 'flex-end',351display: 'block',352fontSize: '13px',353padding: '4px 0px',354textDecoration: 'none',355width: 'auto'356});357show(issueRepoName);358} else if (issueRepoName) {359// clear styles360issueRepoName.removeAttribute('style');361hide(issueRepoName);362}363}364365private updateInternalGithubButton(container: HTMLElement): void {366// eslint-disable-next-line no-restricted-syntax367const issueReporterElement = this.getElementById('issue-reporter');368if (!issueReporterElement) {369return;370}371372// Dispose of the existing button373if (this.internalGithubButton) {374this.internalGithubButton.dispose();375}376377if (this.data.githubAccessToken && this.data.privateUri) {378this.internalGithubButton = this._register(new Button(container, unthemedButtonStyles));379this._register(this.internalGithubButton.onDidClick(() => {380this.privateAction.run();381}));382383this.internalGithubButton.element.id = 'internal-create-btn';384this.internalGithubButton.element.classList.add('internal-create-subtle');385this.internalGithubButton.label = localize('createInternally', "Create Internally");386this.internalGithubButton.enabled = true;387this.internalGithubButton.setTitle(this.data.privateUri.path!.slice(1));388}389}390391private updateInternalElementsVisibility(): void {392// eslint-disable-next-line no-restricted-syntax393const container = this.getElementById('internal-elements');394if (!container) {395// shouldn't happen396return;397}398399if (this.data.githubAccessToken && this.data.privateUri) {400show(container);401container.style.display = ''; //todo: necessary even with show?402if (this.internalGithubButton) {403this.internalGithubButton.enabled = this.publicGithubButton?.enabled ?? false;404}405} else {406hide(container);407container.style.display = 'none'; //todo: necessary even with hide?408}409}410411private async updateIssueReporterUri(extension: IssueReporterExtensionData): Promise<void> {412try {413if (extension.uri) {414const uri = URI.revive(extension.uri);415extension.bugsUrl = uri.toString();416}417} catch (e) {418this.renderBlocks();419}420}421422private handleExtensionData(extensions: IssueReporterExtensionData[]) {423const installedExtensions = extensions.filter(x => !x.isBuiltin);424const { nonThemes, themes } = groupBy(installedExtensions, ext => {425return ext.isTheme ? 'themes' : 'nonThemes';426});427428const numberOfThemeExtesions = (themes && themes.length) ?? 0;429this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: installedExtensions });430this.updateExtensionTable(nonThemes ?? [], numberOfThemeExtesions);431if (this.disableExtensions || installedExtensions.length === 0) {432// eslint-disable-next-line no-restricted-syntax433(<HTMLButtonElement>this.getElementById('disableExtensions')).disabled = true;434}435436this.updateExtensionSelector(installedExtensions);437}438439private updateExtensionSelector(extensions: IssueReporterExtensionData[]): void {440interface IOption {441name: string;442id: string;443}444445const extensionOptions: IOption[] = extensions.map(extension => {446return {447name: extension.displayName || extension.name || '',448id: extension.id449};450});451452// Sort extensions by name453extensionOptions.sort((a, b) => {454const aName = a.name.toLowerCase();455const bName = b.name.toLowerCase();456if (aName > bName) {457return 1;458}459460if (aName < bName) {461return -1;462}463464return 0;465});466467const makeOption = (extension: IOption, selectedExtension?: IssueReporterExtensionData): HTMLOptionElement => {468const selected = selectedExtension && extension.id === selectedExtension.id;469return $<HTMLOptionElement>('option', {470'value': extension.id,471'selected': selected || ''472}, extension.name);473};474475// eslint-disable-next-line no-restricted-syntax476const extensionsSelector = this.getElementById<HTMLSelectElement>('extension-selector');477if (extensionsSelector) {478const { selectedExtension } = this.issueReporterModel.getData();479reset(extensionsSelector, this.makeOption('', localize('selectExtension', "Select extension"), true), ...extensionOptions.map(extension => makeOption(extension, selectedExtension)));480481if (!selectedExtension) {482extensionsSelector.selectedIndex = 0;483}484485this.addEventListener('extension-selector', 'change', async (e: Event) => {486this.clearExtensionData();487const selectedExtensionId = (<HTMLInputElement>e.target).value;488this.selectedExtension = selectedExtensionId;489const extensions = this.issueReporterModel.getData().allExtensions;490const matches = extensions.filter(extension => extension.id === selectedExtensionId);491if (matches.length) {492this.issueReporterModel.update({ selectedExtension: matches[0] });493const selectedExtension = this.issueReporterModel.getData().selectedExtension;494if (selectedExtension) {495const iconElement = document.createElement('span');496iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin');497this.setLoading(iconElement);498const openReporterData = await this.sendReporterMenu(selectedExtension);499if (openReporterData) {500if (this.selectedExtension === selectedExtensionId) {501this.removeLoading(iconElement, true);502this.data = openReporterData;503}504}505else {506if (!this.loadingExtensionData) {507iconElement.classList.remove(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin');508}509this.removeLoading(iconElement);510// if not using command, should have no configuration data in fields we care about and check later.511this.clearExtensionData();512513// case when previous extension was opened from normal openIssueReporter command514selectedExtension.data = undefined;515selectedExtension.uri = undefined;516}517if (this.selectedExtension === selectedExtensionId) {518// repopulates the fields with the new data given the selected extension.519this.updateExtensionStatus(matches[0]);520this.openReporter = false;521}522} else {523this.issueReporterModel.update({ selectedExtension: undefined });524this.clearSearchResults();525this.clearExtensionData();526this.validateSelectedExtension();527this.updateExtensionStatus(matches[0]);528}529}530531// Update internal action visibility after explicit selection532this.updateInternalElementsVisibility();533});534}535536this.addEventListener('problem-source', 'change', (_) => {537this.clearExtensionData();538this.validateSelectedExtension();539});540}541542private async sendReporterMenu(extension: IssueReporterExtensionData): Promise<IssueReporterData | undefined> {543try {544const timeoutPromise = new Promise<undefined>((_, reject) =>545setTimeout(() => reject(new Error('sendReporterMenu timed out')), 10000)546);547const data = await Promise.race([548this.issueFormService.sendReporterMenu(extension.id),549timeoutPromise550]);551return data;552} catch (e) {553console.error(e);554return undefined;555}556}557558private updateAcknowledgementState() {559// eslint-disable-next-line no-restricted-syntax560const acknowledgementCheckbox = this.getElementById<HTMLInputElement>('includeAcknowledgement');561if (acknowledgementCheckbox) {562this.acknowledged = acknowledgementCheckbox.checked;563this.updateButtonStates();564}565}566567public setEventHandlers(): void {568(['includeSystemInfo', 'includeProcessInfo', 'includeWorkspaceInfo', 'includeExtensions', 'includeExperiments', 'includeExtensionData'] as const).forEach(elementId => {569this.addEventListener(elementId, 'click', (event: Event) => {570event.stopPropagation();571this.issueReporterModel.update({ [elementId]: !this.issueReporterModel.getData()[elementId] });572});573});574575this.addEventListener('includeAcknowledgement', 'click', (event: Event) => {576event.stopPropagation();577this.updateAcknowledgementState();578});579580// eslint-disable-next-line no-restricted-syntax581const showInfoElements = this.window.document.getElementsByClassName('showInfo');582for (let i = 0; i < showInfoElements.length; i++) {583const showInfo = showInfoElements.item(i)!;584(showInfo as HTMLAnchorElement).addEventListener('click', (e: MouseEvent) => {585e.preventDefault();586const label = (<HTMLDivElement>e.target);587if (label) {588const containingElement = label.parentElement && label.parentElement.parentElement;589const info = containingElement && containingElement.lastElementChild;590if (info && info.classList.contains('hidden')) {591show(info);592label.textContent = localize('hide', "hide");593} else {594hide(info);595label.textContent = localize('show', "show");596}597}598});599}600601this.addEventListener('issue-source', 'change', (e: Event) => {602const value = (<HTMLInputElement>e.target).value;603// eslint-disable-next-line no-restricted-syntax604const problemSourceHelpText = this.getElementById('problem-source-help-text')!;605if (value === '') {606this.issueReporterModel.update({ fileOnExtension: undefined });607show(problemSourceHelpText);608this.clearSearchResults();609this.render();610return;611} else {612hide(problemSourceHelpText);613}614615// eslint-disable-next-line no-restricted-syntax616const descriptionTextArea = <HTMLInputElement>this.getElementById('issue-title');617if (value === IssueSource.VSCode) {618descriptionTextArea.placeholder = localize('vscodePlaceholder', "E.g Workbench is missing problems panel");619} else if (value === IssueSource.Extension) {620descriptionTextArea.placeholder = localize('extensionPlaceholder', "E.g. Missing alt text on extension readme image");621} else if (value === IssueSource.Marketplace) {622descriptionTextArea.placeholder = localize('marketplacePlaceholder', "E.g Cannot disable installed extension");623} else {624descriptionTextArea.placeholder = localize('undefinedPlaceholder', "Please enter a title");625}626627let fileOnExtension, fileOnMarketplace, fileOnProduct = false;628if (value === IssueSource.Extension) {629fileOnExtension = true;630} else if (value === IssueSource.Marketplace) {631fileOnMarketplace = true;632} else if (value === IssueSource.VSCode) {633fileOnProduct = true;634}635636this.issueReporterModel.update({ fileOnExtension, fileOnMarketplace, fileOnProduct });637this.render();638639// eslint-disable-next-line no-restricted-syntax640const title = (<HTMLInputElement>this.getElementById('issue-title')).value;641this.searchIssues(title, fileOnExtension, fileOnMarketplace);642});643644this.addEventListener('description', 'input', (e: Event) => {645const issueDescription = (<HTMLInputElement>e.target).value;646this.issueReporterModel.update({ issueDescription });647648// Only search for extension issues on title change649if (this.issueReporterModel.fileOnExtension() === false) {650// eslint-disable-next-line no-restricted-syntax651const title = (<HTMLInputElement>this.getElementById('issue-title')).value;652this.searchVSCodeIssues(title, issueDescription);653}654});655656this.addEventListener('issue-title', 'input', _ => {657// eslint-disable-next-line no-restricted-syntax658const titleElement = this.getElementById('issue-title') as HTMLInputElement;659if (titleElement) {660const title = titleElement.value;661this.issueReporterModel.update({ issueTitle: title });662}663});664665this.addEventListener('issue-title', 'input', (e: Event) => {666const title = (<HTMLInputElement>e.target).value;667// eslint-disable-next-line no-restricted-syntax668const lengthValidationMessage = this.getElementById('issue-title-length-validation-error');669const issueUrl = this.getIssueUrl();670if (title && this.getIssueUrlWithTitle(title, issueUrl).length > MAX_URL_LENGTH) {671show(lengthValidationMessage);672} else {673hide(lengthValidationMessage);674}675// eslint-disable-next-line no-restricted-syntax676const issueSource = this.getElementById<HTMLSelectElement>('issue-source');677if (!issueSource || issueSource.value === '') {678return;679}680681const { fileOnExtension, fileOnMarketplace } = this.issueReporterModel.getData();682this.searchIssues(title, fileOnExtension, fileOnMarketplace);683});684685// We handle clicks in the dropdown actions now686687this.addEventListener('disableExtensions', 'click', () => {688this.issueFormService.reloadWithExtensionsDisabled();689});690691this.addEventListener('extensionBugsLink', 'click', (e: Event) => {692const url = (<HTMLElement>e.target).innerText;693this.openLink(url);694});695696this.addEventListener('disableExtensions', 'keydown', (e: Event) => {697e.stopPropagation();698if ((e as KeyboardEvent).key === 'Enter' || (e as KeyboardEvent).key === ' ') {699this.issueFormService.reloadWithExtensionsDisabled();700}701});702703this.window.document.onkeydown = async (e: KeyboardEvent) => {704const cmdOrCtrlKey = isMacintosh ? e.metaKey : e.ctrlKey;705// Cmd/Ctrl+Enter previews issue and closes window706if (cmdOrCtrlKey && e.key === 'Enter') {707this.delayedSubmit.trigger(async () => {708if (await this.createIssue()) {709this.close();710}711});712}713714// Cmd/Ctrl + w closes issue window715if (cmdOrCtrlKey && e.key === 'w') {716e.stopPropagation();717e.preventDefault();718719// eslint-disable-next-line no-restricted-syntax720const issueTitle = (<HTMLInputElement>this.getElementById('issue-title'))!.value;721const { issueDescription } = this.issueReporterModel.getData();722if (!this.hasBeenSubmitted && (issueTitle || issueDescription)) {723// fire and forget724this.issueFormService.showConfirmCloseDialog();725} else {726this.close();727}728}729730// With latest electron upgrade, cmd+a is no longer propagating correctly for inputs in this window on mac731// Manually perform the selection732if (isMacintosh) {733if (cmdOrCtrlKey && e.key === 'a' && e.target) {734if (isHTMLInputElement(e.target) || isHTMLTextAreaElement(e.target)) {735(<HTMLInputElement>e.target).select();736}737}738}739};740741// Handle the guidance link specifically to use openerService742this.addEventListener('review-guidance-help-text', 'click', (e: Event) => {743const target = e.target as HTMLElement;744if (target.tagName === 'A' && target.getAttribute('target') === '_blank') {745this.openLink(<MouseEvent>e);746}747});748}749750public updatePerformanceInfo(info: Partial<IssueReporterData>) {751this.issueReporterModel.update(info);752this.receivedPerformanceInfo = true;753754const state = this.issueReporterModel.getData();755this.updateProcessInfo(state);756this.updateWorkspaceInfo(state);757this.updateButtonStates();758}759760private isPreviewEnabled() {761const issueType = this.issueReporterModel.getData().issueType;762763if (this.loadingExtensionData) {764return false;765}766767if (this.isWeb) {768if (issueType === IssueType.FeatureRequest || issueType === IssueType.PerformanceIssue || issueType === IssueType.Bug) {769return true;770}771} else {772if (issueType === IssueType.Bug && this.receivedSystemInfo) {773return true;774}775776if (issueType === IssueType.PerformanceIssue && this.receivedSystemInfo && this.receivedPerformanceInfo) {777return true;778}779780if (issueType === IssueType.FeatureRequest) {781return true;782}783}784785return false;786}787788private getExtensionRepositoryUrl(): string | undefined {789const selectedExtension = this.issueReporterModel.getData().selectedExtension;790return selectedExtension && selectedExtension.repositoryUrl;791}792793public getExtensionBugsUrl(): string | undefined {794const selectedExtension = this.issueReporterModel.getData().selectedExtension;795return selectedExtension && selectedExtension.bugsUrl;796}797798public searchVSCodeIssues(title: string, issueDescription?: string): void {799if (title) {800this.searchDuplicates(title, issueDescription);801} else {802this.clearSearchResults();803}804}805806public searchIssues(title: string, fileOnExtension: boolean | undefined, fileOnMarketplace: boolean | undefined): void {807if (fileOnExtension) {808return this.searchExtensionIssues(title);809}810811if (fileOnMarketplace) {812return this.searchMarketplaceIssues(title);813}814815const description = this.issueReporterModel.getData().issueDescription;816this.searchVSCodeIssues(title, description);817}818819private searchExtensionIssues(title: string): void {820const url = this.getExtensionGitHubUrl();821if (title) {822const matches = /^https?:\/\/github\.com\/(.*)/.exec(url);823if (matches && matches.length) {824const repo = matches[1];825return this.searchGitHub(repo, title);826}827828// If the extension has no repository, display empty search results829if (this.issueReporterModel.getData().selectedExtension) {830this.clearSearchResults();831return this.displaySearchResults([]);832833}834}835836this.clearSearchResults();837}838839private searchMarketplaceIssues(title: string): void {840if (title) {841const gitHubInfo = this.parseGitHubUrl(this.product.reportMarketplaceIssueUrl!);842if (gitHubInfo) {843return this.searchGitHub(`${gitHubInfo.owner}/${gitHubInfo.repositoryName}`, title);844}845}846}847848public async close(): Promise<void> {849await this.issueFormService.closeReporter();850}851852public clearSearchResults(): void {853// eslint-disable-next-line no-restricted-syntax854const similarIssues = this.getElementById('similar-issues')!;855similarIssues.innerText = '';856this.numberOfSearchResultsDisplayed = 0;857}858859@debounce(300)860private searchGitHub(repo: string, title: string): void {861const query = `is:issue+repo:${repo}+${title}`;862// eslint-disable-next-line no-restricted-syntax863const similarIssues = this.getElementById('similar-issues')!;864865fetch(`https://api.github.com/search/issues?q=${query}`).then((response) => {866response.json().then(result => {867similarIssues.innerText = '';868if (result && result.items) {869this.displaySearchResults(result.items);870}871}).catch(_ => {872console.warn('Timeout or query limit exceeded');873});874}).catch(_ => {875console.warn('Error fetching GitHub issues');876});877}878879@debounce(300)880private searchDuplicates(title: string, body?: string): void {881const url = 'https://vscode-probot.westus.cloudapp.azure.com:7890/duplicate_candidates';882const init = {883method: 'POST',884body: JSON.stringify({885title,886body887}),888headers: new Headers({889'Content-Type': 'application/json'890})891};892893fetch(url, init).then((response) => {894response.json().then(result => {895this.clearSearchResults();896897if (result && result.candidates) {898this.displaySearchResults(result.candidates);899} else {900throw new Error('Unexpected response, no candidates property');901}902}).catch(_ => {903// Ignore904});905}).catch(_ => {906// Ignore907});908}909910private displaySearchResults(results: SearchResult[]) {911// eslint-disable-next-line no-restricted-syntax912const similarIssues = this.getElementById('similar-issues')!;913if (results.length) {914const issues = $('div.issues-container');915const issuesText = $('div.list-title');916issuesText.textContent = localize('similarIssues', "Similar issues");917918this.numberOfSearchResultsDisplayed = results.length < 5 ? results.length : 5;919for (let i = 0; i < this.numberOfSearchResultsDisplayed; i++) {920const issue = results[i];921const link = $('a.issue-link', { href: issue.html_url });922link.textContent = issue.title;923link.title = issue.title;924link.addEventListener('click', (e) => this.openLink(e));925link.addEventListener('auxclick', (e) => this.openLink(<MouseEvent>e));926927let issueState: HTMLElement;928let item: HTMLElement;929if (issue.state) {930issueState = $('span.issue-state');931932const issueIcon = $('span.issue-icon');933issueIcon.appendChild(renderIcon(issue.state === 'open' ? Codicon.issueOpened : Codicon.issueClosed));934935const issueStateLabel = $('span.issue-state.label');936issueStateLabel.textContent = issue.state === 'open' ? localize('open', "Open") : localize('closed', "Closed");937938issueState.title = issue.state === 'open' ? localize('open', "Open") : localize('closed', "Closed");939issueState.appendChild(issueIcon);940issueState.appendChild(issueStateLabel);941942item = $('div.issue', undefined, issueState, link);943} else {944item = $('div.issue', undefined, link);945}946947issues.appendChild(item);948}949950similarIssues.appendChild(issuesText);951similarIssues.appendChild(issues);952}953}954955private setUpTypes(): void {956const makeOption = (issueType: IssueType, description: string) => $('option', { 'value': issueType.valueOf() }, escape(description));957958// eslint-disable-next-line no-restricted-syntax959const typeSelect = this.getElementById('issue-type')! as HTMLSelectElement;960const { issueType } = this.issueReporterModel.getData();961reset(typeSelect,962makeOption(IssueType.Bug, localize('bugReporter', "Bug Report")),963makeOption(IssueType.FeatureRequest, localize('featureRequest', "Feature Request")),964makeOption(IssueType.PerformanceIssue, localize('performanceIssue', "Performance Issue (freeze, slow, crash)"))965);966967typeSelect.value = issueType.toString();968969this.setSourceOptions();970}971972public makeOption(value: string, description: string, disabled: boolean): HTMLOptionElement {973const option: HTMLOptionElement = document.createElement('option');974option.disabled = disabled;975option.value = value;976option.textContent = description;977978return option;979}980981public setSourceOptions(): void {982// eslint-disable-next-line no-restricted-syntax983const sourceSelect = this.getElementById('issue-source')! as HTMLSelectElement;984const { issueType, fileOnExtension, selectedExtension, fileOnMarketplace, fileOnProduct } = this.issueReporterModel.getData();985let selected = sourceSelect.selectedIndex;986if (selected === -1) {987if (fileOnExtension !== undefined) {988selected = fileOnExtension ? 2 : 1;989} else if (selectedExtension?.isBuiltin) {990selected = 1;991} else if (fileOnMarketplace) {992selected = 3;993} else if (fileOnProduct) {994selected = 1;995}996}997998sourceSelect.innerText = '';999sourceSelect.append(this.makeOption('', localize('selectSource', "Select source"), true));1000sourceSelect.append(this.makeOption(IssueSource.VSCode, localize('vscode', "Visual Studio Code"), false));1001sourceSelect.append(this.makeOption(IssueSource.Extension, localize('extension', "A VS Code extension"), false));1002if (this.product.reportMarketplaceIssueUrl) {1003sourceSelect.append(this.makeOption(IssueSource.Marketplace, localize('marketplace', "Extensions Marketplace"), false));1004}10051006if (issueType !== IssueType.FeatureRequest) {1007sourceSelect.append(this.makeOption(IssueSource.Unknown, localize('unknown', "Don't know"), false));1008}10091010if (selected !== -1 && selected < sourceSelect.options.length) {1011sourceSelect.selectedIndex = selected;1012} else {1013sourceSelect.selectedIndex = 0;1014// eslint-disable-next-line no-restricted-syntax1015hide(this.getElementById('problem-source-help-text'));1016}1017}10181019public async renderBlocks(): Promise<void> {1020// Depending on Issue Type, we render different blocks and text1021const { issueType, fileOnExtension, fileOnMarketplace, selectedExtension } = this.issueReporterModel.getData();1022// eslint-disable-next-line no-restricted-syntax1023const blockContainer = this.getElementById('block-container');1024// eslint-disable-next-line no-restricted-syntax1025const systemBlock = this.window.document.querySelector('.block-system');1026// eslint-disable-next-line no-restricted-syntax1027const processBlock = this.window.document.querySelector('.block-process');1028// eslint-disable-next-line no-restricted-syntax1029const workspaceBlock = this.window.document.querySelector('.block-workspace');1030// eslint-disable-next-line no-restricted-syntax1031const extensionsBlock = this.window.document.querySelector('.block-extensions');1032// eslint-disable-next-line no-restricted-syntax1033const experimentsBlock = this.window.document.querySelector('.block-experiments');1034// eslint-disable-next-line no-restricted-syntax1035const extensionDataBlock = this.window.document.querySelector('.block-extension-data');10361037// eslint-disable-next-line no-restricted-syntax1038const problemSource = this.getElementById('problem-source')!;1039// eslint-disable-next-line no-restricted-syntax1040const descriptionTitle = this.getElementById('issue-description-label')!;1041// eslint-disable-next-line no-restricted-syntax1042const descriptionSubtitle = this.getElementById('issue-description-subtitle')!;1043// eslint-disable-next-line no-restricted-syntax1044const extensionSelector = this.getElementById('extension-selection')!;1045// eslint-disable-next-line no-restricted-syntax1046const downloadExtensionDataLink = <HTMLAnchorElement>this.getElementById('extension-data-download')!;10471048// eslint-disable-next-line no-restricted-syntax1049const titleTextArea = this.getElementById('issue-title-container')!;1050// eslint-disable-next-line no-restricted-syntax1051const descriptionTextArea = this.getElementById('description')!;1052// eslint-disable-next-line no-restricted-syntax1053const extensionDataTextArea = this.getElementById('extension-data')!;10541055// Hide all by default1056hide(blockContainer);1057hide(systemBlock);1058hide(processBlock);1059hide(workspaceBlock);1060hide(extensionsBlock);1061hide(experimentsBlock);1062hide(extensionSelector);1063hide(extensionDataTextArea);1064hide(extensionDataBlock);1065hide(downloadExtensionDataLink);10661067show(problemSource);1068show(titleTextArea);1069show(descriptionTextArea);10701071if (fileOnExtension) {1072show(extensionSelector);1073}10741075const extensionData = this.issueReporterModel.getData().extensionData;1076if (extensionData && extensionData.length > MAX_EXTENSION_DATA_LENGTH) {1077show(downloadExtensionDataLink);1078const date = new Date();1079const formattedDate = date.toISOString().split('T')[0]; // YYYY-MM-DD1080const formattedTime = date.toTimeString().split(' ')[0].replace(/:/g, '-'); // HH-MM-SS1081const fileName = `extensionData_${formattedDate}_${formattedTime}.md`;1082const handleLinkClick = async () => {1083const downloadPath = await this.fileDialogService.showSaveDialog({1084title: localize('saveExtensionData', "Save Extension Data"),1085availableFileSystems: [Schemas.file],1086defaultUri: joinPath(await this.fileDialogService.defaultFilePath(Schemas.file), fileName),1087});10881089if (downloadPath) {1090await this.fileService.writeFile(downloadPath, VSBuffer.fromString(extensionData));1091}1092};10931094downloadExtensionDataLink.addEventListener('click', handleLinkClick);10951096this._register({1097dispose: () => downloadExtensionDataLink.removeEventListener('click', handleLinkClick)1098});1099}11001101if (selectedExtension && this.nonGitHubIssueUrl) {1102hide(titleTextArea);1103hide(descriptionTextArea);1104reset(descriptionTitle, localize('handlesIssuesElsewhere', "This extension handles issues outside of VS Code"));1105reset(descriptionSubtitle, localize('elsewhereDescription', "The '{0}' extension prefers to use an external issue reporter. To be taken to that issue reporting experience, click the button below.", selectedExtension.displayName));1106this.publicGithubButton.label = localize('openIssueReporter', "Open External Issue Reporter");1107return;1108}11091110if (fileOnExtension && selectedExtension?.data) {1111const data = selectedExtension?.data;1112(extensionDataTextArea as HTMLElement).innerText = data.toString();1113(extensionDataTextArea as HTMLTextAreaElement).readOnly = true;1114show(extensionDataBlock);1115}11161117// only if we know comes from the open reporter command1118if (fileOnExtension && this.openReporter) {1119(extensionDataTextArea as HTMLTextAreaElement).readOnly = true;1120setTimeout(() => {1121// delay to make sure from command or not1122if (this.openReporter) {1123show(extensionDataBlock);1124}1125}, 100);1126show(extensionDataBlock);1127}11281129if (issueType === IssueType.Bug) {1130if (!fileOnMarketplace) {1131show(blockContainer);1132show(systemBlock);1133show(experimentsBlock);1134if (!fileOnExtension) {1135show(extensionsBlock);1136}1137}11381139reset(descriptionTitle, localize('stepsToReproduce', "Steps to Reproduce") + ' ', $('span.required-input', undefined, '*'));1140reset(descriptionSubtitle, 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."));1141} else if (issueType === IssueType.PerformanceIssue) {1142if (!fileOnMarketplace) {1143show(blockContainer);1144show(systemBlock);1145show(processBlock);1146show(workspaceBlock);1147show(experimentsBlock);1148}11491150if (fileOnExtension) {1151show(extensionSelector);1152} else if (!fileOnMarketplace) {1153show(extensionsBlock);1154}11551156reset(descriptionTitle, localize('stepsToReproduce', "Steps to Reproduce") + ' ', $('span.required-input', undefined, '*'));1157reset(descriptionSubtitle, 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."));1158} else if (issueType === IssueType.FeatureRequest) {1159reset(descriptionTitle, localize('description', "Description") + ' ', $('span.required-input', undefined, '*'));1160reset(descriptionSubtitle, 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."));1161}1162}11631164public validateInput(inputId: string): boolean {1165// eslint-disable-next-line no-restricted-syntax1166const inputElement = (<HTMLInputElement>this.getElementById(inputId));1167// eslint-disable-next-line no-restricted-syntax1168const inputValidationMessage = this.getElementById(`${inputId}-empty-error`);1169// eslint-disable-next-line no-restricted-syntax1170const descriptionShortMessage = this.getElementById(`description-short-error`);1171if (inputId === 'description' && this.nonGitHubIssueUrl && this.data.extensionId) {1172return true;1173} else if (!inputElement.value) {1174inputElement.classList.add('invalid-input');1175inputValidationMessage?.classList.remove('hidden');1176descriptionShortMessage?.classList.add('hidden');1177return false;1178} else if (inputId === 'description' && inputElement.value.length < 10) {1179inputElement.classList.add('invalid-input');1180descriptionShortMessage?.classList.remove('hidden');1181inputValidationMessage?.classList.add('hidden');1182return false;1183} else {1184inputElement.classList.remove('invalid-input');1185inputValidationMessage?.classList.add('hidden');1186if (inputId === 'description') {1187descriptionShortMessage?.classList.add('hidden');1188}1189return true;1190}1191}11921193public validateInputs(): boolean {1194let isValid = true;1195['issue-title', 'description', 'issue-source'].forEach(elementId => {1196isValid = this.validateInput(elementId) && isValid;1197});11981199if (this.issueReporterModel.fileOnExtension()) {1200isValid = this.validateInput('extension-selector') && isValid;1201}12021203return isValid;1204}12051206public async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string; repositoryName: string }): Promise<boolean> {1207const url = `https://api.github.com/repos/${gitHubDetails.owner}/${gitHubDetails.repositoryName}/issues`;1208const init = {1209method: 'POST',1210body: JSON.stringify({1211title: issueTitle,1212body: issueBody1213}),1214headers: new Headers({1215'Content-Type': 'application/json',1216'Authorization': `Bearer ${this.data.githubAccessToken}`,1217'User-Agent': 'request'1218})1219};12201221const response = await fetch(url, init);1222if (!response.ok) {1223console.error('Invalid GitHub URL provided.');1224return false;1225}1226const result = await response.json();1227await this.openLink(result.html_url);1228this.close();1229return true;1230}12311232public async createIssue(shouldCreate?: boolean, privateUri?: boolean): Promise<boolean> {1233const selectedExtension = this.issueReporterModel.getData().selectedExtension;1234// Short circuit if the extension provides a custom issue handler1235if (this.nonGitHubIssueUrl) {1236const url = this.getExtensionBugsUrl();1237if (url) {1238this.hasBeenSubmitted = true;1239return true;1240}1241}12421243if (!this.validateInputs()) {1244// If inputs are invalid, set focus to the first one and add listeners on them1245// to detect further changes1246// eslint-disable-next-line no-restricted-syntax1247const invalidInput = this.window.document.getElementsByClassName('invalid-input');1248if (invalidInput.length) {1249(<HTMLInputElement>invalidInput[0]).focus();1250}12511252this.addEventListener('issue-title', 'input', _ => {1253this.validateInput('issue-title');1254});12551256this.addEventListener('description', 'input', _ => {1257this.validateInput('description');1258});12591260this.addEventListener('issue-source', 'change', _ => {1261this.validateInput('issue-source');1262});12631264if (this.issueReporterModel.fileOnExtension()) {1265this.addEventListener('extension-selector', 'change', _ => {1266this.validateInput('extension-selector');1267});1268}12691270return false;1271}12721273this.hasBeenSubmitted = true;12741275// eslint-disable-next-line no-restricted-syntax1276const issueTitle = (<HTMLInputElement>this.getElementById('issue-title')).value;1277const issueBody = this.issueReporterModel.serialize();12781279let issueUrl = privateUri ? this.getPrivateIssueUrl() : this.getIssueUrl();1280if (!issueUrl) {1281console.error(`No ${privateUri ? 'private ' : ''}issue url found`);1282return false;1283}1284if (selectedExtension?.uri) {1285const uri = URI.revive(selectedExtension.uri);1286issueUrl = uri.toString();1287}12881289const gitHubDetails = this.parseGitHubUrl(issueUrl);1290if (this.data.githubAccessToken && gitHubDetails && shouldCreate) {1291return this.submitToGitHub(issueTitle, issueBody, gitHubDetails);1292}12931294// eslint-disable-next-line no-restricted-syntax1295const baseUrl = this.getIssueUrlWithTitle((<HTMLInputElement>this.getElementById('issue-title')).value, issueUrl);1296let url = baseUrl + `&body=${encodeURIComponent(issueBody)}`;12971298url = this.addTemplateToUrl(url, gitHubDetails?.owner, gitHubDetails?.repositoryName);12991300if (url.length > MAX_URL_LENGTH) {1301try {1302url = await this.writeToClipboard(baseUrl, issueBody);1303url = this.addTemplateToUrl(url, gitHubDetails?.owner, gitHubDetails?.repositoryName);1304} catch (_) {1305console.error('Writing to clipboard failed');1306return false;1307}1308}13091310await this.openLink(url);13111312return true;1313}13141315public async writeToClipboard(baseUrl: string, issueBody: string): Promise<string> {1316const shouldWrite = await this.issueFormService.showClipboardDialog();1317if (!shouldWrite) {1318throw new CancellationError();1319}13201321return baseUrl + `&body=${encodeURIComponent(localize('pasteData', "We have written the needed data into your clipboard because it was too large to send. Please paste."))}`;1322}13231324public addTemplateToUrl(baseUrl: string, owner?: string, repositoryName?: string): string {1325const isVscode = this.issueReporterModel.getData().fileOnProduct;1326const isMicrosoft = owner?.toLowerCase() === 'microsoft';1327const needsTemplate = isVscode || (isMicrosoft && (repositoryName === 'vscode' || repositoryName === 'vscode-python'));13281329if (needsTemplate) {1330try {1331const url = new URL(baseUrl);1332url.searchParams.set('template', 'bug_report.md');1333return url.toString();1334} catch {1335// fallback if baseUrl is not a valid URL1336return baseUrl + '&template=bug_report.md';1337}1338}1339return baseUrl;1340}13411342public getIssueUrl(): string {1343return this.issueReporterModel.fileOnExtension()1344? this.getExtensionGitHubUrl()1345: this.issueReporterModel.getData().fileOnMarketplace1346? this.product.reportMarketplaceIssueUrl!1347: this.product.reportIssueUrl!;1348}13491350// for when command 'workbench.action.openIssueReporter' passes along a1351// `privateUri` UriComponents value1352public getPrivateIssueUrl(): string | undefined {1353return URI.revive(this.data.privateUri)?.toString();1354}13551356public parseGitHubUrl(url: string): undefined | { repositoryName: string; owner: string } {1357// Assumes a GitHub url to a particular repo, https://github.com/repositoryName/owner.1358// Repository name and owner cannot contain '/'1359const match = /^https?:\/\/github\.com\/([^\/]*)\/([^\/]*).*/.exec(url);1360if (match && match.length) {1361return {1362owner: match[1],1363repositoryName: match[2]1364};1365} else {1366console.error('No GitHub issues match');1367}13681369return undefined;1370}13711372private getExtensionGitHubUrl(): string {1373let repositoryUrl = '';1374const bugsUrl = this.getExtensionBugsUrl();1375const extensionUrl = this.getExtensionRepositoryUrl();1376// If given, try to match the extension's bug url1377if (bugsUrl && bugsUrl.match(/^https?:\/\/github\.com\/([^\/]*)\/([^\/]*)\/?(\/issues)?$/)) {1378// matches exactly: https://github.com/owner/repo/issues1379repositoryUrl = normalizeGitHubUrl(bugsUrl);1380} else if (extensionUrl && extensionUrl.match(/^https?:\/\/github\.com\/([^\/]*)\/([^\/]*)$/)) {1381// matches exactly: https://github.com/owner/repo1382repositoryUrl = normalizeGitHubUrl(extensionUrl);1383} else {1384this.nonGitHubIssueUrl = true;1385repositoryUrl = bugsUrl || extensionUrl || '';1386}13871388return repositoryUrl;1389}13901391public getIssueUrlWithTitle(issueTitle: string, repositoryUrl: string): string {1392if (this.issueReporterModel.fileOnExtension()) {1393repositoryUrl = repositoryUrl + '/issues/new';1394}13951396const queryStringPrefix = repositoryUrl.indexOf('?') === -1 ? '?' : '&';1397return `${repositoryUrl}${queryStringPrefix}title=${encodeURIComponent(issueTitle)}`;1398}13991400public clearExtensionData(): void {1401this.nonGitHubIssueUrl = false;1402this.issueReporterModel.update({ extensionData: undefined });1403this.data.issueBody = this.data.issueBody || '';1404this.data.data = undefined;1405this.data.uri = undefined;1406this.data.privateUri = undefined;1407}14081409public async updateExtensionStatus(extension: IssueReporterExtensionData) {1410this.issueReporterModel.update({ selectedExtension: extension });14111412// uses this.configuuration.data to ensure that data is coming from `openReporter` command.1413const template = this.data.issueBody;1414if (template) {1415// eslint-disable-next-line no-restricted-syntax1416const descriptionTextArea = this.getElementById('description')!;1417const descriptionText = (descriptionTextArea as HTMLTextAreaElement).value;1418if (descriptionText === '' || !descriptionText.includes(template.toString())) {1419const fullTextArea = descriptionText + (descriptionText === '' ? '' : '\n') + template.toString();1420(descriptionTextArea as HTMLTextAreaElement).value = fullTextArea;1421this.issueReporterModel.update({ issueDescription: fullTextArea });1422}1423}14241425const data = this.data.data;1426if (data) {1427this.issueReporterModel.update({ extensionData: data });1428extension.data = data;1429// eslint-disable-next-line no-restricted-syntax1430const extensionDataBlock = this.window.document.querySelector('.block-extension-data')!;1431show(extensionDataBlock);1432this.renderBlocks();1433}14341435const uri = this.data.uri;1436if (uri) {1437extension.uri = uri;1438this.updateIssueReporterUri(extension);1439}14401441this.validateSelectedExtension();1442// eslint-disable-next-line no-restricted-syntax1443const title = (<HTMLInputElement>this.getElementById('issue-title')).value;1444this.searchExtensionIssues(title);14451446this.updateButtonStates();1447this.renderBlocks();1448}14491450public validateSelectedExtension(): void {1451// eslint-disable-next-line no-restricted-syntax1452const extensionValidationMessage = this.getElementById('extension-selection-validation-error')!;1453// eslint-disable-next-line no-restricted-syntax1454const extensionValidationNoUrlsMessage = this.getElementById('extension-selection-validation-error-no-url')!;1455hide(extensionValidationMessage);1456hide(extensionValidationNoUrlsMessage);14571458const extension = this.issueReporterModel.getData().selectedExtension;1459if (!extension) {1460this.publicGithubButton.enabled = true;1461return;1462}14631464if (this.loadingExtensionData) {1465return;1466}14671468const hasValidGitHubUrl = this.getExtensionGitHubUrl();1469if (hasValidGitHubUrl) {1470this.publicGithubButton.enabled = true;1471} else {1472this.setExtensionValidationMessage();1473this.publicGithubButton.enabled = false;1474}1475}14761477public setLoading(element: HTMLElement) {1478// Show loading1479this.openReporter = true;1480this.loadingExtensionData = true;1481this.updateButtonStates();14821483// eslint-disable-next-line no-restricted-syntax1484const extensionDataCaption = this.getElementById('extension-id')!;1485hide(extensionDataCaption);14861487// eslint-disable-next-line no-restricted-syntax1488const extensionDataCaption2 = Array.from(this.window.document.querySelectorAll('.ext-parens'));1489extensionDataCaption2.forEach(extensionDataCaption2 => hide(extensionDataCaption2));14901491// eslint-disable-next-line no-restricted-syntax1492const showLoading = this.getElementById('ext-loading')!;1493show(showLoading);1494while (showLoading.firstChild) {1495showLoading.firstChild.remove();1496}1497showLoading.append(element);14981499this.renderBlocks();1500}15011502public removeLoading(element: HTMLElement, fromReporter: boolean = false) {1503this.openReporter = fromReporter;1504this.loadingExtensionData = false;1505this.updateButtonStates();15061507// eslint-disable-next-line no-restricted-syntax1508const extensionDataCaption = this.getElementById('extension-id')!;1509show(extensionDataCaption);15101511// eslint-disable-next-line no-restricted-syntax1512const extensionDataCaption2 = Array.from(this.window.document.querySelectorAll('.ext-parens'));1513extensionDataCaption2.forEach(extensionDataCaption2 => show(extensionDataCaption2));15141515// eslint-disable-next-line no-restricted-syntax1516const hideLoading = this.getElementById('ext-loading')!;1517hide(hideLoading);1518if (hideLoading.firstChild) {1519element.remove();1520}1521this.renderBlocks();1522}15231524private setExtensionValidationMessage(): void {1525// eslint-disable-next-line no-restricted-syntax1526const extensionValidationMessage = this.getElementById('extension-selection-validation-error')!;1527// eslint-disable-next-line no-restricted-syntax1528const extensionValidationNoUrlsMessage = this.getElementById('extension-selection-validation-error-no-url')!;1529const bugsUrl = this.getExtensionBugsUrl();1530if (bugsUrl) {1531show(extensionValidationMessage);1532// eslint-disable-next-line no-restricted-syntax1533const link = this.getElementById('extensionBugsLink')!;1534link.textContent = bugsUrl;1535return;1536}15371538const extensionUrl = this.getExtensionRepositoryUrl();1539if (extensionUrl) {1540show(extensionValidationMessage);1541// eslint-disable-next-line no-restricted-syntax1542const link = this.getElementById('extensionBugsLink');1543link!.textContent = extensionUrl;1544return;1545}15461547show(extensionValidationNoUrlsMessage);1548}15491550private updateProcessInfo(state: IssueReporterModelData) {1551// eslint-disable-next-line no-restricted-syntax1552const target = this.window.document.querySelector('.block-process .block-info') as HTMLElement;1553if (target) {1554reset(target, $('code', undefined, state.processInfo ?? ''));1555}1556}15571558private updateWorkspaceInfo(state: IssueReporterModelData) {1559// eslint-disable-next-line no-restricted-syntax1560this.window.document.querySelector('.block-workspace .block-info code')!.textContent = '\n' + state.workspaceInfo;1561}15621563public updateExtensionTable(extensions: IssueReporterExtensionData[], numThemeExtensions: number): void {1564// eslint-disable-next-line no-restricted-syntax1565const target = this.window.document.querySelector<HTMLElement>('.block-extensions .block-info');1566if (target) {1567if (this.disableExtensions) {1568reset(target, localize('disabledExtensions', "Extensions are disabled"));1569return;1570}15711572const themeExclusionStr = numThemeExtensions ? `\n(${numThemeExtensions} theme extensions excluded)` : '';1573extensions = extensions || [];15741575if (!extensions.length) {1576target.innerText = 'Extensions: none' + themeExclusionStr;1577return;1578}15791580reset(target, this.getExtensionTableHtml(extensions), document.createTextNode(themeExclusionStr));1581}1582}15831584private getExtensionTableHtml(extensions: IssueReporterExtensionData[]): HTMLTableElement {1585return $('table', undefined,1586$('tr', undefined,1587$('th', undefined, 'Extension'),1588$('th', undefined, 'Author (truncated)' as string),1589$('th', undefined, 'Version')1590),1591...extensions.map(extension => $('tr', undefined,1592$('td', undefined, extension.name),1593$('td', undefined, extension.publisher?.substr(0, 3) ?? 'N/A'),1594$('td', undefined, extension.version)1595))1596);1597}15981599private async openLink(eventOrUrl: MouseEvent | string): Promise<void> {1600if (typeof eventOrUrl === 'string') {1601// Direct URL call1602await this.openerService.open(eventOrUrl, { openExternal: true });1603} else {1604// MouseEvent call1605const event = eventOrUrl;1606event.preventDefault();1607event.stopPropagation();1608// Exclude right click1609if (event.which < 3) {1610await this.openerService.open((<HTMLAnchorElement>event.target).href, { openExternal: true });1611}1612}1613}16141615public getElementById<T extends HTMLElement = HTMLElement>(elementId: string): T | undefined {1616// eslint-disable-next-line no-restricted-syntax1617const element = this.window.document.getElementById(elementId) as T | undefined;1618if (element) {1619return element;1620} else {1621return undefined;1622}1623}16241625public addEventListener(elementId: string, eventType: string, handler: (event: Event) => void): void {1626// eslint-disable-next-line no-restricted-syntax1627const element = this.getElementById(elementId);1628element?.addEventListener(eventType, handler);1629}1630}16311632// helper functions16331634export function hide(el: Element | undefined | null) {1635el?.classList.add('hidden');1636}1637export function show(el: Element | undefined | null) {1638el?.classList.remove('hidden');1639}164016411642