diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index 17633097592ae..8c6a04f03a514 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -11,6 +11,7 @@ import { localize } from 'vs/nls'; import { $ } from 'vs/base/browser/dom'; import * as collections from 'vs/base/common/collections'; import * as browser from 'vs/base/browser/browser'; +import { escape } from 'vs/base/common/strings'; import product from 'vs/platform/node/product'; import pkg from 'vs/platform/node/package'; import * as os from 'os'; @@ -30,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 } from 'vs/platform/issue/common/issue'; +import { IssueReporterData, IssueReporterStyles, IssueType, ISettingsSearchIssueReporterData } 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'; @@ -67,7 +68,8 @@ export class IssueReporter extends Disposable { includeSystemInfo: true, includeWorkspaceInfo: true, includeProcessInfo: true, - includeExtensions: true, + includeSearchedExtensions: true, + includeSettingsSearchDetails: true, versionInfo: { vscodeVersion: `${pkg.name} ${pkg.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`, os: `${os.type()} ${os.arch()} ${os.release()}` @@ -101,14 +103,18 @@ export class IssueReporter extends Disposable { show(document.getElementById('english')); } + this.setUpTypes(); this.setEventHandlers(); this.applyZoom(configuration.data.zoomLevel); this.applyStyles(configuration.data.styles); this.handleExtensionData(configuration.data.enabledExtensions); + + if (configuration.data.issueType === IssueType.SettingsSearchIssue) { + this.handleSettingsSearchData(configuration.data); + } } render(): void { - (document.getElementById('issue-type')).value = this.issueReporterModel.getData().issueType.toString(); this.renderBlocks(); } @@ -203,6 +209,46 @@ export class IssueReporter extends Disposable { } } + private handleSettingsSearchData(data: ISettingsSearchIssueReporterData): void { + this.issueReporterModel.update({ + actualSearchResults: data.actualSearchResults, + query: data.query, + filterResultCount: data.filterResultCount + }); + this.updateSearchedExtensionTable(data.enabledExtensions); + this.updateSettingsSearchDetails(data); + } + + private updateSettingsSearchDetails(data: ISettingsSearchIssueReporterData): void { + const target = document.querySelector('.block-settingsSearchResults .block-info'); + + const details = ` +
+
Query: "${data.query}"
+
Literal match count: ${data.filterResultCount}
+
+ `; + + let table = ` + + Setting + Extension + Score + `; + + data.actualSearchResults + .forEach(setting => { + table += ` + + ${setting.key} + ${setting.extensionId} + ${String(setting.score).slice(0, 5)} + `; + }); + + target.innerHTML = `${details}${table}
`; + } + private initServices(configuration: IWindowConfiguration): void { const serviceCollection = new ServiceCollection(); const mainProcessClient = new ElectronIPCClient(String(`window${configuration.windowId}`)); @@ -238,7 +284,7 @@ export class IssueReporter extends Disposable { this.render(); }); - ['includeSystemInfo', 'includeProcessInfo', 'includeWorkspaceInfo', 'includeExtensions'].forEach(elementId => { + ['includeSystemInfo', 'includeProcessInfo', 'includeWorkspaceInfo', 'includeExtensions', 'includeSearchedExtensions', 'includeSettingsSearchDetails'].forEach(elementId => { document.getElementById(elementId).addEventListener('click', (event: Event) => { event.stopPropagation(); this.issueReporterModel.update({ [elementId]: !this.issueReporterModel.getData()[elementId] }); @@ -351,6 +397,10 @@ export class IssueReporter extends Disposable { return true; } + if (issueType === IssueType.SettingsSearchIssue) { + return true; + } + return false; } @@ -425,6 +475,25 @@ export class IssueReporter extends Disposable { this.telemetryService.publicLog('issueReporterSearchError', { message: error.message }); } + private setUpTypes(): void { + const makeOption = (issueType: IssueType, description: string) => ``; + + const typeSelect = (document.getElementById('issue-type')); + const { issueType } = this.issueReporterModel.getData(); + if (issueType === IssueType.SettingsSearchIssue) { + typeSelect.innerHTML = makeOption(IssueType.SettingsSearchIssue, localize('settingsSearchIssue', "Settings Search Issue")); + typeSelect.disabled = true; + } else { + typeSelect.innerHTML = [ + makeOption(IssueType.Bug, localize('bugReporter', "Bug Report")), + makeOption(IssueType.PerformanceIssue, localize('performanceIssue', "Performance Issue")), + makeOption(IssueType.FeatureRequest, localize('featureRequest', "Feature Request")) + ].join('\n'); + } + + typeSelect.value = issueType.toString(); + } + private renderBlocks(): void { // Depending on Issue Type, we render different blocks and text const { issueType } = this.issueReporterModel.getData(); @@ -432,16 +501,24 @@ export class IssueReporter extends Disposable { const processBlock = document.querySelector('.block-process'); const workspaceBlock = document.querySelector('.block-workspace'); const extensionsBlock = document.querySelector('.block-extensions'); - const disabledExtensions = document.getElementById('disabledExtensions'); + const searchedExtensionsBlock = document.querySelector('.block-searchedExtensions'); + const settingsSearchResultsBlock = document.querySelector('.block-settingsSearchResults'); + const disabledExtensions = document.getElementById('disabledExtensions'); const descriptionTitle = document.getElementById('issue-description-label'); const descriptionSubtitle = document.getElementById('issue-description-subtitle'); + // Hide all by default + hide(systemBlock); + hide(processBlock); + hide(workspaceBlock); + hide(extensionsBlock); + hide(searchedExtensionsBlock); + hide(settingsSearchResultsBlock); + hide(disabledExtensions); if (issueType === IssueType.Bug) { show(systemBlock); - hide(processBlock); - hide(workspaceBlock); show(extensionsBlock); show(disabledExtensions); @@ -456,15 +533,15 @@ export class IssueReporter extends Disposable { descriptionTitle.innerHTML = `${localize('stepsToReproduce', "Steps to Reproduce")} *`; descriptionSubtitle.innerHTML = localize('performanceIssueDesciption', "When did this performance issue happen? Does it occur on startup or after a specific series of actions? We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub."); - } else { - hide(systemBlock); - hide(processBlock); - hide(workspaceBlock); - hide(extensionsBlock); - hide(disabledExtensions); - + } else if (issueType === IssueType.FeatureRequest) { descriptionTitle.innerHTML = `${localize('description', "Description")} *`; descriptionSubtitle.innerHTML = localize('featureRequestDescription', "Please describe the feature you would like to see. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub."); + } else if (issueType === IssueType.SettingsSearchIssue) { + show(searchedExtensionsBlock); + show(settingsSearchResultsBlock); + + descriptionTitle.innerHTML = `${localize('expectedResults', "Expected Results")} *`; + descriptionSubtitle.innerHTML = localize('settingsSearchResultsDescription', "Please list the results that you were expecting to see when you searched with this query. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub."); } } @@ -591,6 +668,23 @@ export class IssueReporter extends Disposable { return; } + const table = this.getExtensionTableHtml(extensions); + target.innerHTML = `${table}
${themeExclusionStr}`; + } + + private updateSearchedExtensionTable(extensions: ILocalExtension[]): void { + const target = document.querySelector('.block-searchedExtensions .block-info'); + + if (!extensions.length) { + target.innerHTML = 'Extensions: none'; + return; + } + + const table = this.getExtensionTableHtml(extensions); + target.innerHTML = `${table}
`; + } + + private getExtensionTableHtml(extensions: ILocalExtension[]): string { let table = ` Extension @@ -598,16 +692,16 @@ export class IssueReporter extends Disposable { Version `; - extensions.forEach(extension => { - table += ` + table += extensions.map(extension => { + return ` ${extension.manifest.name} ${extension.manifest.publisher.substr(0, 3)} ${extension.manifest.version} `; - }); + }).join(''); - target.innerHTML = `${table}
${themeExclusionStr}`; + return table; } } diff --git a/src/vs/code/electron-browser/issue/issueReporterModel.ts b/src/vs/code/electron-browser/issue/issueReporterModel.ts index e9590c1506874..4ae150e965156 100644 --- a/src/vs/code/electron-browser/issue/issueReporterModel.ts +++ b/src/vs/code/electron-browser/issue/issueReporterModel.ts @@ -7,7 +7,7 @@ import { assign } from 'vs/base/common/objects'; import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IssueType } from 'vs/platform/issue/common/issue'; +import { IssueType, ISettingSearchResult } from 'vs/platform/issue/common/issue'; export interface IssueReporterData { issueType?: IssueType; @@ -22,11 +22,16 @@ export interface IssueReporterData { includeWorkspaceInfo?: boolean; includeProcessInfo?: boolean; includeExtensions?: boolean; + includeSearchedExtensions?: boolean; + includeSettingsSearchDetails?: boolean; numberOfThemeExtesions?: number; enabledNonThemeExtesions?: ILocalExtension[]; extensionsDisabled?: boolean; reprosWithoutExtensions?: boolean; + actualSearchResults?: ISettingSearchResult[]; + query?: string; + filterResultCount?: number; } export class IssueReporterModel { @@ -99,6 +104,17 @@ ${this.getInfos()}`; info += this._data.reprosWithoutExtensions ? '\nReproduces without extensions' : '\nReproduces only with extensions'; } + if (this._data.issueType === IssueType.SettingsSearchIssue) { + if (this._data.includeSearchedExtensions) { + info += this.generateExtensionsMd(); + } + + if (this._data.includeSettingsSearchDetails) { + info += this.generateSettingSearchResultsMd(); + info += '\n' + this.generateSettingsSearchResultDetailsMd(); + } + } + return info; } @@ -171,6 +187,35 @@ ${tableHeader} ${table} ${themeExclusionStr} +`; + } + + private generateSettingsSearchResultDetailsMd(): string { + return ` +Query: ${this._data.query} +Literal matches: ${this._data.filterResultCount}`; + } + + private generateSettingSearchResultsMd(): string { + if (!this._data.actualSearchResults) { + return ''; + } + + if (!this._data.actualSearchResults.length) { + return `No fuzzy results`; + } + + let tableHeader = `Setting|Extension|Score +---|---|---`; + const table = this._data.actualSearchResults.map(setting => { + return `${setting.key}|${setting.extensionId}|${String(setting.score).slice(0, 5)}`; + }).join('\n'); + + return `
Results + +${tableHeader} +${table} +
`; } } \ No newline at end of file diff --git a/src/vs/code/electron-browser/issue/issueReporterPage.ts b/src/vs/code/electron-browser/issue/issueReporterPage.ts index 638a365632da9..1b889915991e5 100644 --- a/src/vs/code/electron-browser/issue/issueReporterPage.ts +++ b/src/vs/code/electron-browser/issue/issueReporterPage.ts @@ -15,9 +15,7 @@ export default (): string => `
@@ -86,6 +84,32 @@ export default (): string => ` +
+
+ ${escape(localize('searchedExtensions', "Searched Extensions"))} +
+ + +
+
+
+ +
+
+
+
+
+ ${escape(localize('settingsSearchDetails', "Settings Search Details"))} +
+ + +
+
+
+ +
+
+
diff --git a/src/vs/code/electron-browser/issue/media/issueReporter.css b/src/vs/code/electron-browser/issue/media/issueReporter.css index 3cc8d87604188..74c25832695c1 100644 --- a/src/vs/code/electron-browser/issue/media/issueReporter.css +++ b/src/vs/code/electron-browser/issue/media/issueReporter.css @@ -30,6 +30,14 @@ td { border-top: 1px solid #e9ecef; } +.block-settingsSearchResults-details { + padding-bottom: .5rem; +} + +.block-settingsSearchResults-details > div { + padding: .5rem .75rem; +} + .section { margin-bottom: 1.5em; } diff --git a/src/vs/platform/issue/common/issue.ts b/src/vs/platform/issue/common/issue.ts index 97ac40453ce57..e095b0e2a4ba6 100644 --- a/src/vs/platform/issue/common/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -9,13 +9,13 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -export const ID = 'issueService'; -export const IIssueService = createDecorator(ID); +export const IIssueService = createDecorator('issueService'); export enum IssueType { Bug, PerformanceIssue, - FeatureRequest + FeatureRequest, + SettingsSearchIssue } export interface IssueReporterStyles { @@ -42,7 +42,20 @@ export interface IssueReporterData { issueType?: IssueType; } +export interface ISettingSearchResult { + extensionId: string; + key: string; + score: number; +} + +export interface ISettingsSearchIssueReporterData extends IssueReporterData { + issueType: IssueType.SettingsSearchIssue; + actualSearchResults: ISettingSearchResult[]; + query: string; + filterResultCount: number; +} + export interface IIssueService { _serviceBrand: any; openReporter(data: IssueReporterData): TPromise; -} \ No newline at end of file +} diff --git a/src/vs/workbench/electron-browser/actions.ts b/src/vs/workbench/electron-browser/actions.ts index 13e91f0442849..f04c6bdbf0ebb 100644 --- a/src/vs/workbench/electron-browser/actions.ts +++ b/src/vs/workbench/electron-browser/actions.ts @@ -20,7 +20,6 @@ import { IMessageService, Severity } from 'vs/platform/message/common/message'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { IExtensionManagementService, LocalExtensionType, IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import paths = require('vs/base/common/paths'); import { isMacintosh, isLinux } from 'vs/base/common/platform'; @@ -44,17 +43,14 @@ import { FileKind } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService, ActivationTimes } from 'vs/platform/extensions/common/extensions'; import { getEntries } from 'vs/base/common/performance'; -import { IIssueService, IssueReporterData, IssueType, IssueReporterStyles } from 'vs/platform/issue/common/issue'; -import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; -import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry'; -import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { IssueType } from 'vs/platform/issue/common/issue'; import { domEvent } from 'vs/base/browser/event'; import { once } from 'vs/base/common/event'; import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; import { getDomNodePagePosition, createStyleSheet, createCSSRule } from 'vs/base/browser/dom'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Context } from 'vs/platform/contextkey/browser/contextKeyService'; +import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; // --- actions @@ -869,25 +865,6 @@ export class CloseMessagesAction extends Action { } } -export function getIssueReporterStyles(theme: ITheme): IssueReporterStyles { - return { - backgroundColor: theme.getColor(SIDE_BAR_BACKGROUND) && theme.getColor(SIDE_BAR_BACKGROUND).toString(), - color: theme.getColor(foreground).toString(), - textLinkColor: theme.getColor(textLinkForeground) && theme.getColor(textLinkForeground).toString(), - inputBackground: theme.getColor(inputBackground) && theme.getColor(inputBackground).toString(), - inputForeground: theme.getColor(inputForeground) && theme.getColor(inputForeground).toString(), - inputBorder: theme.getColor(inputBorder) && theme.getColor(inputBorder).toString(), - inputActiveBorder: theme.getColor(inputActiveOptionBorder) && theme.getColor(inputActiveOptionBorder).toString(), - inputErrorBorder: theme.getColor(inputValidationErrorBorder) && theme.getColor(inputValidationErrorBorder).toString(), - buttonBackground: theme.getColor(buttonBackground) && theme.getColor(buttonBackground).toString(), - buttonForeground: theme.getColor(buttonForeground) && theme.getColor(buttonForeground).toString(), - buttonHoverBackground: theme.getColor(buttonHoverBackground) && theme.getColor(buttonHoverBackground).toString(), - sliderActiveColor: theme.getColor(scrollbarSliderActiveBackground) && theme.getColor(scrollbarSliderActiveBackground).toString(), - sliderBackgroundColor: theme.getColor(scrollbarSliderBackground) && theme.getColor(scrollbarSliderBackground).toString(), - sliderHoverColor: theme.getColor(scrollbarSliderHoverBackground) && theme.getColor(scrollbarSliderHoverBackground).toString() - }; -} - export class OpenIssueReporterAction extends Action { public static readonly ID = 'workbench.action.openIssueReporter'; public static readonly LABEL = nls.localize({ key: 'reportIssueInEnglish', comment: ['Translate this to "Report Issue in English" in all languages please!'] }, "Report Issue"); @@ -895,28 +872,14 @@ export class OpenIssueReporterAction extends Action { constructor( id: string, label: string, - @IIssueService private issueService: IIssueService, - @IThemeService private themeService: IThemeService, - @IExtensionManagementService private extensionManagementService: IExtensionManagementService, - @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService + @IWorkbenchIssueService private issueService: IWorkbenchIssueService ) { super(id, label); } public run(): TPromise { - return this.extensionManagementService.getInstalled(LocalExtensionType.User).then(extensions => { - const enabledExtensions = extensions.filter(extension => this.extensionEnablementService.isEnabled({ id: getGalleryExtensionIdFromLocal(extension) })); - const theme = this.themeService.getTheme(); - const issueReporterData: IssueReporterData = { - styles: getIssueReporterStyles(theme), - zoomLevel: webFrame.getZoomLevel(), - enabledExtensions - }; - - return this.issueService.openReporter(issueReporterData).then(() => { - return TPromise.as(true); - }); - }); + return this.issueService.openReporter() + .then(() => true); } } @@ -927,30 +890,15 @@ export class ReportPerformanceIssueUsingReporterAction extends Action { constructor( id: string, label: string, - @IIssueService private issueService: IIssueService, - @IThemeService private themeService: IThemeService, - @IExtensionManagementService private extensionManagementService: IExtensionManagementService, - @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService + @IWorkbenchIssueService private issueService: IWorkbenchIssueService ) { super(id, label); } public run(): TPromise { - return this.extensionManagementService.getInstalled(LocalExtensionType.User).then(extensions => { - const enabledExtensions = extensions.filter(extension => this.extensionEnablementService.isEnabled(extension.identifier)); - const theme = this.themeService.getTheme(); - const issueReporterData: IssueReporterData = { - styles: getIssueReporterStyles(theme), - zoomLevel: webFrame.getZoomLevel(), - enabledExtensions, - issueType: IssueType.PerformanceIssue - }; - - // TODO: Reporter should send timings table as well - return this.issueService.openReporter(issueReporterData).then(() => { - return TPromise.as(true); - }); - }); + // TODO: Reporter should send timings table as well + return this.issueService.openReporter({ issueType: IssueType.PerformanceIssue }) + .then(() => true); } } diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index dfce549450b22..7dee2b7d03c23 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -92,6 +92,8 @@ import { stat } from 'fs'; import { join } from 'path'; import { ILocalizationsChannel, LocalizationsChannelClient } from 'vs/platform/localizations/common/localizationsIpc'; import { ILocalizationsService } from 'vs/platform/localizations/common/localizations'; +import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; +import { WorkbenchIssueService } from 'vs/workbench/services/issue/electron-browser/workbenchIssueService'; /** * Services that we require for the Shell @@ -445,6 +447,8 @@ export class WorkbenchShell { serviceCollection.set(ISearchService, new SyncDescriptor(SearchService)); + serviceCollection.set(IWorkbenchIssueService, new SyncDescriptor(WorkbenchIssueService)); + serviceCollection.set(ICodeEditorService, new SyncDescriptor(CodeEditorServiceImpl)); serviceCollection.set(IIntegrityService, new SyncDescriptor(IntegrityServiceImpl)); diff --git a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts index 85cb33132aa9e..805bbbd8ebad5 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesRenderers.ts @@ -18,7 +18,7 @@ import * as editorCommon from 'vs/editor/common/editorCommon'; import { Range, IRange } from 'vs/editor/common/core/range'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IPreferencesService, ISettingsGroup, ISetting, IPreferencesEditorModel, IFilterResult, ISettingsEditorModel, IScoredResults, IWorkbenchSettingsConfiguration, IExtensionSetting } from 'vs/workbench/parts/preferences/common/preferences'; +import { IPreferencesService, ISettingsGroup, ISetting, IPreferencesEditorModel, IFilterResult, ISettingsEditorModel, IWorkbenchSettingsConfiguration, IExtensionSetting, IScoredResults } from 'vs/workbench/parts/preferences/common/preferences'; import { SettingsEditorModel, DefaultSettingsEditorModel, WorkspaceConfigurationEditorModel } from 'vs/workbench/parts/preferences/common/preferencesModels'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { IContextMenuService, ContextSubMenu } from 'vs/platform/contextview/browser/contextView'; @@ -26,8 +26,7 @@ import { SettingsGroupTitleWidget, EditPreferenceWidget, SettingsHeaderWidget, D import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RangeHighlightDecorations } from 'vs/workbench/browser/parts/editor/rangeDecorations'; import { IMarkerService, IMarkerData } from 'vs/platform/markers/common/markers'; -import { IMessageService, Severity } from 'vs/platform/message/common/message'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { Severity, IMessageService } from 'vs/platform/message/common/message'; import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; @@ -38,6 +37,10 @@ import { ITextModel, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/ed import { CodeLensProviderRegistry, CodeLensProvider, ICodeLensSymbol } from 'vs/editor/common/modes'; import { CancellationToken } from 'vs/base/common/cancellation'; import { getDomNodePagePosition } from 'vs/base/browser/dom'; +import { IssueType, ISettingsSearchIssueReporterData, ISettingSearchResult } from 'vs/platform/issue/common/issue'; +import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; export interface IPreferencesRenderer extends IDisposable { readonly preferencesModel: IPreferencesEditorModel; @@ -247,6 +250,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR private filteredMatchesRenderer: FilteredMatchesRenderer; private hiddenAreasRenderer: HiddenAreasRenderer; private editSettingActionRenderer: EditSettingRenderer; + private issueWidgetRenderer: IssueWidgetRenderer; private feedbackWidgetRenderer: FeedbackWidgetRenderer; private bracesHidingRenderer: BracesHidingRenderer; private extensionCodelensRenderer: ExtensionCodelensRenderer; @@ -263,7 +267,8 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR constructor(protected editor: ICodeEditor, public readonly preferencesModel: DefaultSettingsEditorModel, @IPreferencesService protected preferencesService: IPreferencesService, - @IInstantiationService protected instantiationService: IInstantiationService + @IInstantiationService protected instantiationService: IInstantiationService, + @IConfigurationService private configurationService: IConfigurationService ) { super(); this.settingHighlighter = this._register(instantiationService.createInstance(SettingHighlighter, editor, this._onFocusPreference, this._onClearFocusPreference)); @@ -271,6 +276,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR this.settingsGroupTitleRenderer = this._register(instantiationService.createInstance(SettingsGroupTitleRenderer, editor)); this.filteredMatchesRenderer = this._register(instantiationService.createInstance(FilteredMatchesRenderer, editor)); this.editSettingActionRenderer = this._register(instantiationService.createInstance(EditSettingRenderer, editor, preferencesModel, this.settingHighlighter)); + this.issueWidgetRenderer = this._register(instantiationService.createInstance(IssueWidgetRenderer, editor)); this.feedbackWidgetRenderer = this._register(instantiationService.createInstance(FeedbackWidgetRenderer, editor)); this.bracesHidingRenderer = this._register(instantiationService.createInstance(BracesHidingRenderer, editor, preferencesModel)); this.hiddenAreasRenderer = this._register(instantiationService.createInstance(HiddenAreasRenderer, editor, [this.settingsGroupTitleRenderer, this.filteredMatchesRenderer, this.bracesHidingRenderer])); @@ -293,6 +299,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR public render() { this.settingsGroupTitleRenderer.render(this.preferencesModel.settingsGroups); this.editSettingActionRenderer.render(this.preferencesModel.settingsGroups, this._associatedPreferencesModel); + this.issueWidgetRenderer.render(null); this.feedbackWidgetRenderer.render(null); this.settingHighlighter.clear(true); this.bracesHidingRenderer.render(null, this.preferencesModel.settingsGroups); @@ -306,7 +313,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR if (filterResult) { this.filteredMatchesRenderer.render(filterResult, this.preferencesModel.settingsGroups); this.settingsGroupTitleRenderer.render(null); - this.feedbackWidgetRenderer.render(filterResult); + this.renderIssueWidget(filterResult); this.settingsHeaderRenderer.render(filterResult); this.settingHighlighter.clear(true); this.bracesHidingRenderer.render(filterResult, this.preferencesModel.settingsGroups); @@ -315,7 +322,7 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR } else { this.settingHighlighter.clear(true); this.filteredMatchesRenderer.render(null, this.preferencesModel.settingsGroups); - this.feedbackWidgetRenderer.render(null); + this.renderIssueWidget(null); this.settingsHeaderRenderer.render(null); this.settingsGroupTitleRenderer.render(this.preferencesModel.settingsGroups); this.settingsGroupTitleRenderer.showGroup(0); @@ -327,6 +334,17 @@ export class DefaultSettingsRenderer extends Disposable implements IPreferencesR this.hiddenAreasRenderer.render(); } + private renderIssueWidget(filterResult: IFilterResult): void { + const workbenchSettings = this.configurationService.getValue().workbench.settings; + if (workbenchSettings.enableNaturalLanguageSearchFeedback) { + this.issueWidgetRenderer.render(null); + this.feedbackWidgetRenderer.render(filterResult); + } else { + this.feedbackWidgetRenderer.render(null); + this.issueWidgetRenderer.render(filterResult); + } + } + public focusPreference(s: ISetting): void { const setting = this.getSetting(s); if (setting) { @@ -594,9 +612,8 @@ export class FeedbackWidgetRenderer extends Disposable { } public render(result: IFilterResult): void { - const workbenchSettings = this.configurationService.getValue().workbench.settings; this._currentResult = result; - if (result && result.metadata && workbenchSettings.enableNaturalLanguageSearchFeedback) { + if (result && result.metadata) { this.showWidget(); } else if (this._feedbackWidget) { this.disposeWidget(); @@ -762,7 +779,88 @@ export class FeedbackWidgetRenderer extends Disposable { public dispose() { this.disposeWidget(); + super.dispose(); + } +} + +export class IssueWidgetRenderer extends Disposable { + private _issueWidget: FloatingClickWidget; + private _currentResult: IFilterResult; + + constructor(private editor: ICodeEditor, + @IInstantiationService private instantiationService: IInstantiationService, + @IWorkbenchIssueService private issueService: IWorkbenchIssueService, + @IEnvironmentService private environmentService: IEnvironmentService + ) { + super(); + } + public render(result: IFilterResult): void { + this._currentResult = result; + if (result && result.metadata && this.environmentService.appQuality !== 'stable') { + this.showWidget(); + } else if (this._issueWidget) { + this.disposeWidget(); + } + } + + private showWidget(): void { + if (!this._issueWidget) { + this._issueWidget = this._register(this.instantiationService.createInstance(FloatingClickWidget, this.editor, nls.localize('reportSettingsSearchIssue', "Report Issue"), null)); + this._register(this._issueWidget.onClick(() => this.showIssueReporter())); + this._issueWidget.render(); + } + } + + private showIssueReporter(): TPromise { + const nlpMetadata = this._currentResult.metadata['nlpResult']; + const results = nlpMetadata.scoredResults; + + const enabledExtensions = nlpMetadata.extensions; + const issueResults = Object.keys(results) + .map(key => ({ + key: key.split('##')[1], + extensionId: results[key].packageId === 'core' ? + 'core' : + this.getExtensionIdByGuid(enabledExtensions, results[key].packageId), + score: results[key].score + })) + .slice(0, 20); + + const issueReporterData: Partial = { + enabledExtensions, + issueType: IssueType.SettingsSearchIssue, + actualSearchResults: issueResults, + filterResultCount: this.getFilterResultCount(), + query: this._currentResult.query + }; + + return this.issueService.openReporter(issueReporterData); + } + + private getFilterResultCount(): number { + const filterResultGroup = arrays.first(this._currentResult.filteredGroups, group => group.id === 'filterResult'); + return filterResultGroup ? + filterResultGroup.sections[0].settings.length : + 0; + } + + private getExtensionIdByGuid(extensions: ILocalExtension[], guid: string): string { + const match = arrays.first(extensions, ext => ext.identifier.uuid === guid); + + // identifier.id includes the version, not needed here + return match && `${match.manifest.publisher}.${match.manifest.name}`; + } + + private disposeWidget(): void { + if (this._issueWidget) { + this._issueWidget.dispose(); + this._issueWidget = null; + } + } + + public dispose() { + this.disposeWidget(); super.dispose(); } } diff --git a/src/vs/workbench/parts/preferences/common/preferences.ts b/src/vs/workbench/parts/preferences/common/preferences.ts index 54ee6366022db..ece19a824cb50 100644 --- a/src/vs/workbench/parts/preferences/common/preferences.ts +++ b/src/vs/workbench/parts/preferences/common/preferences.ts @@ -17,6 +17,7 @@ import { join } from 'vs/base/common/paths'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import Event from 'vs/base/common/event'; import { IStringDictionary } from 'vs/base/common/collections'; +import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; export interface IWorkbenchSettingsConfiguration { workbench: { @@ -110,6 +111,9 @@ export interface IFilterMetadata { timestamp: number; duration: number; scoredResults: IScoredResults; + extensions?: ILocalExtension[]; + + /** The number of requests made, since requests are split by number of filters */ requestCount?: number; /** The name of the server that actually served the request */ diff --git a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts index 95dfdc1d9b65f..cbe1c4bb8f006 100644 --- a/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts +++ b/src/vs/workbench/parts/preferences/electron-browser/preferencesSearch.ts @@ -42,8 +42,10 @@ export class PreferencesSearchService extends Disposable implements IPreferences // This request goes to the shared process but results won't change during a window's lifetime, so cache the results. this._installedExtensions = this.extensionManagementService.getInstalled(LocalExtensionType.User).then(exts => { - // Filter to enabled extensions - return exts.filter(ext => this.extensionEnablementService.isEnabled(ext.identifier)); + // Filter to enabled extensions that have settings + return exts + .filter(ext => this.extensionEnablementService.isEnabled(ext.identifier)) + .filter(ext => ext.manifest.contributes && ext.manifest.contributes.configuration); }); } @@ -132,6 +134,7 @@ interface IBingRequestDetails { url: string; body?: string; hasMoreFilters?: boolean; + extensions?: ILocalExtension[]; } class RemoteSearchProvider implements ISearchProvider { @@ -280,7 +283,8 @@ class RemoteSearchProvider implements ISearchProvider { duration, timestamp, scoredResults, - context: result['@odata.context'] + context: result['@odata.context'], + extensions: details.extensions }; }); } @@ -315,9 +319,10 @@ class RemoteSearchProvider implements ISearchProvider { url += `${API_VERSION}&${QUERY_TYPE}`; } + const extensions = await this.installedExtensions; const filters = this.options.newExtensionsOnly ? [`diminish eq 'latest'`] : - await this.getVersionFilters(this.environmentService.settingsSearchBuildId); + this.getVersionFilters(extensions, this.environmentService.settingsSearchBuildId); const filterStr = filters .slice(filterPage * RemoteSearchProvider.MAX_REQUEST_FILTERS, (filterPage + 1) * RemoteSearchProvider.MAX_REQUEST_FILTERS) @@ -333,23 +338,22 @@ class RemoteSearchProvider implements ISearchProvider { return { url, body, - hasMoreFilters + hasMoreFilters, + extensions }; } - private getVersionFilters(buildNumber?: number): TPromise { - return this.installedExtensions.then(exts => { - // Only search extensions that contribute settings - const filters = exts - .filter(ext => ext.manifest.contributes && ext.manifest.contributes.configuration) - .map(ext => this.getExtensionFilter(ext)); + private getVersionFilters(exts: ILocalExtension[], buildNumber?: number): string[] { + // Only search extensions that contribute settings + const filters = exts + .filter(ext => ext.manifest.contributes && ext.manifest.contributes.configuration) + .map(ext => this.getExtensionFilter(ext)); - if (buildNumber) { - filters.push(`(packageid eq 'core' and startbuildno le '${buildNumber}' and endbuildno ge '${buildNumber}')`); - } + if (buildNumber) { + filters.push(`(packageid eq 'core' and startbuildno le '${buildNumber}' and endbuildno ge '${buildNumber}')`); + } - return filters; - }); + return filters; } private getExtensionFilter(ext: ILocalExtension): string { diff --git a/src/vs/workbench/services/issue/common/issue.ts b/src/vs/workbench/services/issue/common/issue.ts new file mode 100644 index 0000000000000..b998241ce1c7e --- /dev/null +++ b/src/vs/workbench/services/issue/common/issue.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { TPromise } from 'vs/base/common/winjs.base'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { IssueReporterData } from 'vs/platform/issue/common/issue'; + +export const IWorkbenchIssueService = createDecorator('workbenchIssueService'); + +export interface IWorkbenchIssueService { + _serviceBrand: any; + openReporter(dataOverrides?: Partial): TPromise; +} diff --git a/src/vs/workbench/services/issue/electron-browser/workbenchIssueService.ts b/src/vs/workbench/services/issue/electron-browser/workbenchIssueService.ts new file mode 100644 index 0000000000000..3905d9542d108 --- /dev/null +++ b/src/vs/workbench/services/issue/electron-browser/workbenchIssueService.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { IssueReporterStyles, IIssueService, IssueReporterData } from 'vs/platform/issue/common/issue'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; +import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { IExtensionManagementService, IExtensionEnablementService, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { webFrame } from 'electron'; +import { assign } from 'vs/base/common/objects'; +import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue'; + +export class WorkbenchIssueService implements IWorkbenchIssueService { + _serviceBrand: any; + + constructor( + @IIssueService private issueService: IIssueService, + @IThemeService private themeService: IThemeService, + @IExtensionManagementService private extensionManagementService: IExtensionManagementService, + @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService + ) { + } + + openReporter(dataOverrides: Partial = {}): TPromise { + return this.extensionManagementService.getInstalled(LocalExtensionType.User).then(extensions => { + const enabledExtensions = extensions.filter(extension => this.extensionEnablementService.isEnabled({ id: getGalleryExtensionIdFromLocal(extension) })); + const theme = this.themeService.getTheme(); + const issueReporterData: IssueReporterData = assign( + { + styles: getIssueReporterStyles(theme), + zoomLevel: webFrame.getZoomLevel(), + enabledExtensions + }, + dataOverrides); + + return this.issueService.openReporter(issueReporterData); + }); + } +} + +export function getIssueReporterStyles(theme: ITheme): IssueReporterStyles { + return { + backgroundColor: theme.getColor(SIDE_BAR_BACKGROUND) && theme.getColor(SIDE_BAR_BACKGROUND).toString(), + color: theme.getColor(foreground).toString(), + textLinkColor: theme.getColor(textLinkForeground) && theme.getColor(textLinkForeground).toString(), + inputBackground: theme.getColor(inputBackground) && theme.getColor(inputBackground).toString(), + inputForeground: theme.getColor(inputForeground) && theme.getColor(inputForeground).toString(), + inputBorder: theme.getColor(inputBorder) && theme.getColor(inputBorder).toString(), + inputActiveBorder: theme.getColor(inputActiveOptionBorder) && theme.getColor(inputActiveOptionBorder).toString(), + inputErrorBorder: theme.getColor(inputValidationErrorBorder) && theme.getColor(inputValidationErrorBorder).toString(), + buttonBackground: theme.getColor(buttonBackground) && theme.getColor(buttonBackground).toString(), + buttonForeground: theme.getColor(buttonForeground) && theme.getColor(buttonForeground).toString(), + buttonHoverBackground: theme.getColor(buttonHoverBackground) && theme.getColor(buttonHoverBackground).toString(), + sliderActiveColor: theme.getColor(scrollbarSliderActiveBackground) && theme.getColor(scrollbarSliderActiveBackground).toString(), + sliderBackgroundColor: theme.getColor(scrollbarSliderBackground) && theme.getColor(scrollbarSliderBackground).toString(), + sliderHoverColor: theme.getColor(scrollbarSliderHoverBackground) && theme.getColor(scrollbarSliderHoverBackground).toString() + }; +}