diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 2066f45128b..b3647590ebb 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -453,23 +453,23 @@ class MouseDownOperation extends Disposable { let mouseColumn = this._getMouseColumn(e); - if (e.posy < editorContent.top) { - let aboveLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(Math.max(this._viewHelper.getScrollTop() - (editorContent.top - e.posy), 0)); + if (e.posy < editorContent.y) { + let aboveLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(Math.max(this._viewHelper.getScrollTop() - (editorContent.y - e.posy), 0)); return new MousePosition(new Position(aboveLineNumber, 1), mouseColumn); } - if (e.posy > editorContent.top + editorContent.height) { - let belowLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(this._viewHelper.getScrollTop() + (e.posy - editorContent.top)); + if (e.posy > editorContent.y + editorContent.height) { + let belowLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(this._viewHelper.getScrollTop() + (e.posy - editorContent.y)); return new MousePosition(new Position(belowLineNumber, this._context.model.getLineMaxColumn(belowLineNumber)), mouseColumn); } - let possibleLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(this._viewHelper.getScrollTop() + (e.posy - editorContent.top)); + let possibleLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(this._viewHelper.getScrollTop() + (e.posy - editorContent.y)); - if (e.posx < editorContent.left) { + if (e.posx < editorContent.x) { return new MousePosition(new Position(possibleLineNumber, 1), mouseColumn); } - if (e.posx > editorContent.left + editorContent.width) { + if (e.posx > editorContent.x + editorContent.width) { return new MousePosition(new Position(possibleLineNumber, this._context.model.getLineMaxColumn(possibleLineNumber)), mouseColumn); } diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index c0610bd7d92..e21d394b214 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -10,11 +10,12 @@ import { EditorLayoutInfo, MouseTargetType } from 'vs/editor/common/editorCommon import { ClassNames, IMouseTarget, IViewZoneData } from 'vs/editor/browser/editorBrowser'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import { IPointerHandlerHelper } from 'vs/editor/browser/controller/mouseHandler'; -import { EditorMouseEvent } from 'vs/editor/browser/editorDom'; -import * as dom from 'vs/base/browser/dom'; +import { EditorMouseEvent, PageCoordinates, ClientCoordinates, EditorPagePosition } from 'vs/editor/browser/editorDom'; import * as browser from 'vs/base/browser/browser'; import { IViewCursorRenderData } from 'vs/editor/browser/viewParts/viewCursors/viewCursor'; import { PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart'; +import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; +import { VisibleRange } from 'vs/editor/common/view/renderingContext'; interface IETextRange { boundingHeight: number; @@ -68,12 +69,12 @@ interface IHitTestResult { class MouseTarget implements IMouseTarget { - public element: Element; - public type: MouseTargetType; - public mouseColumn: number; - public position: Position; - public range: EditorRange; - public detail: any; + public readonly element: Element; + public readonly type: MouseTargetType; + public readonly mouseColumn: number; + public readonly position: Position; + public readonly range: EditorRange; + public readonly detail: any; constructor(element: Element, type: MouseTargetType, mouseColumn: number = 0, position: Position = null, range: EditorRange = null, detail: any = null) { this.element = element; @@ -229,413 +230,26 @@ class ElementPath { } } -export class MouseTargetFactory { +class HitTestContext { - private _context: ViewContext; - private _viewHelper: IPointerHandlerHelper; + public readonly model: IViewModel; + public readonly layoutInfo: EditorLayoutInfo; + public readonly viewDomNode: HTMLElement; + public readonly lineHeight: number; - constructor(context: ViewContext, viewHelper: IPointerHandlerHelper) { + private readonly _context: ViewContext; + private readonly _viewHelper: IPointerHandlerHelper; + + constructor(context: ViewContext, viewHelper: IPointerHandlerHelper, layoutInfo: EditorLayoutInfo) { + this.model = context.model; + this.layoutInfo = layoutInfo; + this.viewDomNode = viewHelper.viewDomNode; + this.lineHeight = context.configuration.editor.lineHeight; this._context = context; this._viewHelper = viewHelper; } - public mouseTargetIsWidget(e: EditorMouseEvent): boolean { - let t = e.target; - let path = PartFingerprints.collect(t, this._viewHelper.viewDomNode); - - // Is it a content widget? - if (ElementPath.isChildOfContentWidgets(path) || ElementPath.isChildOfOverflowingContentWidgets(path)) { - return true; - } - - // Is it an overlay widget? - if (ElementPath.isChildOfOverlayWidgets(path)) { - return true; - } - - return false; - } - - public createMouseTarget(layoutInfo: EditorLayoutInfo, lastViewCursorsRenderData: IViewCursorRenderData[], e: EditorMouseEvent, testEventTarget: boolean): IMouseTarget { - try { - let r = this._unsafeCreateMouseTarget(layoutInfo, lastViewCursorsRenderData, e, testEventTarget); - return r; - } catch (e) { - return this.createMouseTargetFromUnknownTarget(e.target); - } - } - - private _unsafeCreateMouseTarget(layoutInfo: EditorLayoutInfo, lastViewCursorsRenderData: IViewCursorRenderData[], 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; - let mouseColumn = this._getMouseColumn(mouseContentHorizontalOffset); - // console.log(`mouseVerticalOffset: ${mouseVerticalOffset}, mouseContentHorizontalOffset: ${mouseContentHorizontalOffset}, mouseColumn: ${mouseColumn}`) - - let t = e.target; - - // Edge has a bug when hit-testing the exact position of a cursor, - // instead of returning the correct dom node, it returns the - // first or last rendered view line dom node, therefore help it out - // and first check if we are on top of a cursor - for (let i = 0, len = lastViewCursorsRenderData.length; i < len; i++) { - let d = lastViewCursorsRenderData[i]; - - if ( - d.contentLeft <= mouseContentHorizontalOffset - && mouseContentHorizontalOffset <= d.contentLeft + d.width - && d.contentTop <= mouseVerticalOffset - && mouseVerticalOffset <= d.contentTop + d.height - ) { - return this.createMouseTargetFromViewCursor(t, d.position.lineNumber, d.position.column, mouseColumn); - } - } - - let path = PartFingerprints.collect(t, this._viewHelper.viewDomNode); - - // Is it a content widget? - if (ElementPath.isChildOfContentWidgets(path) || ElementPath.isChildOfOverflowingContentWidgets(path)) { - return this.createMouseTargetFromContentWidgetsChild(t, mouseColumn); - } - - // Is it an overlay widget? - if (ElementPath.isChildOfOverlayWidgets(path)) { - return this.createMouseTargetFromOverlayWidgetsChild(t, mouseColumn); - } - - // Is it a cursor ? - let lineNumberAttribute = t.hasAttribute && t.hasAttribute('lineNumber') ? t.getAttribute('lineNumber') : null; - let columnAttribute = t.hasAttribute && t.hasAttribute('column') ? t.getAttribute('column') : null; - if (lineNumberAttribute && columnAttribute) { - return this.createMouseTargetFromViewCursor(t, parseInt(lineNumberAttribute, 10), parseInt(columnAttribute, 10), mouseColumn); - } - - // Is it the textarea cover? - if (ElementPath.isTextAreaCover(path)) { - if (this._context.configuration.editor.viewInfo.glyphMargin) { - return this.createMouseTargetFromGlyphMargin(t, mouseVerticalOffset, mouseColumn); - } else if (this._context.configuration.editor.viewInfo.renderLineNumbers) { - return this.createMouseTargetFromLineNumbers(t, mouseVerticalOffset, mouseColumn); - } else { - return this.createMouseTargetFromLinesDecorationsChild(t, mouseVerticalOffset, mouseColumn); - } - } - - // Is it the textarea? - if (ElementPath.isTextArea(path)) { - return new MouseTarget(t, MouseTargetType.TEXTAREA); - } - - // Is it a view zone? - if (ElementPath.isChildOfViewZones(path)) { - // Check if it is at a view zone - let viewZoneData = this._getZoneAtCoord(mouseVerticalOffset); - if (viewZoneData) { - return new MouseTarget(t, MouseTargetType.CONTENT_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData); - } - return this.createMouseTargetFromUnknownTarget(t); - } - - // Is it the view lines container? - if (ElementPath.isViewLines(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 - if (this._viewHelper.isAfterLines(mouseVerticalOffset)) { - return this.createMouseTargetFromViewLines(t, mouseVerticalOffset, mouseColumn); - } - - // Check if it is at a view zone - let viewZoneData = this._getZoneAtCoord(mouseVerticalOffset); - if (viewZoneData) { - return new MouseTarget(t, MouseTargetType.CONTENT_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData); - } - - // Check if it hits a position - let hitTestResult = this._doHitTest(e, mouseVerticalOffset); - if (hitTestResult.position) { - return this.createMouseTargetFromHitTestPosition(t, hitTestResult.position.lineNumber, hitTestResult.position.column, mouseContentHorizontalOffset, mouseColumn); - } - - // Fall back to view lines - return this.createMouseTargetFromViewLines(t, mouseVerticalOffset, mouseColumn); - } - - // Is it a child of the view lines container? - if (!testEventTarget || ElementPath.isChildOfViewLines(path)) { - let hitTestResult = this._doHitTest(e, mouseVerticalOffset); - if (hitTestResult.position) { - return this.createMouseTargetFromHitTestPosition(t, hitTestResult.position.lineNumber, hitTestResult.position.column, mouseContentHorizontalOffset, mouseColumn); - } else if (hitTestResult.hitTarget) { - t = hitTestResult.hitTarget; - path = PartFingerprints.collect(t, this._viewHelper.viewDomNode); - - // TODO@Alex: try again with this different target, but guard against recursion. - // Is it a cursor ? - let lineNumberAttribute = t.hasAttribute && t.hasAttribute('lineNumber') ? t.getAttribute('lineNumber') : null; - let columnAttribute = t.hasAttribute && t.hasAttribute('column') ? t.getAttribute('column') : null; - if (lineNumberAttribute && columnAttribute) { - return this.createMouseTargetFromViewCursor(t, parseInt(lineNumberAttribute, 10), parseInt(columnAttribute, 10), mouseColumn); - } - } else { - // Hit testing completely failed... - let possibleLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(mouseVerticalOffset); - let maxColumn = this._context.model.getLineMaxColumn(possibleLineNumber); - return new MouseTarget(t, MouseTargetType.CONTENT_EMPTY, mouseColumn, new Position(possibleLineNumber, maxColumn)); - } - } - - // Is it the cursors layer? - if (ElementPath.isCursorsLayer(path)) { - return new MouseTarget(t, MouseTargetType.UNKNOWN); - } - - // Is it a child of the scrollable element? - if (ElementPath.isChildOfScrollableElement(path)) { - return this.createMouseTargetFromScrollbar(t, mouseVerticalOffset, mouseColumn); - } - - if (ElementPath.isChildOfMargin(path)) { - let offset = Math.abs(e.posx - e.editorPos.left); - - if (offset <= layoutInfo.glyphMarginWidth) { - // On the glyph margin - return this.createMouseTargetFromGlyphMargin(t, mouseVerticalOffset, mouseColumn); - } - offset -= layoutInfo.glyphMarginWidth; - - if (offset <= layoutInfo.lineNumbersWidth) { - // On the line numbers - return this.createMouseTargetFromLineNumbers(t, mouseVerticalOffset, mouseColumn); - } - offset -= layoutInfo.lineNumbersWidth; - - // On the line decorations - return this.createMouseTargetFromLinesDecorationsChild(t, mouseVerticalOffset, mouseColumn); - } - - if (ElementPath.isOverviewRuler(path)) { - return this.createMouseTargetFromScrollbar(t, mouseVerticalOffset, mouseColumn); - } - - 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.parentNode; - } - return null; - } - - /** - * Most probably WebKit browsers and Edge - */ - private _doHitTestWithCaretRangeFromPoint(e: EditorMouseEvent, mouseVerticalOffset: number): IHitTestResult { - - // 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 - 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); - - if (adjustedPosy <= e.editorPos.top) { - adjustedPosy = e.editorPos.top + 1; - } - if (adjustedPosy >= e.editorPos.top + this._context.configuration.editor.layoutInfo.height) { - adjustedPosy = e.editorPos.top + this._context.configuration.editor.layoutInfo.height - 1; - } - - let r = this._actualDoHitTestWithCaretRangeFromPoint(e.viewportx, adjustedPosy - dom.StandardWindow.scrollY); - 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) - return this._actualDoHitTestWithCaretRangeFromPoint(e.viewportx, e.viewporty); - } - - private _actualDoHitTestWithCaretRangeFromPoint(hitx: number, hity: number): IHitTestResult { - - let range: Range = (document).caretRangeFromPoint(hitx, hity); - - if (!range || !range.startContainer) { - return { - position: null, - hitTarget: null - }; - } - - // Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span - let startContainer = range.startContainer; - let hitTarget: HTMLElement; - - 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 ? (parent3).className : null; - - if (parent3ClassName === ClassNames.VIEW_LINE) { - let p = this._viewHelper.getPositionFromDOMInfo(parent1, range.startOffset); - return { - position: p, - hitTarget: null - }; - } else { - hitTarget = startContainer.parentNode; - } - } 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 ? (parent2).className : null; - - if (parent2ClassName === ClassNames.VIEW_LINE) { - let p = this._viewHelper.getPositionFromDOMInfo(startContainer, (startContainer).textContent.length); - return { - position: p, - hitTarget: null - }; - } else { - hitTarget = startContainer; - } - } - - return { - position: null, - hitTarget: hitTarget - }; - } - - /** - * Most probably Gecko - */ - private _doHitTestWithCaretPositionFromPoint(e: EditorMouseEvent): IHitTestResult { - let hitResult: { offsetNode: Node; offset: number; } = (document).caretPositionFromPoint(e.viewportx, e.viewporty); - - let range = document.createRange(); - range.setStart(hitResult.offsetNode, hitResult.offset); - range.collapse(true); - let resultPosition = this._viewHelper.getPositionFromDOMInfo(range.startContainer.parentNode, range.startOffset); - range.detach(); - - return { - position: resultPosition, - hitTarget: null - }; - } - - /** - * Most probably IE - */ - private _doHitTestWithMoveToPoint(e: EditorMouseEvent): IHitTestResult { - let resultPosition: Position = null; - let resultHitTarget: Element = null; - - let textRange: IETextRange = (document.body).createTextRange(); - try { - textRange.moveToPoint(e.viewportx, e.viewporty); - } catch (err) { - return { - position: null, - hitTarget: null - }; - } - - textRange.collapse(true); - - // Now, let's do our best to figure out what we hit :) - let parentElement = textRange ? textRange.parentElement() : null; - let parent1 = parentElement ? parentElement.parentNode : null; - let parent2 = parent1 ? parent1.parentNode : null; - - let parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (parent2).className : ''; - - if (parent2ClassName === ClassNames.VIEW_LINE) { - let rangeToContainEntireSpan = textRange.duplicate(); - rangeToContainEntireSpan.moveToElementText(parentElement); - rangeToContainEntireSpan.setEndPoint('EndToStart', textRange); - - resultPosition = this._viewHelper.getPositionFromDOMInfo(parentElement, rangeToContainEntireSpan.text.length); - // 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 ('-') - rangeToContainEntireSpan.moveToElementText(this._viewHelper.viewDomNode); - } 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 ('-') - textRange.moveToElementText(this._viewHelper.viewDomNode); - - return { - position: resultPosition, - hitTarget: resultHitTarget - }; - } - - private _doHitTest(e: EditorMouseEvent, mouseVerticalOffset: number): IHitTestResult { - // 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: - // - they have a proprietary method on ranges, moveToPoint: https://msdn.microsoft.com/en-us/library/ie/ms536632(v=vs.85).aspx - - // 24.08.2016: Edge has added WebKit's document.caretRangeFromPoint, but it is quite buggy - // - when hit testing the cursor it returns the first or the last line in the viewport - // - it inconsistently hits text nodes or span nodes, while WebKit only hits text nodes - // - when toggling render whitespace on, and hit testing in the empty content after a line, it always hits offset 0 of the first span of the line - - // Thank you browsers for making this so 'easy' :) - - if ((document).caretRangeFromPoint) { - - return this._doHitTestWithCaretRangeFromPoint(e, mouseVerticalOffset); - - } else if ((document).caretPositionFromPoint) { - - return this._doHitTestWithCaretPositionFromPoint(e); - - } else if ((document.body).createTextRange) { - - return this._doHitTestWithMoveToPoint(e); - - } - - return { - position: null, - hitTarget: null - }; - } - - private _getZoneAtCoord(mouseVerticalOffset: number): IViewZoneData { + public getZoneAtCoord(mouseVerticalOffset: number): IViewZoneData { // The target is either a view zone or the empty space after the last view-line let viewZoneWhitespace = this._viewHelper.getWhitespaceAtVerticalOffset(mouseVerticalOffset); @@ -676,7 +290,7 @@ export class MouseTargetFactory { return null; } - private _getFullLineRangeAtCoord(mouseVerticalOffset: number): { range: EditorRange; isAfterLines: boolean; } { + public getFullLineRangeAtCoord(mouseVerticalOffset: number): { range: EditorRange; isAfterLines: boolean; } { if (this._viewHelper.isAfterLines(mouseVerticalOffset)) { // Below the last line let lineNumber = this._context.model.getLineCount(); @@ -695,8 +309,436 @@ export class MouseTargetFactory { }; } - public getMouseColumn(layoutInfo: EditorLayoutInfo, e: EditorMouseEvent): number { - let mouseContentHorizontalOffset = this._viewHelper.getScrollLeft() + (e.posx - e.editorPos.left) - layoutInfo.contentLeft; + public getLineNumberAtVerticalOffset(mouseVerticalOffset: number): number { + return this._viewHelper.getLineNumberAtVerticalOffset(mouseVerticalOffset); + } + + public getVerticalOffsetForLineNumber(lineNumber: number): number { + return this._viewHelper.getVerticalOffsetForLineNumber(lineNumber); + } + + public findAttribute(element: Element, attr: string): string { + return HitTestContext._findAttribute(element, attr, this._viewHelper.viewDomNode); + } + + private static _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.parentNode; + } + return null; + } + + public getLineWidth(lineNumber: number): number { + return this._viewHelper.getLineWidth(lineNumber); + } + + public visibleRangeForPosition2(lineNumber: number, column: number) { + return this._viewHelper.visibleRangeForPosition2(lineNumber, column); + } + + public getPositionFromDOMInfo(spanNode: HTMLElement, offset: number): Position { + return this._viewHelper.getPositionFromDOMInfo(spanNode, offset); + } +} + +export interface ISimplifiedEditorMouseEvent { + readonly page: PageCoordinates; + readonly editorPos: EditorPagePosition; + readonly target: HTMLElement; +} + +export class MouseTargetFactory { + + private _context: ViewContext; + private _viewHelper: IPointerHandlerHelper; + + constructor(context: ViewContext, viewHelper: IPointerHandlerHelper) { + this._context = context; + this._viewHelper = viewHelper; + } + + public mouseTargetIsWidget(e: EditorMouseEvent): boolean { + let t = e.target; + let path = PartFingerprints.collect(t, this._viewHelper.viewDomNode); + + // Is it a content widget? + if (ElementPath.isChildOfContentWidgets(path) || ElementPath.isChildOfOverflowingContentWidgets(path)) { + return true; + } + + // Is it an overlay widget? + if (ElementPath.isChildOfOverlayWidgets(path)) { + return true; + } + + return false; + } + + public createMouseTarget(layoutInfo: EditorLayoutInfo, lastViewCursorsRenderData: IViewCursorRenderData[], e: ISimplifiedEditorMouseEvent, testEventTarget: boolean): IMouseTarget { + const ctx = new HitTestContext(this._context, this._viewHelper, layoutInfo); + try { + let r = this._unsafeCreateMouseTarget(ctx, layoutInfo, lastViewCursorsRenderData, e, testEventTarget); + // console.log(r.toString()); + return r; + } catch (e) { + return MouseTargetFactory.createMouseTargetFromUnknownTarget(ctx, e.target); + } + } + + private _unsafeCreateMouseTarget(ctx: HitTestContext, layoutInfo: EditorLayoutInfo, lastViewCursorsRenderData: IViewCursorRenderData[], e: ISimplifiedEditorMouseEvent, testEventTarget: boolean): IMouseTarget { + + let mouseVerticalOffset = Math.max(0, this._viewHelper.getScrollTop() + e.page.y - e.editorPos.y); + let mouseContentHorizontalOffset = this._viewHelper.getScrollLeft() + e.page.x - e.editorPos.x - layoutInfo.contentLeft; + let mouseColumn = this._getMouseColumn(mouseContentHorizontalOffset); + + let t = e.target; + + // Edge has a bug when hit-testing the exact position of a cursor, + // instead of returning the correct dom node, it returns the + // first or last rendered view line dom node, therefore help it out + // and first check if we are on top of a cursor + for (let i = 0, len = lastViewCursorsRenderData.length; i < len; i++) { + let d = lastViewCursorsRenderData[i]; + + if ( + d.contentLeft <= mouseContentHorizontalOffset + && mouseContentHorizontalOffset <= d.contentLeft + d.width + && d.contentTop <= mouseVerticalOffset + && mouseVerticalOffset <= d.contentTop + d.height + ) { + return MouseTargetFactory.createMouseTargetFromViewCursor(t, d.position.lineNumber, d.position.column, mouseColumn); + } + } + + let path = PartFingerprints.collect(t, this._viewHelper.viewDomNode); + + // Is it a content widget? + if (ElementPath.isChildOfContentWidgets(path) || ElementPath.isChildOfOverflowingContentWidgets(path)) { + return MouseTargetFactory.createMouseTargetFromContentWidgetsChild(ctx, t, mouseColumn); + } + + // Is it an overlay widget? + if (ElementPath.isChildOfOverlayWidgets(path)) { + return MouseTargetFactory.createMouseTargetFromOverlayWidgetsChild(ctx, t, mouseColumn); + } + + // Is it a cursor ? + let lineNumberAttribute = t.hasAttribute && t.hasAttribute('lineNumber') ? t.getAttribute('lineNumber') : null; + let columnAttribute = t.hasAttribute && t.hasAttribute('column') ? t.getAttribute('column') : null; + if (lineNumberAttribute && columnAttribute) { + return MouseTargetFactory.createMouseTargetFromViewCursor(t, parseInt(lineNumberAttribute, 10), parseInt(columnAttribute, 10), mouseColumn); + } + + // Is it the textarea cover? + if (ElementPath.isTextAreaCover(path)) { + if (this._context.configuration.editor.viewInfo.glyphMargin) { + return MouseTargetFactory.createMouseTargetFromGlyphMargin(ctx, t, mouseVerticalOffset, mouseColumn); + } else if (this._context.configuration.editor.viewInfo.renderLineNumbers) { + return MouseTargetFactory.createMouseTargetFromLineNumbers(ctx, t, mouseVerticalOffset, mouseColumn); + } else { + return MouseTargetFactory.createMouseTargetFromLinesDecorationsChild(ctx, t, mouseVerticalOffset, mouseColumn); + } + } + + // Is it the textarea? + if (ElementPath.isTextArea(path)) { + return new MouseTarget(t, MouseTargetType.TEXTAREA); + } + + // Is it a view zone? + if (ElementPath.isChildOfViewZones(path)) { + // Check if it is at a view zone + let viewZoneData = ctx.getZoneAtCoord(mouseVerticalOffset); + if (viewZoneData) { + return new MouseTarget(t, MouseTargetType.CONTENT_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData); + } + return MouseTargetFactory.createMouseTargetFromUnknownTarget(ctx, t); + } + + // Is it the view lines container? + if (ElementPath.isViewLines(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 + if (this._viewHelper.isAfterLines(mouseVerticalOffset)) { + return MouseTargetFactory.createMouseTargetFromViewLines(ctx, t, mouseVerticalOffset, mouseColumn); + } + + // Check if it is at a view zone + let viewZoneData = ctx.getZoneAtCoord(mouseVerticalOffset); + if (viewZoneData) { + return new MouseTarget(t, MouseTargetType.CONTENT_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData); + } + + // Check if it hits a position + let hitTestResult = MouseTargetFactory._doHitTest(ctx, e, mouseVerticalOffset); + if (hitTestResult.position) { + return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, t, hitTestResult.position.lineNumber, hitTestResult.position.column, mouseContentHorizontalOffset, mouseColumn); + } + + // Fall back to view lines + return MouseTargetFactory.createMouseTargetFromViewLines(ctx, t, mouseVerticalOffset, mouseColumn); + } + + // Is it a child of the view lines container? + if (!testEventTarget || ElementPath.isChildOfViewLines(path)) { + let hitTestResult = MouseTargetFactory._doHitTest(ctx, e, mouseVerticalOffset); + if (hitTestResult.position) { + return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, t, hitTestResult.position.lineNumber, hitTestResult.position.column, mouseContentHorizontalOffset, mouseColumn); + } else if (hitTestResult.hitTarget) { + t = hitTestResult.hitTarget; + path = PartFingerprints.collect(t, this._viewHelper.viewDomNode); + + // TODO@Alex: try again with this different target, but guard against recursion. + // Is it a cursor ? + let lineNumberAttribute = t.hasAttribute && t.hasAttribute('lineNumber') ? t.getAttribute('lineNumber') : null; + let columnAttribute = t.hasAttribute && t.hasAttribute('column') ? t.getAttribute('column') : null; + if (lineNumberAttribute && columnAttribute) { + return MouseTargetFactory.createMouseTargetFromViewCursor(t, parseInt(lineNumberAttribute, 10), parseInt(columnAttribute, 10), mouseColumn); + } + } else { + // Hit testing completely failed... + let possibleLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(mouseVerticalOffset); + let maxColumn = this._context.model.getLineMaxColumn(possibleLineNumber); + return new MouseTarget(t, MouseTargetType.CONTENT_EMPTY, mouseColumn, new Position(possibleLineNumber, maxColumn)); + } + } + + // Is it the cursors layer? + if (ElementPath.isCursorsLayer(path)) { + return new MouseTarget(t, MouseTargetType.UNKNOWN); + } + + // Is it a child of the scrollable element? + if (ElementPath.isChildOfScrollableElement(path)) { + return MouseTargetFactory.createMouseTargetFromScrollbar(ctx, t, mouseVerticalOffset, mouseColumn); + } + + if (ElementPath.isChildOfMargin(path)) { + let offset = Math.abs(e.page.x - e.editorPos.x); + + if (offset <= ctx.layoutInfo.glyphMarginWidth) { + // On the glyph margin + return MouseTargetFactory.createMouseTargetFromGlyphMargin(ctx, t, mouseVerticalOffset, mouseColumn); + } + offset -= ctx.layoutInfo.glyphMarginWidth; + + if (offset <= ctx.layoutInfo.lineNumbersWidth) { + // On the line numbers + return MouseTargetFactory.createMouseTargetFromLineNumbers(ctx, t, mouseVerticalOffset, mouseColumn); + } + offset -= ctx.layoutInfo.lineNumbersWidth; + + // On the line decorations + return MouseTargetFactory.createMouseTargetFromLinesDecorationsChild(ctx, t, mouseVerticalOffset, mouseColumn); + } + + if (ElementPath.isOverviewRuler(path)) { + return MouseTargetFactory.createMouseTargetFromScrollbar(ctx, t, mouseVerticalOffset, mouseColumn); + } + + return MouseTargetFactory.createMouseTargetFromUnknownTarget(ctx, t); + } + + /** + * Most probably WebKit browsers and Edge + */ + private static _doHitTestWithCaretRangeFromPoint(ctx: HitTestContext, e: ISimplifiedEditorMouseEvent, mouseVerticalOffset: number): IHitTestResult { + + // 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 + let lineNumber = ctx.getLineNumberAtVerticalOffset(mouseVerticalOffset); + let lineVerticalOffset = ctx.getVerticalOffsetForLineNumber(lineNumber); + let lineCenteredVerticalOffset = lineVerticalOffset + Math.floor(ctx.lineHeight / 2); + let adjustedPageY = e.page.y + (lineCenteredVerticalOffset - mouseVerticalOffset); + + if (adjustedPageY <= e.editorPos.y) { + adjustedPageY = e.editorPos.y + 1; + } + if (adjustedPageY >= e.editorPos.y + ctx.layoutInfo.height) { + adjustedPageY = e.editorPos.y + ctx.layoutInfo.height - 1; + } + + let adjustedPage = new PageCoordinates(e.page.x, adjustedPageY); + + let r = this._actualDoHitTestWithCaretRangeFromPoint(ctx, adjustedPage.toClientCoordinates()); + 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) + return this._actualDoHitTestWithCaretRangeFromPoint(ctx, e.page.toClientCoordinates()); + } + + private static _actualDoHitTestWithCaretRangeFromPoint(ctx: HitTestContext, coords: ClientCoordinates): IHitTestResult { + + let range: Range = document.caretRangeFromPoint(coords.clientX, coords.clientY); + + if (!range || !range.startContainer) { + return { + position: null, + hitTarget: null + }; + } + + // Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span + let startContainer = range.startContainer; + let hitTarget: HTMLElement; + + 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 ? (parent3).className : null; + + if (parent3ClassName === ClassNames.VIEW_LINE) { + let p = ctx.getPositionFromDOMInfo(parent1, range.startOffset); + return { + position: p, + hitTarget: null + }; + } else { + hitTarget = startContainer.parentNode; + } + } 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 ? (parent2).className : null; + + if (parent2ClassName === ClassNames.VIEW_LINE) { + let p = ctx.getPositionFromDOMInfo(startContainer, (startContainer).textContent.length); + return { + position: p, + hitTarget: null + }; + } else { + hitTarget = startContainer; + } + } + + return { + position: null, + hitTarget: hitTarget + }; + } + + /** + * Most probably Gecko + */ + private static _doHitTestWithCaretPositionFromPoint(ctx: HitTestContext, coords: ClientCoordinates): IHitTestResult { + let hitResult: { offsetNode: Node; offset: number; } = (document).caretPositionFromPoint(coords.clientX, coords.clientY); + + let range = document.createRange(); + range.setStart(hitResult.offsetNode, hitResult.offset); + range.collapse(true); + let resultPosition = ctx.getPositionFromDOMInfo(range.startContainer.parentNode, range.startOffset); + range.detach(); + + return { + position: resultPosition, + hitTarget: null + }; + } + + /** + * Most probably IE + */ + private static _doHitTestWithMoveToPoint(ctx: HitTestContext, coords: ClientCoordinates): IHitTestResult { + let resultPosition: Position = null; + let resultHitTarget: Element = null; + + let textRange: IETextRange = (document.body).createTextRange(); + try { + textRange.moveToPoint(coords.clientX, coords.clientY); + } catch (err) { + return { + position: null, + hitTarget: null + }; + } + + textRange.collapse(true); + + // Now, let's do our best to figure out what we hit :) + let parentElement = textRange ? textRange.parentElement() : null; + let parent1 = parentElement ? parentElement.parentNode : null; + let parent2 = parent1 ? parent1.parentNode : null; + + let parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (parent2).className : ''; + + if (parent2ClassName === ClassNames.VIEW_LINE) { + let rangeToContainEntireSpan = textRange.duplicate(); + rangeToContainEntireSpan.moveToElementText(parentElement); + rangeToContainEntireSpan.setEndPoint('EndToStart', textRange); + + resultPosition = ctx.getPositionFromDOMInfo(parentElement, rangeToContainEntireSpan.text.length); + // 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 ('-') + rangeToContainEntireSpan.moveToElementText(ctx.viewDomNode); + } 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 ('-') + textRange.moveToElementText(ctx.viewDomNode); + + return { + position: resultPosition, + hitTarget: resultHitTarget + }; + } + + private static _doHitTest(ctx: HitTestContext, e: ISimplifiedEditorMouseEvent, mouseVerticalOffset: number): IHitTestResult { + // 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: + // - they have a proprietary method on ranges, moveToPoint: https://msdn.microsoft.com/en-us/library/ie/ms536632(v=vs.85).aspx + + // 24.08.2016: Edge has added WebKit's document.caretRangeFromPoint, but it is quite buggy + // - when hit testing the cursor it returns the first or the last line in the viewport + // - it inconsistently hits text nodes or span nodes, while WebKit only hits text nodes + // - when toggling render whitespace on, and hit testing in the empty content after a line, it always hits offset 0 of the first span of the line + + // Thank you browsers for making this so 'easy' :) + + if (document.caretRangeFromPoint) { + + return this._doHitTestWithCaretRangeFromPoint(ctx, e, mouseVerticalOffset); + + } else if ((document).caretPositionFromPoint) { + + return this._doHitTestWithCaretPositionFromPoint(ctx, e.page.toClientCoordinates()); + + } else if ((document.body).createTextRange) { + + return this._doHitTestWithMoveToPoint(ctx, e.page.toClientCoordinates()); + + } + + return { + position: null, + hitTarget: null + }; + } + + public getMouseColumn(layoutInfo: EditorLayoutInfo, e: ISimplifiedEditorMouseEvent): number { + let mouseContentHorizontalOffset = this._viewHelper.getScrollLeft() + e.page.x - e.editorPos.x - layoutInfo.contentLeft; return this._getMouseColumn(mouseContentHorizontalOffset); } @@ -709,31 +751,31 @@ export class MouseTargetFactory { return (chars + 1); } - private createMouseTargetFromViewCursor(target: Element, lineNumber: number, column: number, mouseColumn: number): MouseTarget { + private static 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 { + private static createMouseTargetFromViewLines(ctx: HitTestContext, target: Element, mouseVerticalOffset: number, mouseColumn: number): MouseTarget { // This most likely indicates it happened after the last view-line - let lineCount = this._context.model.getLineCount(); - let maxLineColumn = this._context.model.getLineMaxColumn(lineCount); + let lineCount = ctx.model.getLineCount(); + let maxLineColumn = ctx.model.getLineMaxColumn(lineCount); return new MouseTarget(target, MouseTargetType.CONTENT_EMPTY, mouseColumn, new Position(lineCount, maxLineColumn)); } - private createMouseTargetFromHitTestPosition(target: Element, lineNumber: number, column: number, mouseHorizontalOffset: number, mouseColumn: number): MouseTarget { + private static createMouseTargetFromHitTestPosition(ctx: HitTestContext, target: Element, lineNumber: number, column: number, mouseHorizontalOffset: number, mouseColumn: number): MouseTarget { let pos = new Position(lineNumber, column); - let lineWidth = this._viewHelper.getLineWidth(lineNumber); + let lineWidth = ctx.getLineWidth(lineNumber); if (mouseHorizontalOffset > lineWidth) { if (browser.isEdge && pos.column === 1) { // See https://github.com/Microsoft/vscode/issues/10875 - return new MouseTarget(target, MouseTargetType.CONTENT_EMPTY, mouseColumn, new Position(lineNumber, this._context.model.getLineMaxColumn(lineNumber))); + return new MouseTarget(target, MouseTargetType.CONTENT_EMPTY, mouseColumn, new Position(lineNumber, ctx.model.getLineMaxColumn(lineNumber))); } return new MouseTarget(target, MouseTargetType.CONTENT_EMPTY, mouseColumn, pos); } - let visibleRange = this._viewHelper.visibleRangeForPosition2(lineNumber, column); + let visibleRange = ctx.visibleRangeForPosition2(lineNumber, column); if (!visibleRange) { return new MouseTarget(target, MouseTargetType.UNKNOWN, mouseColumn, pos); @@ -757,9 +799,9 @@ export class MouseTargetFactory { } } - let lineMaxColumn = this._context.model.getLineMaxColumn(lineNumber); + let lineMaxColumn = ctx.model.getLineMaxColumn(lineNumber); if (column < lineMaxColumn) { - let nextColumnVisibleRange = this._viewHelper.visibleRangeForPosition2(lineNumber, column + 1); + let nextColumnVisibleRange = ctx.visibleRangeForPosition2(lineNumber, column + 1); if (nextColumnVisibleRange) { let nextColumnHorizontalOffset = nextColumnVisibleRange.left; mouseIsBetween = false; @@ -775,8 +817,8 @@ export class MouseTargetFactory { return new MouseTarget(target, MouseTargetType.CONTENT_TEXT, mouseColumn, pos); } - private createMouseTargetFromContentWidgetsChild(target: Element, mouseColumn: number): MouseTarget { - let widgetId = this._findAttribute(target, 'widgetId', this._viewHelper.viewDomNode); + private static createMouseTargetFromContentWidgetsChild(ctx: HitTestContext, target: Element, mouseColumn: number): MouseTarget { + let widgetId = ctx.findAttribute(target, 'widgetId'); if (widgetId) { return new MouseTarget(target, MouseTargetType.CONTENT_WIDGET, mouseColumn, null, null, widgetId); @@ -785,8 +827,8 @@ export class MouseTargetFactory { } } - private createMouseTargetFromOverlayWidgetsChild(target: Element, mouseColumn: number): MouseTarget { - let widgetId = this._findAttribute(target, 'widgetId', this._viewHelper.viewDomNode); + private static createMouseTargetFromOverlayWidgetsChild(ctx: HitTestContext, target: Element, mouseColumn: number): MouseTarget { + let widgetId = ctx.findAttribute(target, 'widgetId'); if (widgetId) { return new MouseTarget(target, MouseTargetType.OVERLAY_WIDGET, mouseColumn, null, null, widgetId); @@ -795,51 +837,47 @@ export class MouseTargetFactory { } } - private createMouseTargetFromLinesDecorationsChild(target: Element, mouseVerticalOffset: number, mouseColumn: number): MouseTarget { - let viewZoneData = this._getZoneAtCoord(mouseVerticalOffset); + private static createMouseTargetFromLinesDecorationsChild(ctx: HitTestContext, target: Element, mouseVerticalOffset: number, mouseColumn: number): MouseTarget { + let viewZoneData = ctx.getZoneAtCoord(mouseVerticalOffset); if (viewZoneData) { return new MouseTarget(target, MouseTargetType.GUTTER_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData); } - let res = this._getFullLineRangeAtCoord(mouseVerticalOffset); + let res = ctx.getFullLineRangeAtCoord(mouseVerticalOffset); return new MouseTarget(target, MouseTargetType.GUTTER_LINE_DECORATIONS, mouseColumn, new Position(res.range.startLineNumber, res.range.startColumn), res.range, res.isAfterLines); } - private createMouseTargetFromLineNumbers(target: Element, mouseVerticalOffset: number, mouseColumn: number): MouseTarget { - let viewZoneData = this._getZoneAtCoord(mouseVerticalOffset); + private static createMouseTargetFromLineNumbers(ctx: HitTestContext, target: Element, mouseVerticalOffset: number, mouseColumn: number): MouseTarget { + let viewZoneData = ctx.getZoneAtCoord(mouseVerticalOffset); if (viewZoneData) { return new MouseTarget(target, MouseTargetType.GUTTER_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData); } - let res = this._getFullLineRangeAtCoord(mouseVerticalOffset); + let res = ctx.getFullLineRangeAtCoord(mouseVerticalOffset); return new MouseTarget(target, MouseTargetType.GUTTER_LINE_NUMBERS, mouseColumn, new Position(res.range.startLineNumber, res.range.startColumn), res.range, res.isAfterLines); } - private createMouseTargetFromGlyphMargin(target: Element, mouseVerticalOffset: number, mouseColumn: number): MouseTarget { - let viewZoneData = this._getZoneAtCoord(mouseVerticalOffset); + private static createMouseTargetFromGlyphMargin(ctx: HitTestContext, target: Element, mouseVerticalOffset: number, mouseColumn: number): MouseTarget { + let viewZoneData = ctx.getZoneAtCoord(mouseVerticalOffset); if (viewZoneData) { return new MouseTarget(target, MouseTargetType.GUTTER_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData); } - let res = this._getFullLineRangeAtCoord(mouseVerticalOffset); + let res = ctx.getFullLineRangeAtCoord(mouseVerticalOffset); return new MouseTarget(target, MouseTargetType.GUTTER_GLYPH_MARGIN, mouseColumn, new Position(res.range.startLineNumber, res.range.startColumn), res.range, res.isAfterLines); } - private createMouseTargetFromScrollbar(target: Element, mouseVerticalOffset: number, mouseColumn: number): MouseTarget { - let possibleLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(mouseVerticalOffset); - let maxColumn = this._context.model.getLineMaxColumn(possibleLineNumber); + private static createMouseTargetFromScrollbar(ctx: HitTestContext, target: Element, mouseVerticalOffset: number, mouseColumn: number): MouseTarget { + let possibleLineNumber = ctx.getLineNumberAtVerticalOffset(mouseVerticalOffset); + let maxColumn = ctx.model.getLineMaxColumn(possibleLineNumber); return new MouseTarget(target, MouseTargetType.SCROLLBAR, mouseColumn, new Position(possibleLineNumber, maxColumn)); } - private createMouseTargetFromUnknownTarget(target: Element): MouseTarget { - let isInView = this._isChild(target, this._viewHelper.viewDomNode, this._viewHelper.viewDomNode); - let widgetId = null; - if (isInView) { - widgetId = this._findAttribute(target, 'widgetId', this._viewHelper.viewDomNode); - } + private static createMouseTargetFromUnknownTarget(ctx: HitTestContext, target: Element): MouseTarget { + let widgetId = ctx.findAttribute(target, 'widgetId'); if (widgetId) { - return new MouseTarget(target, MouseTargetType.OVERLAY_WIDGET, null, null, widgetId); + return new MouseTarget(target, MouseTargetType.OVERLAY_WIDGET, 0, null, null, widgetId); } else { return new MouseTarget(target, MouseTargetType.UNKNOWN); } diff --git a/src/vs/editor/browser/editorDom.ts b/src/vs/editor/browser/editorDom.ts index 2a227addb95..f491aacda9b 100644 --- a/src/vs/editor/browser/editorDom.ts +++ b/src/vs/editor/browser/editorDom.ts @@ -9,27 +9,81 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import * as dom from 'vs/base/browser/dom'; import { GlobalMouseMoveMonitor } from 'vs/base/browser/globalMouseMoveMonitor'; +/** + * Coordinates relative to the whole document (e.g. mouse event's pageX and pageY) + */ +export class PageCoordinates { + _pageCoordinatesBrand: void; + public readonly x: number; + public readonly y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + + public toClientCoordinates(): ClientCoordinates { + return new ClientCoordinates(this.x - dom.StandardWindow.scrollX, this.y - dom.StandardWindow.scrollY); + } +} + +/** + * Coordinates within the application's client area (i.e. origin is document's scroll position). + * + * For example, clicking in the top-left corner of the client area will + * always result in a mouse event with a client.x value of 0, regardless + * of whether the page is scrolled horizontally. + */ +export class ClientCoordinates { + _clientCoordinatesBrand: void; + + public readonly clientX: number; + public readonly clientY: number; + + constructor(clientX: number, clientY: number) { + this.clientX = clientX; + this.clientY = clientY; + } +} + +/** + * The position of the editor in the page. + */ +export class EditorPagePosition { + _editorPagePositionBrand: void; + + public readonly x: number; + public readonly y: number; + public readonly width: number; + public readonly height: number; + + constructor(x: number, y: number, width: number, height: number) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } +} + export class EditorMouseEvent extends StandardMouseEvent { _editorMouseEventBrand: void; - editorPos: dom.IDomNodePagePosition; + /** + * Coordinates relative to the whole document. + */ + public readonly page: PageCoordinates; /** - * The horizontal position of the cursor relative to the viewport (i.e. scrolled). + * Editor's coordinates relative to the whole document. */ - viewportx: number; - - /** - * The vertical position of the cursor relative to the viewport (i.e. scrolled). - */ - viewporty: number; + public readonly editorPos: EditorPagePosition; constructor(e: MouseEvent, editorViewDomNode: HTMLElement) { super(e); - this.editorPos = dom.getDomNodePagePosition(editorViewDomNode); + this.page = new PageCoordinates(this.posx, this.posy); - this.viewportx = this.posx - dom.StandardWindow.scrollX; - this.viewporty = this.posy - dom.StandardWindow.scrollY; + let editorPos = dom.getDomNodePagePosition(editorViewDomNode); + this.editorPos = new EditorPagePosition(editorPos.left, editorPos.top, editorPos.width, editorPos.height); } }