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

import {Position} from 'vs/editor/common/core/position';
A
Alex Dima 已提交
8
import {Range as EditorRange} from 'vs/editor/common/core/range';
9
import {EditorLayoutInfo, IPosition, MouseTargetType} from 'vs/editor/common/editorCommon';
10
import {ClassNames, IMouseTarget, IViewZoneData} from 'vs/editor/browser/editorBrowser';
A
Alex Dima 已提交
11
import {ViewContext} from 'vs/editor/common/view/viewContext';
12
import {IPointerHandlerHelper} from 'vs/editor/browser/controller/mouseHandler';
13
import {EditorMouseEvent} from 'vs/editor/browser/editorDom';
14
import * as dom from 'vs/base/browser/dom';
E
Erich Gamma 已提交
15 16

interface IHitTestResult {
A
Alex Dima 已提交
17
	position: IPosition;
E
Erich Gamma 已提交
18 19 20
	hitTarget: Element;
}

A
Alex Dima 已提交
21
class MouseTarget implements IMouseTarget {
E
Erich Gamma 已提交
22 23

	public element: Element;
A
Alex Dima 已提交
24
	public type: MouseTargetType;
25
	public mouseColumn: number;
A
Alex Dima 已提交
26
	public position: Position;
27
	public range: EditorRange;
E
Erich Gamma 已提交
28 29
	public detail: any;

30
	constructor(element: Element, type: MouseTargetType, mouseColumn:number = 0, position:Position = null, range: EditorRange = null, detail: any = null) {
E
Erich Gamma 已提交
31 32
		this.element = element;
		this.type = type;
33
		this.mouseColumn = mouseColumn;
E
Erich Gamma 已提交
34 35 36 37 38 39 40 41 42
		this.position = position;
		if (!range && position) {
			range = new EditorRange(position.lineNumber, position.column, position.lineNumber, position.column);
		}
		this.range = range;
		this.detail = detail;
	}

	private _typeToString(): string {
A
Alex Dima 已提交
43
		if (this.type === MouseTargetType.TEXTAREA) {
E
Erich Gamma 已提交
44 45
			return 'TEXTAREA';
		}
A
Alex Dima 已提交
46
		if (this.type === MouseTargetType.GUTTER_GLYPH_MARGIN) {
E
Erich Gamma 已提交
47 48
			return 'GUTTER_GLYPH_MARGIN';
		}
A
Alex Dima 已提交
49
		if (this.type === MouseTargetType.GUTTER_LINE_NUMBERS) {
E
Erich Gamma 已提交
50 51
			return 'GUTTER_LINE_NUMBERS';
		}
A
Alex Dima 已提交
52
		if (this.type === MouseTargetType.GUTTER_LINE_DECORATIONS) {
E
Erich Gamma 已提交
53 54
			return 'GUTTER_LINE_DECORATIONS';
		}
A
Alex Dima 已提交
55
		if (this.type === MouseTargetType.GUTTER_VIEW_ZONE) {
E
Erich Gamma 已提交
56 57
			return 'GUTTER_VIEW_ZONE';
		}
A
Alex Dima 已提交
58
		if (this.type === MouseTargetType.CONTENT_TEXT) {
E
Erich Gamma 已提交
59 60
			return 'CONTENT_TEXT';
		}
A
Alex Dima 已提交
61
		if (this.type === MouseTargetType.CONTENT_EMPTY) {
E
Erich Gamma 已提交
62 63
			return 'CONTENT_EMPTY';
		}
A
Alex Dima 已提交
64
		if (this.type === MouseTargetType.CONTENT_VIEW_ZONE) {
E
Erich Gamma 已提交
65 66
			return 'CONTENT_VIEW_ZONE';
		}
A
Alex Dima 已提交
67
		if (this.type === MouseTargetType.CONTENT_WIDGET) {
E
Erich Gamma 已提交
68 69
			return 'CONTENT_WIDGET';
		}
A
Alex Dima 已提交
70
		if (this.type === MouseTargetType.OVERVIEW_RULER) {
E
Erich Gamma 已提交
71 72
			return 'OVERVIEW_RULER';
		}
A
Alex Dima 已提交
73
		if (this.type === MouseTargetType.SCROLLBAR) {
E
Erich Gamma 已提交
74 75
			return 'SCROLLBAR';
		}
A
Alex Dima 已提交
76
		if (this.type === MouseTargetType.OVERLAY_WIDGET) {
E
Erich Gamma 已提交
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
			return 'OVERLAY_WIDGET';
		}
		return 'UNKNOWN';
	}

	public toString(): string {
		return this._typeToString() + ': ' + this.position + ' - ' + this.range + ' - ' + this.detail;
	}
}


