Skip to content

Commit 9b0b596

Browse files
authored
Merge pull request microsoft#80502 from dgozman/fix-79198
Linkify variable values in repl; microsoft#79198
2 parents 4f39995 + c526947 commit 9b0b596

6 files changed

Lines changed: 112 additions & 40 deletions

File tree

src/vs/workbench/contrib/debug/browser/baseDebugView.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { KeyCode } from 'vs/base/common/keyCodes';
1616
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
1717
import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
1818
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
19+
import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector';
1920

2021
export const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024;
2122
export const twistiePixels = 20;
@@ -29,6 +30,7 @@ export interface IRenderValueOptions {
2930
maxValueLength?: number;
3031
showHover?: boolean;
3132
colorize?: boolean;
33+
linkDetector?: LinkDetector;
3234
}
3335

3436
export interface IVariableTemplateData {
@@ -83,16 +85,22 @@ export function renderExpressionValue(expressionOrValue: IExpressionContainer |
8385
value = value.substr(0, options.maxValueLength) + '...';
8486
}
8587
if (value && !options.preserveWhitespace) {
86-
container.textContent = replaceWhitespace(value);
88+
value = replaceWhitespace(value);
8789
} else {
88-
container.textContent = value || '';
90+
value = value || '';
91+
}
92+
if (options.linkDetector) {
93+
container.textContent = '';
94+
container.appendChild(options.linkDetector.handleLinks(value));
95+
} else {
96+
container.textContent = value;
8997
}
9098
if (options.showHover) {
9199
container.title = value || '';
92100
}
93101
}
94102

95-
export function renderVariable(variable: Variable, data: IVariableTemplateData, showChanged: boolean, highlights: IHighlight[]): void {
103+
export function renderVariable(variable: Variable, data: IVariableTemplateData, showChanged: boolean, highlights: IHighlight[], linkDetector?: LinkDetector): void {
96104
if (variable.available) {
97105
let text = replaceWhitespace(variable.name);
98106
if (variable.value && typeof variable.name === 'string') {
@@ -109,7 +117,8 @@ export function renderVariable(variable: Variable, data: IVariableTemplateData,
109117
maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET,
110118
preserveWhitespace: false,
111119
showHover: true,
112-
colorize: true
120+
colorize: true,
121+
linkDetector
113122
});
114123
}
115124

@@ -209,14 +218,17 @@ export abstract class AbstractExpressionsRenderer implements ITreeRenderer<IExpr
209218
renderElement(node: ITreeNode<IExpression, FuzzyScore>, index: number, data: IExpressionTemplateData): void {
210219
const { element } = node;
211220
if (element === this.debugService.getViewModel().getSelectedExpression()) {
212-
data.enableInputBox(element, this.getInputBoxOptions(element));
213-
} else {
214-
this.renderExpression(element, data, createMatches(node.filterData));
221+
const options = this.getInputBoxOptions(element);
222+
if (options) {
223+
data.enableInputBox(element, options);
224+
return;
225+
}
215226
}
227+
this.renderExpression(element, data, createMatches(node.filterData));
216228
}
217229

218230
protected abstract renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void;
219-
protected abstract getInputBoxOptions(expression: IExpression): IInputBoxOptions;
231+
protected abstract getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined;
220232

221233
disposeTemplate(templateData: IExpressionTemplateData): void {
222234
dispose(templateData.toDispose);

src/vs/workbench/contrib/debug/browser/linkDetector.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { URI as uri } from 'vs/base/common/uri';
99
import { isMacintosh } from 'vs/base/common/platform';
1010
import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent';
1111
import * as nls from 'vs/nls';
12-
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService';
12+
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
1313

1414
export class LinkDetector {
1515
private static readonly MAX_LENGTH = 500;
@@ -87,11 +87,13 @@ export class LinkDetector {
8787

8888
const link = document.createElement('a');
8989
link.textContent = line.substr(match.index, match[0].length);
90-
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)");
90+
link.title = isMacintosh ? nls.localize('fileLinkMac', "Cmd + click to follow link") : nls.localize('fileLink', "Ctrl + click to follow link");
9191
lineContainer.appendChild(link);
9292
const lineNumber = Number(match[3]);
9393
const columnNumber = match[4] ? Number(match[4]) : undefined;
9494
link.onclick = (e) => this.onLinkClick(new StandardMouseEvent(e), resource!, lineNumber, columnNumber);
95+
link.onmousemove = (event) => link.classList.toggle('pointer', isMacintosh ? event.metaKey : event.ctrlKey);
96+
link.onmouseleave = () => link.classList.remove('pointer');
9597

9698
lastMatchIndex = pattern.lastIndex;
9799
const currentMatch = match;
@@ -141,9 +143,11 @@ export class LinkDetector {
141143
if (!selection || selection.type === 'Range') {
142144
return; // do not navigate when user is selecting
143145
}
146+
if (!(isMacintosh ? event.metaKey : event.ctrlKey)) {
147+
return;
148+
}
144149

145150
event.preventDefault();
146-
const group = event.ctrlKey || event.metaKey ? SIDE_GROUP : ACTIVE_GROUP;
147151

148152
this.editorService.openEditor({
149153
resource,
@@ -153,6 +157,6 @@ export class LinkDetector {
153157
startColumn: column
154158
}
155159
}
156-
}, group);
160+
});
157161
}
158162
}

