debugHover.ts 11.1 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
import lifecycle = require('vs/base/common/lifecycle');
I
isidor 已提交
7 8
import {TPromise} from 'vs/base/common/winjs.base';
import {CommonKeybindings} from 'vs/base/common/keyCodes';
E
Erich Gamma 已提交
9
import dom = require('vs/base/browser/dom');
B
Benjamin Pasero 已提交
10
import * as nls from 'vs/nls';
I
isidor 已提交
11 12 13 14
import {ITree} from 'vs/base/parts/tree/browser/tree';
import {Tree} from 'vs/base/parts/tree/browser/treeImpl';
import {DefaultController, ICancelableEvent} from 'vs/base/parts/tree/browser/treeDefaults';
import {IConfigurationChangedEvent} from 'vs/editor/common/editorCommon';
E
Erich Gamma 已提交
15
import editorbrowser = require('vs/editor/browser/editorBrowser');
I
isidor 已提交
16
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
I
isidor 已提交
17
import debug = require('vs/workbench/parts/debug/common/debug');
18
import {evaluateExpression, Expression} from 'vs/workbench/parts/debug/common/debugModel';
19
import viewer = require('vs/workbench/parts/debug/electron-browser/debugViewer');
A
Cleanup  
Alex Dima 已提交
20
import {IKeyboardEvent} from 'vs/base/browser/keyboardEvent';
A
Alex Dima 已提交
21
import {Position} from 'vs/editor/common/core/position';
22
import {Range} from 'vs/editor/common/core/range';
E
Erich Gamma 已提交
23

J
Joao Moreno 已提交
24
const $ = dom.$;
I
isidor 已提交
25
const debugTreeOptions = {
26
	indentPixels: 6,
B
Benjamin Pasero 已提交
27 28
	twistiePixels: 15,
	ariaLabel: nls.localize('treeAriaLabel', "Debug Hover")
I
isidor 已提交
29
};
30
const MAX_ELEMENTS_SHOWN = 18;
31
const MAX_VALUE_RENDER_LENGTH_IN_HOVER = 4096;
E
Erich Gamma 已提交
32 33 34 35

export class DebugHoverWidget implements editorbrowser.IContentWidget {

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

	private domNode: HTMLElement;
I
isidor 已提交
40
	public isVisible: boolean;
I
isidor 已提交
41
	private tree: ITree;
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[];
E
Erich Gamma 已提交
50

I
isidor 已提交
51
	constructor(private editor: editorbrowser.ICodeEditor, private debugService: debug.IDebugService, private instantiationService: IInstantiationService) {
E
Erich Gamma 已提交
52
		this.domNode = $('.debug-hover-widget monaco-editor-background');
53 54 55
		this.complexValueContainer = dom.append(this.domNode, $('.complex-value'));
		this.complexValueTitle = dom.append(this.complexValueContainer, $('.title'));
		this.treeContainer = dom.append(this.complexValueContainer, $('.debug-hover-tree'));
I
isidor 已提交
56
		this.treeContainer.setAttribute('role', 'tree');
57
		this.tree = new Tree(this.treeContainer, {
I
isidor 已提交
58
			dataSource: new viewer.VariablesDataSource(this.debugService),
59
			renderer: this.instantiationService.createInstance(VariablesHoverRenderer),
B
Benjamin Pasero 已提交
60
			controller: new DebugHoverController(editor)
I
isidor 已提交
61
		}, debugTreeOptions);
I
isidor 已提交
62

I
isidor 已提交
63
		this.toDispose = [];
I
isidor 已提交
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
		this.registerListeners();

		this.valueContainer = dom.append(this.domNode, $('.value'));
		this.valueContainer.tabIndex = 0;
		this.valueContainer.setAttribute('role', 'tooltip');

		this.isVisible = false;
		this.showAtPosition = null;
		this.highlightDecorations = [];

		this.editor.addContentWidget(this);
		this.editor.applyFontInfo(this.domNode);
	}