// e.g. of paths:
// - overflow-guard/monaco-scrollable-element editor-scrollable vs/lines-content/view-lines/view-line
// - overflow-guard/monaco-scrollable-element editor-scrollable vs/lines-content/view-lines/view-line/token comment js
// etc.
92
let REGEX = (function() {
E
Erich Gamma 已提交
93 94 95 96 97 98 99 100 101

	function nodeWithClass(className:string): string {
		return '[^/]*' + className + '[^/]*';
	}

	function anyNode(): string {
		return '[^/]+';
	}

102
	let ANCHOR = '^' + ClassNames.OVERFLOW_GUARD + '\\/';
E
Erich Gamma 已提交
103 104

	function createRegExp(...pieces:string[]): RegExp {
105
		let forceEndMatch = false;
E
Erich Gamma 已提交
106 107 108 109 110 111 112 113
		if (pieces[pieces.length - 1] === '$') {
			forceEndMatch = true;
			pieces.pop();
		}
		return new RegExp(ANCHOR + pieces.join('\\/') + (forceEndMatch ? '$' : ''));
	}

	return {
A
Alex Dima 已提交
114 115 116 117 118 119 120 121 122 123 124
		IS_TEXTAREA_COVER: createRegExp(nodeWithClass(ClassNames.TEXTAREA_COVER), '$'),
		IS_TEXTAREA: createRegExp(ClassNames.TEXTAREA, '$'),
		IS_VIEW_LINES: createRegExp(anyNode(), anyNode(), ClassNames.VIEW_LINES, '$'),
		IS_CURSORS_LAYER: createRegExp(anyNode(), anyNode(), nodeWithClass(ClassNames.VIEW_CURSORS_LAYER), '$'),
		IS_CHILD_OF_VIEW_LINES: createRegExp(anyNode(), anyNode(), ClassNames.VIEW_LINES),
		IS_CHILD_OF_SCROLLABLE_ELEMENT: createRegExp(nodeWithClass(ClassNames.SCROLLABLE_ELEMENT)),
		IS_CHILD_OF_CONTENT_WIDGETS: createRegExp(anyNode(), anyNode(), ClassNames.CONTENT_WIDGETS),
		IS_CHILD_OF_OVERFLOWING_CONTENT_WIDGETS: new RegExp('^' + ClassNames.OVERFLOWING_CONTENT_WIDGETS + '\\/'),
		IS_CHILD_OF_OVERLAY_WIDGETS: createRegExp(ClassNames.OVERLAY_WIDGETS),
		IS_CHILD_OF_VIEW_OVERLAYS: createRegExp(ClassNames.MARGIN_VIEW_OVERLAYS),
		IS_CHILD_OF_VIEW_ZONES: createRegExp(anyNode(), anyNode(), ClassNames.VIEW_ZONES),
E
Erich Gamma 已提交
125 126 127 128 129
	};
})();

export class MouseTargetFactory {

130 131
	private _context: ViewContext;
	private _viewHelper: IPointerHandlerHelper;
E
Erich Gamma 已提交
132

A
Alex Dima 已提交
133
	constructor(context:ViewContext, viewHelper:IPointerHandlerHelper) {
134 135
		this._context = context;
		this._viewHelper = viewHelper;
E
Erich Gamma 已提交
136 137 138
	}

