123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819 |
- /*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
- import { PageCoordinates } from '../editorDom.js';
- import { PartFingerprints } from '../view/viewPart.js';
- import { ViewLine } from '../viewParts/lines/viewLine.js';
- import { Position } from '../../common/core/position.js';
- import { Range as EditorRange } from '../../common/core/range.js';
- import { CursorColumns } from '../../common/controller/cursorCommon.js';
- import * as dom from '../../../base/browser/dom.js';
- import { AtomicTabMoveOperations } from '../../common/controller/cursorAtomicMoveOperations.js';
- class UnknownHitTestResult {
- constructor(hitTarget = null) {
- this.hitTarget = hitTarget;
- this.type = 0 /* Unknown */;
- }
- }
- class ContentHitTestResult {
- constructor(position, spanNode, injectedText) {
- this.position = position;
- this.spanNode = spanNode;
- this.injectedText = injectedText;
- this.type = 1 /* Content */;
- }
- }
- var HitTestResult;
- (function (HitTestResult) {
- function createFromDOMInfo(ctx, spanNode, offset) {
- const position = ctx.getPositionFromDOMInfo(spanNode, offset);
- if (position) {
- return new ContentHitTestResult(position, spanNode, null);
- }
- return new UnknownHitTestResult(spanNode);
- }
- HitTestResult.createFromDOMInfo = createFromDOMInfo;
- })(HitTestResult || (HitTestResult = {}));
- export class PointerHandlerLastRenderData {
- constructor(lastViewCursorsRenderData, lastTextareaPosition) {
- this.lastViewCursorsRenderData = lastViewCursorsRenderData;
- this.lastTextareaPosition = lastTextareaPosition;
- }
- }
- export class MouseTarget {
- constructor(element, type, mouseColumn = 0, position = null, range = null, detail = null) {
- this.element = element;
- this.type = type;
- this.mouseColumn = mouseColumn;
- this.position = position;
- if (!range && position) {
- range = new EditorRange(position.lineNumber, position.column, position.lineNumber, position.column);
- }
- this.range = range;
- this.detail = detail;
- }
- static _typeToString(type) {
- if (type === 1 /* TEXTAREA */) {
- return 'TEXTAREA';
- }
- if (type === 2 /* GUTTER_GLYPH_MARGIN */) {
- return 'GUTTER_GLYPH_MARGIN';
- }
- if (type === 3 /* GUTTER_LINE_NUMBERS */) {
- return 'GUTTER_LINE_NUMBERS';
- }
- if (type === 4 /* GUTTER_LINE_DECORATIONS */) {
- return 'GUTTER_LINE_DECORATIONS';
- }
- if (type === 5 /* GUTTER_VIEW_ZONE */) {
- return 'GUTTER_VIEW_ZONE';
- }
- if (type === 6 /* CONTENT_TEXT */) {
- return 'CONTENT_TEXT';
- }
- if (type === 7 /* CONTENT_EMPTY */) {
- return 'CONTENT_EMPTY';
- }
- if (type === 8 /* CONTENT_VIEW_ZONE */) {
- return 'CONTENT_VIEW_ZONE';
- }
- if (type === 9 /* CONTENT_WIDGET */) {
- return 'CONTENT_WIDGET';
- }
- if (type === 10 /* OVERVIEW_RULER */) {
- return 'OVERVIEW_RULER';
- }
- if (type === 11 /* SCROLLBAR */) {
- return 'SCROLLBAR';
- }
- if (type === 12 /* OVERLAY_WIDGET */) {
- return 'OVERLAY_WIDGET';
- }
- return 'UNKNOWN';
- }
- static toString(target) {
- return this._typeToString(target.type) + ': ' + target.position + ' - ' + target.range + ' - ' + target.detail;
- }
- toString() {
- return MouseTarget.toString(this);
- }
- }
- class ElementPath {
- static isTextArea(path) {
- return (path.length === 2
- && path[0] === 3 /* OverflowGuard */
- && path[1] === 6 /* TextArea */);
- }
- static isChildOfViewLines(path) {
- return (path.length >= 4
- && path[0] === 3 /* OverflowGuard */
- && path[3] === 7 /* ViewLines */);
- }
- static isStrictChildOfViewLines(path) {
- return (path.length > 4
- && path[0] === 3 /* OverflowGuard */
- && path[3] === 7 /* ViewLines */);
- }
- static isChildOfScrollableElement(path) {
- return (path.length >= 2
- && path[0] === 3 /* OverflowGuard */
- && path[1] === 5 /* ScrollableElement */);
- }
- static isChildOfMinimap(path) {
- return (path.length >= 2
- && path[0] === 3 /* OverflowGuard */
- && path[1] === 8 /* Minimap */);
- }
- static isChildOfContentWidgets(path) {
- return (path.length >= 4
- && path[0] === 3 /* OverflowGuard */
- && path[3] === 1 /* ContentWidgets */);
- }
- static isChildOfOverflowingContentWidgets(path) {
- return (path.length >= 1
- && path[0] === 2 /* OverflowingContentWidgets */);
- }
- static isChildOfOverlayWidgets(path) {
- return (path.length >= 2
- && path[0] === 3 /* OverflowGuard */
- && path[1] === 4 /* OverlayWidgets */);
- }
- }
- export class HitTestContext {
- constructor(context, viewHelper, lastRenderData) {
- this.model = context.model;
- const options = context.configuration.options;
- this.layoutInfo = options.get(130 /* layoutInfo */);
- this.viewDomNode = viewHelper.viewDomNode;
- this.lineHeight = options.get(58 /* lineHeight */);
- this.stickyTabStops = options.get(103 /* stickyTabStops */);
- this.typicalHalfwidthCharacterWidth = options.get(43 /* fontInfo */).typicalHalfwidthCharacterWidth;
- this.lastRenderData = lastRenderData;
- this._context = context;
- this._viewHelper = viewHelper;
- }
- getZoneAtCoord(mouseVerticalOffset) {
- return HitTestContext.getZoneAtCoord(this._context, mouseVerticalOffset);
- }
- static getZoneAtCoord(context, mouseVerticalOffset) {
- // The target is either a view zone or the empty space after the last view-line
- const viewZoneWhitespace = context.viewLayout.getWhitespaceAtVerticalOffset(mouseVerticalOffset);
- if (viewZoneWhitespace) {
- const viewZoneMiddle = viewZoneWhitespace.verticalOffset + viewZoneWhitespace.height / 2;
- const lineCount = context.model.getLineCount();
- let positionBefore = null;
- let position;
- let positionAfter = null;
- 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
- positionBefore = new Position(viewZoneWhitespace.afterLineNumber, context.model.getLineMaxColumn(viewZoneWhitespace.afterLineNumber));
- }
- 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;
- }
- getFullLineRangeAtCoord(mouseVerticalOffset) {
- if (this._context.viewLayout.isAfterLines(mouseVerticalOffset)) {
- // Below the last line
- const lineNumber = this._context.model.getLineCount();
- const maxLineColumn = this._context.model.getLineMaxColumn(lineNumber);
- return {
- range: new EditorRange(lineNumber, maxLineColumn, lineNumber, maxLineColumn),
- isAfterLines: true
- };
- }
- const lineNumber = this._context.viewLayout.getLineNumberAtVerticalOffset(mouseVerticalOffset);
- const maxLineColumn = this._context.model.getLineMaxColumn(lineNumber);
- return {
- range: new EditorRange(lineNumber, 1, lineNumber, maxLineColumn),
- isAfterLines: false
- };
- }
- getLineNumberAtVerticalOffset(mouseVerticalOffset) {
- return this._context.viewLayout.getLineNumberAtVerticalOffset(mouseVerticalOffset);
- }
- isAfterLines(mouseVerticalOffset) {
- return this._context.viewLayout.isAfterLines(mouseVerticalOffset);
- }
- isInTopPadding(mouseVerticalOffset) {
- return this._context.viewLayout.isInTopPadding(mouseVerticalOffset);
- }
- isInBottomPadding(mouseVerticalOffset) {
- return this._context.viewLayout.isInBottomPadding(mouseVerticalOffset);
- }
- getVerticalOffsetForLineNumber(lineNumber) {
- return this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber);
- }
- findAttribute(element, attr) {
- return HitTestContext._findAttribute(element, attr, this._viewHelper.viewDomNode);
- }
- static _findAttribute(element, attr, stopAt) {
- 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;
- }
- getLineWidth(lineNumber) {
- return this._viewHelper.getLineWidth(lineNumber);
- }
- visibleRangeForPosition(lineNumber, column) {
- return this._viewHelper.visibleRangeForPosition(lineNumber, column);
- }
- getPositionFromDOMInfo(spanNode, offset) {
- return this._viewHelper.getPositionFromDOMInfo(spanNode, offset);
- }
- getCurrentScrollTop() {
- return this._context.viewLayout.getCurrentScrollTop();
- }
- getCurrentScrollLeft() {
- return this._context.viewLayout.getCurrentScrollLeft();
- }
- }
- class BareHitTestRequest {
- constructor(ctx, editorPos, pos) {
- this.editorPos = editorPos;
- this.pos = pos;
- this.mouseVerticalOffset = Math.max(0, ctx.getCurrentScrollTop() + pos.y - editorPos.y);
- this.mouseContentHorizontalOffset = ctx.getCurrentScrollLeft() + pos.x - editorPos.x - ctx.layoutInfo.contentLeft;
- this.isInMarginArea = (pos.x - editorPos.x < ctx.layoutInfo.contentLeft && pos.x - editorPos.x >= ctx.layoutInfo.glyphMarginLeft);
- this.isInContentArea = !this.isInMarginArea;
- this.mouseColumn = Math.max(0, MouseTargetFactory._getMouseColumn(this.mouseContentHorizontalOffset, ctx.typicalHalfwidthCharacterWidth));
- }
- }
- class HitTestRequest extends BareHitTestRequest {
- constructor(ctx, editorPos, pos, target) {
- super(ctx, editorPos, pos);
- this._ctx = ctx;
- if (target) {
- this.target = target;
- this.targetPath = PartFingerprints.collect(target, ctx.viewDomNode);
- }
- else {
- this.target = null;
- this.targetPath = new Uint8Array(0);
- }
- }
- toString() {
- return `pos(${this.pos.x},${this.pos.y}), editorPos(${this.editorPos.x},${this.editorPos.y}), mouseVerticalOffset: ${this.mouseVerticalOffset}, mouseContentHorizontalOffset: ${this.mouseContentHorizontalOffset}\n\ttarget: ${this.target ? this.target.outerHTML : null}`;
- }
- // public fulfill(type: MouseTargetType.OVERVIEW_RULER, position?: Position | null, range?: EditorRange | null, detail?: any): MouseTarget;
- // public fulfill(type: MouseTargetType.OUTSIDE_EDITOR, position?: Position | null, range?: EditorRange | null, detail?: any): MouseTarget;
- fulfill(type, position = null, range = null, detail = null) {
- let mouseColumn = this.mouseColumn;
- if (position && position.column < this._ctx.model.getLineMaxColumn(position.lineNumber)) {
- // Most likely, the line contains foreign decorations...
- mouseColumn = CursorColumns.visibleColumnFromColumn(this._ctx.model.getLineContent(position.lineNumber), position.column, this._ctx.model.getTextModelOptions().tabSize) + 1;
- }
- return new MouseTarget(this.target, type, mouseColumn, position, range, detail);
- }
- withTarget(target) {
- return new HitTestRequest(this._ctx, this.editorPos, this.pos, target);
- }
- }
- const EMPTY_CONTENT_AFTER_LINES = { isAfterLines: true };
- function createEmptyContentDataInLines(horizontalDistanceToText) {
- return {
- isAfterLines: false,
- horizontalDistanceToText: horizontalDistanceToText
- };
- }
- export class MouseTargetFactory {
- constructor(context, viewHelper) {
- this._context = context;
- this._viewHelper = viewHelper;
- }
- mouseTargetIsWidget(e) {
- const t = e.target;
- const 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;
- }
- createMouseTarget(lastRenderData, editorPos, pos, target) {
- const ctx = new HitTestContext(this._context, this._viewHelper, lastRenderData);
- const request = new HitTestRequest(ctx, editorPos, pos, target);
- try {
- const r = MouseTargetFactory._createMouseTarget(ctx, request, false);
- // console.log(r.toString());
- return r;
- }
- catch (err) {
- // console.log(err);
- return request.fulfill(0 /* UNKNOWN */);
- }
- }
- static _createMouseTarget(ctx, request, domHitTestExecuted) {
- // console.log(`${domHitTestExecuted ? '=>' : ''}CAME IN REQUEST: ${request}`);
- // First ensure the request has a target
- if (request.target === null) {
- if (domHitTestExecuted) {
- // Still no target... and we have already executed hit test...
- return request.fulfill(0 /* UNKNOWN */);
- }
- const hitTestResult = MouseTargetFactory._doHitTest(ctx, request);
- if (hitTestResult.type === 1 /* Content */) {
- return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position, hitTestResult.injectedText);
- }
- return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true);
- }
- // we know for a fact that request.target is not null
- const resolvedRequest = request;
- let result = null;
- result = result || MouseTargetFactory._hitTestContentWidget(ctx, resolvedRequest);
- result = result || MouseTargetFactory._hitTestOverlayWidget(ctx, resolvedRequest);
- result = result || MouseTargetFactory._hitTestMinimap(ctx, resolvedRequest);
- result = result || MouseTargetFactory._hitTestScrollbarSlider(ctx, resolvedRequest);
- result = result || MouseTargetFactory._hitTestViewZone(ctx, resolvedRequest);
- result = result || MouseTargetFactory._hitTestMargin(ctx, resolvedRequest);
- result = result || MouseTargetFactory._hitTestViewCursor(ctx, resolvedRequest);
- result = result || MouseTargetFactory._hitTestTextArea(ctx, resolvedRequest);
- result = result || MouseTargetFactory._hitTestViewLines(ctx, resolvedRequest, domHitTestExecuted);
- result = result || MouseTargetFactory._hitTestScrollbar(ctx, resolvedRequest);
- return (result || request.fulfill(0 /* UNKNOWN */));
- }
- static _hitTestContentWidget(ctx, request) {
- // Is it a content widget?
- if (ElementPath.isChildOfContentWidgets(request.targetPath) || ElementPath.isChildOfOverflowingContentWidgets(request.targetPath)) {
- const widgetId = ctx.findAttribute(request.target, 'widgetId');
- if (widgetId) {
- return request.fulfill(9 /* CONTENT_WIDGET */, null, null, widgetId);
- }
- else {
- return request.fulfill(0 /* UNKNOWN */);
- }
- }
- return null;
- }
- static _hitTestOverlayWidget(ctx, request) {
- // Is it an overlay widget?
- if (ElementPath.isChildOfOverlayWidgets(request.targetPath)) {
- const widgetId = ctx.findAttribute(request.target, 'widgetId');
- if (widgetId) {
- return request.fulfill(12 /* OVERLAY_WIDGET */, null, null, widgetId);
- }
- else {
- return request.fulfill(0 /* UNKNOWN */);
- }
- }
- return null;
- }
- static _hitTestViewCursor(ctx, request) {
- if (request.target) {
- // Check if we've hit a painted cursor
- const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData;
- for (const d of lastViewCursorsRenderData) {
- if (request.target === d.domNode) {
- return request.fulfill(6 /* CONTENT_TEXT */, d.position, null, { mightBeForeignElement: false });
- }
- }
- }
- if (request.isInContentArea) {
- // 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
- const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData;
- const mouseContentHorizontalOffset = request.mouseContentHorizontalOffset;
- const mouseVerticalOffset = request.mouseVerticalOffset;
- for (const d of lastViewCursorsRenderData) {
- if (mouseContentHorizontalOffset < d.contentLeft) {
- // mouse position is to the left of the cursor
- continue;
- }
- if (mouseContentHorizontalOffset > d.contentLeft + d.width) {
- // mouse position is to the right of the cursor
- continue;
- }
- const cursorVerticalOffset = ctx.getVerticalOffsetForLineNumber(d.position.lineNumber);
- if (cursorVerticalOffset <= mouseVerticalOffset
- && mouseVerticalOffset <= cursorVerticalOffset + d.height) {
- return request.fulfill(6 /* CONTENT_TEXT */, d.position, null, { mightBeForeignElement: false });
- }
- }
- }
- return null;
- }
- static _hitTestViewZone(ctx, request) {
- const viewZoneData = ctx.getZoneAtCoord(request.mouseVerticalOffset);
- if (viewZoneData) {
- const mouseTargetType = (request.isInContentArea ? 8 /* CONTENT_VIEW_ZONE */ : 5 /* GUTTER_VIEW_ZONE */);
- return request.fulfill(mouseTargetType, viewZoneData.position, null, viewZoneData);
- }
- return null;
- }
- static _hitTestTextArea(ctx, request) {
- // Is it the textarea?
- if (ElementPath.isTextArea(request.targetPath)) {
- if (ctx.lastRenderData.lastTextareaPosition) {
- return request.fulfill(6 /* CONTENT_TEXT */, ctx.lastRenderData.lastTextareaPosition, null, { mightBeForeignElement: false });
- }
- return request.fulfill(1 /* TEXTAREA */, ctx.lastRenderData.lastTextareaPosition);
- }
- return null;
- }
- static _hitTestMargin(ctx, request) {
- if (request.isInMarginArea) {
- const res = ctx.getFullLineRangeAtCoord(request.mouseVerticalOffset);
- const pos = res.range.getStartPosition();
- let offset = Math.abs(request.pos.x - request.editorPos.x);
- const detail = {
- isAfterLines: res.isAfterLines,
- glyphMarginLeft: ctx.layoutInfo.glyphMarginLeft,
- glyphMarginWidth: ctx.layoutInfo.glyphMarginWidth,
- lineNumbersWidth: ctx.layoutInfo.lineNumbersWidth,
- offsetX: offset
- };
- offset -= ctx.layoutInfo.glyphMarginLeft;
- if (offset <= ctx.layoutInfo.glyphMarginWidth) {
- // On the glyph margin
- return request.fulfill(2 /* GUTTER_GLYPH_MARGIN */, pos, res.range, detail);
- }
- offset -= ctx.layoutInfo.glyphMarginWidth;
- if (offset <= ctx.layoutInfo.lineNumbersWidth) {
- // On the line numbers
- return request.fulfill(3 /* GUTTER_LINE_NUMBERS */, pos, res.range, detail);
- }
- offset -= ctx.layoutInfo.lineNumbersWidth;
- // On the line decorations
- return request.fulfill(4 /* GUTTER_LINE_DECORATIONS */, pos, res.range, detail);
- }
- return null;
- }
- static _hitTestViewLines(ctx, request, domHitTestExecuted) {
- if (!ElementPath.isChildOfViewLines(request.targetPath)) {
- return null;
- }
- if (ctx.isInTopPadding(request.mouseVerticalOffset)) {
- return request.fulfill(7 /* CONTENT_EMPTY */, new Position(1, 1), null, EMPTY_CONTENT_AFTER_LINES);
- }
- // Check if it is below any lines and any view zones
- if (ctx.isAfterLines(request.mouseVerticalOffset) || ctx.isInBottomPadding(request.mouseVerticalOffset)) {
- // This most likely indicates it happened after the last view-line
- const lineCount = ctx.model.getLineCount();
- const maxLineColumn = ctx.model.getLineMaxColumn(lineCount);
- return request.fulfill(7 /* CONTENT_EMPTY */, new Position(lineCount, maxLineColumn), null, EMPTY_CONTENT_AFTER_LINES);
- }
- if (domHitTestExecuted) {
- // Check if we are hitting a view-line (can happen in the case of inline decorations on empty lines)
- // See https://github.com/microsoft/vscode/issues/46942
- if (ElementPath.isStrictChildOfViewLines(request.targetPath)) {
- const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
- if (ctx.model.getLineLength(lineNumber) === 0) {
- const lineWidth = ctx.getLineWidth(lineNumber);
- const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
- return request.fulfill(7 /* CONTENT_EMPTY */, new Position(lineNumber, 1), null, detail);
- }
- const lineWidth = ctx.getLineWidth(lineNumber);
- if (request.mouseContentHorizontalOffset >= lineWidth) {
- const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
- const pos = new Position(lineNumber, ctx.model.getLineMaxColumn(lineNumber));
- return request.fulfill(7 /* CONTENT_EMPTY */, pos, null, detail);
- }
- }
- // We have already executed hit test...
- return request.fulfill(0 /* UNKNOWN */);
- }
- const hitTestResult = MouseTargetFactory._doHitTest(ctx, request);
- if (hitTestResult.type === 1 /* Content */) {
- return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position, hitTestResult.injectedText);
- }
- return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true);
- }
- static _hitTestMinimap(ctx, request) {
- if (ElementPath.isChildOfMinimap(request.targetPath)) {
- const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
- const maxColumn = ctx.model.getLineMaxColumn(possibleLineNumber);
- return request.fulfill(11 /* SCROLLBAR */, new Position(possibleLineNumber, maxColumn));
- }
- return null;
- }
- static _hitTestScrollbarSlider(ctx, request) {
- if (ElementPath.isChildOfScrollableElement(request.targetPath)) {
- if (request.target && request.target.nodeType === 1) {
- const className = request.target.className;
- if (className && /\b(slider|scrollbar)\b/.test(className)) {
- const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
- const maxColumn = ctx.model.getLineMaxColumn(possibleLineNumber);
- return request.fulfill(11 /* SCROLLBAR */, new Position(possibleLineNumber, maxColumn));
- }
- }
- }
- return null;
- }
- static _hitTestScrollbar(ctx, request) {
- // Is it the overview ruler?
- // Is it a child of the scrollable element?
- if (ElementPath.isChildOfScrollableElement(request.targetPath)) {
- const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
- const maxColumn = ctx.model.getLineMaxColumn(possibleLineNumber);
- return request.fulfill(11 /* SCROLLBAR */, new Position(possibleLineNumber, maxColumn));
- }
- return null;
- }
- getMouseColumn(editorPos, pos) {
- const options = this._context.configuration.options;
- const layoutInfo = options.get(130 /* layoutInfo */);
- const mouseContentHorizontalOffset = this._context.viewLayout.getCurrentScrollLeft() + pos.x - editorPos.x - layoutInfo.contentLeft;
- return MouseTargetFactory._getMouseColumn(mouseContentHorizontalOffset, options.get(43 /* fontInfo */).typicalHalfwidthCharacterWidth);
- }
- static _getMouseColumn(mouseContentHorizontalOffset, typicalHalfwidthCharacterWidth) {
- if (mouseContentHorizontalOffset < 0) {
- return 1;
- }
- const chars = Math.round(mouseContentHorizontalOffset / typicalHalfwidthCharacterWidth);
- return (chars + 1);
- }
- static createMouseTargetFromHitTestPosition(ctx, request, spanNode, pos, injectedText) {
- const lineNumber = pos.lineNumber;
- const column = pos.column;
- const lineWidth = ctx.getLineWidth(lineNumber);
- if (request.mouseContentHorizontalOffset > lineWidth) {
- const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
- return request.fulfill(7 /* CONTENT_EMPTY */, pos, null, detail);
- }
- const visibleRange = ctx.visibleRangeForPosition(lineNumber, column);
- if (!visibleRange) {
- return request.fulfill(0 /* UNKNOWN */, pos);
- }
- const columnHorizontalOffset = visibleRange.left;
- if (request.mouseContentHorizontalOffset === columnHorizontalOffset) {
- return request.fulfill(6 /* CONTENT_TEXT */, pos, null, { mightBeForeignElement: !!injectedText });
- }
- const points = [];
- points.push({ offset: visibleRange.left, column: column });
- if (column > 1) {
- const visibleRange = ctx.visibleRangeForPosition(lineNumber, column - 1);
- if (visibleRange) {
- points.push({ offset: visibleRange.left, column: column - 1 });
- }
- }
- const lineMaxColumn = ctx.model.getLineMaxColumn(lineNumber);
- if (column < lineMaxColumn) {
- const visibleRange = ctx.visibleRangeForPosition(lineNumber, column + 1);
- if (visibleRange) {
- points.push({ offset: visibleRange.left, column: column + 1 });
- }
- }
- points.sort((a, b) => a.offset - b.offset);
- const mouseCoordinates = request.pos.toClientCoordinates();
- const spanNodeClientRect = spanNode.getBoundingClientRect();
- const mouseIsOverSpanNode = (spanNodeClientRect.left <= mouseCoordinates.clientX && mouseCoordinates.clientX <= spanNodeClientRect.right);
- for (let i = 1; i < points.length; i++) {
- const prev = points[i - 1];
- const curr = points[i];
- if (prev.offset <= request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset <= curr.offset) {
- const rng = new EditorRange(lineNumber, prev.column, lineNumber, curr.column);
- return request.fulfill(6 /* CONTENT_TEXT */, pos, rng, { mightBeForeignElement: !mouseIsOverSpanNode || !!injectedText });
- }
- }
- return request.fulfill(6 /* CONTENT_TEXT */, pos, null, { mightBeForeignElement: !mouseIsOverSpanNode || !!injectedText });
- }
- /**
- * Most probably WebKit browsers and Edge
- */
- static _doHitTestWithCaretRangeFromPoint(ctx, request) {
- // 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
- const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
- const lineVerticalOffset = ctx.getVerticalOffsetForLineNumber(lineNumber);
- const lineCenteredVerticalOffset = lineVerticalOffset + Math.floor(ctx.lineHeight / 2);
- let adjustedPageY = request.pos.y + (lineCenteredVerticalOffset - request.mouseVerticalOffset);
- if (adjustedPageY <= request.editorPos.y) {
- adjustedPageY = request.editorPos.y + 1;
- }
- if (adjustedPageY >= request.editorPos.y + ctx.layoutInfo.height) {
- adjustedPageY = request.editorPos.y + ctx.layoutInfo.height - 1;
- }
- const adjustedPage = new PageCoordinates(request.pos.x, adjustedPageY);
- const r = this._actualDoHitTestWithCaretRangeFromPoint(ctx, adjustedPage.toClientCoordinates());
- if (r.type === 1 /* Content */) {
- 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, request.pos.toClientCoordinates());
- }
- static _actualDoHitTestWithCaretRangeFromPoint(ctx, coords) {
- const shadowRoot = dom.getShadowRoot(ctx.viewDomNode);
- let range;
- if (shadowRoot) {
- if (typeof shadowRoot.caretRangeFromPoint === 'undefined') {
- range = shadowCaretRangeFromPoint(shadowRoot, coords.clientX, coords.clientY);
- }
- else {
- range = shadowRoot.caretRangeFromPoint(coords.clientX, coords.clientY);
- }
- }
- else {
- range = document.caretRangeFromPoint(coords.clientX, coords.clientY);
- }
- if (!range || !range.startContainer) {
- return new UnknownHitTestResult();
- }
- // Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span
- const startContainer = range.startContainer;
- if (startContainer.nodeType === startContainer.TEXT_NODE) {
- // startContainer is expected to be the token text
- const parent1 = startContainer.parentNode; // expected to be the token span
- const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span
- const parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div
- const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? parent3.className : null;
- if (parent3ClassName === ViewLine.CLASS_NAME) {
- return HitTestResult.createFromDOMInfo(ctx, parent1, range.startOffset);
- }
- else {
- return new UnknownHitTestResult(startContainer.parentNode);
- }
- }
- else if (startContainer.nodeType === startContainer.ELEMENT_NODE) {
- // startContainer is expected to be the token span
- const parent1 = startContainer.parentNode; // expected to be the view line container span
- const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line div
- const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? parent2.className : null;
- if (parent2ClassName === ViewLine.CLASS_NAME) {
- return HitTestResult.createFromDOMInfo(ctx, startContainer, startContainer.textContent.length);
- }
- else {
- return new UnknownHitTestResult(startContainer);
- }
- }
- return new UnknownHitTestResult();
- }
- /**
- * Most probably Gecko
- */
- static _doHitTestWithCaretPositionFromPoint(ctx, coords) {
- const hitResult = document.caretPositionFromPoint(coords.clientX, coords.clientY);
- if (hitResult.offsetNode.nodeType === hitResult.offsetNode.TEXT_NODE) {
- // offsetNode is expected to be the token text
- const parent1 = hitResult.offsetNode.parentNode; // expected to be the token span
- const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span
- const parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div
- const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? parent3.className : null;
- if (parent3ClassName === ViewLine.CLASS_NAME) {
- return HitTestResult.createFromDOMInfo(ctx, hitResult.offsetNode.parentNode, hitResult.offset);
- }
- else {
- return new UnknownHitTestResult(hitResult.offsetNode.parentNode);
- }
- }
- // For inline decorations, Gecko sometimes returns the `<span>` of the line and the offset is the `<span>` with the inline decoration
- // Some other times, it returns the `<span>` with the inline decoration
- if (hitResult.offsetNode.nodeType === hitResult.offsetNode.ELEMENT_NODE) {
- const parent1 = hitResult.offsetNode.parentNode;
- const parent1ClassName = parent1 && parent1.nodeType === parent1.ELEMENT_NODE ? parent1.className : null;
- const parent2 = parent1 ? parent1.parentNode : null;
- const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? parent2.className : null;
- if (parent1ClassName === ViewLine.CLASS_NAME) {
- // it returned the `<span>` of the line and the offset is the `<span>` with the inline decoration
- const tokenSpan = hitResult.offsetNode.childNodes[Math.min(hitResult.offset, hitResult.offsetNode.childNodes.length - 1)];
- if (tokenSpan) {
- return HitTestResult.createFromDOMInfo(ctx, tokenSpan, 0);
- }
- }
- else if (parent2ClassName === ViewLine.CLASS_NAME) {
- // it returned the `<span>` with the inline decoration
- return HitTestResult.createFromDOMInfo(ctx, hitResult.offsetNode, 0);
- }
- }
- return new UnknownHitTestResult(hitResult.offsetNode);
- }
- static _snapToSoftTabBoundary(position, viewModel) {
- const lineContent = viewModel.getLineContent(position.lineNumber);
- const { tabSize } = viewModel.getTextModelOptions();
- const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - 1, tabSize, 2 /* Nearest */);
- if (newPosition !== -1) {
- return new Position(position.lineNumber, newPosition + 1);
- }
- return position;
- }
- static _doHitTest(ctx, request) {
- let result = new UnknownHitTestResult();
- if (typeof document.caretRangeFromPoint === 'function') {
- result = this._doHitTestWithCaretRangeFromPoint(ctx, request);
- }
- else if (document.caretPositionFromPoint) {
- result = this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates());
- }
- if (result.type === 1 /* Content */) {
- const injectedText = ctx.model.getInjectedTextAt(result.position);
- const normalizedPosition = ctx.model.normalizePosition(result.position, 2 /* None */);
- if (injectedText || !normalizedPosition.equals(result.position)) {
- result = new ContentHitTestResult(normalizedPosition, result.spanNode, injectedText);
- }
- }
- // Snap to the nearest soft tab boundary if atomic soft tabs are enabled.
- if (result.type === 1 /* Content */ && ctx.stickyTabStops) {
- result = new ContentHitTestResult(this._snapToSoftTabBoundary(result.position, ctx.model), result.spanNode, result.injectedText);
- }
- return result;
- }
- }
- export function shadowCaretRangeFromPoint(shadowRoot, x, y) {
- const range = document.createRange();
- // Get the element under the point
- let el = shadowRoot.elementFromPoint(x, y);
- if (el !== null) {
- // Get the last child of the element until its firstChild is a text node
- // This assumes that the pointer is on the right of the line, out of the tokens
- // and that we want to get the offset of the last token of the line
- while (el && el.firstChild && el.firstChild.nodeType !== el.firstChild.TEXT_NODE && el.lastChild && el.lastChild.firstChild) {
- el = el.lastChild;
- }
- // Grab its rect
- const rect = el.getBoundingClientRect();
- // And its font
- const font = window.getComputedStyle(el, null).getPropertyValue('font');
- // And also its txt content
- const text = el.innerText;
- // Position the pixel cursor at the left of the element
- let pixelCursor = rect.left;
- let offset = 0;
- let step;
- // If the point is on the right of the box put the cursor after the last character
- if (x > rect.left + rect.width) {
- offset = text.length;
- }
- else {
- const charWidthReader = CharWidthReader.getInstance();
- // Goes through all the characters of the innerText, and checks if the x of the point
- // belongs to the character.
- for (let i = 0; i < text.length + 1; i++) {
- // The step is half the width of the character
- step = charWidthReader.getCharWidth(text.charAt(i), font) / 2;
- // Move to the center of the character
- pixelCursor += step;
- // If the x of the point is smaller that the position of the cursor, the point is over that character
- if (x < pixelCursor) {
- offset = i;
- break;
- }
- // Move between the current character and the next
- pixelCursor += step;
- }
- }
- // Creates a range with the text node of the element and set the offset found
- range.setStart(el.firstChild, offset);
- range.setEnd(el.firstChild, offset);
- }
- return range;
- }
- class CharWidthReader {
- constructor() {
- this._cache = {};
- this._canvas = document.createElement('canvas');
- }
- static getInstance() {
- if (!CharWidthReader._INSTANCE) {
- CharWidthReader._INSTANCE = new CharWidthReader();
- }
- return CharWidthReader._INSTANCE;
- }
- getCharWidth(char, font) {
- const cacheKey = char + font;
- if (this._cache[cacheKey]) {
- return this._cache[cacheKey];
- }
- const context = this._canvas.getContext('2d');
- context.font = font;
- const metrics = context.measureText(char);
- const width = metrics.width;
- this._cache[cacheKey] = width;
- return width;
- }
- }
- CharWidthReader._INSTANCE = null;
|