diff --git a/src/vs/code/electron-browser/issue/issueReporter.html b/src/vs/code/electron-browser/issue/issueReporter.html index 695de78a4cb9e455069110872b7e640864cd0936..b3c0a427fc71a79c589ab6b5545ee1889680aa15 100644 --- a/src/vs/code/electron-browser/issue/issueReporter.html +++ b/src/vs/code/electron-browser/issue/issueReporter.html @@ -4,7 +4,8 @@ - + + diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index 8c6a04f03a5145789ecad9f302221f0f9c2fc42c..d3a82fe23779861f8d725df3b5a082a3fdc56e02 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -31,7 +31,7 @@ import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProper import { WindowsChannelClient } from 'vs/platform/windows/common/windowsIpc'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { IssueReporterModel } from 'vs/code/electron-browser/issue/issueReporterModel'; -import { IssueReporterData, IssueReporterStyles, IssueType, ISettingsSearchIssueReporterData } from 'vs/platform/issue/common/issue'; +import { IssueReporterData, IssueReporterStyles, IssueType, ISettingsSearchIssueReporterData, IssueReporterFeatures } from 'vs/platform/issue/common/issue'; import BaseHtml from 'vs/code/electron-browser/issue/issueReporterPage'; import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { debounce } from 'vs/base/common/decorators'; @@ -39,8 +39,14 @@ import * as platform from 'vs/base/common/platform'; const MAX_URL_LENGTH = 5400; +interface SearchResult { + html_url: string; + title: string; +} + export interface IssueReporterConfiguration extends IWindowConfiguration { data: IssueReporterData; + features: IssueReporterFeatures; } export function startup(configuration: IssueReporterConfiguration) { @@ -55,6 +61,7 @@ export class IssueReporter extends Disposable { private telemetryService: ITelemetryService; private issueReporterModel: IssueReporterModel; private shouldQueueSearch = true; + private features: IssueReporterFeatures; private receivedSystemInfo = false; private receivedPerformanceInfo = false; @@ -78,6 +85,8 @@ export class IssueReporter extends Disposable { reprosWithoutExtensions: false }); + this.features = configuration.features; + ipcRenderer.on('issuePerformanceInfoResponse', (event, info) => { this.issueReporterModel.update(info); this.receivedPerformanceInfo = true; @@ -175,15 +184,15 @@ export class IssueReporter extends Disposable { } if (styles.sliderBackgroundColor) { - content.push(`body::-webkit-scrollbar-thumb { background-color: ${styles.sliderBackgroundColor}; }`); + content.push(`.issues-container::-webkit-scrollbar-thumb, body::-webkit-scrollbar-thumb { background-color: ${styles.sliderBackgroundColor}; }`); } if (styles.sliderActiveColor) { - content.push(`body::-webkit-scrollbar-thumb:active { background-color: ${styles.sliderActiveColor}; }`); + content.push(`.issues-container::-webkit-scrollbar-thumb:active, body::-webkit-scrollbar-thumb:active { background-color: ${styles.sliderActiveColor}; }`); } if (styles.sliderHoverColor) { - content.push(`body::-webkit-scrollbar-thumb:hover { background-color: ${styles.sliderHoverColor}; }`); + content.push(`.issues-container::-webkit-scrollbar-thumb:hover, body::-webkit-scrollbar-thumb:hover { background-color: ${styles.sliderHoverColor}; }`); } styleTag.innerHTML = content.join('\n'); @@ -321,7 +330,7 @@ export class IssueReporter extends Disposable { this.issueReporterModel.update({ issueDescription: (event.target).value }); }); - document.getElementById('issue-title').addEventListener('input', (e) => { this.searchGitHub(e); }); + document.getElementById('issue-title').addEventListener('input', (e) => { this.searchIssues(e); }); document.getElementById('github-submit-btn').addEventListener('click', () => this.createIssue()); @@ -405,62 +414,120 @@ export class IssueReporter extends Disposable { } @debounce(300) - private searchGitHub(event: Event): void { + private searchIssues(event: Event): void { const title = (event.target).value; - const similarIssues = document.getElementById('similar-issues'); if (title) { - const query = `is:issue+repo:microsoft/vscode+${title}`; - window.fetch(`https://api.github.com/search/issues?q=${query}`).then((response) => { - response.json().then(result => { - similarIssues.innerHTML = ''; - if (result && result.items && result.items.length) { - const issues = $('ul'); - const issuesText = $('div.list-title'); - issuesText.textContent = localize('similarIssues', "Similar issues"); - - const { items } = result; - const numResultsToDisplay = items.length < 5 ? items.length : 5; - for (let i = 0; i < numResultsToDisplay; i++) { - const link = $('a', { href: items[i].html_url }); - link.textContent = items[i].title; - link.addEventListener('click', openLink); - link.addEventListener('auxclick', openLink); - - const item = $('li', {}, link); - issues.appendChild(item); - } - - similarIssues.appendChild(issuesText); - similarIssues.appendChild(issues); - } else if (result && result.items) { - const message = $('div.list-title'); - message.textContent = localize('noResults', "No results found"); - similarIssues.appendChild(message); - } else { - const message = $('div.list-title'); - message.textContent = localize('rateLimited', "GitHub query limit exceeded. Please wait."); - similarIssues.appendChild(message); - - const resetTime = response.headers.get('X-RateLimit-Reset'); - const timeToWait = parseInt(resetTime) - Math.floor(Date.now() / 1000); - if (this.shouldQueueSearch) { - this.shouldQueueSearch = false; - setTimeout(() => { - this.searchGitHub(event); - this.shouldQueueSearch = true; - }, timeToWait * 1000); - } - - throw new Error(result.message); + if (this.features.useDuplicateSearch) { + this.searchDuplicates(title); + } else { + this.searchGitHub(title); + } + } else { + this.clearSearchResults(); + } + } + + private clearSearchResults(): void { + const similarIssues = document.getElementById('similar-issues'); + similarIssues.innerHTML = ''; + } + + private searchDuplicates(title: string): void { + // TODO: Change to HTTPS + const url = 'http://vscode-probot.westus.cloudapp.azure.com:5010/duplicate_candidates'; + const init = { + method: 'POST', + body: JSON.stringify({ + title + }), + headers: new Headers({ + 'Content-Type': 'application/json' + }) + }; + + window.fetch(url, init).then((response) => { + response.json().then(result => { + this.clearSearchResults(); + + if (result && result.candidates) { + const normalizedResults = result.candidates.map(result => { + return { + html_url: `https://github.com/Microsoft/vscode/issues/${result.number}`, + title: result.title + }; + }); + + this.displaySearchResults(normalizedResults); + } else { + throw new Error(); + } + }).catch((error) => { + this.logSearchError(error); + }); + }).catch((error) => { + this.logSearchError(error); + }); + } + + private searchGitHub(title: string): void { + const query = `is:issue+repo:microsoft/vscode+${title}`; + const similarIssues = document.getElementById('similar-issues'); + + window.fetch(`https://api.github.com/search/issues?q=${query}`).then((response) => { + response.json().then(result => { + similarIssues.innerHTML = ''; + if (result && result.items) { + this.displaySearchResults(result.items); + } else { + // If the items property isn't present, the rate limit has been hit + const message = $('div.list-title'); + message.textContent = localize('rateLimited', "GitHub query limit exceeded. Please wait."); + similarIssues.appendChild(message); + + const resetTime = response.headers.get('X-RateLimit-Reset'); + const timeToWait = parseInt(resetTime) - Math.floor(Date.now() / 1000); + if (this.shouldQueueSearch) { + this.shouldQueueSearch = false; + setTimeout(() => { + this.searchGitHub(title); + this.shouldQueueSearch = true; + }, timeToWait * 1000); } - }).catch((error) => { - this.logSearchError(error); - }); + + throw new Error(result.message); + } }).catch((error) => { this.logSearchError(error); }); + }).catch((error) => { + this.logSearchError(error); + }); + } + + private displaySearchResults(results: SearchResult[]) { + const similarIssues = document.getElementById('similar-issues'); + if (results.length) { + const issues = $('ul.issues-container'); + const issuesText = $('div.list-title'); + issuesText.textContent = localize('similarIssues', "Similar issues"); + + const numResultsToDisplay = results.length < 5 ? results.length : 5; + for (let i = 0; i < numResultsToDisplay; i++) { + const link = $('a', { href: results[i].html_url }); + link.textContent = results[i].title; + link.addEventListener('click', (e) => this.openLink(e)); + link.addEventListener('auxclick', (e) => this.openLink(e)); + + const item = $('li.issue', {}, link); + issues.appendChild(item); + } + + similarIssues.appendChild(issuesText); + similarIssues.appendChild(issues); } else { - similarIssues.innerHTML = ''; + const message = $('div.list-title'); + message.textContent = localize('noResults', "No results found"); + similarIssues.appendChild(message); } } @@ -703,6 +770,22 @@ export class IssueReporter extends Disposable { return table; } + + private openLink(event: MouseEvent): void { + event.preventDefault(); + event.stopPropagation(); + // Exclude right click + if (event.which < 3) { + shell.openExternal((event.target).href); + + /* __GDPR__ + "issueReporterViewSimilarIssue" : { + "usingDuplicatesAPI" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('issueReporterViewSimilarIssue', { usingDuplicatesAPI: this.features.useDuplicateSearch }); + } + } } // helper functions @@ -713,12 +796,3 @@ function hide(el) { function show(el) { el.classList.remove('hidden'); } - -function openLink(event: MouseEvent) { - event.preventDefault(); - event.stopPropagation(); - // Exclude right click - if (event.which < 3) { - shell.openExternal((event.target).href); - } -} diff --git a/src/vs/code/electron-browser/issue/media/issueReporter.css b/src/vs/code/electron-browser/issue/media/issueReporter.css index 74c25832695c128a5d4efe8a7481ef2c48d0891d..6e89ecb0dce2fe568331339d05f034c2f3e85da7 100644 --- a/src/vs/code/electron-browser/issue/media/issueReporter.css +++ b/src/vs/code/electron-browser/issue/media/issueReporter.css @@ -281,6 +281,7 @@ input, select, textarea { a { color: #CCCCCC; + text-decoration: none; } .invalid-input { @@ -347,10 +348,20 @@ button { } } -body::-webkit-scrollbar { +.issues-container::-webkit-scrollbar, body::-webkit-scrollbar { width: 14px; } -body::-webkit-scrollbar-thumb { +.issues-container::-webkit-scrollbar, body::-webkit-scrollbar-thumb { min-height: 20px; } + +.issues-container { + margin-top: .5em; + height: 108px; + overflow-y: auto; +} + +.issues-container > .issue { + padding: 1px 0; +} \ No newline at end of file diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index e095b0e2a4ba66332fa104c32bbcd3afb637e618..31ab391651443c0043e1bf726c1a986d63bb55be 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -55,6 +55,10 @@ export interface ISettingsSearchIssueReporterData extends IssueReporterData { filterResultCount: number; } +export interface IssueReporterFeatures { + useDuplicateSearch: boolean; +} + export interface IIssueService { _serviceBrand: any; openReporter(data: IssueReporterData): TPromise; diff --git a/src/vs/platform/issue/electron-main/issueService.ts b/src/vs/platform/issue/electron-main/issueService.ts index 13179eb7184140f169854f289e8f8782a785eeb2..ff083572f83579a52156d4b37654a7853980a16d 100644 --- a/src/vs/platform/issue/electron-main/issueService.ts +++ b/src/vs/platform/issue/electron-main/issueService.ts @@ -9,12 +9,13 @@ import { TPromise, Promise } from 'vs/base/common/winjs.base'; import { localize } from 'vs/nls'; import * as objects from 'vs/base/common/objects'; import { parseArgs } from 'vs/platform/environment/node/argv'; -import { IIssueService, IssueReporterData } from 'vs/platform/issue/common/issue'; +import { IIssueService, IssueReporterData, IssueReporterFeatures } from 'vs/platform/issue/common/issue'; import { BrowserWindow, ipcMain, screen } from 'electron'; import { ILaunchService } from 'vs/code/electron-main/launch'; import { getPerformanceInfo, PerformanceInfo, getSystemInfo, SystemInfo } from 'vs/code/electron-main/diagnostics'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { isMacintosh } from 'vs/base/common/platform'; +import { IConfigurationService } from '../../configuration/common/configuration'; const DEFAULT_BACKGROUND_COLOR = '#1E1E1E'; @@ -26,7 +27,8 @@ export class IssueService implements IIssueService { constructor( private machineId: string, @IEnvironmentService private environmentService: IEnvironmentService, - @ILaunchService private launchService: ILaunchService + @ILaunchService private launchService: ILaunchService, + @IConfigurationService private configurationService: IConfigurationService ) { } openReporter(data: IssueReporterData): TPromise { @@ -61,7 +63,11 @@ export class IssueService implements IIssueService { this._issueWindow.setMenuBarVisibility(false); // workaround for now, until a menu is implemented - this._issueWindow.loadURL(this.getIssueReporterPath(data)); + const features: IssueReporterFeatures = { + useDuplicateSearch: this.configurationService.getValue('issueReporter.searchDuplicates') + }; + + this._issueWindow.loadURL(this.getIssueReporterPath(data, features)); return TPromise.as(null); } @@ -158,13 +164,14 @@ export class IssueService implements IIssueService { }); } - private getIssueReporterPath(data: IssueReporterData) { + private getIssueReporterPath(data: IssueReporterData, features: IssueReporterFeatures) { const windowConfiguration = { appRoot: this.environmentService.appRoot, nodeCachedDataDir: this.environmentService.nodeCachedDataDir, windowId: this._issueWindow.id, machineId: this.machineId, - data + data, + features }; const environment = parseArgs(process.argv);