	private getClassNamePathTo(child:Node, stopAt:Node): string {
139
		let path:string[] = [],
E
Erich Gamma 已提交
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
			className: string;

		while (child && child !== document.body) {
			if (child === stopAt) {
				break;
			}
			if (child.nodeType === child.ELEMENT_NODE) {
				className = (<HTMLElement>child).className;
				if (className) {
					path.unshift(className);
				}
			}
			child = child.parentNode;
		}

		return path.join('/');
	}

158 159 160
	public mouseTargetIsWidget(e:EditorMouseEvent): boolean {
		let t = <Element>e.target;
		let path = this.getClassNamePathTo(t, this._viewHelper.viewDomNode);
E
Erich Gamma 已提交
161 162

		// Is it a content widget?
163
		if (REGEX.IS_CHILD_OF_CONTENT_WIDGETS.test(path) || REGEX.IS_CHILD_OF_OVERFLOWING_CONTENT_WIDGETS.test(path)) {
E
Erich Gamma 已提交
164 165 166 167 168 169 170 171 172 173 174
			return true;
		}

		// Is it an overlay widget?
		if (REGEX.IS_CHILD_OF_OVERLAY_WIDGETS.test(path)) {
			return true;
		}

		return false;
	}

175
	public createMouseTarget(layoutInfo:EditorLayoutInfo, e:EditorMouseEvent, testEventTarget:boolean): IMouseTarget {
E
Erich Gamma 已提交
176
		try {
177
			let r = this._unsafeCreateMouseTarget(layoutInfo, e, testEventTarget);
E
Erich Gamma 已提交
178 179 180 181 182 183
			return r;
		} catch (e) {
			return this.createMouseTargetFromUnknownTarget(e.target);
		}
	}

184 185 186
	private _unsafeCreateMouseTarget(layoutInfo:EditorLayoutInfo, e:EditorMouseEvent, testEventTarget:boolean): IMouseTarget {
		let mouseVerticalOffset = Math.max(0, this._viewHelper.getScrollTop() + (e.posy - e.editorPos.top));
		let mouseContentHorizontalOffset = this._viewHelper.getScrollLeft() + (e.posx - e.editorPos.left) - layoutInfo.contentLeft;
187
		let mouseColumn = this._getMouseColumn(mouseContentHorizontalOffset);
E
Erich Gamma 已提交
188

189 190
		let t = <Element>e.target;
		let path = this.getClassNamePathTo(t, this._viewHelper.viewDomNode);
E
Erich Gamma 已提交
191 192

		// Is it a content widget?
193
		if (REGEX.IS_CHILD_OF_CONTENT_WIDGETS.test(path) || REGEX.IS_CHILD_OF_OVERFLOWING_CONTENT_WIDGETS.test(path)) {
194
			return this.createMouseTargetFromContentWidgetsChild(t, mouseColumn);
E
Erich Gamma 已提交
195 196 197 198
		}

		// Is it an overlay widget?
		if (REGEX.IS_CHILD_OF_OVERLAY_WIDGETS.test(path)) {
199
			return this.createMouseTargetFromOverlayWidgetsChild(t, mouseColumn);
E
Erich Gamma 已提交
200 201
		}

202
		// Is it a cursor ?
203 204
		let lineNumberAttribute = t.hasAttribute && t.hasAttribute('lineNumber') ? t.getAttribute('lineNumber') : null;
		let columnAttribute = t.hasAttribute && t.hasAttribute('column') ? t.getAttribute('column') : null;
205 206 207 208
		if (lineNumberAttribute && columnAttribute) {
			return this.createMouseTargetFromViewCursor(t, parseInt(lineNumberAttribute, 10), parseInt(columnAttribute, 10), mouseColumn);
		}

E
Erich Gamma 已提交
209 210
		// Is it the textarea cover?
		if (REGEX.IS_TEXTAREA_COVER.test(path)) {
211
			if (this._context.configuration.editor.viewInfo.glyphMargin) {
212
				return this.createMouseTargetFromGlyphMargin(t, mouseVerticalOffset, mouseColumn);
213
			} else if (this._context.configuration.editor.viewInfo.lineNumbers) {
214
				return this.createMouseTargetFromLineNumbers(t, mouseVerticalOffset, mouseColumn);
E
Erich Gamma 已提交
215
			} else {
216
				return this.createMouseTargetFromLinesDecorationsChild(t, mouseVerticalOffset, mouseColumn);
E
Erich Gamma 已提交
217 218 219 220 221
			}
		}

		// Is it the textarea?
		if (REGEX.IS_TEXTAREA.test(path)) {
A
Alex Dima 已提交
222
			return new MouseTarget(t, MouseTargetType.TEXTAREA);
E
Erich Gamma 已提交
223 224 225 226 227
		}

		// Is it a view zone?
		if (REGEX.IS_CHILD_OF_VIEW_ZONES.test(path)) {
			// Check if it is at a view zone
228
			let viewZoneData = this._getZoneAtCoord(mouseVerticalOffset);
E
Erich Gamma 已提交
229
			if (viewZoneData) {
230
				return new MouseTarget(t, MouseTargetType.CONTENT_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData);
E
Erich Gamma 已提交
231 232 233 234 235 236 237 238 239 240
			}
			return this.createMouseTargetFromUnknownTarget(t);
		}

		// Is it the view lines container?
		if (REGEX.IS_VIEW_LINES.test(path)) {
			// Sometimes, IE returns this target when right clicking on top of text
			// -> See Bug #12990: [F12] Context menu shows incorrect position while doing a resize

			// Check if it is below any lines and any view zones
241
			if (this._viewHelper.isAfterLines(mouseVerticalOffset)) {
242
				return this.createMouseTargetFromViewLines(t, mouseVerticalOffset, mouseColumn);
E
Erich Gamma 已提交
243 244 245
			}

			// Check if it is at a view zone
246
			let viewZoneData = this._getZoneAtCoord(mouseVerticalOffset);
E
Erich Gamma 已提交
247
			if (viewZoneData) {
248
				return new MouseTarget(t, MouseTargetType.CONTENT_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData);
E
Erich Gamma 已提交
249 250 251
			}

			// Check if it hits a position
252
			let hitTestResult = this._doHitTest(e, mouseVerticalOffset);
E
Erich Gamma 已提交
253
			if (hitTestResult.position) {
254
				return this.createMouseTargetFromHitTestPosition(t, hitTestResult.position.lineNumber, hitTestResult.position.column, mouseContentHorizontalOffset, mouseColumn);
E
Erich Gamma 已提交
255 256 257
			}

			// Fall back to view lines
258
			return this.createMouseTargetFromViewLines(t, mouseVerticalOffset, mouseColumn);
E
Erich Gamma 已提交
259 260 261 262
		}

		// Is it a child of the view lines container?
		if (!testEventTarget || REGEX.IS_CHILD_OF_VIEW_LINES.test(path)) {
263
			let hitTestResult = this._doHitTest(e, mouseVerticalOffset);
E
Erich Gamma 已提交
264
			if (hitTestResult.position) {
265
				return this.createMouseTargetFromHitTestPosition(t, hitTestResult.position.lineNumber, hitTestResult.position.column, mouseContentHorizontalOffset, mouseColumn);
E
Erich Gamma 已提交
266 267
			} else if (hitTestResult.hitTarget) {
				t = hitTestResult.hitTarget;
268
				path = this.getClassNamePathTo(t, this._viewHelper.viewDomNode);
269 270 271

				// TODO@Alex: try again with this different target, but guard against recursion.
				// Is it a cursor ?
272 273
				let lineNumberAttribute = t.hasAttribute && t.hasAttribute('lineNumber') ? t.getAttribute('lineNumber') : null;
				let columnAttribute = t.hasAttribute && t.hasAttribute('column') ? t.getAttribute('column') : null;
274 275 276
				if (lineNumberAttribute && columnAttribute) {
					return this.createMouseTargetFromViewCursor(t, parseInt(lineNumberAttribute, 10), parseInt(columnAttribute, 10), mouseColumn);
				}
E
Erich Gamma 已提交
277 278 279 280 281
			}
		}

		// Is it the cursors layer?
		if (REGEX.IS_CURSORS_LAYER.test(path)) {
A
Alex Dima 已提交
282
			return new MouseTarget(t, MouseTargetType.UNKNOWN);
E
Erich Gamma 已提交
283 284 285 286
		}

		// Is it a child of the scrollable element?
		if (REGEX.IS_CHILD_OF_SCROLLABLE_ELEMENT.test(path)) {
287
			return this.createMouseTargetFromScrollbar(t, mouseVerticalOffset, mouseColumn);
E
Erich Gamma 已提交
288 289 290
		}

		if (REGEX.IS_CHILD_OF_VIEW_OVERLAYS.test(path)) {
291
			let offset = Math.abs(e.posx - e.editorPos.left);
E
Erich Gamma 已提交
292 293 294

			if (offset <= layoutInfo.glyphMarginWidth) {
				// On the glyph margin
295
				return this.createMouseTargetFromGlyphMargin(t, mouseVerticalOffset, mouseColumn);
E
Erich Gamma 已提交
296 297 298 299 300
			}
			offset -= layoutInfo.glyphMarginWidth;

			if (offset <= layoutInfo.lineNumbersWidth) {
				// On the line numbers
301
				return this.createMouseTargetFromLineNumbers(t, mouseVerticalOffset, mouseColumn);
E
Erich Gamma 已提交
302 303 304 305
			}
			offset -= layoutInfo.lineNumbersWidth;

			// On the line decorations
306
			return this.createMouseTargetFromLinesDecorationsChild(t, mouseVerticalOffset, mouseColumn);
E
Erich Gamma 已提交
307 308 309
		}

		if (/OverviewRuler/i.test(path)) {
310
			return this.createMouseTargetFromScrollbar(t, mouseVerticalOffset, mouseColumn);
E
Erich Gamma 已提交
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
		}

		return this.createMouseTargetFromUnknownTarget(t);
	}