	private registerListeners(): void {
I
isidor 已提交
79
		this.toDispose.push(this.tree.addListener2('item:expanded', () => {
80
			this.layoutTree();
I
isidor 已提交
81 82
		}));
		this.toDispose.push(this.tree.addListener2('item:collapsed', () => {
83
			this.layoutTree();
I
isidor 已提交
84
		}));
85

A
Cleanup  
Alex Dima 已提交
86
		this.toDispose.push(dom.addStandardDisposableListener(this.domNode, 'keydown', (e: IKeyboardEvent) => {
I
isidor 已提交
87 88 89 90
			if (e.equals(CommonKeybindings.ESCAPE)) {
				this.hide();
			}
		}));
A
Alex Dima 已提交
91
		this.toDispose.push(this.editor.onDidChangeConfiguration((e: IConfigurationChangedEvent) => {
I
isidor 已提交
92 93 94 95
			if (e.fontInfo) {
				this.editor.applyFontInfo(this.domNode);
			}
		}));
E
Erich Gamma 已提交
96 97 98 99 100 101 102 103 104 105
	}

	public getId(): string {
		return DebugHoverWidget.ID;
	}

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

I
isidor 已提交
106
	private getExactExpressionRange(lineContent: string, range: Range) : Range {
R
rajkumar42 已提交
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
		let matchingExpression = undefined;
		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;
		let result = undefined;

		// 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;
			let subExpressionResult = undefined;
			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);
	}

149
	public showAt(range: Range, hoveringOver: string, focus: boolean): TPromise<void> {
E
Erich Gamma 已提交
150
		const pos = range.getStartPosition();
151
		const focusedStackFrame = this.debugService.getViewModel().getFocusedStackFrame();
152
		if (!hoveringOver || !focusedStackFrame || (focusedStackFrame.source.uri.toString() !== this.editor.getModel().uri.toString())) {
E
Erich Gamma 已提交
153 154 155
			return;
		}

R
rajkumar42 已提交
156
		const session = this.debugService.getActiveSession();
157 158 159 160
		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);
R
rajkumar42 已提交
161

162 163 164
		const evaluatedExpression = session.configuration.capabilities.supportsEvaluateForHovers ?
			evaluateExpression(session, focusedStackFrame, new Expression(matchingExpression, true), 'hover') :
			this.findExpressionInStackFrame(matchingExpression.split('.').map(word => word.trim()).filter(word => !!word));
E
Erich Gamma 已提交
165

R
rajkumar42 已提交
166
		return evaluatedExpression.then(expression => {
167
			if (!expression || !expression.available) {
168 169 170 171
				this.hide();
				return;
			}

I
isidor 已提交
172
			this.highlightDecorations = this.editor.deltaDecorations(this.highlightDecorations, [{
173
				range: new Range(pos.lineNumber, expressionRange.startColumn, pos.lineNumber, expressionRange.startColumn + matchingExpression.length),
I
isidor 已提交
174 175 176 177
				options: {
					className: 'hoverHighlight'
				}
			}]);
I
isidor 已提交
178 179 180

			return this.doShow(pos, expression, focus);
		});
181 182
	}

183 184 185 186 187 188 189 190
	private doFindExpression(container: debug.IExpressionContainer, namesToFind: string[]): TPromise<debug.IExpression> {
		return container.getChildren(this.debugService).then(children => {
			// look for our variable in the list. First find the parents of the hovered variable if there are any.
			// some languages pass the type as part of the name, so need to check if the last word of the name matches.
			const filtered = children.filter(v => typeof v.name === 'string' && (namesToFind[0] === v.name || namesToFind[0] === v.name.substr(v.name.lastIndexOf(' ') + 1)));
			if (filtered.length !== 1) {
				return null;
			}
E
Erich Gamma 已提交
191

192 193 194 195 196 197 198
			if (namesToFind.length === 1) {
				return filtered[0];
			} else {
				return this.doFindExpression(filtered[0], namesToFind.slice(1));
			}
		});
	}
E
Erich Gamma 已提交
199

200 201
	private findExpressionInStackFrame(namesToFind: string[]): TPromise<debug.IExpression> {
		return this.debugService.getViewModel().getFocusedStackFrame().getScopes(this.debugService)
E
Erich Gamma 已提交
202
			// no expensive scopes
203 204 205 206 207
			.then(scopes => scopes.filter(scope => !scope.expensive))
			.then(scopes => TPromise.join(scopes.map(scope => this.doFindExpression(scope, namesToFind))))
			.then(expressions => expressions.filter(exp => !!exp))
			// only show if there are no duplicates across scopes
			.then(expressions => expressions.length === 1 ? expressions[0] : null);
208
	}
