Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,35 @@ import { Button, unthemedButtonStyles } from '../../../../base/browser/ui/button
import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
import { mainWindow } from '../../../../base/browser/window.js';
import { Delayer, RunOnceScheduler } from '../../../../base/common/async.js';
import { VSBuffer } from '../../../../base/common/buffer.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { groupBy } from '../../../../base/common/collections.js';
import { debounce } from '../../../../base/common/decorators.js';
import { CancellationError } from '../../../../base/common/errors.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { Schemas } from '../../../../base/common/network.js';
import { isLinuxSnap, isMacintosh } from '../../../../base/common/platform.js';
import { IProductConfiguration } from '../../../../base/common/product.js';
import { joinPath } from '../../../../base/common/resources.js';
import { escape } from '../../../../base/common/strings.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { URI } from '../../../../base/common/uri.js';
import { localize } from '../../../../nls.js';
import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { getIconsStyleSheet } from '../../../../platform/theme/browser/iconsStyleSheet.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from './issueReporterModel.js';
import { IIssueFormService, IssueReporterData, IssueReporterExtensionData, IssueReporterStyles, IssueType } from '../common/issue.js';
import { normalizeGitHubUrl } from '../common/issueReporterUtil.js';
import { IssueReporterModel, IssueReporterData as IssueReporterModelData } from './issueReporterModel.js';

const MAX_URL_LENGTH = 7500;

// 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.
// We round down to be safe.
// ref https://github.com/github/issues/issues/12858
const MAX_EXTENSION_DATA_LENGTH = 55000;