	private _isChild(testChild:Node, testAncestor:Node, stopAt:Node): boolean {
		while (testChild && testChild !== document.body) {
			if (testChild === testAncestor) {
				return true;
			}
			if (testChild === stopAt) {
				return false;
			}
			testChild = testChild.parentNode;
		}
		return false;
	}

	private _findAttribute(element:Element, attr:string, stopAt:Element): string {
		while (element && element !== document.body) {
			if (element.hasAttribute && element.hasAttribute(attr)) {
				return element.getAttribute(attr);
			}
			if (element === stopAt) {
				return null;
			}
			element = <Element>element.parentNode;
		}
		return null;
	}

	/**
	 * Most probably WebKit browsers
	 */
345
	private _doHitTestWithCaretRangeFromPoint(e: EditorMouseEvent, mouseVerticalOffset: number): IHitTestResult {
E
Erich Gamma 已提交
346 347 348

		// In Chrome, especially on Linux it is possible to click between lines,
		// so try to adjust the `hity` below so that it lands in the center of a line
349 350 351 352
		let lineNumber = this._viewHelper.getLineNumberAtVerticalOffset(mouseVerticalOffset);
		let lineVerticalOffset = this._viewHelper.getVerticalOffsetForLineNumber(lineNumber);
		let centeredVerticalOffset = lineVerticalOffset + Math.floor(this._context.configuration.editor.lineHeight / 2);
		let adjustedPosy = e.posy + (centeredVerticalOffset - mouseVerticalOffset);
E
Erich Gamma 已提交
353

354 355
		if (adjustedPosy <= e.editorPos.top) {
			adjustedPosy = e.editorPos.top + 1;
E
Erich Gamma 已提交
356
		}
357 358
		if (adjustedPosy >= e.editorPos.top + this._context.configuration.editor.layoutInfo.height) {
			adjustedPosy = e.editorPos.top + this._context.configuration.editor.layoutInfo.height - 1;
E
Erich Gamma 已提交
359 360
		}

361
		let r = this._actualDoHitTestWithCaretRangeFromPoint(e.viewportx, adjustedPosy - dom.StandardWindow.scrollY);
E
Erich Gamma 已提交
362 363 364 365 366
		if (r.position) {
			return r;
		}

		// Also try to hit test without the adjustment (for the edge cases that we are near the top or bottom)
367
		return this._actualDoHitTestWithCaretRangeFromPoint(e.viewportx, e.viewporty);
E
Erich Gamma 已提交
368 369 370 371
	}

