debugHover.ts 11.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';
I
isidor 已提交
10
import * as dom from 'vs/base/browser/dom';
J
Johannes Rieken 已提交
11 12
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
I
isidor 已提交
13
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
J
Johannes Rieken 已提交
14 15 16 17
import { DefaultController, ICancelableEvent } from 'vs/base/parts/tree/browser/treeDefaults';
import { IConfigurationChangedEvent } from 'vs/editor/common/editorCommon';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
I
isidor 已提交
18 19 20 21 22
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';
import { VariablesRenderer, renderExpressionValue, VariablesDataSource } from 'vs/workbench/parts/debug/electron-browser/debugViewer';
E
Erich Gamma 已提交
23

J
Joao Moreno 已提交
24
const $ = dom.$;
25
const MAX_ELEMENTS_SHOWN = 18;
26
const MAX_VALUE_RENDER_LENGTH_IN_HOVER = 4096;
E
Erich Gamma 已提交
27

I
isidor 已提交
28
export class DebugHoverWidget implements IContentWidget {
E
Erich Gamma 已提交
29 30

	public static ID = 'debug.hoverWidget';
I
isidor 已提交
31
	// editor.IContentWidget.allowEditorOverflow
E
Erich Gamma 已提交
32 33
	public allowEditorOverflow = true;

I
isidor 已提交
34
	private _isVisible: boolean;
E
Erich Gamma 已提交
35
	private domNode: HTMLElement;
I
isidor 已提交
36
	private tree: ITree;
A
Alex Dima 已提交
37
	private showAtPosition: Position;
I
isidor 已提交
38
	private highlightDecorations: string[];
39
	private complexValueContainer: HTMLElement;
40
	private treeContainer: HTMLElement;
41
	private complexValueTitle: HTMLElement;
42
	private valueContainer: HTMLElement;
I
isidor 已提交
43 44
	private stoleFocus: boolean;
	private toDispose: lifecycle.IDisposable[];
E
Erich Gamma 已提交
45

I
isidor 已提交
46
	constructor(private editor: ICodeEditor, private debugService: IDebugService, instantiationService: IInstantiationService) {
I
isidor 已提交
47
		this.toDispose = [];
I
isidor 已提交
48
		this.create(instantiationService);
I
isidor 已提交
49 50 51 52 53 54
		this.registerListeners();

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

I
isidor 已提交
55
		this._isVisible = false;
I
isidor 已提交
56 57 58 59 60 61 62
		this.showAtPosition = null;
		this.highlightDecorations = [];

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

I
isidor 已提交
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
	private create(instantiationService: IInstantiationService): void {
		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');
		this.tree = new Tree(this.treeContainer, {
			dataSource: new VariablesDataSource(),
			renderer: instantiationService.createInstance(VariablesHoverRenderer),
			controller: new DebugHoverController(this.editor)
		}, {
				indentPixels: 6,
				twistiePixels: 15,
				ariaLabel: nls.localize('treeAriaLabel', "Debug Hover")
			});
	}

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

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

I
isidor 已提交
100 101 102 103
	public isVisible(): boolean {
		return this._isVisible;
	}

E
Erich Gamma 已提交
104 105 106 107 108 109 110 111
	public getId(): string {
		return DebugHoverWidget.ID;
	}

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

J
Johannes Rieken 已提交
112
	private getExactExpressionRange(lineContent: string, range: Range): Range {
R
rajkumar42 已提交
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 149 150 151 152 153 154
		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);
	}

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

158
		const process = this.debugService.getViewModel().focusedProcess;
159 160 161 162
		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);
I
isidor 已提交
163
		let promise: TPromise<IExpression>;
164
		if (process.session.configuration.capabilities.supportsEvaluateForHovers) {
165
			const result = new Expression(matchingExpression);
166
			promise = result.evaluate(process, this.debugService.getViewModel().focusedStackFrame, 'hover').then(() => result);
167
		} else {
I
isidor 已提交
168
			promise = this.findExpressionInStackFrame(matchingExpression.split('.').map(word => word.trim()).filter(word => !!word), expressionRange);
169
		}
R
rajkumar42 已提交
170

171 172
		return promise.then(expression => {
			if (!expression || (expression instanceof Expression && !expression.available)) {
173 174 175 176
				this.hide();
				return;
			}

I
isidor 已提交
177
			this.highlightDecorations = this.editor.deltaDecorations(this.highlightDecorations, [{
178
				range: new Range(pos.lineNumber, expressionRange.startColumn, pos.lineNumber, expressionRange.startColumn + matchingExpression.length),
I
isidor 已提交
179 180 181 182
				options: {
					className: 'hoverHighlight'
				}
			}]);
I
isidor 已提交
183 184 185

			return this.doShow(pos, expression, focus);
		});
186 187
	}

I
isidor 已提交
188
	private doFindExpression(container: IExpressionContainer, namesToFind: string[]): TPromise<IExpression> {
I
isidor 已提交
189
		return container.getChildren().then(children => {
190 191 192 193 194 195
			// 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 已提交
196

197 198 199 200 201 202 203
			if (namesToFind.length === 1) {
				return filtered[0];
			} else {
				return this.doFindExpression(filtered[0], namesToFind.slice(1));
			}
		});
	}