src/vs/workbench/contrib/debug/browser/media/debug.contribution.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,16 @@
147147
margin-left: 6px;
148148
}
149149

150+
/* Links */
151+
152+
.monaco-workbench .monaco-list-row .expression .value a {
153+
text-decoration: underline;
154+
}
155+
156+
.monaco-workbench .monaco-list-row .expression .value a.pointer {
157+
cursor: pointer;
158+
}
159+
150160
/* White color when element is selected and list is focused. White looks better on blue selection background. */
151161
.monaco-workbench .monaco-list:focus .monaco-list-row.selected .expression .name,
152162
.monaco-workbench .monaco-list:focus .monaco-list-row.selected .expression .value {

src/vs/workbench/contrib/debug/browser/media/repl.css

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,3 @@
134134
.monaco-workbench .repl .repl-tree .output.expression .code-bold { font-weight: bold; }
135135
.monaco-workbench .repl .repl-tree .output.expression .code-italic { font-style: italic; }
136136
.monaco-workbench .repl .repl-tree .output.expression .code-underline { text-decoration: underline; }
137-
138-
/* Links */
139-
.monaco-workbench .repl .repl-tree .output.expression a,
140-
.monaco-workbench .repl .repl-tree .evaluation-result.expression a {
141-
text-decoration: underline;
142-
cursor: pointer;
143-
}

src/vs/workbench/contrib/debug/browser/repl.ts

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,20 @@ import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplEvalu
4949
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
5050
import { ITreeRenderer, ITreeNode, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree';
5151
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
52-
import { renderExpressionValue } from 'vs/workbench/contrib/debug/browser/baseDebugView';
52+
import { renderExpressionValue, AbstractExpressionsRenderer, IExpressionTemplateData, renderVariable, IInputBoxOptions } from 'vs/workbench/contrib/debug/browser/baseDebugView';
5353
import { handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling';
5454
import { ILabelService } from 'vs/platform/label/common/label';
5555
import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector';
5656
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
57-
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
57+
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
5858
import { removeAnsiEscapeCodes } from 'vs/base/common/strings';
5959
import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
6060
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
6161
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
6262
import { RunOnceScheduler } from 'vs/base/common/async';
6363
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
64-
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
64+
import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
6565
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
66-
import { VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesView';
6766

6867
const $ = dom.$;
6968

@@ -405,17 +404,18 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati
405404
this.replDelegate = new ReplDelegate(this.configurationService);
406405
const wordWrap = this.configurationService.getValue<IDebugConfiguration>('debug').console.wordWrap;
407406
dom.toggleClass(treeContainer, 'word-wrap', wordWrap);
407+
const linkDetector = this.instantiationService.createInstance(LinkDetector);
408408
this.tree = this.instantiationService.createInstance(
409409
WorkbenchAsyncDataTree,
410410
'DebugRepl',
411411
treeContainer,
412412
this.replDelegate,
413413
[
414-
this.instantiationService.createInstance(VariablesRenderer),
415-
this.instantiationService.createInstance(ReplSimpleElementsRenderer),
414+
this.instantiationService.createInstance(ReplVariablesRenderer, linkDetector),
415+
this.instantiationService.createInstance(ReplSimpleElementsRenderer, linkDetector),
416416
new ReplEvaluationInputsRenderer(),
417-
new ReplEvaluationResultsRenderer(),
418-
new ReplRawObjectsRenderer()
417+
new ReplEvaluationResultsRenderer(linkDetector),
418+
new ReplRawObjectsRenderer(linkDetector),
419419
],
420420
// https://github.com/microsoft/TypeScript/issues/32526
421421
new ReplDataSource() as IAsyncDataSource<IDebugSession, IReplElement>,
@@ -626,6 +626,8 @@ class ReplEvaluationResultsRenderer implements ITreeRenderer<ReplEvaluationResul
626626
return ReplEvaluationResultsRenderer.ID;
627627
}
628628

629+
constructor(private readonly linkDetector: LinkDetector) { }
630+
629631
renderTemplate(container: HTMLElement): IReplEvaluationResultTemplateData {
630632
const output = dom.append(container, $('.evaluation-result.expression'));
631633
const value = dom.append(output, $('span.value'));
@@ -639,7 +641,8 @@ class ReplEvaluationResultsRenderer implements ITreeRenderer<ReplEvaluationResul
639641
renderExpressionValue(expression, templateData.value, {
640642
preserveWhitespace: !expression.hasChildren,
641643
showHover: false,
642-
colorize: true
644+
colorize: true,
645+
linkDetector: this.linkDetector
643646
});
644647
if (expression.hasChildren) {
645648
templateData.annotation.className = 'annotation octicon octicon-info';
@@ -656,21 +659,16 @@ class ReplSimpleElementsRenderer implements ITreeRenderer<SimpleReplElement, Fuz
656659
static readonly ID = 'simpleReplElement';
657660

658661
constructor(
662+
private readonly linkDetector: LinkDetector,
659663
@IEditorService private readonly editorService: IEditorService,
660664
@ILabelService private readonly labelService: ILabelService,
661-
@IInstantiationService private readonly instantiationService: IInstantiationService,
662665
@IThemeService private readonly themeService: IThemeService
663666
) { }
664667

665668
get templateId(): string {
666669
return ReplSimpleElementsRenderer.ID;
667670
}
668671

669-
@memoize
670-
get linkDetector(): LinkDetector {
671-
return this.instantiationService.createInstance(LinkDetector);
672-
}
673-
674672
renderTemplate(container: HTMLElement): ISimpleReplElementTemplateData {
675673
const data: ISimpleReplElementTemplateData = Object.create(null);
676674
dom.addClass(container, 'output');
@@ -716,9 +714,37 @@ class ReplSimpleElementsRenderer implements ITreeRenderer<SimpleReplElement, Fuz
716714
}
717715
}
718716

717+
export class ReplVariablesRenderer extends AbstractExpressionsRenderer {
718+
719+
static readonly ID = 'replVariable';
720+
721+
get templateId(): string {
722+
return ReplVariablesRenderer.ID;
723+
}
724+
725+
constructor(
726+
private readonly linkDetector: LinkDetector,
727+
@IDebugService debugService: IDebugService,
728+
@IContextViewService contextViewService: IContextViewService,
729+
@IThemeService themeService: IThemeService,
730+
) {
731+
super(debugService, contextViewService, themeService);
732+
}
733+
734+
protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void {
735+
renderVariable(expression as Variable, data, true, highlights, this.linkDetector);
736+
}
737+
738+
protected getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined {
739+
return undefined;
740+
}
741+
}
742+
719743
class ReplRawObjectsRenderer implements ITreeRenderer<RawObjectReplElement, FuzzyScore, IRawObjectReplTemplateData> {
720744
static readonly ID = 'rawObject';
721745

746+
constructor(private readonly linkDetector: LinkDetector) { }
747+
722748
get templateId(): string {
723749
return ReplRawObjectsRenderer.ID;
724750
}
@@ -748,7 +774,8 @@ class ReplRawObjectsRenderer implements ITreeRenderer<RawObjectReplElement, Fuzz
748774
// value
749775
renderExpressionValue(element.value, templateData.value, {
750776
preserveWhitespace: true,
751-
showHover: false
777+
showHover: false,
778+
linkDetector: this.linkDetector
752779
});
753780

754781
// annotation if any
@@ -807,7 +834,7 @@ class ReplDelegate implements IListVirtualDelegate<IReplElement> {
807834

808835
getTemplateId(element: IReplElement): string {
809836
if (element instanceof Variable && element.name) {
810-
return VariablesRenderer.ID;
837+
return ReplVariablesRenderer.ID;
811838
}
812839
if (element instanceof ReplEvaluationResult) {
813840
return ReplEvaluationResultsRenderer.ID;

src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,21 @@ import * as dom from 'vs/base/browser/dom';
99
import { Expression, Variable, Scope, StackFrame, Thread } from 'vs/workbench/contrib/debug/common/debugModel';
1010
import { MockSession } from 'vs/workbench/contrib/debug/test/common/mockDebug';
1111
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
12+
import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector';
13+
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
14+
import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices';
1215
const $ = dom.$;
1316

1417
suite('Debug - Base Debug View', () => {
18+
let linkDetector: LinkDetector;
19+
20+
/**
21+
* Instantiate services for use by the functions being tested.
22+
*/
23+
setup(() => {
24+
const instantiationService: TestInstantiationService = <TestInstantiationService>workbenchInstantiationService();
25+
linkDetector = instantiationService.createInstance(LinkDetector);
26+
});
1527

1628
test('replace whitespace', () => {
1729
assert.equal(replaceWhitespace('hey there'), 'hey there');
@@ -36,7 +48,7 @@ suite('Debug - Base Debug View', () => {
3648
expression.available = true;
3749
expression.value = '"string value"';
3850
container = $('.container');
39-
renderExpressionValue(expression, container, { colorize: true });
51+
renderExpressionValue(expression, container, { colorize: true, linkDetector });
4052
assert.equal(container.className, 'value string');
4153
assert.equal(container.textContent, '"string value"');
4254

@@ -48,8 +60,14 @@ suite('Debug - Base Debug View', () => {
4860

4961
expression.value = 'this is a long string';
5062
container = $('.container');
51-
renderExpressionValue(expression, container, { colorize: true, maxValueLength: 4 });
63+
renderExpressionValue(expression, container, { colorize: true, maxValueLength: 4, linkDetector });
5264
assert.equal(container.textContent, 'this...');
65+
66+
expression.value = process.platform === 'win32' ? 'C:\\foo.js:5' : '/foo.js:5';
67+
container = $('.container');
68+
renderExpressionValue(expression, container, { colorize: true, linkDetector });
69+
assert.ok(container.querySelector('a'));
70+
assert.equal(container.querySelector('a')!.textContent, expression.value);
5371
});
5472

5573
test('render variable', () => {
@@ -73,16 +91,24 @@ suite('Debug - Base Debug View', () => {
7391
expression = $('.');
7492
name = $('.');
7593
value = $('.');
76-
renderVariable(variable, { expression, name, value, label }, false, []);
94+
renderVariable(variable, { expression, name, value, label }, false, [], linkDetector);
7795
assert.equal(value.textContent, 'hey');
7896
assert.equal(label.element.textContent, 'foo:');
7997
assert.equal(label.element.title, 'string');
8098

99+
variable.value = process.platform === 'win32' ? 'C:\\foo.js:5' : '/foo.js:5';
100+
expression = $('.');
101+
name = $('.');
102+
value = $('.');
103+
renderVariable(variable, { expression, name, value, label }, false, [], linkDetector);
104+
assert.ok(value.querySelector('a'));
105+
assert.equal(value.querySelector('a')!.textContent, variable.value);
106+
81107
variable = new Variable(session, scope, 2, 'console', 'console', '5', 0, 0, { kind: 'virtual' });
82108
expression = $('.');
83109
name = $('.');
84110
value = $('.');
85-
renderVariable(variable, { expression, name, value, label }, false, []);
111+
renderVariable(variable, { expression, name, value, label }, false, [], linkDetector);
86112
assert.equal(name.className, 'virtual');
87113
assert.equal(label.element.textContent, 'console:');
88114
assert.equal(label.element.title, 'console');

0 commit comments

Comments
 (0)