	private _actualDoHitTestWithCaretRangeFromPoint(hitx:number, hity:number): IHitTestResult {

A
Alex Dima 已提交
372
		let range:Range = (<any>document).caretRangeFromPoint(hitx, hity);
E
Erich Gamma 已提交
373

A
Alex Dima 已提交
374 375 376 377 378 379
		if (!range || !range.startContainer) {
			return {
				position: null,
				hitTarget: null
			};
		}
E
Erich Gamma 已提交
380

A
Alex Dima 已提交
381 382
		// Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span
		let startContainer = range.startContainer;
383
		let hitTarget: HTMLElement;
E
Erich Gamma 已提交
384

A
Alex Dima 已提交
385 386 387 388 389 390
		if (startContainer.nodeType === startContainer.TEXT_NODE) {
			// startContainer is expected to be the token text
			let parent1 = startContainer.parentNode; // expected to be the token span
			let parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span
			let parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div
			let parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (<HTMLElement>parent3).className : null;
E
Erich Gamma 已提交
391

A
Alex Dima 已提交
392 393
			if (parent3ClassName === ClassNames.VIEW_LINE) {
				return {
394
					position: this._viewHelper.getPositionFromDOMInfo(<HTMLElement>parent1, range.startOffset),
A
Alex Dima 已提交
395 396
					hitTarget: null
				};
397 398
			} else {
				hitTarget = <HTMLElement>startContainer.parentNode;
A
Alex Dima 已提交
399 400 401 402 403 404 405 406 407
			}
		} else if (startContainer.nodeType === startContainer.ELEMENT_NODE) {
			// startContainer is expected to be the token span
			let parent1 = startContainer.parentNode; // expected to be the view line container span
			let parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line div
			let parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (<HTMLElement>parent2).className : null;

			if (parent2ClassName === ClassNames.VIEW_LINE) {
				return {
408
					position: this._viewHelper.getPositionFromDOMInfo(<HTMLElement>startContainer, (<HTMLElement>startContainer).textContent.length),
A
Alex Dima 已提交
409 410
					hitTarget: null
				};
411 412
			} else {
				hitTarget = <HTMLElement>startContainer;
A
Alex Dima 已提交
413 414
			}
		}
E
Erich Gamma 已提交
415 416

		return {
A
Alex Dima 已提交
417
			position: null,
418
			hitTarget: hitTarget
E
Erich Gamma 已提交
419 420 421 422 423 424
		};
	}

	/**
	 * Most probably Gecko
	 */
425 426
	private _doHitTestWithCaretPositionFromPoint(e: EditorMouseEvent): IHitTestResult {
		let hitResult:{ offsetNode: Node; offset: number; } = (<any>document).caretPositionFromPoint(e.viewportx, e.viewporty);
E
Erich Gamma 已提交
427

428
		let range = document.createRange();
E
Erich Gamma 已提交
429 430
		range.setStart(hitResult.offsetNode, hitResult.offset);
		range.collapse(true);
431
		let resultPosition = this._viewHelper.getPositionFromDOMInfo(<HTMLElement>range.startContainer.parentNode, range.startOffset);
E
Erich Gamma 已提交
432 433 434 435
		range.detach();

		return {
			position: resultPosition,
436
			hitTarget: null
E
Erich Gamma 已提交
437 438 439 440 441 442
		};
	}