E
Erich Gamma 已提交
204

I
isidor 已提交
205
	private findExpressionInStackFrame(namesToFind: string[], expressionRange: Range): TPromise<IExpression> {
206
		return this.debugService.getViewModel().focusedStackFrame.getScopes()
I
isidor 已提交
207 208
			// no expensive scopes and if a range of scope is defined it needs to contain the variable
			.then(scopes => scopes.filter(scope => !scope.expensive && (!scope.range || Range.containsRange(scope.range, expressionRange))))
209 210
			.then(scopes => TPromise.join(scopes.map(scope => this.doFindExpression(scope, namesToFind))))
			.then(expressions => expressions.filter(exp => !!exp))
211 212
			// 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);
213
	}
E
Erich Gamma 已提交
214

I
isidor 已提交
215
	private doShow(position: Position, expression: IExpression, focus: boolean, forceValueHover = false): TPromise<void> {
216
		this.showAtPosition = position;
I
isidor 已提交
217
		this._isVisible = true;
I
isidor 已提交
218
		this.stoleFocus = focus;
219

220
		if (!expression.hasChildren || forceValueHover) {
221
			this.complexValueContainer.hidden = true;
222
			this.valueContainer.hidden = false;
223 224 225 226 227
			renderExpressionValue(expression, this.valueContainer, {
				showChanged: false,
				maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_HOVER,
				preserveWhitespace: true
			});
I
isidor 已提交
228
			this.valueContainer.title = '';
229
			this.editor.layoutContentWidget(this);
I
isidor 已提交
230 231 232 233
			if (focus) {
				this.editor.render();
				this.valueContainer.focus();
			}
234

I
isidor 已提交
235
			return TPromise.as(null);
236
		}
I
isidor 已提交
237 238

		this.valueContainer.hidden = true;
239
		this.complexValueContainer.hidden = false;
I
isidor 已提交
240 241

		return this.tree.setInput(expression).then(() => {
242
			this.complexValueTitle.textContent = expression.value;
I
isidor 已提交
243
			this.complexValueTitle.title = expression.value;
I
isidor 已提交
244 245 246 247 248 249 250
			this.layoutTree();
			this.editor.layoutContentWidget(this);
			if (focus) {
				this.editor.render();
				this.tree.DOMFocus();
			}
		});
E
Erich Gamma 已提交
251 252
	}

253 254 255 256 257 258 259
	private layoutTree(): void {
		const navigator = this.tree.getNavigator();
		let visibleElementsCount = 0;
		while (navigator.next()) {
			visibleElementsCount++;
		}

260
		if (visibleElementsCount === 0) {
I
isidor 已提交
261
			this.doShow(this.showAtPosition, this.tree.getInput(), false, true);
262 263 264 265
		} else {
			const height = Math.min(visibleElementsCount, MAX_ELEMENTS_SHOWN) * 18;

			if (this.treeContainer.clientHeight !== height) {
J
Johannes Rieken 已提交
266
				this.treeContainer.style.height = `${height}px`;
267 268
				this.tree.layout();
			}
269
		}
270 271
	}

E
Erich Gamma 已提交
272
	public hide(): void {
I
isidor 已提交
273
		if (!this._isVisible) {
E
Erich Gamma 已提交
274 275
			return;
		}
276

I
isidor 已提交
277
		this._isVisible = false;
I
isidor 已提交
278 279
		this.editor.deltaDecorations(this.highlightDecorations, []);
		this.highlightDecorations = [];
E
Erich Gamma 已提交
280
		this.editor.layoutContentWidget(this);
I
isidor 已提交
281 282 283
		if (this.stoleFocus) {
			this.editor.focus();
		}
E
Erich Gamma 已提交
284 285
	}

I
isidor 已提交
286
	public getPosition(): IContentWidgetPosition {
I
isidor 已提交
287
		return this._isVisible ? {
E
Erich Gamma 已提交
288 289
			position: this.showAtPosition,
			preference: [
I
isidor 已提交
290 291
				ContentWidgetPositionPreference.ABOVE,
				ContentWidgetPositionPreference.BELOW
E
Erich Gamma 已提交
292 293 294
			]
		} : null;
	}
I
isidor 已提交
295 296

	public dispose(): void {
J
Joao Moreno 已提交
297
		this.toDispose = lifecycle.dispose(this.toDispose);
I
isidor 已提交
298
	}
E
Erich Gamma 已提交
299
}
I
isidor 已提交
300 301 302

class DebugHoverController extends DefaultController {

I
isidor 已提交
303
	constructor(private editor: ICodeEditor) {
B
Benjamin Pasero 已提交
304 305 306
		super();
	}

I
isidor 已提交
307
	protected onLeftClick(tree: ITree, element: any, eventish: ICancelableEvent, origin = 'mouse'): boolean {
I
isidor 已提交
308 309 310 311
		if (element.reference > 0) {
			super.onLeftClick(tree, element, eventish, origin);
			tree.clearFocus();
			tree.deselect(element);
B
Benjamin Pasero 已提交
312
			this.editor.focus();
I
isidor 已提交
313 314 315 316 317
		}

		return true;
	}
}
318

I
isidor 已提交
319
class VariablesHoverRenderer extends VariablesRenderer {
320 321 322 323 324

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