debugHover.ts 11.8 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()
207 208 209 210 211 212 213 214 215 216 217 218
			.then(scopes => scopes.filter(scope => !scope.expensive))
			.then(scopes => {
				// no expensive scopes and if a range of scope is defined it needs to contain the variable
				const haveRangeInfo = scopes.some(s => !!s.range);
				if (!haveRangeInfo) {
					return scopes;
				}

				// Find the most specific scope containing the range #16632
				return [scopes.filter(scope => Range.containsRange(scope.range, expressionRange))
					.sort((first, second) => (first.range.endLineNumber - first.range.startLineNumber) - (second.range.endLineNumber - second.range.startLineNumber)).shift()];
			})
219 220
			.then(scopes => TPromise.join(scopes.map(scope => this.doFindExpression(scope, namesToFind))))
			.then(expressions => expressions.filter(exp => !!exp))
221 222
			// 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);
223
	}
E
Erich Gamma 已提交
224

I
isidor 已提交
225
	private doShow(position: Position, expression: IExpression, focus: boolean, forceValueHover = false): TPromise<void> {
226
		this.showAtPosition = position;
I
isidor 已提交
227
		this._isVisible = true;
I
isidor 已提交
228
		this.stoleFocus = focus;
229

230
		if (!expression.hasChildren || forceValueHover) {
231
			this.complexValueContainer.hidden = true;
232
			this.valueContainer.hidden = false;
233 234 235 236 237
			renderExpressionValue(expression, this.valueContainer, {
				showChanged: false,
				maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_HOVER,
				preserveWhitespace: true
			});
I
isidor 已提交
238
			this.valueContainer.title = '';
239
			this.editor.layoutContentWidget(this);
I
isidor 已提交
240 241 242 243
			if (focus) {
				this.editor.render();
				this.valueContainer.focus();
			}
244

I
isidor 已提交
245
			return TPromise.as(null);
246
		}
I
isidor 已提交
247 248

		this.valueContainer.hidden = true;
249
		this.complexValueContainer.hidden = false;
I
isidor 已提交
250 251

		return this.tree.setInput(expression).then(() => {
252
			this.complexValueTitle.textContent = expression.value;
I
isidor 已提交
253
			this.complexValueTitle.title = expression.value;
I
isidor 已提交
254 255 256 257 258 259 260
			this.layoutTree();
			this.editor.layoutContentWidget(this);
			if (focus) {
				this.editor.render();
				this.tree.DOMFocus();
			}
		});
E
Erich Gamma 已提交
261 262
	}

263 264 265 266 267 268 269
	private layoutTree(): void {
		const navigator = this.tree.getNavigator();
		let visibleElementsCount = 0;
		while (navigator.next()) {
			visibleElementsCount++;
		}

270
		if (visibleElementsCount === 0) {
I
isidor 已提交
271
			this.doShow(this.showAtPosition, this.tree.getInput(), false, true);
272 273 274 275
		} else {
			const height = Math.min(visibleElementsCount, MAX_ELEMENTS_SHOWN) * 18;

			if (this.treeContainer.clientHeight !== height) {
J
Johannes Rieken 已提交
276
				this.treeContainer.style.height = `${height}px`;
277 278
				this.tree.layout();
			}
279
		}
280 281
	}

E
Erich Gamma 已提交
282
	public hide(): void {
I
isidor 已提交
283
		if (!this._isVisible) {
E
Erich Gamma 已提交
284 285
			return;
		}
286

I
isidor 已提交
287
		this._isVisible = false;
I
isidor 已提交
288 289
		this.editor.deltaDecorations(this.highlightDecorations, []);
		this.highlightDecorations = [];
E
Erich Gamma 已提交
290
		this.editor.layoutContentWidget(this);
I
isidor 已提交
291 292 293
		if (this.stoleFocus) {
			this.editor.focus();
		}
E
Erich Gamma 已提交
294 295
	}

I
isidor 已提交
296
	public getPosition(): IContentWidgetPosition {
I
isidor 已提交
297
		return this._isVisible ? {
E
Erich Gamma 已提交
298 299
			position: this.showAtPosition,
			preference: [
I
isidor 已提交
300 301
				ContentWidgetPositionPreference.ABOVE,
				ContentWidgetPositionPreference.BELOW
E
Erich Gamma 已提交
302 303 304
			]
		} : null;
	}
I
isidor 已提交
305 306

	public dispose(): void {
J
Joao Moreno 已提交
307
		this.toDispose = lifecycle.dispose(this.toDispose);
I
isidor 已提交
308
	}
E
Erich Gamma 已提交
309
}
I
isidor 已提交
310 311 312

class DebugHoverController extends DefaultController {

I
isidor 已提交
313
	constructor(private editor: ICodeEditor) {
B
Benjamin Pasero 已提交
314 315 316
		super();
	}

I
isidor 已提交
317
	protected onLeftClick(tree: ITree, element: any, eventish: ICancelableEvent, origin = 'mouse'): boolean {
I
isidor 已提交
318 319 320 321
		if (element.reference > 0) {
			super.onLeftClick(tree, element, eventish, origin);
			tree.clearFocus();
			tree.deselect(element);
B
Benjamin Pasero 已提交
322
			this.editor.focus();
I
isidor 已提交
323 324 325 326 327
		}

		return true;
	}
}
328

I
isidor 已提交
329
class VariablesHoverRenderer extends VariablesRenderer {
330 331 332 333 334

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