Path: blob/main/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts
3296 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) {152const issueTitleElement = this.getElementById<HTMLInputElement>('issue-title');153if (issueTitleElement) {154issueTitleElement.value = issueTitle;155}156}157158const issueBody = data.issueBody;159if (issueBody) {160const description = this.getElementById<HTMLTextAreaElement>('description');161if (description) {162description.value = issueBody;163this.issueReporterModel.update({ issueDescription: issueBody });164}165}166167if (this.window.document.documentElement.lang !== 'en') {168show(this.getElementById('english'));169}170171const codiconStyleSheet = createStyleSheet();172codiconStyleSheet.id = 'codiconStyles';173174const iconsStyleSheet = this._register(getIconsStyleSheet(this.themeService));175function updateAll() {176codiconStyleSheet.textContent = iconsStyleSheet.getCSS();177}178179const delayer = new RunOnceScheduler(updateAll, 0);180this._register(iconsStyleSheet.onDidChange(() => delayer.schedule()));181delayer.schedule();182183this.handleExtensionData(data.enabledExtensions);184this.setUpTypes();185186// Handle case where extension is pre-selected through the command187if ((data.data || data.uri) && targetExtension) {188this.updateExtensionStatus(targetExtension);189}190191// initialize the reporting button(s)192const issueReporterElement = this.getElementById('issue-reporter');193if (issueReporterElement) {194this.updateButtonStates();195}196}197198render(): void {199this.renderBlocks();200}201202setInitialFocus() {203const { fileOnExtension } = this.issueReporterModel.getData();204if (fileOnExtension) {205const issueTitle = this.window.document.getElementById('issue-title');206issueTitle?.focus();207} else {208const issueType = this.window.document.getElementById('issue-type');209issueType?.focus();210}211}212213public updateButtonStates() {214const issueReporterElement = this.getElementById('issue-reporter');215if (!issueReporterElement) {216// shouldn't occur -- throw?217return;218}219220221// public elements section222let publicElements = this.getElementById('public-elements');223if (!publicElements) {224publicElements = document.createElement('div');225publicElements.id = 'public-elements';226publicElements.classList.add('public-elements');227issueReporterElement.appendChild(publicElements);228}229this.updatePublicGithubButton(publicElements);230this.updatePublicRepoLink(publicElements);231232233// private filing section234let internalElements = this.getElementById('internal-elements');235if (!internalElements) {236internalElements = document.createElement('div');237internalElements.id = 'internal-elements';238internalElements.classList.add('internal-elements');239internalElements.classList.add('hidden');240issueReporterElement.appendChild(internalElements);241}242let filingRow = this.getElementById('internal-top-row');243if (!filingRow) {244filingRow = document.createElement('div');245filingRow.id = 'internal-top-row';246filingRow.classList.add('internal-top-row');247internalElements.appendChild(filingRow);248}249this.updateInternalFilingNote(filingRow);250this.updateInternalGithubButton(filingRow);251this.updateInternalElementsVisibility();252}253254private updateInternalFilingNote(container: HTMLElement) {255let filingNote = this.getElementById('internal-preview-message');256if (!filingNote) {257filingNote = document.createElement('span');258filingNote.id = 'internal-preview-message';259filingNote.classList.add('internal-preview-message');260container.appendChild(filingNote);261}262263filingNote.textContent = escape(localize('internalPreviewMessage', 'If your copilot debug logs contain private information:'));264}265266private updatePublicGithubButton(container: HTMLElement): void {267const issueReporterElement = this.getElementById('issue-reporter');268if (!issueReporterElement) {269return;270}271272// Dispose of the existing button273if (this.publicGithubButton) {274this.publicGithubButton.dispose();275}276277// setup button + dropdown if applicable278if (!this.acknowledged && this.needsUpdate) { // * old version and hasn't ack'd279this.publicGithubButton = this._register(new Button(container, unthemedButtonStyles));280this.publicGithubButton.label = localize('acknowledge', "Confirm Version Acknowledgement");281this.publicGithubButton.enabled = false;282} else if (this.data.githubAccessToken && this.isPreviewEnabled()) { // * has access token, create by default, preview dropdown283this.publicGithubButton = this._register(new ButtonWithDropdown(container, {284contextMenuProvider: this.contextMenuService,285actions: [this.previewAction],286addPrimaryActionToDropdown: false,287...unthemedButtonStyles288}));289this._register(this.publicGithubButton.onDidClick(() => {290this.createAction.run();291}));292this.publicGithubButton.label = localize('createOnGitHub', "Create on GitHub");293this.publicGithubButton.enabled = true;294} else if (this.data.githubAccessToken && !this.isPreviewEnabled()) { // * Access token but invalid preview state: simple Button (create only)295this.publicGithubButton = this._register(new Button(container, unthemedButtonStyles));296this._register(this.publicGithubButton.onDidClick(() => {297this.createAction.run();298}));299this.publicGithubButton.label = localize('createOnGitHub', "Create on GitHub");300this.publicGithubButton.enabled = true;301} else { // * No access token: simple Button (preview only)302this.publicGithubButton = this._register(new Button(container, unthemedButtonStyles));303this._register(this.publicGithubButton.onDidClick(() => {304this.previewAction.run();305}));306this.publicGithubButton.label = localize('previewOnGitHub', "Preview on GitHub");307this.publicGithubButton.enabled = true;308}309310// make sure that the repo link is after the button311const repoLink = this.getElementById('show-repo-name');312if (repoLink) {313container.insertBefore(this.publicGithubButton.element, repoLink);314}315}316317private updatePublicRepoLink(container: HTMLElement): void {318let issueRepoName = this.getElementById('show-repo-name') as HTMLAnchorElement;319if (!issueRepoName) {320issueRepoName = document.createElement('a');321issueRepoName.id = 'show-repo-name';322issueRepoName.classList.add('hidden');323container.appendChild(issueRepoName);324}325326327const selectedExtension = this.issueReporterModel.getData().selectedExtension;328if (selectedExtension && selectedExtension.uri) {329const urlString = URI.revive(selectedExtension.uri).toString();330issueRepoName.href = urlString;331issueRepoName.addEventListener('click', (e) => this.openLink(e));332issueRepoName.addEventListener('auxclick', (e) => this.openLink(<MouseEvent>e));333const gitHubInfo = this.parseGitHubUrl(urlString);334issueRepoName.textContent = gitHubInfo ? gitHubInfo.owner + '/' + gitHubInfo.repositoryName : urlString;335Object.assign(issueRepoName.style, {336alignSelf: 'flex-end',337display: 'block',338fontSize: '13px',339padding: '4px 0px',340textDecoration: 'none',341width: 'auto'342});343show(issueRepoName);344} else if (issueRepoName) {345// clear styles346issueRepoName.removeAttribute('style');347hide(issueRepoName);348}349}350351private updateInternalGithubButton(container: HTMLElement): void {352const issueReporterElement = this.getElementById('issue-reporter');353if (!issueReporterElement) {354return;355}356357// Dispose of the existing button358if (this.internalGithubButton) {359this.internalGithubButton.dispose();360}361362if (this.data.githubAccessToken && this.data.privateUri) {363this.internalGithubButton = this._register(new Button(container, unthemedButtonStyles));364this._register(this.internalGithubButton.onDidClick(() => {365this.privateAction.run();366}));367368this.internalGithubButton.element.id = 'internal-create-btn';369this.internalGithubButton.element.classList.add('internal-create-subtle');370this.internalGithubButton.label = localize('createInternally', "Create Internally");371this.internalGithubButton.enabled = true;372this.internalGithubButton.setTitle(this.data.privateUri.path!.slice(1));373}374}375376private updateInternalElementsVisibility(): void {377const container = this.getElementById('internal-elements');378if (!container) {379// shouldn't happen380return;381}382383if (this.data.githubAccessToken && this.data.privateUri) {384show(container);385container.style.display = ''; //todo: necessary even with show?386if (this.internalGithubButton) {387this.internalGithubButton.enabled = this.publicGithubButton?.enabled ?? false;388}389} else {390hide(container);391container.style.display = 'none'; //todo: necessary even with hide?392}393}394395private async updateIssueReporterUri(extension: IssueReporterExtensionData): Promise<void> {396try {397if (extension.uri) {398const uri = URI.revive(extension.uri);399extension.bugsUrl = uri.toString();400}401} catch (e) {402this.renderBlocks();403}404}405406private handleExtensionData(extensions: IssueReporterExtensionData[]) {407const installedExtensions = extensions.filter(x => !x.isBuiltin);408const { nonThemes, themes } = groupBy(installedExtensions, ext => {409return ext.isTheme ? 'themes' : 'nonThemes';410});411412const numberOfThemeExtesions = themes && themes.length;413this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: installedExtensions });414this.updateExtensionTable(nonThemes, numberOfThemeExtesions);415if (this.disableExtensions || installedExtensions.length === 0) {416(<HTMLButtonElement>this.getElementById('disableExtensions')).disabled = true;417}418419this.updateExtensionSelector(installedExtensions);420}421422private updateExtensionSelector(extensions: IssueReporterExtensionData[]): void {423interface IOption {424name: string;425id: string;426}427428const extensionOptions: IOption[] = extensions.map(extension => {429return {430name: extension.displayName || extension.name || '',431id: extension.id432};433});434435// Sort extensions by name436extensionOptions.sort((a, b) => {437const aName = a.name.toLowerCase();438const bName = b.name.toLowerCase();439if (aName > bName) {440return 1;441}442443if (aName < bName) {444return -1;445}446447return 0;448});449450const makeOption = (extension: IOption, selectedExtension?: IssueReporterExtensionData): HTMLOptionElement => {451const selected = selectedExtension && extension.id === selectedExtension.id;452return $<HTMLOptionElement>('option', {453'value': extension.id,454'selected': selected || ''455}, extension.name);456};457458const extensionsSelector = this.getElementById<HTMLSelectElement>('extension-selector');459if (extensionsSelector) {460const { selectedExtension } = this.issueReporterModel.getData();461reset(extensionsSelector, this.makeOption('', localize('selectExtension', "Select extension"), true), ...extensionOptions.map(extension => makeOption(extension, selectedExtension)));462463if (!selectedExtension) {464extensionsSelector.selectedIndex = 0;465}466467this.addEventListener('extension-selector', 'change', async (e: Event) => {468this.clearExtensionData();469const selectedExtensionId = (<HTMLInputElement>e.target).value;470this.selectedExtension = selectedExtensionId;471const extensions = this.issueReporterModel.getData().allExtensions;472const matches = extensions.filter(extension => extension.id === selectedExtensionId);473if (matches.length) {474this.issueReporterModel.update({ selectedExtension: matches[0] });475const selectedExtension = this.issueReporterModel.getData().selectedExtension;476if (selectedExtension) {477const iconElement = document.createElement('span');478iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin');479this.setLoading(iconElement);480const openReporterData = await this.sendReporterMenu(selectedExtension);481if (openReporterData) {482if (this.selectedExtension === selectedExtensionId) {483this.removeLoading(iconElement, true);484this.data = openReporterData;485}486}487else {488if (!this.loadingExtensionData) {489iconElement.classList.remove(...ThemeIcon.asClassNameArray(Codicon.loading), 'codicon-modifier-spin');490}491this.removeLoading(iconElement);492// if not using command, should have no configuration data in fields we care about and check later.493this.clearExtensionData();494495// case when previous extension was opened from normal openIssueReporter command496selectedExtension.data = undefined;497selectedExtension.uri = undefined;498}499if (this.selectedExtension === selectedExtensionId) {500// repopulates the fields with the new data given the selected extension.501this.updateExtensionStatus(matches[0]);502this.openReporter = false;503}504} else {505this.issueReporterModel.update({ selectedExtension: undefined });506this.clearSearchResults();507this.clearExtensionData();508this.validateSelectedExtension();509this.updateExtensionStatus(matches[0]);510}511}512513// Update internal action visibility after explicit selection514this.updateInternalElementsVisibility();515});516}517518this.addEventListener('problem-source', 'change', (_) => {519this.clearExtensionData();520this.validateSelectedExtension();521});522}523524private async sendReporterMenu(extension: IssueReporterExtensionData): Promise<IssueReporterData | undefined> {525try {526const timeoutPromise = new Promise<undefined>((_, reject) =>527setTimeout(() => reject(new Error('sendReporterMenu timed out')), 10000)528);529const data = await Promise.race([530this.issueFormService.sendReporterMenu(extension.id),531timeoutPromise532]);533return data;534} catch (e) {535console.error(e);536return undefined;537}538}539540private updateAcknowledgementState() {541const acknowledgementCheckbox = this.getElementById<HTMLInputElement>('includeAcknowledgement');542if (acknowledgementCheckbox) {543this.acknowledged = acknowledgementCheckbox.checked;544this.updateButtonStates();545}546}547548public setEventHandlers(): void {549(['includeSystemInfo', 'includeProcessInfo', 'includeWorkspaceInfo', 'includeExtensions', 'includeExperiments', 'includeExtensionData'] as const).forEach(elementId => {550this.addEventListener(elementId, 'click', (event: Event) => {551event.stopPropagation();552this.issueReporterModel.update({ [elementId]: !this.issueReporterModel.getData()[elementId] });553});554});555556this.addEventListener('includeAcknowledgement', 'click', (event: Event) => {557event.stopPropagation();558this.updateAcknowledgementState();559});560561const showInfoElements = this.window.document.getElementsByClassName('showInfo');562for (let i = 0; i < showInfoElements.length; i++) {563const showInfo = showInfoElements.item(i)!;564(showInfo as HTMLAnchorElement).addEventListener('click', (e: MouseEvent) => {565e.preventDefault();566const label = (<HTMLDivElement>e.target);567if (label) {568const containingElement = label.parentElement && label.parentElement.parentElement;569const info = containingElement && containingElement.lastElementChild;570if (info && info.classList.contains('hidden')) {571show(info);572label.textContent = localize('hide', "hide");573} else {574hide(info);575label.textContent = localize('show', "show");576}577}578});579}580581this.addEventListener('issue-source', 'change', (e: Event) => {582const value = (<HTMLInputElement>e.target).value;583const problemSourceHelpText = this.getElementById('problem-source-help-text')!;584if (value === '') {585this.issueReporterModel.update({ fileOnExtension: undefined });586show(problemSourceHelpText);587this.clearSearchResults();588this.render();589return;590} else {591hide(problemSourceHelpText);592}593594const descriptionTextArea = <HTMLInputElement>this.getElementById('issue-title');595if (value === IssueSource.VSCode) {596descriptionTextArea.placeholder = localize('vscodePlaceholder', "E.g Workbench is missing problems panel");597} else if (value === IssueSource.Extension) {598descriptionTextArea.placeholder = localize('extensionPlaceholder', "E.g. Missing alt text on extension readme image");599} else if (value === IssueSource.Marketplace) {600descriptionTextArea.placeholder = localize('marketplacePlaceholder', "E.g Cannot disable installed extension");601} else {602descriptionTextArea.placeholder = localize('undefinedPlaceholder', "Please enter a title");603}604605let fileOnExtension, fileOnMarketplace, fileOnProduct = false;606if (value === IssueSource.Extension) {607fileOnExtension = true;608} else if (value === IssueSource.Marketplace) {609fileOnMarketplace = true;610} else if (value === IssueSource.VSCode) {611fileOnProduct = true;612}613614this.issueReporterModel.update({ fileOnExtension, fileOnMarketplace, fileOnProduct });615this.render();616617const title = (<HTMLInputElement>this.getElementById('issue-title')).value;618this.searchIssues(title, fileOnExtension, fileOnMarketplace);619});620621this.addEventListener('description', 'input', (e: Event) => {622const issueDescription = (<HTMLInputElement>e.target).value;623this.issueReporterModel.update({ issueDescription });624625// Only search for extension issues on title change626if (this.issueReporterModel.fileOnExtension() === false) {627const title = (<HTMLInputElement>this.getElementById('issue-title')).value;628this.searchVSCodeIssues(title, issueDescription);629}630});631632this.addEventListener('issue-title', 'input', _ => {633const titleElement = this.getElementById('issue-title') as HTMLInputElement;634if (titleElement) {635const title = titleElement.value;636this.issueReporterModel.update({ issueTitle: title });637}638});639640this.addEventListener('issue-title', 'input', (e: Event) => {641const title = (<HTMLInputElement>e.target).value;642const lengthValidationMessage = this.getElementById('issue-title-length-validation-error');643const issueUrl = this.getIssueUrl();644if (title && this.getIssueUrlWithTitle(title, issueUrl).length > MAX_URL_LENGTH) {645show(lengthValidationMessage);646} else {647hide(lengthValidationMessage);648}649const issueSource = this.getElementById<HTMLSelectElement>('issue-source');650if (!issueSource || issueSource.value === '') {651return;652}653654const { fileOnExtension, fileOnMarketplace } = this.issueReporterModel.getData();655this.searchIssues(title, fileOnExtension, fileOnMarketplace);656});657658// We handle clicks in the dropdown actions now659660this.addEventListener('disableExtensions', 'click', () => {661this.issueFormService.reloadWithExtensionsDisabled();662});663664this.addEventListener('extensionBugsLink', 'click', (e: Event) => {665const url = (<HTMLElement>e.target).innerText;666this.openLink(url);667});668669this.addEventListener('disableExtensions', 'keydown', (e: Event) => {670e.stopPropagation();671if ((e as KeyboardEvent).key === 'Enter' || (e as KeyboardEvent).key === ' ') {672this.issueFormService.reloadWithExtensionsDisabled();673}674});675676this.window.document.onkeydown = async (e: KeyboardEvent) => {677const cmdOrCtrlKey = isMacintosh ? e.metaKey : e.ctrlKey;678// Cmd/Ctrl+Enter previews issue and closes window679if (cmdOrCtrlKey && e.key === 'Enter') {680this.delayedSubmit.trigger(async () => {681if (await this.createIssue()) {682this.close();683}684});685}686687// Cmd/Ctrl + w closes issue window688if (cmdOrCtrlKey && e.key === 'w') {689e.stopPropagation();690e.preventDefault();691692const issueTitle = (<HTMLInputElement>this.getElementById('issue-title'))!.value;693const { issueDescription } = this.issueReporterModel.getData();694if (!this.hasBeenSubmitted && (issueTitle || issueDescription)) {695// fire and forget696this.issueFormService.showConfirmCloseDialog();697} else {698this.close();699}700}701702// With latest electron upgrade, cmd+a is no longer propagating correctly for inputs in this window on mac703// Manually perform the selection704if (isMacintosh) {705if (cmdOrCtrlKey && e.key === 'a' && e.target) {706if (isHTMLInputElement(e.target) || isHTMLTextAreaElement(e.target)) {707(<HTMLInputElement>e.target).select();708}709}710}711};712713// Handle the guidance link specifically to use openerService714this.addEventListener('review-guidance-help-text', 'click', (e: Event) => {715const target = e.target as HTMLElement;716if (target.tagName === 'A' && target.getAttribute('target') === '_blank') {717this.openLink(<MouseEvent>e);718}719});720}721722public updatePerformanceInfo(info: Partial<IssueReporterData>) {723this.issueReporterModel.update(info);724this.receivedPerformanceInfo = true;725726const state = this.issueReporterModel.getData();727this.updateProcessInfo(state);728this.updateWorkspaceInfo(state);729this.updateButtonStates();730}731732private isPreviewEnabled() {733const issueType = this.issueReporterModel.getData().issueType;734735if (this.loadingExtensionData) {736return false;737}738739if (this.isWeb) {740if (issueType === IssueType.FeatureRequest || issueType === IssueType.PerformanceIssue || issueType === IssueType.Bug) {741return true;742}743} else {744if (issueType === IssueType.Bug && this.receivedSystemInfo) {745return true;746}747748if (issueType === IssueType.PerformanceIssue && this.receivedSystemInfo && this.receivedPerformanceInfo) {749return true;750}751752if (issueType === IssueType.FeatureRequest) {753return true;754}755}756757return false;758}759760private getExtensionRepositoryUrl(): string | undefined {761const selectedExtension = this.issueReporterModel.getData().selectedExtension;762return selectedExtension && selectedExtension.repositoryUrl;763}764765public getExtensionBugsUrl(): string | undefined {766const selectedExtension = this.issueReporterModel.getData().selectedExtension;767return selectedExtension && selectedExtension.bugsUrl;768}769770public searchVSCodeIssues(title: string, issueDescription?: string): void {771if (title) {772this.searchDuplicates(title, issueDescription);773} else {774this.clearSearchResults();775}776}777778public searchIssues(title: string, fileOnExtension: boolean | undefined, fileOnMarketplace: boolean | undefined): void {779if (fileOnExtension) {780return this.searchExtensionIssues(title);781}782783if (fileOnMarketplace) {784return this.searchMarketplaceIssues(title);785}786787const description = this.issueReporterModel.getData().issueDescription;788this.searchVSCodeIssues(title, description);789}790791private searchExtensionIssues(title: string): void {792const url = this.getExtensionGitHubUrl();793if (title) {794const matches = /^https?:\/\/github\.com\/(.*)/.exec(url);795if (matches && matches.length) {796const repo = matches[1];797return this.searchGitHub(repo, title);798}799800// If the extension has no repository, display empty search results801if (this.issueReporterModel.getData().selectedExtension) {802this.clearSearchResults();803return this.displaySearchResults([]);804805}806}807808this.clearSearchResults();809}810811private searchMarketplaceIssues(title: string): void {812if (title) {813const gitHubInfo = this.parseGitHubUrl(this.product.reportMarketplaceIssueUrl!);814if (gitHubInfo) {815return this.searchGitHub(`${gitHubInfo.owner}/${gitHubInfo.repositoryName}`, title);816}817}818}819820public async close(): Promise<void> {821await this.issueFormService.closeReporter();822}823824public clearSearchResults(): void {825const similarIssues = this.getElementById('similar-issues')!;826similarIssues.innerText = '';827this.numberOfSearchResultsDisplayed = 0;828}829830@debounce(300)831private searchGitHub(repo: string, title: string): void {832const query = `is:issue+repo:${repo}+${title}`;833const similarIssues = this.getElementById('similar-issues')!;834835fetch(`https://api.github.com/search/issues?q=${query}`).then((response) => {836response.json().then(result => {837similarIssues.innerText = '';838if (result && result.items) {839this.displaySearchResults(result.items);840}841}).catch(_ => {842console.warn('Timeout or query limit exceeded');843});844}).catch(_ => {845console.warn('Error fetching GitHub issues');846});847}848849@debounce(300)850private searchDuplicates(title: string, body?: string): void {851const url = 'https://vscode-probot.westus.cloudapp.azure.com:7890/duplicate_candidates';852const init = {853method: 'POST',854body: JSON.stringify({855title,856body857}),858headers: new Headers({859'Content-Type': 'application/json'860})861};862863fetch(url, init).then((response) => {864response.json().then(result => {865this.clearSearchResults();866867if (result && result.candidates) {868this.displaySearchResults(result.candidates);869} else {870throw new Error('Unexpected response, no candidates property');871}872}).catch(_ => {873// Ignore874});875}).catch(_ => {876// Ignore877});878}879880private displaySearchResults(results: SearchResult[]) {881const similarIssues = this.getElementById('similar-issues')!;882if (results.length) {883const issues = $('div.issues-container');884const issuesText = $('div.list-title');885issuesText.textContent = localize('similarIssues', "Similar issues");886887this.numberOfSearchResultsDisplayed = results.length < 5 ? results.length : 5;888for (let i = 0; i < this.numberOfSearchResultsDisplayed; i++) {889const issue = results[i];890const link = $('a.issue-link', { href: issue.html_url });891link.textContent = issue.title;892link.title = issue.title;893link.addEventListener('click', (e) => this.openLink(e));894link.addEventListener('auxclick', (e) => this.openLink(<MouseEvent>e));895896let issueState: HTMLElement;897let item: HTMLElement;898if (issue.state) {899issueState = $('span.issue-state');900901const issueIcon = $('span.issue-icon');902issueIcon.appendChild(renderIcon(issue.state === 'open' ? Codicon.issueOpened : Codicon.issueClosed));903904const issueStateLabel = $('span.issue-state.label');905issueStateLabel.textContent = issue.state === 'open' ? localize('open', "Open") : localize('closed', "Closed");906907issueState.title = issue.state === 'open' ? localize('open', "Open") : localize('closed', "Closed");908issueState.appendChild(issueIcon);909issueState.appendChild(issueStateLabel);910911item = $('div.issue', undefined, issueState, link);912} else {913item = $('div.issue', undefined, link);914}915916issues.appendChild(item);917}918919similarIssues.appendChild(issuesText);920similarIssues.appendChild(issues);921}922}923924private setUpTypes(): void {925const makeOption = (issueType: IssueType, description: string) => $('option', { 'value': issueType.valueOf() }, escape(description));926927const typeSelect = this.getElementById('issue-type')! as HTMLSelectElement;928const { issueType } = this.issueReporterModel.getData();929reset(typeSelect,930makeOption(IssueType.Bug, localize('bugReporter', "Bug Report")),931makeOption(IssueType.FeatureRequest, localize('featureRequest', "Feature Request")),932makeOption(IssueType.PerformanceIssue, localize('performanceIssue', "Performance Issue (freeze, slow, crash)"))933);934935typeSelect.value = issueType.toString();936937this.setSourceOptions();938}939940public makeOption(value: string, description: string, disabled: boolean): HTMLOptionElement {941const option: HTMLOptionElement = document.createElement('option');942option.disabled = disabled;943option.value = value;944option.textContent = description;945946return option;947}948949public setSourceOptions(): void {950const sourceSelect = this.getElementById('issue-source')! as HTMLSelectElement;951const { issueType, fileOnExtension, selectedExtension, fileOnMarketplace, fileOnProduct } = this.issueReporterModel.getData();952let selected = sourceSelect.selectedIndex;953if (selected === -1) {954if (fileOnExtension !== undefined) {955selected = fileOnExtension ? 2 : 1;956} else if (selectedExtension?.isBuiltin) {957selected = 1;958} else if (fileOnMarketplace) {959selected = 3;960} else if (fileOnProduct) {961selected = 1;962}963}964965sourceSelect.innerText = '';966sourceSelect.append(this.makeOption('', localize('selectSource', "Select source"), true));967sourceSelect.append(this.makeOption(IssueSource.VSCode, localize('vscode', "Visual Studio Code"), false));968sourceSelect.append(this.makeOption(IssueSource.Extension, localize('extension', "A VS Code extension"), false));969if (this.product.reportMarketplaceIssueUrl) {970sourceSelect.append(this.makeOption(IssueSource.Marketplace, localize('marketplace', "Extensions Marketplace"), false));971}972973if (issueType !== IssueType.FeatureRequest) {974sourceSelect.append(this.makeOption(IssueSource.Unknown, localize('unknown', "Don't know"), false));975}976977if (selected !== -1 && selected < sourceSelect.options.length) {978sourceSelect.selectedIndex = selected;979} else {980sourceSelect.selectedIndex = 0;981hide(this.getElementById('problem-source-help-text'));982}983}984985public async renderBlocks(): Promise<void> {986// Depending on Issue Type, we render different blocks and text987const { issueType, fileOnExtension, fileOnMarketplace, selectedExtension } = this.issueReporterModel.getData();988const blockContainer = this.getElementById('block-container');989const systemBlock = this.window.document.querySelector('.block-system');990const processBlock = this.window.document.querySelector('.block-process');991const workspaceBlock = this.window.document.querySelector('.block-workspace');992const extensionsBlock = this.window.document.querySelector('.block-extensions');993const experimentsBlock = this.window.document.querySelector('.block-experiments');994const extensionDataBlock = this.window.document.querySelector('.block-extension-data');995996const problemSource = this.getElementById('problem-source')!;997const descriptionTitle = this.getElementById('issue-description-label')!;998const descriptionSubtitle = this.getElementById('issue-description-subtitle')!;999const extensionSelector = this.getElementById('extension-selection')!;1000const downloadExtensionDataLink = <HTMLAnchorElement>this.getElementById('extension-data-download')!;10011002const titleTextArea = this.getElementById('issue-title-container')!;1003const descriptionTextArea = this.getElementById('description')!;1004const extensionDataTextArea = this.getElementById('extension-data')!;10051006// Hide all by default1007hide(blockContainer);1008hide(systemBlock);1009hide(processBlock);1010hide(workspaceBlock);1011hide(extensionsBlock);1012hide(experimentsBlock);1013hide(extensionSelector);1014hide(extensionDataTextArea);1015hide(extensionDataBlock);1016hide(downloadExtensionDataLink);10171018show(problemSource);1019show(titleTextArea);1020show(descriptionTextArea);10211022if (fileOnExtension) {1023show(extensionSelector);1024}10251026const extensionData = this.issueReporterModel.getData().extensionData;1027if (extensionData && extensionData.length > MAX_EXTENSION_DATA_LENGTH) {1028show(downloadExtensionDataLink);1029const date = new Date();1030const formattedDate = date.toISOString().split('T')[0]; // YYYY-MM-DD1031const formattedTime = date.toTimeString().split(' ')[0].replace(/:/g, '-'); // HH-MM-SS1032const fileName = `extensionData_${formattedDate}_${formattedTime}.md`;1033const handleLinkClick = async () => {1034const downloadPath = await this.fileDialogService.showSaveDialog({1035title: localize('saveExtensionData', "Save Extension Data"),1036availableFileSystems: [Schemas.file],1037defaultUri: joinPath(await this.fileDialogService.defaultFilePath(Schemas.file), fileName),1038});10391040if (downloadPath) {1041await this.fileService.writeFile(downloadPath, VSBuffer.fromString(extensionData));1042}1043};10441045downloadExtensionDataLink.addEventListener('click', handleLinkClick);10461047this._register({1048dispose: () => downloadExtensionDataLink.removeEventListener('click', handleLinkClick)1049});1050}10511052if (selectedExtension && this.nonGitHubIssueUrl) {1053hide(titleTextArea);1054hide(descriptionTextArea);1055reset(descriptionTitle, localize('handlesIssuesElsewhere', "This extension handles issues outside of VS Code"));1056reset(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));1057this.publicGithubButton.label = localize('openIssueReporter', "Open External Issue Reporter");1058return;1059}10601061if (fileOnExtension && selectedExtension?.data) {1062const data = selectedExtension?.data;1063(extensionDataTextArea as HTMLElement).innerText = data.toString();1064(extensionDataTextArea as HTMLTextAreaElement).readOnly = true;1065show(extensionDataBlock);1066}10671068// only if we know comes from the open reporter command1069if (fileOnExtension && this.openReporter) {1070(extensionDataTextArea as HTMLTextAreaElement).readOnly = true;1071setTimeout(() => {1072// delay to make sure from command or not1073if (this.openReporter) {1074show(extensionDataBlock);1075}1076}, 100);1077show(extensionDataBlock);1078}10791080if (issueType === IssueType.Bug) {1081if (!fileOnMarketplace) {1082show(blockContainer);1083show(systemBlock);1084show(experimentsBlock);1085if (!fileOnExtension) {1086show(extensionsBlock);1087}1088}10891090reset(descriptionTitle, localize('stepsToReproduce', "Steps to Reproduce") + ' ', $('span.required-input', undefined, '*'));1091reset(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."));1092} else if (issueType === IssueType.PerformanceIssue) {1093if (!fileOnMarketplace) {1094show(blockContainer);1095show(systemBlock);1096show(processBlock);1097show(workspaceBlock);1098show(experimentsBlock);1099}11001101if (fileOnExtension) {1102show(extensionSelector);1103} else if (!fileOnMarketplace) {1104show(extensionsBlock);1105}11061107reset(descriptionTitle, localize('stepsToReproduce', "Steps to Reproduce") + ' ', $('span.required-input', undefined, '*'));1108reset(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."));1109} else if (issueType === IssueType.FeatureRequest) {1110reset(descriptionTitle, localize('description', "Description") + ' ', $('span.required-input', undefined, '*'));1111reset(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."));1112}1113}11141115public validateInput(inputId: string): boolean {1116const inputElement = (<HTMLInputElement>this.getElementById(inputId));1117const inputValidationMessage = this.getElementById(`${inputId}-empty-error`);1118const descriptionShortMessage = this.getElementById(`description-short-error`);1119if (inputId === 'description' && this.nonGitHubIssueUrl && this.data.extensionId) {1120return true;1121} else if (!inputElement.value) {1122inputElement.classList.add('invalid-input');1123inputValidationMessage?.classList.remove('hidden');1124descriptionShortMessage?.classList.add('hidden');1125return false;1126} else if (inputId === 'description' && inputElement.value.length < 10) {1127inputElement.classList.add('invalid-input');1128descriptionShortMessage?.classList.remove('hidden');1129inputValidationMessage?.classList.add('hidden');1130return false;1131} else {1132inputElement.classList.remove('invalid-input');1133inputValidationMessage?.classList.add('hidden');1134if (inputId === 'description') {1135descriptionShortMessage?.classList.add('hidden');1136}1137return true;1138}1139}11401141public validateInputs(): boolean {1142let isValid = true;1143['issue-title', 'description', 'issue-source'].forEach(elementId => {1144isValid = this.validateInput(elementId) && isValid;1145});11461147if (this.issueReporterModel.fileOnExtension()) {1148isValid = this.validateInput('extension-selector') && isValid;1149}11501151return isValid;1152}11531154public async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string; repositoryName: string }): Promise<boolean> {1155const url = `https://api.github.com/repos/${gitHubDetails.owner}/${gitHubDetails.repositoryName}/issues`;1156const init = {1157method: 'POST',1158body: JSON.stringify({1159title: issueTitle,1160body: issueBody1161}),1162headers: new Headers({1163'Content-Type': 'application/json',1164'Authorization': `Bearer ${this.data.githubAccessToken}`,1165'User-Agent': 'request'1166})1167};11681169const response = await fetch(url, init);1170if (!response.ok) {1171console.error('Invalid GitHub URL provided.');1172return false;1173}1174const result = await response.json();1175await this.openLink(result.html_url);1176this.close();1177return true;1178}11791180public async createIssue(shouldCreate?: boolean, privateUri?: boolean): Promise<boolean> {1181const selectedExtension = this.issueReporterModel.getData().selectedExtension;1182// Short circuit if the extension provides a custom issue handler1183if (this.nonGitHubIssueUrl) {1184const url = this.getExtensionBugsUrl();1185if (url) {1186this.hasBeenSubmitted = true;1187return true;1188}1189}11901191if (!this.validateInputs()) {1192// If inputs are invalid, set focus to the first one and add listeners on them1193// to detect further changes1194const invalidInput = this.window.document.getElementsByClassName('invalid-input');1195if (invalidInput.length) {1196(<HTMLInputElement>invalidInput[0]).focus();1197}11981199this.addEventListener('issue-title', 'input', _ => {1200this.validateInput('issue-title');1201});12021203this.addEventListener('description', 'input', _ => {1204this.validateInput('description');1205});12061207this.addEventListener('issue-source', 'change', _ => {1208this.validateInput('issue-source');1209});12101211if (this.issueReporterModel.fileOnExtension()) {1212this.addEventListener('extension-selector', 'change', _ => {1213this.validateInput('extension-selector');1214});1215}12161217return false;1218}12191220this.hasBeenSubmitted = true;12211222const issueTitle = (<HTMLInputElement>this.getElementById('issue-title')).value;1223const issueBody = this.issueReporterModel.serialize();12241225let issueUrl = privateUri ? this.getPrivateIssueUrl() : this.getIssueUrl();1226if (!issueUrl) {1227console.error(`No ${privateUri ? 'private ' : ''}issue url found`);1228return false;1229}1230if (selectedExtension?.uri) {1231const uri = URI.revive(selectedExtension.uri);1232issueUrl = uri.toString();1233}12341235const gitHubDetails = this.parseGitHubUrl(issueUrl);1236if (this.data.githubAccessToken && gitHubDetails && shouldCreate) {1237return this.submitToGitHub(issueTitle, issueBody, gitHubDetails);1238}12391240const baseUrl = this.getIssueUrlWithTitle((<HTMLInputElement>this.getElementById('issue-title')).value, issueUrl);1241let url = baseUrl + `&body=${encodeURIComponent(issueBody)}`;12421243url = this.addTemplateToUrl(url, gitHubDetails?.owner, gitHubDetails?.repositoryName);12441245if (url.length > MAX_URL_LENGTH) {1246try {1247url = await this.writeToClipboard(baseUrl, issueBody);1248url = this.addTemplateToUrl(url, gitHubDetails?.owner, gitHubDetails?.repositoryName);1249} catch (_) {1250console.error('Writing to clipboard failed');1251return false;1252}1253}12541255await this.openLink(url);12561257return true;1258}12591260public async writeToClipboard(baseUrl: string, issueBody: string): Promise<string> {1261const shouldWrite = await this.issueFormService.showClipboardDialog();1262if (!shouldWrite) {1263throw new CancellationError();1264}12651266return baseUrl + `&body=${encodeURIComponent(localize('pasteData', "We have written the needed data into your clipboard because it was too large to send. Please paste."))}`;1267}12681269public addTemplateToUrl(baseUrl: string, owner?: string, repositoryName?: string): string {1270const isVscode = this.issueReporterModel.getData().fileOnProduct;1271const isMicrosoft = owner?.toLowerCase() === 'microsoft';1272const needsTemplate = isVscode || (isMicrosoft && (repositoryName === 'vscode' || repositoryName === 'vscode-python'));12731274if (needsTemplate) {1275try {1276const url = new URL(baseUrl);1277url.searchParams.set('template', 'bug_report.md');1278return url.toString();1279} catch {1280// fallback if baseUrl is not a valid URL1281return baseUrl + '&template=bug_report.md';1282}1283}1284return baseUrl;1285}12861287public getIssueUrl(): string {1288return this.issueReporterModel.fileOnExtension()1289? this.getExtensionGitHubUrl()1290: this.issueReporterModel.getData().fileOnMarketplace1291? this.product.reportMarketplaceIssueUrl!1292: this.product.reportIssueUrl!;1293}12941295// for when command 'workbench.action.openIssueReporter' passes along a1296// `privateUri` UriComponents value1297public getPrivateIssueUrl(): string | undefined {1298return URI.revive(this.data.privateUri)?.toString();1299}13001301public parseGitHubUrl(url: string): undefined | { repositoryName: string; owner: string } {1302// Assumes a GitHub url to a particular repo, https://github.com/repositoryName/owner.1303// Repository name and owner cannot contain '/'1304const match = /^https?:\/\/github\.com\/([^\/]*)\/([^\/]*).*/.exec(url);1305if (match && match.length) {1306return {1307owner: match[1],1308repositoryName: match[2]1309};1310} else {1311console.error('No GitHub issues match');1312}13131314return undefined;1315}13161317private getExtensionGitHubUrl(): string {1318let repositoryUrl = '';1319const bugsUrl = this.getExtensionBugsUrl();1320const extensionUrl = this.getExtensionRepositoryUrl();1321// If given, try to match the extension's bug url1322if (bugsUrl && bugsUrl.match(/^https?:\/\/github\.com\/([^\/]*)\/([^\/]*)\/?(\/issues)?$/)) {1323// matches exactly: https://github.com/owner/repo/issues1324repositoryUrl = normalizeGitHubUrl(bugsUrl);1325} else if (extensionUrl && extensionUrl.match(/^https?:\/\/github\.com\/([^\/]*)\/([^\/]*)$/)) {1326// matches exactly: https://github.com/owner/repo1327repositoryUrl = normalizeGitHubUrl(extensionUrl);1328} else {1329this.nonGitHubIssueUrl = true;1330repositoryUrl = bugsUrl || extensionUrl || '';1331}13321333return repositoryUrl;1334}13351336public getIssueUrlWithTitle(issueTitle: string, repositoryUrl: string): string {1337if (this.issueReporterModel.fileOnExtension()) {1338repositoryUrl = repositoryUrl + '/issues/new';1339}13401341const queryStringPrefix = repositoryUrl.indexOf('?') === -1 ? '?' : '&';1342return `${repositoryUrl}${queryStringPrefix}title=${encodeURIComponent(issueTitle)}`;1343}13441345public clearExtensionData(): void {1346this.nonGitHubIssueUrl = false;1347this.issueReporterModel.update({ extensionData: undefined });1348this.data.issueBody = this.data.issueBody || '';1349this.data.data = undefined;1350this.data.uri = undefined;1351this.data.privateUri = undefined;1352}13531354public async updateExtensionStatus(extension: IssueReporterExtensionData) {1355this.issueReporterModel.update({ selectedExtension: extension });13561357// uses this.configuuration.data to ensure that data is coming from `openReporter` command.1358const template = this.data.issueBody;1359if (template) {1360const descriptionTextArea = this.getElementById('description')!;1361const descriptionText = (descriptionTextArea as HTMLTextAreaElement).value;1362if (descriptionText === '' || !descriptionText.includes(template.toString())) {1363const fullTextArea = descriptionText + (descriptionText === '' ? '' : '\n') + template.toString();1364(descriptionTextArea as HTMLTextAreaElement).value = fullTextArea;1365this.issueReporterModel.update({ issueDescription: fullTextArea });1366}1367}13681369const data = this.data.data;1370if (data) {1371this.issueReporterModel.update({ extensionData: data });1372extension.data = data;1373const extensionDataBlock = this.window.document.querySelector('.block-extension-data')!;1374show(extensionDataBlock);1375this.renderBlocks();1376}13771378const uri = this.data.uri;1379if (uri) {1380extension.uri = uri;1381this.updateIssueReporterUri(extension);1382}13831384this.validateSelectedExtension();1385const title = (<HTMLInputElement>this.getElementById('issue-title')).value;1386this.searchExtensionIssues(title);13871388this.updateButtonStates();1389this.renderBlocks();1390}13911392public validateSelectedExtension(): void {1393const extensionValidationMessage = this.getElementById('extension-selection-validation-error')!;1394const extensionValidationNoUrlsMessage = this.getElementById('extension-selection-validation-error-no-url')!;1395hide(extensionValidationMessage);1396hide(extensionValidationNoUrlsMessage);13971398const extension = this.issueReporterModel.getData().selectedExtension;1399if (!extension) {1400this.publicGithubButton.enabled = true;1401return;1402}14031404if (this.loadingExtensionData) {1405return;1406}14071408const hasValidGitHubUrl = this.getExtensionGitHubUrl();1409if (hasValidGitHubUrl) {1410this.publicGithubButton.enabled = true;1411} else {1412this.setExtensionValidationMessage();1413this.publicGithubButton.enabled = false;1414}1415}14161417public setLoading(element: HTMLElement) {1418// Show loading1419this.openReporter = true;1420this.loadingExtensionData = true;1421this.updateButtonStates();14221423const extensionDataCaption = this.getElementById('extension-id')!;1424hide(extensionDataCaption);14251426const extensionDataCaption2 = Array.from(this.window.document.querySelectorAll('.ext-parens'));1427extensionDataCaption2.forEach(extensionDataCaption2 => hide(extensionDataCaption2));14281429const showLoading = this.getElementById('ext-loading')!;1430show(showLoading);1431while (showLoading.firstChild) {1432showLoading.firstChild.remove();1433}1434showLoading.append(element);14351436this.renderBlocks();1437}14381439public removeLoading(element: HTMLElement, fromReporter: boolean = false) {1440this.openReporter = fromReporter;1441this.loadingExtensionData = false;1442this.updateButtonStates();14431444const extensionDataCaption = this.getElementById('extension-id')!;1445show(extensionDataCaption);14461447const extensionDataCaption2 = Array.from(this.window.document.querySelectorAll('.ext-parens'));1448extensionDataCaption2.forEach(extensionDataCaption2 => show(extensionDataCaption2));14491450const hideLoading = this.getElementById('ext-loading')!;1451hide(hideLoading);1452if (hideLoading.firstChild) {1453element.remove();1454}1455this.renderBlocks();1456}14571458private setExtensionValidationMessage(): void {1459const extensionValidationMessage = this.getElementById('extension-selection-validation-error')!;1460const extensionValidationNoUrlsMessage = this.getElementById('extension-selection-validation-error-no-url')!;1461const bugsUrl = this.getExtensionBugsUrl();1462if (bugsUrl) {1463show(extensionValidationMessage);1464const link = this.getElementById('extensionBugsLink')!;1465link.textContent = bugsUrl;1466return;1467}14681469const extensionUrl = this.getExtensionRepositoryUrl();1470if (extensionUrl) {1471show(extensionValidationMessage);1472const link = this.getElementById('extensionBugsLink');1473link!.textContent = extensionUrl;1474return;1475}14761477show(extensionValidationNoUrlsMessage);1478}14791480private updateProcessInfo(state: IssueReporterModelData) {1481const target = this.window.document.querySelector('.block-process .block-info') as HTMLElement;1482if (target) {1483reset(target, $('code', undefined, state.processInfo ?? ''));1484}1485}14861487private updateWorkspaceInfo(state: IssueReporterModelData) {1488this.window.document.querySelector('.block-workspace .block-info code')!.textContent = '\n' + state.workspaceInfo;1489}14901491public updateExtensionTable(extensions: IssueReporterExtensionData[], numThemeExtensions: number): void {1492const target = this.window.document.querySelector<HTMLElement>('.block-extensions .block-info');1493if (target) {1494if (this.disableExtensions) {1495reset(target, localize('disabledExtensions', "Extensions are disabled"));1496return;1497}14981499const themeExclusionStr = numThemeExtensions ? `\n(${numThemeExtensions} theme extensions excluded)` : '';1500extensions = extensions || [];15011502if (!extensions.length) {1503target.innerText = 'Extensions: none' + themeExclusionStr;1504return;1505}15061507reset(target, this.getExtensionTableHtml(extensions), document.createTextNode(themeExclusionStr));1508}1509}15101511private getExtensionTableHtml(extensions: IssueReporterExtensionData[]): HTMLTableElement {1512return $('table', undefined,1513$('tr', undefined,1514$('th', undefined, 'Extension'),1515$('th', undefined, 'Author (truncated)' as string),1516$('th', undefined, 'Version')1517),1518...extensions.map(extension => $('tr', undefined,1519$('td', undefined, extension.name),1520$('td', undefined, extension.publisher?.substr(0, 3) ?? 'N/A'),1521$('td', undefined, extension.version)1522))1523);1524}15251526private async openLink(eventOrUrl: MouseEvent | string): Promise<void> {1527if (typeof eventOrUrl === 'string') {1528// Direct URL call1529await this.openerService.open(eventOrUrl, { openExternal: true });1530} else {1531// MouseEvent call1532const event = eventOrUrl;1533event.preventDefault();1534event.stopPropagation();1535// Exclude right click1536if (event.which < 3) {1537await this.openerService.open((<HTMLAnchorElement>event.target).href, { openExternal: true });1538}1539}1540}15411542public getElementById<T extends HTMLElement = HTMLElement>(elementId: string): T | undefined {1543const element = this.window.document.getElementById(elementId) as T | undefined;1544if (element) {1545return element;1546} else {1547return undefined;1548}1549}15501551public addEventListener(elementId: string, eventType: string, handler: (event: Event) => void): void {1552const element = this.getElementById(elementId);1553element?.addEventListener(eventType, handler);1554}1555}15561557// helper functions15581559export function hide(el: Element | undefined | null) {1560el?.classList.add('hidden');1561}1562export function show(el: Element | undefined | null) {1563el?.classList.remove('hidden');1564}156515661567