interface SearchResult {
html_url: string;
title: string;
Expand Down Expand Up @@ -68,6 +78,8 @@ export class BaseIssueReporterService extends Disposable {
public readonly isWeb: boolean,
@IIssueFormService public readonly issueFormService: IIssueFormService,
@IThemeService public readonly themeService: IThemeService,
@IFileService public readonly fileService: IFileService,
@IFileDialogService public readonly fileDialogService: IFileDialogService,
) {
super();
const targetExtension = data.extensionId ? data.enabledExtensions.find(extension => extension.id.toLocaleLowerCase() === data.extensionId?.toLocaleLowerCase()) : undefined;
Expand Down Expand Up @@ -861,7 +873,7 @@ export class BaseIssueReporterService extends Disposable {
}
}

public renderBlocks(): void {
public async renderBlocks(): Promise<void> {
// Depending on Issue Type, we render different blocks and text
const { issueType, fileOnExtension, fileOnMarketplace, selectedExtension } = this.issueReporterModel.getData();
const blockContainer = this.getElementById('block-container');
Expand All @@ -876,6 +888,7 @@ export class BaseIssueReporterService extends Disposable {
const descriptionTitle = this.getElementById('issue-description-label')!;
const descriptionSubtitle = this.getElementById('issue-description-subtitle')!;
const extensionSelector = this.getElementById('extension-selection')!;
const downloadExtensionDataLink = <HTMLAnchorElement>this.getElementById('extension-data-download')!;

const titleTextArea = this.getElementById('issue-title-container')!;
const descriptionTextArea = this.getElementById('description')!;
Expand All @@ -891,6 +904,7 @@ export class BaseIssueReporterService extends Disposable {
hide(extensionSelector);
hide(extensionDataTextArea);
hide(extensionDataBlock);
hide(downloadExtensionDataLink);

show(problemSource);
show(titleTextArea);
Expand All @@ -900,6 +914,31 @@ export class BaseIssueReporterService extends Disposable {
show(extensionSelector);
}

const extensionData = this.issueReporterModel.getData().extensionData;
if (extensionData && extensionData.length > MAX_EXTENSION_DATA_LENGTH) {
show(downloadExtensionDataLink);
const date = new Date();
const formattedDate = date.toISOString().split('T')[0]; // YYYY-MM-DD
const formattedTime = date.toTimeString().split(' ')[0].replace(/:/g, '-'); // HH-MM-SS
const fileName = `extensionData_${formattedDate}_${formattedTime}.md`;
const handleLinkClick = async () => {
const downloadPath = await this.fileDialogService.showSaveDialog({
title: localize('saveExtensionData', "Save Extension Data"),
availableFileSystems: [Schemas.file],
defaultUri: joinPath(await this.fileDialogService.defaultFilePath(Schemas.file), fileName),
});

if (downloadPath) {
await this.fileService.writeFile(downloadPath, VSBuffer.fromString(extensionData));
}
};

downloadExtensionDataLink.addEventListener('click', handleLinkClick);

this._register({
dispose: () => downloadExtensionDataLink.removeEventListener('click', handleLinkClick)
});
}

if (selectedExtension && this.nonGitHubIssueUrl) {
hide(titleTextArea);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export default (): string => `
${sendExtensionData}
<span id="ext-loading" hidden></span>
<span class="ext-parens" hidden>(</span><a href="#" class="showInfo" id="extension-id">${escape(localize('show', "show"))}</a><span class="ext-parens" hidden>)</span>
<a id="extension-data-download">${escape(localize('downloadExtensionData', "Download Extension Data"))}</a>
</label>
<pre class="block-info" id="extension-data" placeholder="${escape(localize('extensionData', "Extension does not have additional data to include."))}" style="white-space: pre-wrap; user-select: text;">
<!-- To be dynamically filled -->
Expand Down
10 changes: 7 additions & 3 deletions src/vs/workbench/contrib/issue/browser/issueReporterService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import { IProductConfiguration } from '../../../../base/common/product.js';
import { localize } from '../../../../nls.js';
import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { BaseIssueReporterService } from './baseIssueReporterService.js';
import { IIssueFormService, IssueReporterData } from '../common/issue.js';
import { BaseIssueReporterService } from './baseIssueReporterService.js';

// GitHub has let us know that we could up our limit here to 8k. We chose 7500 to play it safe.
// ref https://github.com/microsoft/vscode/issues/159191
Expand All @@ -23,9 +25,11 @@ export class IssueWebReporter extends BaseIssueReporterService {
product: IProductConfiguration,
window: Window,
@IIssueFormService issueFormService: IIssueFormService,
@IThemeService themeService: IThemeService
@IThemeService themeService: IThemeService,
@IFileService fileService: IFileService,
@IFileDialogService fileDialogService: IFileDialogService
) {
super(disableExtensions, data, os, product, window, true, issueFormService, themeService);
super(disableExtensions, data, os, product, window, true, issueFormService, themeService, fileService, fileDialogService);

const target = this.window.document.querySelector<HTMLElement>('.block-system .block-info');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { IProductConfiguration } from '../../../../base/common/product.js';
import { URI } from '../../../../base/common/uri.js';
import { localize } from '../../../../nls.js';
import { isRemoteDiagnosticError } from '../../../../platform/diagnostics/common/diagnostics.js';
import { IProcessMainService } from '../../../../platform/process/common/process.js';
import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { INativeHostService } from '../../../../platform/native/common/native.js';
import { IProcessMainService } from '../../../../platform/process/common/process.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { applyZoom } from '../../../../platform/window/electron-sandbox/window.js';
import { BaseIssueReporterService } from '../browser/baseIssueReporterService.js';
Expand Down Expand Up @@ -40,9 +42,11 @@ export class IssueReporter extends BaseIssueReporterService {
@INativeHostService private readonly nativeHostService: INativeHostService,
@IIssueFormService issueFormService: IIssueFormService,
@IProcessMainService processMainService: IProcessMainService,
@IThemeService themeService: IThemeService
@IThemeService themeService: IThemeService,
@IFileService fileService: IFileService,
@IFileDialogService fileDialogService: IFileDialogService
) {
super(disableExtensions, data, os, product, window, false, issueFormService, themeService);
super(disableExtensions, data, os, product, window, false, issueFormService, themeService, fileService, fileDialogService);
this.processMainService = processMainService;
this.processMainService.$getSystemInfo().then(info => {
this.issueReporterModel.update({ systemInfo: info });
Expand Down Expand Up @@ -90,7 +94,6 @@ export class IssueReporter extends BaseIssueReporterService {

public override async submitToGitHub(issueTitle: string, issueBody: string, gitHubDetails: { owner: string; repositoryName: string }): Promise<boolean> {
if (issueBody.length > MAX_GITHUB_API_LENGTH) {
console.error('Issue body is too long.');
return false;
}
const url = `https://api.github.com/repos/${gitHubDetails.owner}/${gitHubDetails.repositoryName}/issues`;
Expand Down