E
Erich Gamma 已提交
209

A
Alex Dima 已提交
210
	private doShow(position: Position, expression: debug.IExpression, focus: boolean, forceValueHover = false): TPromise<void> {
211 212
		this.showAtPosition = position;
		this.isVisible = true;
I
isidor 已提交
213
		this.stoleFocus = focus;
214

I
isidor 已提交
215
		if (expression.reference === 0 || forceValueHover) {
216
			this.complexValueContainer.hidden = true;
217
			this.valueContainer.hidden = false;
218
			viewer.renderExpressionValue(expression, this.valueContainer, false, MAX_VALUE_RENDER_LENGTH_IN_HOVER);
I
isidor 已提交
219
			this.valueContainer.title = '';
220
			this.editor.layoutContentWidget(this);
I
isidor 已提交
221 222 223 224
			if (focus) {
				this.editor.render();
				this.valueContainer.focus();
			}
225

I
isidor 已提交
226
			return TPromise.as(null);
227
		}
I
isidor 已提交
228 229

		this.valueContainer.hidden = true;
230
		this.complexValueContainer.hidden = false;
I
isidor 已提交
231 232

		return this.tree.setInput(expression).then(() => {
233
			this.complexValueTitle.textContent = expression.value;
I
isidor 已提交
234
			this.complexValueTitle.title = expression.value;
I
isidor 已提交
235 236 237 238 239 240 241
			this.layoutTree();
			this.editor.layoutContentWidget(this);
			if (focus) {
				this.editor.render();
				this.tree.DOMFocus();
			}
		});
E
Erich Gamma 已提交
242 243
	}

244 245 246 247 248 249 250
	private layoutTree(): void {
		const navigator = this.tree.getNavigator();
		let visibleElementsCount = 0;
		while (navigator.next()) {
			visibleElementsCount++;
		}

251
		if (visibleElementsCount === 0) {
I
isidor 已提交
252
			this.doShow(this.showAtPosition, this.tree.getInput(), false, true);
253 254 255 256 257 258 259
		} else {
			const height = Math.min(visibleElementsCount, MAX_ELEMENTS_SHOWN) * 18;

			if (this.treeContainer.clientHeight !== height) {
				this.treeContainer.style.height = `${ height }px`;
				this.tree.layout();
			}
260
		}
261 262
	}

E
Erich Gamma 已提交
263 264 265 266
	public hide(): void {
		if (!this.isVisible) {
			return;
		}
267

E
Erich Gamma 已提交
268
		this.isVisible = false;
I
isidor 已提交
269 270
		this.editor.deltaDecorations(this.highlightDecorations, []);
		this.highlightDecorations = [];
E
Erich Gamma 已提交
271
		this.editor.layoutContentWidget(this);
I
isidor 已提交
272 273 274
		if (this.stoleFocus) {
			this.editor.focus();
		}
E
Erich Gamma 已提交
275 276 277 278 279 280 281 282 283 284 285
	}

	public getPosition(): editorbrowser.IContentWidgetPosition {
		return this.isVisible ? {
			position: this.showAtPosition,
			preference: [
				editorbrowser.ContentWidgetPositionPreference.ABOVE,
				editorbrowser.ContentWidgetPositionPreference.BELOW
			]
		} : null;
	}
I
isidor 已提交
286 287

	public dispose(): void {
J
Joao Moreno 已提交
288
		this.toDispose = lifecycle.dispose(this.toDispose);
I
isidor 已提交
289
	}
E
Erich Gamma 已提交
290
}
I
isidor 已提交
291 292 293

class DebugHoverController extends DefaultController {

B
Benjamin Pasero 已提交
294 295 296 297
	constructor(private editor: editorbrowser.ICodeEditor) {
		super();
	}

I
isidor 已提交
298 299 300 301 302
	/* protected */ public onLeftClick(tree: ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean {
		if (element.reference > 0) {
			super.onLeftClick(tree, element, eventish, origin);
			tree.clearFocus();
			tree.deselect(element);
B
Benjamin Pasero 已提交
303
			this.editor.focus();
I
isidor 已提交
304 305 306 307 308
		}

		return true;
	}
}
309 310 311 312 313 314 315

class VariablesHoverRenderer extends viewer.VariablesRenderer {

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