debugHover.ts 12.4 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

I
isidor 已提交
6 7
import * as nls from 'vs/nls';
import * as lifecycle from 'vs/base/common/lifecycle';
J
Johannes Rieken 已提交
8 9
import { TPromise } from 'vs/base/common/winjs.base';
import { KeyCode } from 'vs/base/common/keyCodes';
10
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
I
isidor 已提交
11
import * as dom from 'vs/base/browser/dom';
J
Johannes Rieken 已提交
12
import { ITree } from 'vs/base/parts/tree/browser/tree';
I
isidor 已提交
13
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
14
import { DefaultController, ICancelableEvent, ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults';
15
import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions';
J
Johannes Rieken 已提交
16 17
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
I
isidor 已提交
18 19 20 21
import { IContentWidget, ICodeEditor, IContentWidgetPosition, ContentWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IDebugService, IExpression, IExpressionContainer } from 'vs/workbench/parts/debug/common/debug';
import { Expression } from 'vs/workbench/parts/debug/common/debugModel';
22 23
import { renderExpressionValue } from 'vs/workbench/parts/debug/electron-browser/baseDebugView';
import { VariablesDataSource, VariablesRenderer } from 'vs/workbench/parts/debug/electron-browser/variablesView';
24
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
25
import { attachStylerCallback } from 'vs/platform/theme/common/styler';
26
import { IThemeService } from 'vs/platform/theme/common/themeService';
27
import { editorHoverBackground, editorHoverBorder } from 'vs/platform/theme/common/colorRegistry';
28
import { WorkbenchTree } from 'vs/platform/list/browser/listService';
E
Erich Gamma 已提交
29

J
Joao Moreno 已提交
30
const $ = dom.$;
31
const MAX_ELEMENTS_SHOWN = 18;
E
Erich Gamma 已提交
32

I
isidor 已提交
33
export class DebugHoverWidget implements IContentWidget {
E
Erich Gamma 已提交
34

M
Matt Bierner 已提交
35
	public static readonly ID = 'debug.hoverWidget';
I
isidor 已提交
36
	// editor.IContentWidget.allowEditorOverflow
E
Erich Gamma 已提交
37 38
	public allowEditorOverflow = true;

I
isidor 已提交
39
	private _isVisible: boolean;
E
Erich Gamma 已提交
40
	private domNode: HTMLElement;
J
Joao Moreno 已提交
41
	private tree: WorkbenchTree;
A
Alex Dima 已提交
42
	private showAtPosition: Position;
I
isidor 已提交
43
	private highlightDecorations: string[];
44
	private complexValueContainer: HTMLElement;
45
	private treeContainer: HTMLElement;
46
	private complexValueTitle: HTMLElement;
47
	private valueContainer: HTMLElement;
I
isidor 已提交
48 49
	private stoleFocus: boolean;
	private toDispose: lifecycle.IDisposable[];
50
	private scrollbar: DomScrollableElement;
E
Erich Gamma 已提交
51

52 53 54
	constructor(
		private editor: ICodeEditor,
		private debugService: IDebugService,
I
isidor 已提交
55
		private instantiationService: IInstantiationService,
56
		private themeService: IThemeService
57
	) {
I
isidor 已提交
58
		this.toDispose = [];
I
isidor 已提交
59

I
isidor 已提交
60
		this._isVisible = false;
I
isidor 已提交
61 62 63 64
		this.showAtPosition = null;
		this.highlightDecorations = [];
	}

I
isidor 已提交
65
	private create(): void {
I
isidor 已提交
66 67 68 69 70
		this.domNode = $('.debug-hover-widget');
		this.complexValueContainer = dom.append(this.domNode, $('.complex-value'));
		this.complexValueTitle = dom.append(this.complexValueContainer, $('.title'));
		this.treeContainer = dom.append(this.complexValueContainer, $('.debug-hover-tree'));
		this.treeContainer.setAttribute('role', 'tree');
71
		this.tree = this.instantiationService.createInstance(WorkbenchTree, this.treeContainer, {
I
isidor 已提交
72
			dataSource: new VariablesDataSource(),
I
isidor 已提交
73
			renderer: this.instantiationService.createInstance(VariablesHoverRenderer),
I
isidor 已提交
74 75 76 77
			controller: new DebugHoverController(this.editor)
		}, {
				indentPixels: 6,
				twistiePixels: 15,
78
				ariaLabel: nls.localize('treeAriaLabel', "Debug Hover")
79
			});
80

I
isidor 已提交
81 82 83 84 85 86 87 88 89
		this.valueContainer = $('.value');
		this.valueContainer.tabIndex = 0;
		this.valueContainer.setAttribute('role', 'tooltip');
		this.scrollbar = new DomScrollableElement(this.valueContainer, { horizontal: ScrollbarVisibility.Hidden });
		this.domNode.appendChild(this.scrollbar.getDomNode());
		this.toDispose.push(this.scrollbar);

		this.editor.applyFontInfo(this.domNode);

90 91 92 93 94 95 96 97
		this.toDispose.push(attachStylerCallback(this.themeService, { editorHoverBackground, editorHoverBorder }, colors => {
			this.domNode.style.backgroundColor = colors.editorHoverBackground;
			if (colors.editorHoverBorder) {
				this.domNode.style.border = `1px solid ${colors.editorHoverBorder}`;
			} else {
				this.domNode.style.border = null;
			}
		}));
I
isidor 已提交
98 99 100

		this.registerListeners();
		this.editor.addContentWidget(this);
I
isidor 已提交
101 102
	}

I
isidor 已提交
103
	private registerListeners(): void {
104
		this.toDispose.push(this.tree.onDidExpandItem(() => {
105
			this.layoutTree();
I
isidor 已提交
106
		}));
107
		this.toDispose.push(this.tree.onDidCollapseItem(() => {
108
			this.layoutTree();
I
isidor 已提交
109
		}));
110

A
Cleanup  
Alex Dima 已提交
111
		this.toDispose.push(dom.addStandardDisposableListener(this.domNode, 'keydown', (e: IKeyboardEvent) => {
A
Alexandru Dima 已提交
112
			if (e.equals(KeyCode.Escape)) {
I
isidor 已提交
113 114 115
				this.hide();
			}
		}));
A
Alex Dima 已提交
116
		this.toDispose.push(this.editor.onDidChangeConfiguration((e: IConfigurationChangedEvent) => {
I
isidor 已提交
117 118 119 120
			if (e.fontInfo) {
				this.editor.applyFontInfo(this.domNode);
			}
		}));
E
Erich Gamma 已提交
121 122
	}

I
isidor 已提交
123 124 125 126
	public isVisible(): boolean {
		return this._isVisible;
	}

E
Erich Gamma 已提交
127 128 129 130 131 132 133 134
	public getId(): string {
		return DebugHoverWidget.ID;
	}

	public getDomNode(): HTMLElement {
		return this.domNode;
	}

J
Johannes Rieken 已提交
135
	private getExactExpressionRange(lineContent: string, range: Range): Range {
B
Benjamin Pasero 已提交
136
		let matchingExpression: string = undefined;
R
rajkumar42 已提交
137 138 139 140 141
		let startOffset = 0;

		// Some example supported expressions: myVar.prop, a.b.c.d, myVar?.prop, myVar->prop, MyClass::StaticProp, *myVar
		// Match any character except a set of characters which often break interesting sub-expressions
		let expression: RegExp = /([^()\[\]{}<>\s+\-/%~#^;=|,`!]|\->)+/g;
B
Benjamin Pasero 已提交
142
		let result: RegExpExecArray = undefined;
R
rajkumar42 已提交
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159

		// First find the full expression under the cursor
		while (result = expression.exec(lineContent)) {
			let start = result.index + 1;
			let end = start + result[0].length;

			if (start <= range.startColumn && end >= range.endColumn) {
				matchingExpression = result[0];
				startOffset = start;
				break;
			}
		}

		// If there are non-word characters after the cursor, we want to truncate the expression then.
		// For example in expression 'a.b.c.d', if the focus was under 'b', 'a.b' would be evaluated.
		if (matchingExpression) {
			let subExpression: RegExp = /\w+/g;
B
Benjamin Pasero 已提交
160
			let subExpressionResult: RegExpExecArray = undefined;
R
rajkumar42 已提交
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
			while (subExpressionResult = subExpression.exec(matchingExpression)) {
				let subEnd = subExpressionResult.index + 1 + startOffset + subExpressionResult[0].length;
				if (subEnd >= range.endColumn) {
					break;
				}
			}

			if (subExpressionResult) {
				matchingExpression = matchingExpression.substring(0, subExpression.lastIndex);
			}
		}

		return matchingExpression ?
			new Range(range.startLineNumber, startOffset, range.endLineNumber, startOffset + matchingExpression.length - 1) :
			new Range(range.startLineNumber, 0, range.endLineNumber, 0);
	}

I
isidor 已提交
178
	public showAt(range: Range, focus: boolean): TPromise<void> {
E
Erich Gamma 已提交
179 180
		const pos = range.getStartPosition();

181
		const process = this.debugService.getViewModel().focusedProcess;
182 183 184 185
		const lineContent = this.editor.getModel().getLineContent(pos.lineNumber);
		const expressionRange = this.getExactExpressionRange(lineContent, range);
		// use regex to extract the sub-expression #9821
		const matchingExpression = lineContent.substring(expressionRange.startColumn - 1, expressionRange.endColumn);
186 187 188 189
		if (!matchingExpression) {
			return TPromise.as(this.hide());
		}

I
isidor 已提交
190
		let promise: TPromise<IExpression>;
191
		if (process.session.capabilities.supportsEvaluateForHovers) {
192
			const result = new Expression(matchingExpression);
193
			promise = result.evaluate(process, this.debugService.getViewModel().focusedStackFrame, 'hover').then(() => result);
194
		} else {
I
isidor 已提交
195
			promise = this.findExpressionInStackFrame(matchingExpression.split('.').map(word => word.trim()).filter(word => !!word), expressionRange);
196
		}
R
rajkumar42 已提交
197

198 199
		return promise.then(expression => {
			if (!expression || (expression instanceof Expression && !expression.available)) {
200
				this.hide();
201
				return undefined;
202 203
			}

I
isidor 已提交
204
			this.highlightDecorations = this.editor.deltaDecorations(this.highlightDecorations, [{
205
				range: new Range(pos.lineNumber, expressionRange.startColumn, pos.lineNumber, expressionRange.startColumn + matchingExpression.length),
I
isidor 已提交
206 207 208 209
				options: {
					className: 'hoverHighlight'
				}
			}]);
I
isidor 已提交
210 211 212

			return this.doShow(pos, expression, focus);
		});
213 214
	}

I
isidor 已提交
215
	private doFindExpression(container: IExpressionContainer, namesToFind: string[]): TPromise<IExpression> {
I
isidor 已提交
216 217 218 219
		if (!container) {
			return TPromise.as(null);
		}

I
isidor 已提交
220
		return container.getChildren().then(children => {
221
			// look for our variable in the list. First find the parents of the hovered variable if there are any.
222
			const filtered = children.filter(v => namesToFind[0] === v.name);
223 224 225
			if (filtered.length !== 1) {
				return null;
			}
E
Erich Gamma 已提交
226

227 228 229 230 231 232 233
			if (namesToFind.length === 1) {
				return filtered[0];
			} else {
				return this.doFindExpression(filtered[0], namesToFind.slice(1));
			}
		});
	}
E
Erich Gamma 已提交
234

I
isidor 已提交
235
	private findExpressionInStackFrame(namesToFind: string[], expressionRange: Range): TPromise<IExpression> {
236 237
		return this.debugService.getViewModel().focusedStackFrame.getScopes()
			.then(scopes => scopes.filter(s => !s.expensive))
238 239
			.then(scopes => TPromise.join(scopes.map(scope => this.doFindExpression(scope, namesToFind))))
			.then(expressions => expressions.filter(exp => !!exp))
240 241
			// only show if all expressions found have the same value
			.then(expressions => (expressions.length > 0 && expressions.every(e => e.value === expressions[0].value)) ? expressions[0] : null);
242
	}
E
Erich Gamma 已提交
243

I
isidor 已提交
244
	private doShow(position: Position, expression: IExpression, focus: boolean, forceValueHover = false): TPromise<void> {
I
isidor 已提交
245 246 247 248
		if (!this.domNode) {
			this.create();
		}

249
		this.showAtPosition = position;
I
isidor 已提交
250
		this._isVisible = true;
I
isidor 已提交
251
		this.stoleFocus = focus;
252

253
		if (!expression.hasChildren || forceValueHover) {
254
			this.complexValueContainer.hidden = true;
255
			this.valueContainer.hidden = false;
256 257
			renderExpressionValue(expression, this.valueContainer, {
				showChanged: false,
258 259
				preserveWhitespace: true,
				colorize: true
260
			});
I
isidor 已提交
261
			this.valueContainer.title = '';
262
			this.editor.layoutContentWidget(this);
263
			this.scrollbar.scanDomNode();
I
isidor 已提交
264 265 266 267
			if (focus) {
				this.editor.render();
				this.valueContainer.focus();
			}
268

I
isidor 已提交
269
			return TPromise.as(null);
270
		}
I
isidor 已提交
271 272

		this.valueContainer.hidden = true;
273
		this.complexValueContainer.hidden = false;
I
isidor 已提交
274 275

		return this.tree.setInput(expression).then(() => {
276
			this.complexValueTitle.textContent = expression.value;
I
isidor 已提交
277
			this.complexValueTitle.title = expression.value;
I
isidor 已提交
278 279
			this.layoutTree();
			this.editor.layoutContentWidget(this);
280
			this.scrollbar.scanDomNode();
I
isidor 已提交
281 282 283 284 285
			if (focus) {
				this.editor.render();
				this.tree.DOMFocus();
			}
		});
E
Erich Gamma 已提交
286 287
	}

288 289 290 291 292 293 294
	private layoutTree(): void {
		const navigator = this.tree.getNavigator();
		let visibleElementsCount = 0;
		while (navigator.next()) {
			visibleElementsCount++;
		}

295
		if (visibleElementsCount === 0) {
I
isidor 已提交
296
			this.doShow(this.showAtPosition, this.tree.getInput(), false, true);
297
		} else {
298
			const height = Math.min(visibleElementsCount, MAX_ELEMENTS_SHOWN) * 18;
299 300

			if (this.treeContainer.clientHeight !== height) {
J
Johannes Rieken 已提交
301
				this.treeContainer.style.height = `${height}px`;
302 303
				this.tree.layout();
			}
304
		}
305 306
	}

E
Erich Gamma 已提交
307
	public hide(): void {
I
isidor 已提交
308
		if (!this._isVisible) {
E
Erich Gamma 已提交
309 310
			return;
		}
311

I
isidor 已提交
312
		this._isVisible = false;
I
isidor 已提交
313 314
		this.editor.deltaDecorations(this.highlightDecorations, []);
		this.highlightDecorations = [];
E
Erich Gamma 已提交
315
		this.editor.layoutContentWidget(this);
I
isidor 已提交
316 317 318
		if (this.stoleFocus) {
			this.editor.focus();
		}
E
Erich Gamma 已提交
319 320
	}

I
isidor 已提交
321
	public getPosition(): IContentWidgetPosition {
I
isidor 已提交
322
		return this._isVisible ? {
E
Erich Gamma 已提交
323 324
			position: this.showAtPosition,
			preference: [
I
isidor 已提交
325 326
				ContentWidgetPositionPreference.ABOVE,
				ContentWidgetPositionPreference.BELOW
E
Erich Gamma 已提交
327 328 329
			]
		} : null;
	}
I
isidor 已提交
330 331

	public dispose(): void {
J
Joao Moreno 已提交
332
		this.toDispose = lifecycle.dispose(this.toDispose);
I
isidor 已提交
333
	}
E
Erich Gamma 已提交
334
}
I
isidor 已提交
335 336 337

class DebugHoverController extends DefaultController {

I
isidor 已提交
338
	constructor(private editor: ICodeEditor) {
339
		super({ clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: false });
B
Benjamin Pasero 已提交
340 341
	}

I
isidor 已提交
342
	protected onLeftClick(tree: ITree, element: any, eventish: ICancelableEvent, origin = 'mouse'): boolean {
I
isidor 已提交
343 344 345 346
		if (element.reference > 0) {
			super.onLeftClick(tree, element, eventish, origin);
			tree.clearFocus();
			tree.deselect(element);
B
Benjamin Pasero 已提交
347
			this.editor.focus();
I
isidor 已提交
348 349 350 351 352
		}

		return true;
	}
}
353

I
isidor 已提交
354
class VariablesHoverRenderer extends VariablesRenderer {
355 356 357 358 359

	public getHeight(tree: ITree, element: any): number {
		return 18;
	}
}