diff --git a/src/vs/editor/contrib/gotoError/gotoError.ts b/src/vs/editor/contrib/gotoError/gotoError.ts index 10f48cad8bb..80e2342e747 100644 --- a/src/vs/editor/contrib/gotoError/gotoError.ts +++ b/src/vs/editor/contrib/gotoError/gotoError.ts @@ -27,6 +27,8 @@ import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { Action } from 'vs/base/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { isEqual } from 'vs/base/common/resources'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; class MarkerModel { @@ -209,7 +211,9 @@ export class MarkerController implements IEditorContribution { @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IThemeService private readonly _themeService: IThemeService, @ICodeEditorService private readonly _editorService: ICodeEditorService, - @IKeybindingService private readonly _keybindingService: IKeybindingService + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IOpenerService private readonly _openerService: IOpenerService, + @IConfigurationService private readonly _configurationService: IConfigurationService ) { this._editor = editor; this._widgetVisible = CONTEXT_MARKERS_NAVIGATION_VISIBLE.bindTo(this._contextKeyService); @@ -243,7 +247,7 @@ export class MarkerController implements IEditorContribution { new Action(NextMarkerAction.ID, NextMarkerAction.LABEL + (nextMarkerKeybinding ? ` (${nextMarkerKeybinding.getLabel()})` : ''), 'show-next-problem codicon-chevron-down', this._model.canNavigate(), async () => { if (this._model) { this._model.move(true, true); } }), new Action(PrevMarkerAction.ID, PrevMarkerAction.LABEL + (prevMarkerKeybinding ? ` (${prevMarkerKeybinding.getLabel()})` : ''), 'show-previous-problem codicon-chevron-up', this._model.canNavigate(), async () => { if (this._model) { this._model.move(false, true); } }) ]; - this._widget = new MarkerNavigationWidget(this._editor, actions, this._themeService); + this._widget = new MarkerNavigationWidget(this._editor, actions, this._themeService, this._openerService, this._configurationService); this._widgetVisible.set(true); this._widget.onDidClose(() => this.closeMarkersNavigation(), this, this._disposeOnClose); diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index 0926872fc13..5190682a7c9 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -26,6 +26,11 @@ import { IAction } from 'vs/base/common/actions'; import { IActionBarOptions, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { OperatingSystem, OS } from 'vs/base/common/platform'; + +type ModifierKey = 'meta' | 'ctrl' | 'alt'; class MessageWidget { @@ -39,7 +44,16 @@ class MessageWidget { private readonly _relatedDiagnostics = new WeakMap(); private readonly _disposables: DisposableStore = new DisposableStore(); - constructor(parent: HTMLElement, editor: ICodeEditor, onRelatedInformation: (related: IRelatedInformation) => void) { + private _clickModifierKey: ModifierKey; + private _codeLink?: HTMLElement; + + constructor( + parent: HTMLElement, + editor: ICodeEditor, + onRelatedInformation: (related: IRelatedInformation) => void, + private readonly _openerService: IOpenerService, + private readonly _configurationService: IConfigurationService + ) { this._editor = editor; const domNode = document.createElement('div'); @@ -74,6 +88,16 @@ class MessageWidget { domNode.style.top = `-${e.scrollTop}px`; })); this._disposables.add(this._scrollable); + + this._clickModifierKey = this._getClickModifierKey(); + this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('editor.multiCursorModifier')) { + this._clickModifierKey = this._getClickModifierKey(); + if (this._codeLink) { + this._codeLink.setAttribute('title', this._getCodelinkTooltip()); + } + } + })); } dispose(): void { @@ -81,12 +105,20 @@ class MessageWidget { } update({ source, message, relatedInformation, code }: IMarker): void { + let sourceAndCodeLength = (source?.length || 0) + '()'.length; + if (code) { + if (typeof code === 'string') { + sourceAndCodeLength += code.length; + } else { + sourceAndCodeLength += code.value.length; + } + } const lines = message.split(/\r\n|\r|\n/g); this._lines = lines.length; this._longestLineLength = 0; for (const line of lines) { - this._longestLineLength = Math.max(line.length, this._longestLineLength); + this._longestLineLength = Math.max(line.length + sourceAndCodeLength, this._longestLineLength); } dom.clearNode(this._messageBlock); @@ -111,10 +143,28 @@ class MessageWidget { detailsElement.appendChild(sourceElement); } if (code) { - const codeElement = document.createElement('span'); - codeElement.innerText = `(${code})`; - dom.addClass(codeElement, 'code'); - detailsElement.appendChild(codeElement); + if (typeof code === 'string') { + const codeElement = document.createElement('span'); + codeElement.innerText = `(${code})`; + dom.addClass(codeElement, 'code'); + detailsElement.appendChild(codeElement); + } else { + this._codeLink = dom.$('a.code-link'); + this._codeLink.setAttribute('title', this._getCodelinkTooltip()); + this._codeLink.setAttribute('href', `${code.link.toString()}`); + + this._codeLink.onclick = (e) => { + e.preventDefault(); + if ((this._clickModifierKey === 'meta' && e.metaKey) || (this._clickModifierKey === 'ctrl' && e.ctrlKey) || (this._clickModifierKey === 'alt' && e.altKey)) { + this._openerService.open(code.link); + e.stopPropagation(); + } + }; + + const codeElement = dom.append(this._codeLink, dom.$('span')); + codeElement.innerText = code.value; + detailsElement.appendChild(this._codeLink); + } } } @@ -161,6 +211,31 @@ class MessageWidget { getHeightInLines(): number { return Math.min(17, this._lines); } + + private _getClickModifierKey(): ModifierKey { + const value = this._configurationService.getValue<'ctrlCmd' | 'alt'>('editor.multiCursorModifier'); + if (value === 'ctrlCmd') { + return 'alt'; + } else { + if (OS === OperatingSystem.Macintosh) { + return 'meta'; + } else { + return 'ctrl'; + } + } + } + + private _getCodelinkTooltip(): string { + const tooltipLabel = nls.localize('links.navigate.follow', 'Follow link'); + const tooltipKeybinding = this._clickModifierKey === 'ctrl' + ? nls.localize('links.navigate.kb.meta', 'ctrl + click') + : + this._clickModifierKey === 'meta' + ? OS === OperatingSystem.Macintosh ? nls.localize('links.navigate.kb.meta.mac', 'cmd + click') : nls.localize('links.navigate.kb.meta', 'ctrl + click') + : OS === OperatingSystem.Macintosh ? nls.localize('links.navigate.kb.alt.mac', 'option + click') : nls.localize('links.navigate.kb.alt', 'alt + click'); + + return `${tooltipLabel} (${tooltipKeybinding})`; + } } export class MarkerNavigationWidget extends PeekViewWidget { @@ -180,7 +255,9 @@ export class MarkerNavigationWidget extends PeekViewWidget { constructor( editor: ICodeEditor, private readonly actions: ReadonlyArray, - private readonly _themeService: IThemeService + private readonly _themeService: IThemeService, + private readonly _openerService: IOpenerService, + private readonly _configurationService: IConfigurationService ) { super(editor, { showArrow: true, showFrame: true, isAccessible: true }); this._severity = MarkerSeverity.Warning; @@ -250,7 +327,7 @@ export class MarkerNavigationWidget extends PeekViewWidget { this._container = document.createElement('div'); container.appendChild(this._container); - this._message = new MessageWidget(this._container, this.editor, related => this._onDidSelectRelatedInformation.fire(related)); + this._message = new MessageWidget(this._container, this.editor, related => this._onDidSelectRelatedInformation.fire(related), this._openerService, this._configurationService); this._disposables.add(this._message); } @@ -329,8 +406,9 @@ export const editorMarkerNavigationInfo = registerColor('editorMarkerNavigationI export const editorMarkerNavigationBackground = registerColor('editorMarkerNavigation.background', { dark: '#2D2D30', light: Color.white, hc: '#0C141F' }, nls.localize('editorMarkerNavigationBackground', 'Editor marker navigation widget background.')); registerThemingParticipant((theme, collector) => { - const link = theme.getColor(textLinkForeground); - if (link) { - collector.addRule(`.monaco-editor .marker-widget a { color: ${link}; }`); + const linkFg = theme.getColor(textLinkForeground); + if (linkFg) { + collector.addRule(`.monaco-editor .marker-widget a { color: ${linkFg}; }`); + collector.addRule(`.monaco-editor .marker-widget a.code-link span:hover { color: ${linkFg}; }`); } }); diff --git a/src/vs/editor/contrib/gotoError/media/gotoErrorWidget.css b/src/vs/editor/contrib/gotoError/media/gotoErrorWidget.css index ee02594a087..c748c365bdc 100644 --- a/src/vs/editor/contrib/gotoError/media/gotoErrorWidget.css +++ b/src/vs/editor/contrib/gotoError/media/gotoErrorWidget.css @@ -45,10 +45,27 @@ } .monaco-editor .marker-widget .descriptioncontainer .message .source, -.monaco-editor .marker-widget .descriptioncontainer .message .code { +.monaco-editor .marker-widget .descriptioncontainer .message span.code { opacity: 0.6; } +.monaco-editor .marker-widget .descriptioncontainer .message a.code-link { + opacity: 0.6; + color: inherit; +} +.monaco-editor .marker-widget .descriptioncontainer .message a.code-link:before { + content: '('; +} +.monaco-editor .marker-widget .descriptioncontainer .message a.code-link:after { + content: ')'; +} +.monaco-editor .marker-widget .descriptioncontainer .message a.code-link > span { + text-decoration: underline; + /** Hack to force underline to show **/ + border-bottom: 1px solid transparent; + text-underline-position: under; +} + .monaco-editor .marker-widget .descriptioncontainer .filename { cursor: pointer; } diff --git a/src/vs/editor/contrib/hover/hover.css b/src/vs/editor/contrib/hover/hover.css index 19f7bcec136..8f35b9aec75 100644 --- a/src/vs/editor/contrib/hover/hover.css +++ b/src/vs/editor/contrib/hover/hover.css @@ -110,3 +110,20 @@ font-size: inherit; vertical-align: middle; } + +.monaco-editor-hover .hover-contents a.code-link:before { + content: '('; +} +.monaco-editor-hover .hover-contents a.code-link:after { + content: ')'; +} + +.monaco-editor-hover .hover-contents a.code-link { + color: inherit; +} +.monaco-editor-hover .hover-contents a.code-link > span { + text-decoration: underline; + /** Hack to force underline to show **/ + border-bottom: 1px solid transparent; + text-underline-position: under; +} diff --git a/src/vs/editor/contrib/hover/hover.ts b/src/vs/editor/contrib/hover/hover.ts index 3a0f4eea252..b77e2b0132a 100644 --- a/src/vs/editor/contrib/hover/hover.ts +++ b/src/vs/editor/contrib/hover/hover.ts @@ -27,6 +27,7 @@ import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDeco import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class ModesHoverController implements IEditorContribution { @@ -66,7 +67,8 @@ export class ModesHoverController implements IEditorContribution { @IModeService private readonly _modeService: IModeService, @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IThemeService private readonly _themeService: IThemeService + @IThemeService private readonly _themeService: IThemeService, + @IConfigurationService private readonly _configurationService: IConfigurationService ) { this._isMouseDown = false; this._hoverClicked = false; @@ -205,7 +207,7 @@ export class ModesHoverController implements IEditorContribution { } private _createHoverWidgets() { - this._contentWidget.value = new ModesContentHoverWidget(this._editor, this._markerDecorationsService, this._themeService, this._keybindingService, this._modeService, this._openerService); + this._contentWidget.value = new ModesContentHoverWidget(this._editor, this._markerDecorationsService, this._themeService, this._keybindingService, this._modeService, this._openerService, this._configurationService); this._glyphWidget.value = new ModesGlyphHoverWidget(this._editor, this._modeService, this._openerService); } diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index ef800dc9f59..c52183db542 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -22,7 +22,7 @@ import { getHover } from 'vs/editor/contrib/hover/getHover'; import { HoverOperation, HoverStartMode, IHoverComputer } from 'vs/editor/contrib/hover/hoverOperation'; import { ContentHoverWidget } from 'vs/editor/contrib/hover/hoverWidgets'; import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { coalesce, isNonEmptyArray, asArray } from 'vs/base/common/arrays'; import { IMarker, IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { basename } from 'vs/base/common/resources'; @@ -39,6 +39,9 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Constants } from 'vs/base/common/uint'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { OperatingSystem, OS } from 'vs/base/common/platform'; +import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; const $ = dom.$; @@ -191,6 +194,8 @@ const markerCodeActionTrigger: CodeActionTrigger = { filter: { include: CodeActionKind.QuickFix } }; +type ModifierKey = 'meta' | 'ctrl' | 'alt'; + export class ModesContentHoverWidget extends ContentHoverWidget { static readonly ID = 'editor.contrib.modesContentHoverWidget'; @@ -204,6 +209,9 @@ export class ModesContentHoverWidget extends ContentHoverWidget { private _shouldFocus: boolean; private _colorPicker: ColorPickerWidget | null; + private _clickModifierKey: ModifierKey; + private _codeLink?: HTMLElement; + private readonly renderDisposable = this._register(new MutableDisposable()); constructor( @@ -213,6 +221,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { private readonly _keybindingService: IKeybindingService, private readonly _modeService: IModeService, private readonly _openerService: IOpenerService = NullOpenerService, + private readonly _configurationService: IConfigurationService ) { super(ModesContentHoverWidget.ID, editor); @@ -249,6 +258,16 @@ export class ModesContentHoverWidget extends ContentHoverWidget { this._renderMessages(this._lastRange, this._messages); } })); + + this._clickModifierKey = this._getClickModifierKey(); + this._register((this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('editor.multiCursorModifier')) { + this._clickModifierKey = this._getClickModifierKey(); + if (this._codeLink) { + this._codeLink.setAttribute('title', this._getCodelinkTooltip()); + } + } + }))); } dispose(): void { @@ -500,10 +519,38 @@ export class ModesContentHoverWidget extends ContentHoverWidget { messageElement.innerText = message; if (source || code) { - const detailsElement = dom.append(markerElement, $('span')); - detailsElement.style.opacity = '0.6'; - detailsElement.style.paddingLeft = '6px'; - detailsElement.innerText = source && code ? `${source}(${code})` : source ? source : `(${code})`; + if (typeof code === 'string') { + const detailsElement = dom.append(markerElement, $('span')); + detailsElement.style.opacity = '0.6'; + detailsElement.style.paddingLeft = '6px'; + detailsElement.innerText = source && code ? `${source}(${code})` : source ? source : `(${code})`; + } else { + if (code) { + const sourceAndCodeElement = $('span'); + if (source) { + const sourceElement = dom.append(sourceAndCodeElement, $('span')); + sourceElement.innerText = source; + } + this._codeLink = dom.append(sourceAndCodeElement, $('a.code-link')); + this._codeLink.setAttribute('title', this._getCodelinkTooltip()); + this._codeLink.setAttribute('href', code.link.toString()); + + this._codeLink.onclick = (e) => { + e.preventDefault(); + if ((this._clickModifierKey === 'meta' && e.metaKey) || (this._clickModifierKey === 'ctrl' && e.ctrlKey) || (this._clickModifierKey === 'alt' && e.altKey)) { + this._openerService.open(code.link); + e.stopPropagation(); + } + }; + + const codeElement = dom.append(this._codeLink, $('span')); + codeElement.innerText = code.value; + + const detailsElement = dom.append(markerElement, sourceAndCodeElement); + detailsElement.style.opacity = '0.6'; + detailsElement.style.paddingLeft = '6px'; + } + } } if (isNonEmptyArray(relatedInformation)) { @@ -624,6 +671,31 @@ export class ModesContentHoverWidget extends ContentHoverWidget { private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ className: 'hoverHighlight' }); + + private _getClickModifierKey(): ModifierKey { + const value = this._configurationService.getValue<'ctrlCmd' | 'alt'>('editor.multiCursorModifier'); + if (value === 'ctrlCmd') { + return 'alt'; + } else { + if (OS === OperatingSystem.Macintosh) { + return 'meta'; + } else { + return 'ctrl'; + } + } + } + + private _getCodelinkTooltip(): string { + const tooltipLabel = nls.localize('links.navigate.follow', 'Follow link'); + const tooltipKeybinding = this._clickModifierKey === 'ctrl' + ? nls.localize('links.navigate.kb.meta', 'ctrl + click') + : + this._clickModifierKey === 'meta' + ? OS === OperatingSystem.Macintosh ? nls.localize('links.navigate.kb.meta.mac', 'cmd + click') : nls.localize('links.navigate.kb.meta', 'ctrl + click') + : OS === OperatingSystem.Macintosh ? nls.localize('links.navigate.kb.alt.mac', 'option + click') : nls.localize('links.navigate.kb.alt', 'alt + click'); + + return `${tooltipLabel} (${tooltipKeybinding})`; + } } function hoverContentsEquals(first: HoverPart[], second: HoverPart[]): boolean { @@ -648,3 +720,11 @@ function hoverContentsEquals(first: HoverPart[], second: HoverPart[]): boolean { } return true; } + +registerThemingParticipant((theme, collector) => { + const linkFg = theme.getColor(textLinkForeground); + if (linkFg) { + collector.addRule(`.monaco-editor-hover .hover-contents a.code-link span:hover { color: ${linkFg}; }`); + } +}); + diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 4412fce4a6f..2e7725c31cc 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1176,7 +1176,10 @@ declare namespace monaco.editor { owner: string; resource: Uri; severity: MarkerSeverity; - code?: string; + code?: string | { + value: string; + link: Uri; + }; message: string; source?: string; startLineNumber: number; @@ -1191,7 +1194,10 @@ declare namespace monaco.editor { * A structure defining a problem/warning/etc. */ export interface IMarkerData { - code?: string; + code?: string | { + value: string; + link: Uri; + }; severity: MarkerSeverity; message: string; source?: string; diff --git a/src/vs/platform/markers/common/markers.ts b/src/vs/platform/markers/common/markers.ts index 4b0f6095f1a..66cd4058541 100644 --- a/src/vs/platform/markers/common/markers.ts +++ b/src/vs/platform/markers/common/markers.ts @@ -87,7 +87,7 @@ export namespace MarkerSeverity { * A structure defining a problem/warning/etc. */ export interface IMarkerData { - code?: string; + code?: string | { value: string; link: URI }; severity: MarkerSeverity; message: string; source?: string; @@ -108,7 +108,7 @@ export interface IMarker { owner: string; resource: URI; severity: MarkerSeverity; - code?: string; + code?: string | { value: string; link: URI }; message: string; source?: string; startLineNumber: number; @@ -140,7 +140,11 @@ export namespace IMarkerData { result.push(emptyString); } if (markerData.code) { - result.push(markerData.code.replace('¦', '\¦')); + if (typeof markerData.code === 'string') { + result.push(markerData.code.replace('¦', '\¦')); + } else { + result.push(markerData.code.value.replace('¦', '\¦')); + } } else { result.push(emptyString); } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 33b1b497c8d..607cc0bcd83 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1446,4 +1446,26 @@ declare module 'vscode' { } //#endregion + + //#region Diagnostic links https://github.com/microsoft/vscode/issues/11847 + + export interface Diagnostic { + /** + * Will be merged into `Diagnostic#code` + */ + code2?: { + /** + * A code or identifier for this diagnostic. + * Should be used for later processing, e.g. when providing [code actions](#CodeActionContext). + */ + value: string | number; + + /** + * A link to a URI with more information about the diagnostic error. + */ + link: Uri; + } + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadDiagnostics.ts b/src/vs/workbench/api/browser/mainThreadDiagnostics.ts index ca934673534..2727860a6cc 100644 --- a/src/vs/workbench/api/browser/mainThreadDiagnostics.ts +++ b/src/vs/workbench/api/browser/mainThreadDiagnostics.ts @@ -54,6 +54,9 @@ export class MainThreadDiagnostics implements MainThreadDiagnosticsShape { relatedInformation.resource = URI.revive(relatedInformation.resource); } } + if (marker.code && typeof marker.code !== 'string') { + marker.code.link = URI.revive(marker.code.link); + } } } this._markerService.changeOne(owner, URI.revive(uri), markers); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 6fbb9b7fb07..861af59f92a 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -127,11 +127,19 @@ export namespace DiagnosticTag { export namespace Diagnostic { export function from(value: vscode.Diagnostic): IMarkerData { + let code: string | { value: string; link: URI } | undefined = isString(value.code) || isNumber(value.code) ? String(value.code) : undefined; + if (value.code2) { + code = { + value: String(value.code2.value), + link: value.code2.link + }; + } + return { ...Range.from(value.range), message: value.message, source: value.source, - code: isString(value.code) || isNumber(value.code) ? String(value.code) : undefined, + code, severity: DiagnosticSeverity.from(value.severity), relatedInformation: value.relatedInformation && value.relatedInformation.map(DiagnosticRelatedInformation.from), tags: Array.isArray(value.tags) ? coalesce(value.tags.map(DiagnosticTag.from)) : undefined, @@ -141,7 +149,7 @@ export namespace Diagnostic { export function to(value: IMarkerData): vscode.Diagnostic { const res = new types.Diagnostic(Range.to(value), value.message, DiagnosticSeverity.to(value.severity)); res.source = value.source; - res.code = value.code; + res.code = isString(value.code) ? value.code : value.code?.value; res.relatedInformation = value.relatedInformation && value.relatedInformation.map(DiagnosticRelatedInformation.to); res.tags = value.tags && coalesce(value.tags.map(DiagnosticTag.to)); return res; diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 3e5306bc9ed..f95d2c7873c 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -14,7 +14,7 @@ import { ResourceMarkers, Marker, RelatedInformation } from 'vs/workbench/contri import Messages from 'vs/workbench/contrib/markers/browser/messages'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IDisposable, dispose, Disposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { QuickFixAction, QuickFixActionViewItem } from 'vs/workbench/contrib/markers/browser/markersViewActions'; @@ -43,6 +43,10 @@ import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/commo import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; import { CodeActionTriggerType } from 'vs/editor/common/modes'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { OS, OperatingSystem } from 'vs/base/common/platform'; export type TreeElement = ResourceMarkers | Marker | RelatedInformation; @@ -217,14 +221,16 @@ export class MarkerRenderer implements ITreeRenderer action.id === QuickFixAction.ID ? instantiationService.createInstance(QuickFixActionViewItem, action) : undefined + actionViewItemProvider: (action: QuickFixAction) => action.id === QuickFixAction.ID ? _instantiationService.createInstance(QuickFixActionViewItem, action) : undefined })); this.icon = dom.append(parent, dom.$('')); this.multilineActionbar = this._register(new ActionBar(dom.append(parent, dom.$('.multiline-actions')))); this.messageAndDetailsContainer = dom.append(parent, dom.$('.marker-message-details-container')); + + this._clickModifierKey = this._getClickModifierKey(); } render(element: Marker, filterData: MarkerFilterData | undefined): void { @@ -273,6 +288,15 @@ class MarkerWidget extends Disposable { this.renderMessageAndDetails(element, filterData); this.disposables.add(dom.addDisposableListener(this.parent, dom.EventType.MOUSE_OVER, () => this.markersViewModel.onMarkerMouseHover(element))); this.disposables.add(dom.addDisposableListener(this.parent, dom.EventType.MOUSE_LEAVE, () => this.markersViewModel.onMarkerMouseLeave(element))); + + this.disposables.add((this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('editor.multiCursorModifier')) { + this._clickModifierKey = this._getClickModifierKey(); + if (this._codeLink) { + this._codeLink.setAttribute('title', this._getCodelinkTooltip()); + } + } + }))); } private renderQuickfixActionbar(marker: Marker): void { @@ -335,15 +359,63 @@ class MarkerWidget extends Disposable { source.set(marker.source, sourceMatches); if (marker.code) { - const code = new HighlightedLabel(dom.append(parent, dom.$('.marker-code')), false); - const codeMatches = filterData && filterData.codeMatches || []; - code.set(marker.code, codeMatches); + if (typeof marker.code === 'string') { + const code = new HighlightedLabel(dom.append(parent, dom.$('.marker-code')), false); + const codeMatches = filterData && filterData.codeMatches || []; + code.set(marker.code, codeMatches); + } else { + this._codeLink = dom.$('a.code-link'); + this._codeLink.setAttribute('title', this._getCodelinkTooltip()); + + const codeUri = marker.code.link; + const codeLink = codeUri.toString(); + + dom.append(parent, this._codeLink); + this._codeLink.setAttribute('href', codeLink); + + this._codeLink.onclick = (e) => { + e.preventDefault(); + if ((this._clickModifierKey === 'meta' && e.metaKey) || (this._clickModifierKey === 'ctrl' && e.ctrlKey) || (this._clickModifierKey === 'alt' && e.altKey)) { + this._openerService.open(codeUri); + e.stopPropagation(); + } + }; + + const code = new HighlightedLabel(dom.append(this._codeLink, dom.$('.marker-code')), false); + const codeMatches = filterData && filterData.codeMatches || []; + code.set(marker.code.value, codeMatches); + } } } const lnCol = dom.append(parent, dom.$('span.marker-line')); lnCol.textContent = Messages.MARKERS_PANEL_AT_LINE_COL_NUMBER(marker.startLineNumber, marker.startColumn); } + + private _getClickModifierKey(): ModifierKey { + const value = this._configurationService.getValue<'ctrlCmd' | 'alt'>('editor.multiCursorModifier'); + if (value === 'ctrlCmd') { + return 'alt'; + } else { + if (OS === OperatingSystem.Macintosh) { + return 'meta'; + } else { + return 'ctrl'; + } + } + } + + private _getCodelinkTooltip(): string { + const tooltipLabel = localize('links.navigate.follow', 'Follow link'); + const tooltipKeybinding = this._clickModifierKey === 'ctrl' + ? localize('links.navigate.kb.meta', 'ctrl + click') + : + this._clickModifierKey === 'meta' + ? OS === OperatingSystem.Macintosh ? localize('links.navigate.kb.meta.mac', 'cmd + click') : localize('links.navigate.kb.meta', 'ctrl + click') + : OS === OperatingSystem.Macintosh ? localize('links.navigate.kb.alt.mac', 'option + click') : localize('links.navigate.kb.alt', 'alt + click'); + + return `${tooltipLabel} (${tooltipKeybinding})`; + } } export class RelatedInformationRenderer implements ITreeRenderer { @@ -451,7 +523,14 @@ export class Filter implements ITreeFilter { lineMatches.push(FilterOptions._messageFilter(this.options.textFilter, line) || []); } const sourceMatches = marker.marker.source && FilterOptions._filter(this.options.textFilter, marker.marker.source); - const codeMatches = marker.marker.code && FilterOptions._filter(this.options.textFilter, marker.marker.code); + + let codeMatches: IMatch[] | null | undefined; + if (marker.marker.code) { + const codeText = typeof marker.marker.code === 'string' ? marker.marker.code : marker.marker.code.value; + codeMatches = FilterOptions._filter(this.options.textFilter, codeText); + } else { + codeMatches = undefined; + } if (sourceMatches || codeMatches || lineMatches.some(lineMatch => lineMatch.length > 0)) { return { visibility: true, data: { type: FilterDataType.Marker, lineMatches, sourceMatches: sourceMatches || [], codeMatches: codeMatches || [] } }; @@ -760,3 +839,10 @@ export class ResourceDragAndDrop implements ITreeDragAndDrop { drop(data: IDragAndDropData, targetElement: TreeElement, targetIndex: number, originalEvent: DragEvent): void { } } + +registerThemingParticipant((theme, collector) => { + const linkFg = theme.getColor(textLinkForeground); + if (linkFg) { + collector.addRule(`.markers-panel .markers-panel-container .tree-container .monaco-tl-contents .details-container a.code-link span:hover { color: ${linkFg}; }`); + } +}); diff --git a/src/vs/workbench/contrib/markers/browser/media/markers.css b/src/vs/workbench/contrib/markers/browser/media/markers.css index a7d18b76c71..f49bead0141 100644 --- a/src/vs/workbench/contrib/markers/browser/media/markers.css +++ b/src/vs/workbench/contrib/markers/browser/media/markers.css @@ -133,6 +133,14 @@ display: flex; } +.markers-panel .markers-panel-container .tree-container .monaco-tl-contents .details-container a.code-link { + color: inherit; +} +.markers-panel .markers-panel-container .tree-container .monaco-tl-contents .details-container a.code-link .monaco-highlighted-label { + text-decoration: underline; + text-underline-position: under; +} + .markers-panel .markers-panel-container .tree-container .monaco-tl-contents .marker-code:before { content: '('; }