	/**
	 * Most probably IE
	 */
443 444 445
	private _doHitTestWithMoveToPoint(e: EditorMouseEvent): IHitTestResult {
		let resultPosition: IPosition = null;
		let resultHitTarget: Element = null;
E
Erich Gamma 已提交
446

447
		let textRange:TextRange = (<any>document.body).createTextRange();
E
Erich Gamma 已提交
448
		try {
449
			textRange.moveToPoint(e.viewportx, e.viewporty);
E
Erich Gamma 已提交
450 451 452 453 454 455 456 457 458 459
		} catch (err) {
			return {
				position: null,
				hitTarget: null
			};
		}

		textRange.collapse(true);

		// Now, let's do our best to figure out what we hit :)
460 461 462
		let parentElement = textRange ? textRange.parentElement() : null;
		let parent1 = parentElement ? parentElement.parentNode : null;
		let parent2 = parent1 ? parent1.parentNode : null;
E
Erich Gamma 已提交
463

464
		let parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (<HTMLElement>parent2).className : '';
E
Erich Gamma 已提交
465

A
Alex Dima 已提交
466
		if (parent2ClassName === ClassNames.VIEW_LINE) {
467
			let rangeToContainEntireSpan = textRange.duplicate();
E
Erich Gamma 已提交
468 469 470
			rangeToContainEntireSpan.moveToElementText(parentElement);
			rangeToContainEntireSpan.setEndPoint('EndToStart', textRange);

471
			resultPosition = this._viewHelper.getPositionFromDOMInfo(<HTMLElement>parentElement, rangeToContainEntireSpan.text.length);
E
Erich Gamma 已提交
472 473
			// Move range out of the span node, IE doesn't like having many ranges in
			// the same spot and will act badly for lines containing dashes ('-')
474
			rangeToContainEntireSpan.moveToElementText(this._viewHelper.viewDomNode);
E
Erich Gamma 已提交
475 476 477 478 479 480 481
		} else {
			// Looks like we've hit the hover or something foreign
			resultHitTarget = parentElement;
		}

		// Move range out of the span node, IE doesn't like having many ranges in
		// the same spot and will act badly for lines containing dashes ('-')
482
		textRange.moveToElementText(this._viewHelper.viewDomNode);
E
Erich Gamma 已提交
483 484 485 486 487 488 489

		return {
			position: resultPosition,
			hitTarget: resultHitTarget
		};
	}

490
	private _doHitTest(e:EditorMouseEvent, mouseVerticalOffset: number): IHitTestResult {
E
Erich Gamma 已提交
491 492 493 494 495 496 497 498
		// State of the art (18.10.2012):
		// The spec says browsers should support document.caretPositionFromPoint, but nobody implemented it (http://dev.w3.org/csswg/cssom-view/)
		// Gecko:
		//    - they tried to implement it once, but failed: https://bugzilla.mozilla.org/show_bug.cgi?id=654352
		//    - however, they do give out rangeParent/rangeOffset properties on mouse events
		// Webkit:
		//    - they have implemented a previous version of the spec which was using document.caretRangeFromPoint
		// IE:
B
Benjamin Pasero 已提交
499
		//    - they have a proprietary method on ranges, moveToPoint: https://msdn.microsoft.com/en-us/library/ie/ms536632(v=vs.85).aspx
E
Erich Gamma 已提交
500 501 502 503 504

		// Thank you browsers for making this so 'easy' :)

		if ((<any>document).caretRangeFromPoint) {

505
			return this._doHitTestWithCaretRangeFromPoint(e, mouseVerticalOffset);
E
Erich Gamma 已提交
506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522

		} else if ((<any>document).caretPositionFromPoint) {

			return this._doHitTestWithCaretPositionFromPoint(e);

		} else if ((<any>document.body).createTextRange) {

			return this._doHitTestWithMoveToPoint(e);

		}

		return {
			position: null,
			hitTarget: null
		};
	}

A
Alex Dima 已提交
523
	private _getZoneAtCoord(mouseVerticalOffset: number): IViewZoneData {
E
Erich Gamma 已提交
524
		// The target is either a view zone or the empty space after the last view-line
525
		let viewZoneWhitespace = this._viewHelper.getWhitespaceAtVerticalOffset(mouseVerticalOffset);
E
Erich Gamma 已提交
526 527

		if (viewZoneWhitespace) {
528
			let viewZoneMiddle = viewZoneWhitespace.verticalOffset + viewZoneWhitespace.height / 2,
529
				lineCount = this._context.model.getLineCount(),
A
Alex Dima 已提交
530 531 532
				positionBefore: Position = null,
				position: Position,
				positionAfter: Position = null;
E
Erich Gamma 已提交
533 534 535 536 537 538 539

			if (viewZoneWhitespace.afterLineNumber !== lineCount) {
				// There are more lines after this view zone
				positionAfter = new Position(viewZoneWhitespace.afterLineNumber + 1, 1);
			}
			if (viewZoneWhitespace.afterLineNumber > 0) {
				// There are more lines above this view zone
540
				positionBefore = new Position(viewZoneWhitespace.afterLineNumber, this._context.model.getLineMaxColumn(viewZoneWhitespace.afterLineNumber));
E
Erich Gamma 已提交
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563
			}

			if (positionAfter === null) {
				position = positionBefore;
			} else if (positionBefore === null) {
				position = positionAfter;
			} else if (mouseVerticalOffset < viewZoneMiddle) {
				position = positionBefore;
			} else {
				position = positionAfter;
			}

			return {
				viewZoneId: viewZoneWhitespace.id,
				afterLineNumber: viewZoneWhitespace.afterLineNumber,
				positionBefore: positionBefore,
				positionAfter: positionAfter,
				position: position
			};
		}
		return null;
	}

564
	private _getFullLineRangeAtCoord(mouseVerticalOffset: number): { range: EditorRange; isAfterLines: boolean; } {
565
		if (this._viewHelper.isAfterLines(mouseVerticalOffset)) {
E
Erich Gamma 已提交
566
			// Below the last line
567 568
			let lineNumber = this._context.model.getLineCount();
			let maxLineColumn = this._context.model.getLineMaxColumn(lineNumber);
E
Erich Gamma 已提交
569 570 571 572 573 574
			return {
				range: new EditorRange(lineNumber, maxLineColumn, lineNumber, maxLineColumn),
				isAfterLines: true
			};
		}

575 576
		let lineNumber = this._viewHelper.getLineNumberAtVerticalOffset(mouseVerticalOffset);
		let maxLineColumn = this._context.model.getLineMaxColumn(lineNumber);
E
Erich Gamma 已提交
577 578 579 580 581 582
		return {
			range: new EditorRange(lineNumber, 1, lineNumber, maxLineColumn),
			isAfterLines: false
		};
	}

583 584
	public getMouseColumn(layoutInfo:EditorLayoutInfo, e:EditorMouseEvent): number {
		let mouseContentHorizontalOffset = this._viewHelper.getScrollLeft() + (e.posx - e.editorPos.left) - layoutInfo.contentLeft;
585
		return this._getMouseColumn(mouseContentHorizontalOffset);
E
Erich Gamma 已提交
586 587
	}

588 589 590 591
	private _getMouseColumn(mouseContentHorizontalOffset:number): number {
		if (mouseContentHorizontalOffset < 0) {
			return 1;
		}
592
		let charWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth;
593 594 595 596 597 598 599 600 601
		let chars = Math.round(mouseContentHorizontalOffset / charWidth);
		return (chars + 1);
	}

