monaco-editor-core
Version:
A browser based code editor
923 lines (922 loc) • 47 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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/core/cursorColumns.js';
import * as dom from '../../../base/browser/dom.js';
import { AtomicTabMoveOperations } from '../../common/cursor/cursorAtomicMoveOperations.js';
import { Lazy } from '../../../base/common/lazy.js';
class UnknownHitTestResult {
constructor(hitTarget = null) {
this.hitTarget = hitTarget;
this.type = 0 /* HitTestResultType.Unknown */;
}
}
class ContentHitTestResult {
get hitTarget() { return this.spanNode; }
constructor(position, spanNode, injectedText) {
this.position = position;
this.spanNode = spanNode;
this.injectedText = injectedText;
this.type = 1 /* HitTestResultType.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 {
static _deduceRage(position, range = null) {
if (!range && position) {
return new EditorRange(position.lineNumber, position.column, position.lineNumber, position.column);
}
return range ?? null;
}
static createUnknown(element, mouseColumn, position) {
return { type: 0 /* MouseTargetType.UNKNOWN */, element, mouseColumn, position, range: this._deduceRage(position) };
}
static createTextarea(element, mouseColumn) {
return { type: 1 /* MouseTargetType.TEXTAREA */, element, mouseColumn, position: null, range: null };
}
static createMargin(type, element, mouseColumn, position, range, detail) {
return { type, element, mouseColumn, position, range, detail };
}
static createViewZone(type, element, mouseColumn, position, detail) {
return { type, element, mouseColumn, position, range: this._deduceRage(position), detail };
}
static createContentText(element, mouseColumn, position, range, detail) {
return { type: 6 /* MouseTargetType.CONTENT_TEXT */, element, mouseColumn, position, range: this._deduceRage(position, range), detail };
}
static createContentEmpty(element, mouseColumn, position, detail) {
return { type: 7 /* MouseTargetType.CONTENT_EMPTY */, element, mouseColumn, position, range: this._deduceRage(position), detail };
}
static createContentWidget(element, mouseColumn, detail) {
return { type: 9 /* MouseTargetType.CONTENT_WIDGET */, element, mouseColumn, position: null, range: null, detail };
}
static createScrollbar(element, mouseColumn, position) {
return { type: 11 /* MouseTargetType.SCROLLBAR */, element, mouseColumn, position, range: this._deduceRage(position) };
}
static createOverlayWidget(element, mouseColumn, detail) {
return { type: 12 /* MouseTargetType.OVERLAY_WIDGET */, element, mouseColumn, position: null, range: null, detail };
}
static createOutsideEditor(mouseColumn, position, outsidePosition, outsideDistance) {
return { type: 13 /* MouseTargetType.OUTSIDE_EDITOR */, element: null, mouseColumn, position, range: this._deduceRage(position), outsidePosition, outsideDistance };
}
static _typeToString(type) {
if (type === 1 /* MouseTargetType.TEXTAREA */) {
return 'TEXTAREA';
}
if (type === 2 /* MouseTargetType.GUTTER_GLYPH_MARGIN */) {
return 'GUTTER_GLYPH_MARGIN';
}
if (type === 3 /* MouseTargetType.GUTTER_LINE_NUMBERS */) {
return 'GUTTER_LINE_NUMBERS';
}
if (type === 4 /* MouseTargetType.GUTTER_LINE_DECORATIONS */) {
return 'GUTTER_LINE_DECORATIONS';
}
if (type === 5 /* MouseTargetType.GUTTER_VIEW_ZONE */) {
return 'GUTTER_VIEW_ZONE';
}
if (type === 6 /* MouseTargetType.CONTENT_TEXT */) {
return 'CONTENT_TEXT';
}
if (type === 7 /* MouseTargetType.CONTENT_EMPTY */) {
return 'CONTENT_EMPTY';
}
if (type === 8 /* MouseTargetType.CONTENT_VIEW_ZONE */) {
return 'CONTENT_VIEW_ZONE';
}
if (type === 9 /* MouseTargetType.CONTENT_WIDGET */) {
return 'CONTENT_WIDGET';
}
if (type === 10 /* MouseTargetType.OVERVIEW_RULER */) {
return 'OVERVIEW_RULER';
}
if (type === 11 /* MouseTargetType.SCROLLBAR */) {
return 'SCROLLBAR';
}
if (type === 12 /* MouseTargetType.OVERLAY_WIDGET */) {
return 'OVERLAY_WIDGET';
}
return 'UNKNOWN';
}
static toString(target) {
return this._typeToString(target.type) + ': ' + target.position + ' - ' + target.range + ' - ' + JSON.stringify(target.detail);
}
}
class ElementPath {
static isTextArea(path) {
return (path.length === 2
&& path[0] === 3 /* PartFingerprint.OverflowGuard */
&& path[1] === 7 /* PartFingerprint.TextArea */);
}
static isChildOfViewLines(path) {
return (path.length >= 4
&& path[0] === 3 /* PartFingerprint.OverflowGuard */
&& path[3] === 8 /* PartFingerprint.ViewLines */);
}
static isStrictChildOfViewLines(path) {
return (path.length > 4
&& path[0] === 3 /* PartFingerprint.OverflowGuard */
&& path[3] === 8 /* PartFingerprint.ViewLines */);
}
static isChildOfScrollableElement(path) {
return (path.length >= 2
&& path[0] === 3 /* PartFingerprint.OverflowGuard */
&& path[1] === 6 /* PartFingerprint.ScrollableElement */);
}
static isChildOfMinimap(path) {
return (path.length >= 2
&& path[0] === 3 /* PartFingerprint.OverflowGuard */
&& path[1] === 9 /* PartFingerprint.Minimap */);
}
static isChildOfContentWidgets(path) {
return (path.length >= 4
&& path[0] === 3 /* PartFingerprint.OverflowGuard */
&& path[3] === 1 /* PartFingerprint.ContentWidgets */);
}
static isChildOfOverflowGuard(path) {
return (path.length >= 1
&& path[0] === 3 /* PartFingerprint.OverflowGuard */);
}
static isChildOfOverflowingContentWidgets(path) {
return (path.length >= 1
&& path[0] === 2 /* PartFingerprint.OverflowingContentWidgets */);
}
static isChildOfOverlayWidgets(path) {
return (path.length >= 2
&& path[0] === 3 /* PartFingerprint.OverflowGuard */
&& path[1] === 4 /* PartFingerprint.OverlayWidgets */);
}
static isChildOfOverflowingOverlayWidgets(path) {
return (path.length >= 1
&& path[0] === 5 /* PartFingerprint.OverflowingOverlayWidgets */);
}
}
export class HitTestContext {
constructor(context, viewHelper, lastRenderData) {
this.viewModel = context.viewModel;
const options = context.configuration.options;
this.layoutInfo = options.get(146 /* EditorOption.layoutInfo */);
this.viewDomNode = viewHelper.viewDomNode;
this.lineHeight = options.get(67 /* EditorOption.lineHeight */);
this.stickyTabStops = options.get(117 /* EditorOption.stickyTabStops */);
this.typicalHalfwidthCharacterWidth = options.get(50 /* EditorOption.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.viewModel.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.viewModel.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.viewModel.getLineCount();
const maxLineColumn = this._context.viewModel.getLineMaxColumn(lineNumber);
return {
range: new EditorRange(lineNumber, maxLineColumn, lineNumber, maxLineColumn),
isAfterLines: true
};
}
const lineNumber = this._context.viewLayout.getLineNumberAtVerticalOffset(mouseVerticalOffset);
const maxLineColumn = this._context.viewModel.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 !== element.ownerDocument.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, relativePos) {
this.editorPos = editorPos;
this.pos = pos;
this.relativePos = relativePos;
this.mouseVerticalOffset = Math.max(0, ctx.getCurrentScrollTop() + this.relativePos.y);
this.mouseContentHorizontalOffset = ctx.getCurrentScrollLeft() + this.relativePos.x - ctx.layoutInfo.contentLeft;
this.isInMarginArea = (this.relativePos.x < ctx.layoutInfo.contentLeft && this.relativePos.x >= ctx.layoutInfo.glyphMarginLeft);
this.isInContentArea = !this.isInMarginArea;
this.mouseColumn = Math.max(0, MouseTargetFactory._getMouseColumn(this.mouseContentHorizontalOffset, ctx.typicalHalfwidthCharacterWidth));
}
}
class HitTestRequest extends BareHitTestRequest {
get target() {
if (this._useHitTestTarget) {
return this.hitTestResult.value.hitTarget;
}
return this._eventTarget;
}
get targetPath() {
if (this._targetPathCacheElement !== this.target) {
this._targetPathCacheElement = this.target;
this._targetPathCacheValue = PartFingerprints.collect(this.target, this._ctx.viewDomNode);
}
return this._targetPathCacheValue;
}
constructor(ctx, editorPos, pos, relativePos, eventTarget) {
super(ctx, editorPos, pos, relativePos);
this.hitTestResult = new Lazy(() => MouseTargetFactory.doHitTest(this._ctx, this));
this._targetPathCacheElement = null;
this._targetPathCacheValue = new Uint8Array(0);
this._ctx = ctx;
this._eventTarget = eventTarget;
// If no event target is passed in, we will use the hit test target
const hasEventTarget = Boolean(this._eventTarget);
this._useHitTestTarget = !hasEventTarget;
}
toString() {
return `pos(${this.pos.x},${this.pos.y}), editorPos(${this.editorPos.x},${this.editorPos.y}), relativePos(${this.relativePos.x},${this.relativePos.y}), mouseVerticalOffset: ${this.mouseVerticalOffset}, mouseContentHorizontalOffset: ${this.mouseContentHorizontalOffset}\n\ttarget: ${this.target ? this.target.outerHTML : null}`;
}
get wouldBenefitFromHitTestTargetSwitch() {
return (!this._useHitTestTarget
&& this.hitTestResult.value.hitTarget !== null
&& this.target !== this.hitTestResult.value.hitTarget);
}
switchToHitTestTarget() {
this._useHitTestTarget = true;
}
_getMouseColumn(position = null) {
if (position && position.column < this._ctx.viewModel.getLineMaxColumn(position.lineNumber)) {
// Most likely, the line contains foreign decorations...
return CursorColumns.visibleColumnFromColumn(this._ctx.viewModel.getLineContent(position.lineNumber), position.column, this._ctx.viewModel.model.getOptions().tabSize) + 1;
}
return this.mouseColumn;
}
fulfillUnknown(position = null) {
return MouseTarget.createUnknown(this.target, this._getMouseColumn(position), position);
}
fulfillTextarea() {
return MouseTarget.createTextarea(this.target, this._getMouseColumn());
}
fulfillMargin(type, position, range, detail) {
return MouseTarget.createMargin(type, this.target, this._getMouseColumn(position), position, range, detail);
}
fulfillViewZone(type, position, detail) {
return MouseTarget.createViewZone(type, this.target, this._getMouseColumn(position), position, detail);
}
fulfillContentText(position, range, detail) {
return MouseTarget.createContentText(this.target, this._getMouseColumn(position), position, range, detail);
}
fulfillContentEmpty(position, detail) {
return MouseTarget.createContentEmpty(this.target, this._getMouseColumn(position), position, detail);
}
fulfillContentWidget(detail) {
return MouseTarget.createContentWidget(this.target, this._getMouseColumn(), detail);
}
fulfillScrollbar(position) {
return MouseTarget.createScrollbar(this.target, this._getMouseColumn(position), position);
}
fulfillOverlayWidget(detail) {
return MouseTarget.createOverlayWidget(this.target, this._getMouseColumn(), detail);
}
}
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) || ElementPath.isChildOfOverflowingOverlayWidgets(path)) {
return true;
}
return false;
}
createMouseTarget(lastRenderData, editorPos, pos, relativePos, target) {
const ctx = new HitTestContext(this._context, this._viewHelper, lastRenderData);
const request = new HitTestRequest(ctx, editorPos, pos, relativePos, target);
try {
const r = MouseTargetFactory._createMouseTarget(ctx, request);
if (r.type === 6 /* MouseTargetType.CONTENT_TEXT */) {
// Snap to the nearest soft tab boundary if atomic soft tabs are enabled.
if (ctx.stickyTabStops && r.position !== null) {
const position = MouseTargetFactory._snapToSoftTabBoundary(r.position, ctx.viewModel);
const range = EditorRange.fromPositions(position, position).plusRange(r.range);
return request.fulfillContentText(position, range, r.detail);
}
}
// console.log(MouseTarget.toString(r));
return r;
}
catch (err) {
// console.log(err);
return request.fulfillUnknown();
}
}
static _createMouseTarget(ctx, request) {
// console.log(`${domHitTestExecuted ? '=>' : ''}CAME IN REQUEST: ${request}`);
if (request.target === null) {
// No target
return request.fulfillUnknown();
}
// we know for a fact that request.target is not null
const resolvedRequest = request;
let result = null;
if (!ElementPath.isChildOfOverflowGuard(request.targetPath) && !ElementPath.isChildOfOverflowingContentWidgets(request.targetPath) && !ElementPath.isChildOfOverflowingOverlayWidgets(request.targetPath)) {
// We only render dom nodes inside the overflow guard or in the overflowing content widgets
result = result || request.fulfillUnknown();
}
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);
result = result || MouseTargetFactory._hitTestScrollbar(ctx, resolvedRequest);
return (result || request.fulfillUnknown());
}
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.fulfillContentWidget(widgetId);
}
else {
return request.fulfillUnknown();
}
}
return null;
}
static _hitTestOverlayWidget(ctx, request) {
// Is it an overlay widget?
if (ElementPath.isChildOfOverlayWidgets(request.targetPath) || ElementPath.isChildOfOverflowingOverlayWidgets(request.targetPath)) {
const widgetId = ctx.findAttribute(request.target, 'widgetId');
if (widgetId) {
return request.fulfillOverlayWidget(widgetId);
}
else {
return request.fulfillUnknown();
}
}
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.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null });
}
}
}
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.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null });
}
}
}
return null;
}
static _hitTestViewZone(ctx, request) {
const viewZoneData = ctx.getZoneAtCoord(request.mouseVerticalOffset);
if (viewZoneData) {
const mouseTargetType = (request.isInContentArea ? 8 /* MouseTargetType.CONTENT_VIEW_ZONE */ : 5 /* MouseTargetType.GUTTER_VIEW_ZONE */);
return request.fulfillViewZone(mouseTargetType, viewZoneData.position, viewZoneData);
}
return null;
}
static _hitTestTextArea(ctx, request) {
// Is it the textarea?
if (ElementPath.isTextArea(request.targetPath)) {
if (ctx.lastRenderData.lastTextareaPosition) {
return request.fulfillContentText(ctx.lastRenderData.lastTextareaPosition, null, { mightBeForeignElement: false, injectedText: null });
}
return request.fulfillTextarea();
}
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.relativePos.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
const modelCoordinate = ctx.viewModel.coordinatesConverter.convertViewPositionToModelPosition(res.range.getStartPosition());
const lanes = ctx.viewModel.glyphLanes.getLanesAtLine(modelCoordinate.lineNumber);
detail.glyphMarginLane = lanes[Math.floor(offset / ctx.lineHeight)];
return request.fulfillMargin(2 /* MouseTargetType.GUTTER_GLYPH_MARGIN */, pos, res.range, detail);
}
offset -= ctx.layoutInfo.glyphMarginWidth;
if (offset <= ctx.layoutInfo.lineNumbersWidth) {
// On the line numbers
return request.fulfillMargin(3 /* MouseTargetType.GUTTER_LINE_NUMBERS */, pos, res.range, detail);
}
offset -= ctx.layoutInfo.lineNumbersWidth;
// On the line decorations
return request.fulfillMargin(4 /* MouseTargetType.GUTTER_LINE_DECORATIONS */, pos, res.range, detail);
}
return null;
}
static _hitTestViewLines(ctx, request) {
if (!ElementPath.isChildOfViewLines(request.targetPath)) {
return null;
}
if (ctx.isInTopPadding(request.mouseVerticalOffset)) {
return request.fulfillContentEmpty(new Position(1, 1), 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.viewModel.getLineCount();
const maxLineColumn = ctx.viewModel.getLineMaxColumn(lineCount);
return request.fulfillContentEmpty(new Position(lineCount, maxLineColumn), EMPTY_CONTENT_AFTER_LINES);
}
// 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.viewModel.getLineLength(lineNumber) === 0) {
const lineWidth = ctx.getLineWidth(lineNumber);
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
return request.fulfillContentEmpty(new Position(lineNumber, 1), detail);
}
const lineWidth = ctx.getLineWidth(lineNumber);
if (request.mouseContentHorizontalOffset >= lineWidth) {
// TODO: This is wrong for RTL
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
return request.fulfillContentEmpty(pos, detail);
}
}
// Do the hit test (if not already done)
const hitTestResult = request.hitTestResult.value;
if (hitTestResult.type === 1 /* HitTestResultType.Content */) {
return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position, hitTestResult.injectedText);
}
// We didn't hit content...
if (request.wouldBenefitFromHitTestTargetSwitch) {
// We actually hit something different... Give it one last change by trying again with this new target
request.switchToHitTestTarget();
return this._createMouseTarget(ctx, request);
}
// We have tried everything...
return request.fulfillUnknown();
}
static _hitTestMinimap(ctx, request) {
if (ElementPath.isChildOfMinimap(request.targetPath)) {
const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);
return request.fulfillScrollbar(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.viewModel.getLineMaxColumn(possibleLineNumber);
return request.fulfillScrollbar(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.viewModel.getLineMaxColumn(possibleLineNumber);
return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));
}
return null;
}
getMouseColumn(relativePos) {
const options = this._context.configuration.options;
const layoutInfo = options.get(146 /* EditorOption.layoutInfo */);
const mouseContentHorizontalOffset = this._context.viewLayout.getCurrentScrollLeft() + relativePos.x - layoutInfo.contentLeft;
return MouseTargetFactory._getMouseColumn(mouseContentHorizontalOffset, options.get(50 /* EditorOption.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.fulfillContentEmpty(pos, detail);
}
const visibleRange = ctx.visibleRangeForPosition(lineNumber, column);
if (!visibleRange) {
return request.fulfillUnknown(pos);
}
const columnHorizontalOffset = visibleRange.left;
if (Math.abs(request.mouseContentHorizontalOffset - columnHorizontalOffset) < 1) {
return request.fulfillContentText(pos, null, { mightBeForeignElement: !!injectedText, 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.viewModel.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(dom.getWindow(ctx.viewDomNode));
const spanNodeClientRect = spanNode.getBoundingClientRect();
const mouseIsOverSpanNode = (spanNodeClientRect.left <= mouseCoordinates.clientX && mouseCoordinates.clientX <= spanNodeClientRect.right);
let rng = null;
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) {
rng = new EditorRange(lineNumber, prev.column, lineNumber, curr.column);
// See https://github.com/microsoft/vscode/issues/152819
// Due to the use of zwj, the browser's hit test result is skewed towards the left
// Here we try to correct that if the mouse horizontal offset is closer to the right than the left
const prevDelta = Math.abs(prev.offset - request.mouseContentHorizontalOffset);
const nextDelta = Math.abs(curr.offset - request.mouseContentHorizontalOffset);
pos = (prevDelta < nextDelta
? new Position(lineNumber, prev.column)
: new Position(lineNumber, curr.column));
break;
}
}
return request.fulfillContentText(pos, rng, { mightBeForeignElement: !mouseIsOverSpanNode || !!injectedText, 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 lineStartVerticalOffset = ctx.getVerticalOffsetForLineNumber(lineNumber);
const lineEndVerticalOffset = lineStartVerticalOffset + ctx.lineHeight;
const isBelowLastLine = (lineNumber === ctx.viewModel.getLineCount()
&& request.mouseVerticalOffset > lineEndVerticalOffset);
if (!isBelowLastLine) {
const lineCenteredVerticalOffset = Math.floor((lineStartVerticalOffset + lineEndVerticalOffset) / 2);
let adjustedPageY = request.pos.y + (lineCenteredVerticalOffset - request.mouseVerticalOffset);
if (adjustedPageY <= request.editorPos.y) {
adjustedPageY = request.editorPos.y + 1;
}
if (adjustedPageY >= request.editorPos.y + request.editorPos.height) {
adjustedPageY = request.editorPos.y + request.editorPos.height - 1;
}
const adjustedPage = new PageCoordinates(request.pos.x, adjustedPageY);
const r = this._actualDoHitTestWithCaretRangeFromPoint(ctx, adjustedPage.toClientCoordinates(dom.getWindow(ctx.viewDomNode)));
if (r.type === 1 /* HitTestResultType.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(dom.getWindow(ctx.viewDomNode)));
}
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 = ctx.viewDomNode.ownerDocument.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 = ctx.viewDomNode.ownerDocument.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.model.getOptions();
const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - 1, tabSize, 2 /* Direction.Nearest */);
if (newPosition !== -1) {
return new Position(position.lineNumber, newPosition + 1);
}
return position;
}
static doHitTest(ctx, request) {
let result = new UnknownHitTestResult();
if (typeof ctx.viewDomNode.ownerDocument.caretRangeFromPoint === 'function') {
result = this._doHitTestWithCaretRangeFromPoint(ctx, request);
}
else if (ctx.viewDomNode.ownerDocument.caretPositionFromPoint) {
result = this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates(dom.getWindow(ctx.viewDomNode)));
}
if (result.type === 1 /* HitTestResultType.Content */) {
const injectedText = ctx.viewModel.getInjectedTextAt(result.position);
const normalizedPosition = ctx.viewModel.normalizePosition(result.position, 2 /* PositionAffinity.None */);
if (injectedText || !normalizedPosition.equals(result.position)) {
result = new ContentHitTestResult(normalizedPosition, result.spanNode, injectedText);
}
}
return result;
}
}
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 (the computed shorthand font property might be empty, see #3217)
const elWindow = dom.getWindow(el);
const fontStyle = elWindow.getComputedStyle(el, null).getPropertyValue('font-style');
const fontVariant = elWindow.getComputedStyle(el, null).getPropertyValue('font-variant');
const fontWeight = elWindow.getComputedStyle(el, null).getPropertyValue('font-weight');
const fontSize = elWindow.getComputedStyle(el, null).getPropertyValue('font-size');
const lineHeight = elWindow.getComputedStyle(el, null).getPropertyValue('line-height');
const fontFamily = elWindow.getComputedStyle(el, null).getPropertyValue('font-family');
const font = `${fontStyle} ${fontVariant} ${fontWeight} ${fontSize}/${lineHeight} ${fontFamily}`;
// 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 {
static { this._INSTANCE = null; }
static getInstance() {
if (!CharWidthReader._INSTANCE) {
CharWidthReader._INSTANCE = new CharWidthReader();
}
return CharWidthReader._INSTANCE;
}
constructor() {
this._cache = {};
this._canvas = document.createElement('canvas');
}
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;
}
}