diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index f9dee4d8ca956..0d2c6c3678e58 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -16,6 +16,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; export const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; export const twistiePixels = 20; @@ -29,6 +30,7 @@ export interface IRenderValueOptions { maxValueLength?: number; showHover?: boolean; colorize?: boolean; + linkDetector?: LinkDetector; } export interface IVariableTemplateData { @@ -83,16 +85,22 @@ export function renderExpressionValue(expressionOrValue: IExpressionContainer | value = value.substr(0, options.maxValueLength) + '...'; } if (value && !options.preserveWhitespace) { - container.textContent = replaceWhitespace(value); + value = replaceWhitespace(value); } else { - container.textContent = value || ''; + value = value || ''; + } + if (options.linkDetector) { + container.textContent = ''; + container.appendChild(options.linkDetector.handleLinks(value)); + } else { + container.textContent = value; } if (options.showHover) { container.title = value || ''; } } -export function renderVariable(variable: Variable, data: IVariableTemplateData, showChanged: boolean, highlights: IHighlight[]): void { +export function renderVariable(variable: Variable, data: IVariableTemplateData, showChanged: boolean, highlights: IHighlight[], linkDetector?: LinkDetector): void { if (variable.available) { let text = replaceWhitespace(variable.name); if (variable.value && typeof variable.name === 'string') { @@ -109,7 +117,8 @@ export function renderVariable(variable: Variable, data: IVariableTemplateData, maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, preserveWhitespace: false, showHover: true, - colorize: true + colorize: true, + linkDetector }); } @@ -209,14 +218,17 @@ export abstract class AbstractExpressionsRenderer implements ITreeRenderer, index: number, data: IExpressionTemplateData): void { const { element } = node; if (element === this.debugService.getViewModel().getSelectedExpression()) { - data.enableInputBox(element, this.getInputBoxOptions(element)); - } else { - this.renderExpression(element, data, createMatches(node.filterData)); + const options = this.getInputBoxOptions(element); + if (options) { + data.enableInputBox(element, options); + return; + } } + this.renderExpression(element, data, createMatches(node.filterData)); } protected abstract renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void; - protected abstract getInputBoxOptions(expression: IExpression): IInputBoxOptions; + protected abstract getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined; disposeTemplate(templateData: IExpressionTemplateData): void { dispose(templateData.toDispose); diff --git a/src/vs/workbench/contrib/debug/browser/linkDetector.ts b/src/vs/workbench/contrib/debug/browser/linkDetector.ts index df911b7d7c9f3..1649ee59cc29a 100644 --- a/src/vs/workbench/contrib/debug/browser/linkDetector.ts +++ b/src/vs/workbench/contrib/debug/browser/linkDetector.ts @@ -9,7 +9,7 @@ import { URI as uri } from 'vs/base/common/uri'; import { isMacintosh } from 'vs/base/common/platform'; import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import * as nls from 'vs/nls'; -import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; export class LinkDetector { private static readonly MAX_LENGTH = 500; @@ -87,11 +87,13 @@ export class LinkDetector { const link = document.createElement('a'); link.textContent = line.substr(match.index, match[0].length); - link.title = isMacintosh ? nls.localize('fileLinkMac', "Click to follow (Cmd + click opens to the side)") : nls.localize('fileLink', "Click to follow (Ctrl + click opens to the side)"); + link.title = isMacintosh ? nls.localize('fileLinkMac', "Cmd + click to follow link") : nls.localize('fileLink', "Ctrl + click to follow link"); lineContainer.appendChild(link); const lineNumber = Number(match[3]); const columnNumber = match[4] ? Number(match[4]) : undefined; link.onclick = (e) => this.onLinkClick(new StandardMouseEvent(e), resource!, lineNumber, columnNumber); + link.onmousemove = (event) => link.classList.toggle('pointer', isMacintosh ? event.metaKey : event.ctrlKey); + link.onmouseleave = () => link.classList.remove('pointer'); lastMatchIndex = pattern.lastIndex; const currentMatch = match; @@ -141,9 +143,11 @@ export class LinkDetector { if (!selection || selection.type === 'Range') { return; // do not navigate when user is selecting } + if (!(isMacintosh ? event.metaKey : event.ctrlKey)) { + return; + } event.preventDefault(); - const group = event.ctrlKey || event.metaKey ? SIDE_GROUP : ACTIVE_GROUP; this.editorService.openEditor({ resource, @@ -153,6 +157,6 @@ export class LinkDetector { startColumn: column } } - }, group); + }); } } diff --git a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css index 422f7b208aba3..12269752f106f 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css +++ b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css @@ -147,6 +147,16 @@ margin-left: 6px; } +/* Links */ + +.monaco-workbench .monaco-list-row .expression .value a { + text-decoration: underline; +} + +.monaco-workbench .monaco-list-row .expression .value a.pointer { + cursor: pointer; +} + /* White color when element is selected and list is focused. White looks better on blue selection background. */ .monaco-workbench .monaco-list:focus .monaco-list-row.selected .expression .name, .monaco-workbench .monaco-list:focus .monaco-list-row.selected .expression .value { diff --git a/src/vs/workbench/contrib/debug/browser/media/repl.css b/src/vs/workbench/contrib/debug/browser/media/repl.css index b0ab0e6e71b58..9d92c972d481f 100644 --- a/src/vs/workbench/contrib/debug/browser/media/repl.css +++ b/src/vs/workbench/contrib/debug/browser/media/repl.css @@ -134,10 +134,3 @@ .monaco-workbench .repl .repl-tree .output.expression .code-bold { font-weight: bold; } .monaco-workbench .repl .repl-tree .output.expression .code-italic { font-style: italic; } .monaco-workbench .repl .repl-tree .output.expression .code-underline { text-decoration: underline; } - -/* Links */ -.monaco-workbench .repl .repl-tree .output.expression a, -.monaco-workbench .repl .repl-tree .evaluation-result.expression a { - text-decoration: underline; - cursor: pointer; -} diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index f00dc8c6d9994..3b32a852e44f0 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -49,21 +49,20 @@ import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplEvalu import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { renderExpressionValue } from 'vs/workbench/contrib/debug/browser/baseDebugView'; +import { renderExpressionValue, AbstractExpressionsRenderer, IExpressionTemplateData, renderVariable, IInputBoxOptions } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling'; import { ILabelService } from 'vs/platform/label/common/label'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { removeAnsiEscapeCodes } from 'vs/base/common/strings'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration'; import { RunOnceScheduler } from 'vs/base/common/async'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; -import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesView'; const $ = dom.$; @@ -405,17 +404,18 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati this.replDelegate = new ReplDelegate(this.configurationService); const wordWrap = this.configurationService.getValue('debug').console.wordWrap; dom.toggleClass(treeContainer, 'word-wrap', wordWrap); + const linkDetector = this.instantiationService.createInstance(LinkDetector); this.tree = this.instantiationService.createInstance( WorkbenchAsyncDataTree, 'DebugRepl', treeContainer, this.replDelegate, [ - this.instantiationService.createInstance(VariablesRenderer), - this.instantiationService.createInstance(ReplSimpleElementsRenderer), + this.instantiationService.createInstance(ReplVariablesRenderer, linkDetector), + this.instantiationService.createInstance(ReplSimpleElementsRenderer, linkDetector), new ReplEvaluationInputsRenderer(), - new ReplEvaluationResultsRenderer(), - new ReplRawObjectsRenderer() + new ReplEvaluationResultsRenderer(linkDetector), + new ReplRawObjectsRenderer(linkDetector), ], // https://github.com/microsoft/TypeScript/issues/32526 new ReplDataSource() as IAsyncDataSource, @@ -626,6 +626,8 @@ class ReplEvaluationResultsRenderer implements ITreeRenderer { static readonly ID = 'rawObject'; + constructor(private readonly linkDetector: LinkDetector) { } + get templateId(): string { return ReplRawObjectsRenderer.ID; } @@ -748,7 +774,8 @@ class ReplRawObjectsRenderer implements ITreeRenderer { getTemplateId(element: IReplElement): string { if (element instanceof Variable && element.name) { - return VariablesRenderer.ID; + return ReplVariablesRenderer.ID; } if (element instanceof ReplEvaluationResult) { return ReplEvaluationResultsRenderer.ID; diff --git a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts index 149b78269a8b8..dd89032131988 100644 --- a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts @@ -9,9 +9,21 @@ import * as dom from 'vs/base/browser/dom'; import { Expression, Variable, Scope, StackFrame, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; import { MockSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; const $ = dom.$; suite('Debug - Base Debug View', () => { + let linkDetector: LinkDetector; + + /** + * Instantiate services for use by the functions being tested. + */ + setup(() => { + const instantiationService: TestInstantiationService = workbenchInstantiationService(); + linkDetector = instantiationService.createInstance(LinkDetector); + }); test('replace whitespace', () => { assert.equal(replaceWhitespace('hey there'), 'hey there'); @@ -36,7 +48,7 @@ suite('Debug - Base Debug View', () => { expression.available = true; expression.value = '"string value"'; container = $('.container'); - renderExpressionValue(expression, container, { colorize: true }); + renderExpressionValue(expression, container, { colorize: true, linkDetector }); assert.equal(container.className, 'value string'); assert.equal(container.textContent, '"string value"'); @@ -48,8 +60,14 @@ suite('Debug - Base Debug View', () => { expression.value = 'this is a long string'; container = $('.container'); - renderExpressionValue(expression, container, { colorize: true, maxValueLength: 4 }); + renderExpressionValue(expression, container, { colorize: true, maxValueLength: 4, linkDetector }); assert.equal(container.textContent, 'this...'); + + expression.value = process.platform === 'win32' ? 'C:\\foo.js:5' : '/foo.js:5'; + container = $('.container'); + renderExpressionValue(expression, container, { colorize: true, linkDetector }); + assert.ok(container.querySelector('a')); + assert.equal(container.querySelector('a')!.textContent, expression.value); }); test('render variable', () => { @@ -73,16 +91,24 @@ suite('Debug - Base Debug View', () => { expression = $('.'); name = $('.'); value = $('.'); - renderVariable(variable, { expression, name, value, label }, false, []); + renderVariable(variable, { expression, name, value, label }, false, [], linkDetector); assert.equal(value.textContent, 'hey'); assert.equal(label.element.textContent, 'foo:'); assert.equal(label.element.title, 'string'); + variable.value = process.platform === 'win32' ? 'C:\\foo.js:5' : '/foo.js:5'; + expression = $('.'); + name = $('.'); + value = $('.'); + renderVariable(variable, { expression, name, value, label }, false, [], linkDetector); + assert.ok(value.querySelector('a')); + assert.equal(value.querySelector('a')!.textContent, variable.value); + variable = new Variable(session, scope, 2, 'console', 'console', '5', 0, 0, { kind: 'virtual' }); expression = $('.'); name = $('.'); value = $('.'); - renderVariable(variable, { expression, name, value, label }, false, []); + renderVariable(variable, { expression, name, value, label }, false, [], linkDetector); assert.equal(name.className, 'virtual'); assert.equal(label.element.textContent, 'console:'); assert.equal(label.element.title, 'console');