	private createMouseTargetFromViewCursor(target:Element, lineNumber: number, column: number, mouseColumn:number): MouseTarget {
		return new MouseTarget(target, MouseTargetType.CONTENT_TEXT, mouseColumn, new Position(lineNumber, column));
	}

	private createMouseTargetFromViewLines(target:Element, mouseVerticalOffset: number, mouseColumn:number): MouseTarget {
E
Erich Gamma 已提交
602
		// This most likely indicates it happened after the last view-line
603 604
		let lineCount = this._context.model.getLineCount();
		let maxLineColumn = this._context.model.getLineMaxColumn(lineCount);
605
		return new MouseTarget(target, MouseTargetType.CONTENT_EMPTY, mouseColumn, new Position(lineCount, maxLineColumn));
E
Erich Gamma 已提交
606 607
	}

608
	private createMouseTargetFromHitTestPosition(target:Element, lineNumber: number, column: number, mouseHorizontalOffset: number, mouseColumn:number): MouseTarget {
609
		let pos = new Position(lineNumber, column);
E
Erich Gamma 已提交
610

611
		let lineWidth = this._viewHelper.getLineWidth(lineNumber);
E
Erich Gamma 已提交
612 613

		if (mouseHorizontalOffset > lineWidth) {
614
			return new MouseTarget(target, MouseTargetType.CONTENT_EMPTY, mouseColumn, pos);
E
Erich Gamma 已提交
615 616
		}

617
		let visibleRange = this._viewHelper.visibleRangeForPosition2(lineNumber, column);
E
Erich Gamma 已提交
618 619

		if (!visibleRange) {
620
			return new MouseTarget(target, MouseTargetType.UNKNOWN, mouseColumn, pos);
E
Erich Gamma 已提交
621 622
		}

623
		let columnHorizontalOffset = visibleRange.left;
E
Erich Gamma 已提交
624 625

		if (mouseHorizontalOffset === columnHorizontalOffset) {
626
			return new MouseTarget(target, MouseTargetType.CONTENT_TEXT, mouseColumn, pos);
E
Erich Gamma 已提交
627 628
		}

629
		let mouseIsBetween: boolean;
E
Erich Gamma 已提交
630
		if (column > 1) {
631
			let prevColumnHorizontalOffset = visibleRange.left;
E
Erich Gamma 已提交
632 633 634 635
			mouseIsBetween = false;
			mouseIsBetween = mouseIsBetween || (prevColumnHorizontalOffset < mouseHorizontalOffset && mouseHorizontalOffset < columnHorizontalOffset); // LTR case
			mouseIsBetween = mouseIsBetween || (columnHorizontalOffset < mouseHorizontalOffset && mouseHorizontalOffset < prevColumnHorizontalOffset); // RTL case
			if (mouseIsBetween) {
636
				let rng = new EditorRange(lineNumber, column, lineNumber, column - 1);
637
				return new MouseTarget(target, MouseTargetType.CONTENT_TEXT, mouseColumn, pos, rng);
E
Erich Gamma 已提交
638 639 640
			}
		}

641
		let lineMaxColumn = this._context.model.getLineMaxColumn(lineNumber);
E
Erich Gamma 已提交
642
		if (column < lineMaxColumn) {
643
			let nextColumnVisibleRange = this._viewHelper.visibleRangeForPosition2(lineNumber, column + 1);
E
Erich Gamma 已提交
644
			if (nextColumnVisibleRange) {
645
				let nextColumnHorizontalOffset = nextColumnVisibleRange.left;
E
Erich Gamma 已提交
646 647 648 649
				mouseIsBetween = false;
				mouseIsBetween = mouseIsBetween || (columnHorizontalOffset < mouseHorizontalOffset && mouseHorizontalOffset < nextColumnHorizontalOffset); // LTR case
				mouseIsBetween = mouseIsBetween || (nextColumnHorizontalOffset < mouseHorizontalOffset && mouseHorizontalOffset < columnHorizontalOffset); // RTL case
				if (mouseIsBetween) {
650
					let rng = new EditorRange(lineNumber, column, lineNumber, column + 1);
651
					return new MouseTarget(target, MouseTargetType.CONTENT_TEXT, mouseColumn, pos, rng);
E
Erich Gamma 已提交
652 653 654 655
				}
			}
		}

656
		return new MouseTarget(target, MouseTargetType.CONTENT_TEXT, mouseColumn, pos);
E
Erich Gamma 已提交
657 658
	}

659
	private createMouseTargetFromContentWidgetsChild(target: Element, mouseColumn:number): MouseTarget {
660
		let widgetId = this._findAttribute(target, 'widgetId', this._viewHelper.viewDomNode);
E
Erich Gamma 已提交
661 662

		if (widgetId) {
663
			return new MouseTarget(target, MouseTargetType.CONTENT_WIDGET, mouseColumn, null, null, widgetId);
E
Erich Gamma 已提交
664
		} else {
A
Alex Dima 已提交
665
			return new MouseTarget(target, MouseTargetType.UNKNOWN);
E
Erich Gamma 已提交
666 667 668
		}
	}

669
	private createMouseTargetFromOverlayWidgetsChild(target: Element, mouseColumn:number): MouseTarget {
670
		let widgetId = this._findAttribute(target, 'widgetId', this._viewHelper.viewDomNode);
E
Erich Gamma 已提交
671 672

		if (widgetId) {
673
			return new MouseTarget(target, MouseTargetType.OVERLAY_WIDGET, mouseColumn, null, null, widgetId);
E
Erich Gamma 已提交
674
		} else {
A
Alex Dima 已提交
675
			return new MouseTarget(target, MouseTargetType.UNKNOWN);
E
Erich Gamma 已提交
676 677 678
		}
	}

679
	private createMouseTargetFromLinesDecorationsChild(target: Element, mouseVerticalOffset: number, mouseColumn:number): MouseTarget {
680
		let viewZoneData = this._getZoneAtCoord(mouseVerticalOffset);
E
Erich Gamma 已提交
681
		if (viewZoneData) {
682
			return new MouseTarget(target, MouseTargetType.GUTTER_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData);
E
Erich Gamma 已提交
683 684
		}

685
		let res = this._getFullLineRangeAtCoord(mouseVerticalOffset);
686
		return new MouseTarget(target, MouseTargetType.GUTTER_LINE_DECORATIONS, mouseColumn, new Position(res.range.startLineNumber, res.range.startColumn), res.range, res.isAfterLines);
E
Erich Gamma 已提交
687 688
	}

689
	private createMouseTargetFromLineNumbers(target: Element, mouseVerticalOffset: number, mouseColumn:number): MouseTarget {
690
		let viewZoneData = this._getZoneAtCoord(mouseVerticalOffset);
E
Erich Gamma 已提交
691
		if (viewZoneData) {
692
			return new MouseTarget(target, MouseTargetType.GUTTER_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData);
E
Erich Gamma 已提交
693 694
		}

695
		let res = this._getFullLineRangeAtCoord(mouseVerticalOffset);
696
		return new MouseTarget(target, MouseTargetType.GUTTER_LINE_NUMBERS, mouseColumn, new Position(res.range.startLineNumber, res.range.startColumn), res.range, res.isAfterLines);
E
Erich Gamma 已提交
697 698
	}

699
	private createMouseTargetFromGlyphMargin(target: Element, mouseVerticalOffset: number, mouseColumn:number): MouseTarget {
700
		let viewZoneData = this._getZoneAtCoord(mouseVerticalOffset);
E
Erich Gamma 已提交
701
		if (viewZoneData) {
702
			return new MouseTarget(target, MouseTargetType.GUTTER_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData);
E
Erich Gamma 已提交
703 704
		}

705
		let res = this._getFullLineRangeAtCoord(mouseVerticalOffset);
706
		return new MouseTarget(target, MouseTargetType.GUTTER_GLYPH_MARGIN, mouseColumn, new Position(res.range.startLineNumber, res.range.startColumn), res.range, res.isAfterLines);
E
Erich Gamma 已提交
707 708
	}

709
	private createMouseTargetFromScrollbar(target: Element, mouseVerticalOffset: number, mouseColumn:number): MouseTarget {
710 711
		let possibleLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(mouseVerticalOffset);
		let maxColumn = this._context.model.getLineMaxColumn(possibleLineNumber);
712
		return new MouseTarget(target, MouseTargetType.SCROLLBAR, mouseColumn, new Position(possibleLineNumber, maxColumn));
E
Erich Gamma 已提交
713 714 715
	}

	private createMouseTargetFromUnknownTarget(target: Element): MouseTarget {
716 717
		let isInView = this._isChild(target, this._viewHelper.viewDomNode, this._viewHelper.viewDomNode);
		let widgetId = null;
E
Erich Gamma 已提交
718
		if (isInView) {
719
			widgetId = this._findAttribute(target, 'widgetId', this._viewHelper.viewDomNode);
E
Erich Gamma 已提交
720 721 722
		}

		if (widgetId) {
A
Alex Dima 已提交
723
			return new MouseTarget(target, MouseTargetType.OVERLAY_WIDGET, null, null, widgetId);
E
Erich Gamma 已提交
724
		} else {
A
Alex Dima 已提交
725
			return new MouseTarget(target, MouseTargetType.UNKNOWN);
E
Erich Gamma 已提交
726 727 728
		}
	}
}