monaco-editor-core
Version:
A browser based code editor
591 lines (590 loc) • 29.4 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
import './hover.css';
import { DisposableStore } from '../../../../base/common/lifecycle.js';
import { Emitter } from '../../../../base/common/event.js';
import * as dom from '../../../../base/browser/dom.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { EDITOR_FONT_DEFAULTS } from '../../../common/config/editorOptions.js';
import { HoverAction, HoverWidget as BaseHoverWidget, getHoverAccessibleViewHint } from '../../../../base/browser/ui/hover/hoverWidget.js';
import { Widget } from '../../../../base/browser/ui/widget.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { MarkdownRenderer, openLinkFromMarkdown } from '../../widget/markdownRenderer/browser/markdownRenderer.js';
import { isMarkdownString } from '../../../../base/common/htmlContent.js';
import { localize } from '../../../../nls.js';
import { isMacintosh } from '../../../../base/common/platform.js';
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
import { status } from '../../../../base/browser/ui/aria/aria.js';
const $ = dom.$;
let HoverWidget = class HoverWidget extends Widget {
get _targetWindow() {
return dom.getWindow(this._target.targetElements[0]);
}
get _targetDocumentElement() {
return dom.getWindow(this._target.targetElements[0]).document.documentElement;
}
get isDisposed() { return this._isDisposed; }
get isMouseIn() { return this._lockMouseTracker.isMouseIn; }
get domNode() { return this._hover.containerDomNode; }
get onDispose() { return this._onDispose.event; }
get onRequestLayout() { return this._onRequestLayout.event; }
get anchor() { return this._hoverPosition === 2 /* HoverPosition.BELOW */ ? 0 /* AnchorPosition.BELOW */ : 1 /* AnchorPosition.ABOVE */; }
get x() { return this._x; }
get y() { return this._y; }
/**
* Whether the hover is "locked" by holding the alt/option key. When locked, the hover will not
* hide and can be hovered regardless of whether the `hideOnHover` hover option is set.
*/
get isLocked() { return this._isLocked; }
set isLocked(value) {
if (this._isLocked === value) {
return;
}
this._isLocked = value;
this._hoverContainer.classList.toggle('locked', this._isLocked);
}
constructor(options, _keybindingService, _configurationService, _openerService, _instantiationService, _accessibilityService) {
super();
this._keybindingService = _keybindingService;
this._configurationService = _configurationService;
this._openerService = _openerService;
this._instantiationService = _instantiationService;
this._accessibilityService = _accessibilityService;
this._messageListeners = new DisposableStore();
this._isDisposed = false;
this._forcePosition = false;
this._x = 0;
this._y = 0;
this._isLocked = false;
this._enableFocusTraps = false;
this._addedFocusTrap = false;
this._onDispose = this._register(new Emitter());
this._onRequestLayout = this._register(new Emitter());
this._linkHandler = options.linkHandler || (url => {
return openLinkFromMarkdown(this._openerService, url, isMarkdownString(options.content) ? options.content.isTrusted : undefined);
});
this._target = 'targetElements' in options.target ? options.target : new ElementHoverTarget(options.target);
this._hoverPointer = options.appearance?.showPointer ? $('div.workbench-hover-pointer') : undefined;
this._hover = this._register(new BaseHoverWidget());
this._hover.containerDomNode.classList.add('workbench-hover', 'fadeIn');
if (options.appearance?.compact) {
this._hover.containerDomNode.classList.add('workbench-hover', 'compact');
}
if (options.appearance?.skipFadeInAnimation) {
this._hover.containerDomNode.classList.add('skip-fade-in');
}
if (options.additionalClasses) {
this._hover.containerDomNode.classList.add(...options.additionalClasses);
}
if (options.position?.forcePosition) {
this._forcePosition = true;
}
if (options.trapFocus) {
this._enableFocusTraps = true;
}
this._hoverPosition = options.position?.hoverPosition ?? 3 /* HoverPosition.ABOVE */;
// Don't allow mousedown out of the widget, otherwise preventDefault will call and text will
// not be selected.
this.onmousedown(this._hover.containerDomNode, e => e.stopPropagation());
// Hide hover on escape
this.onkeydown(this._hover.containerDomNode, e => {
if (e.equals(9 /* KeyCode.Escape */)) {
this.dispose();
}
});
// Hide when the window loses focus
this._register(dom.addDisposableListener(this._targetWindow, 'blur', () => this.dispose()));
const rowElement = $('div.hover-row.markdown-hover');
const contentsElement = $('div.hover-contents');
if (typeof options.content === 'string') {
contentsElement.textContent = options.content;
contentsElement.style.whiteSpace = 'pre-wrap';
}
else if (dom.isHTMLElement(options.content)) {
contentsElement.appendChild(options.content);
contentsElement.classList.add('html-hover-contents');
}
else {
const markdown = options.content;
const mdRenderer = this._instantiationService.createInstance(MarkdownRenderer, { codeBlockFontFamily: this._configurationService.getValue('editor').fontFamily || EDITOR_FONT_DEFAULTS.fontFamily });
const { element } = mdRenderer.render(markdown, {
actionHandler: {
callback: (content) => this._linkHandler(content),
disposables: this._messageListeners
},
asyncRenderCallback: () => {
contentsElement.classList.add('code-hover-contents');
this.layout();
// This changes the dimensions of the hover so trigger a layout
this._onRequestLayout.fire();
}
});
contentsElement.appendChild(element);
}
rowElement.appendChild(contentsElement);
this._hover.contentsDomNode.appendChild(rowElement);
if (options.actions && options.actions.length > 0) {
const statusBarElement = $('div.hover-row.status-bar');
const actionsElement = $('div.actions');
options.actions.forEach(action => {
const keybinding = this._keybindingService.lookupKeybinding(action.commandId);
const keybindingLabel = keybinding ? keybinding.getLabel() : null;
HoverAction.render(actionsElement, {
label: action.label,
commandId: action.commandId,
run: e => {
action.run(e);
this.dispose();
},
iconClass: action.iconClass
}, keybindingLabel);
});
statusBarElement.appendChild(actionsElement);
this._hover.containerDomNode.appendChild(statusBarElement);
}
this._hoverContainer = $('div.workbench-hover-container');
if (this._hoverPointer) {
this._hoverContainer.appendChild(this._hoverPointer);
}
this._hoverContainer.appendChild(this._hover.containerDomNode);
// Determine whether to hide on hover
let hideOnHover;
if (options.actions && options.actions.length > 0) {
// If there are actions, require hover so they can be accessed
hideOnHover = false;
}
else {
if (options.persistence?.hideOnHover === undefined) {
// When unset, will default to true when it's a string or when it's markdown that
// appears to have a link using a naive check for '](' and '</a>'
hideOnHover = typeof options.content === 'string' ||
isMarkdownString(options.content) && !options.content.value.includes('](') && !options.content.value.includes('</a>');
}
else {
// It's set explicitly
hideOnHover = options.persistence.hideOnHover;
}
}
// Show the hover hint if needed
if (options.appearance?.showHoverHint) {
const statusBarElement = $('div.hover-row.status-bar');
const infoElement = $('div.info');
infoElement.textContent = localize('hoverhint', 'Hold {0} key to mouse over', isMacintosh ? 'Option' : 'Alt');
statusBarElement.appendChild(infoElement);
this._hover.containerDomNode.appendChild(statusBarElement);
}
const mouseTrackerTargets = [...this._target.targetElements];
if (!hideOnHover) {
mouseTrackerTargets.push(this._hoverContainer);
}
const mouseTracker = this._register(new CompositeMouseTracker(mouseTrackerTargets));
this._register(mouseTracker.onMouseOut(() => {
if (!this._isLocked) {
this.dispose();
}
}));
// Setup another mouse tracker when hideOnHover is set in order to track the hover as well
// when it is locked. This ensures the hover will hide on mouseout after alt has been
// released to unlock the element.
if (hideOnHover) {
const mouseTracker2Targets = [...this._target.targetElements, this._hoverContainer];
this._lockMouseTracker = this._register(new CompositeMouseTracker(mouseTracker2Targets));
this._register(this._lockMouseTracker.onMouseOut(() => {
if (!this._isLocked) {
this.dispose();
}
}));
}
else {
this._lockMouseTracker = mouseTracker;
}
}
addFocusTrap() {
if (!this._enableFocusTraps || this._addedFocusTrap) {
return;
}
this._addedFocusTrap = true;
// Add a hover tab loop if the hover has at least one element with a valid tabIndex
const firstContainerFocusElement = this._hover.containerDomNode;
const lastContainerFocusElement = this.findLastFocusableChild(this._hover.containerDomNode);
if (lastContainerFocusElement) {
const beforeContainerFocusElement = dom.prepend(this._hoverContainer, $('div'));
const afterContainerFocusElement = dom.append(this._hoverContainer, $('div'));
beforeContainerFocusElement.tabIndex = 0;
afterContainerFocusElement.tabIndex = 0;
this._register(dom.addDisposableListener(afterContainerFocusElement, 'focus', (e) => {
firstContainerFocusElement.focus();
e.preventDefault();
}));
this._register(dom.addDisposableListener(beforeContainerFocusElement, 'focus', (e) => {
lastContainerFocusElement.focus();
e.preventDefault();
}));
}
}
findLastFocusableChild(root) {
if (root.hasChildNodes()) {
for (let i = 0; i < root.childNodes.length; i++) {
const node = root.childNodes.item(root.childNodes.length - i - 1);
if (node.nodeType === node.ELEMENT_NODE) {
const parsedNode = node;
if (typeof parsedNode.tabIndex === 'number' && parsedNode.tabIndex >= 0) {
return parsedNode;
}
}
const recursivelyFoundElement = this.findLastFocusableChild(node);
if (recursivelyFoundElement) {
return recursivelyFoundElement;
}
}
}
return undefined;
}
render(container) {
container.appendChild(this._hoverContainer);
const hoverFocused = this._hoverContainer.contains(this._hoverContainer.ownerDocument.activeElement);
const accessibleViewHint = hoverFocused && getHoverAccessibleViewHint(this._configurationService.getValue('accessibility.verbosity.hover') === true && this._accessibilityService.isScreenReaderOptimized(), this._keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel());
if (accessibleViewHint) {
status(accessibleViewHint);
}
this.layout();
this.addFocusTrap();
}
layout() {
this._hover.containerDomNode.classList.remove('right-aligned');
this._hover.contentsDomNode.style.maxHeight = '';
const getZoomAccountedBoundingClientRect = (e) => {
const zoom = dom.getDomNodeZoomLevel(e);
const boundingRect = e.getBoundingClientRect();
return {
top: boundingRect.top * zoom,
bottom: boundingRect.bottom * zoom,
right: boundingRect.right * zoom,
left: boundingRect.left * zoom,
};
};
const targetBounds = this._target.targetElements.map(e => getZoomAccountedBoundingClientRect(e));
const { top, right, bottom, left } = targetBounds[0];
const width = right - left;
const height = bottom - top;
const targetRect = {
top, right, bottom, left, width, height,
center: {
x: left + (width / 2),
y: top + (height / 2)
}
};
// These calls adjust the position depending on spacing.
this.adjustHorizontalHoverPosition(targetRect);
this.adjustVerticalHoverPosition(targetRect);
// This call limits the maximum height of the hover.
this.adjustHoverMaxHeight(targetRect);
// Offset the hover position if there is a pointer so it aligns with the target element
this._hoverContainer.style.padding = '';
this._hoverContainer.style.margin = '';
if (this._hoverPointer) {
switch (this._hoverPosition) {
case 1 /* HoverPosition.RIGHT */:
targetRect.left += 3 /* Constants.PointerSize */;
targetRect.right += 3 /* Constants.PointerSize */;
this._hoverContainer.style.paddingLeft = `${3 /* Constants.PointerSize */}px`;
this._hoverContainer.style.marginLeft = `${-3 /* Constants.PointerSize */}px`;
break;
case 0 /* HoverPosition.LEFT */:
targetRect.left -= 3 /* Constants.PointerSize */;
targetRect.right -= 3 /* Constants.PointerSize */;
this._hoverContainer.style.paddingRight = `${3 /* Constants.PointerSize */}px`;
this._hoverContainer.style.marginRight = `${-3 /* Constants.PointerSize */}px`;
break;
case 2 /* HoverPosition.BELOW */:
targetRect.top += 3 /* Constants.PointerSize */;
targetRect.bottom += 3 /* Constants.PointerSize */;
this._hoverContainer.style.paddingTop = `${3 /* Constants.PointerSize */}px`;
this._hoverContainer.style.marginTop = `${-3 /* Constants.PointerSize */}px`;
break;
case 3 /* HoverPosition.ABOVE */:
targetRect.top -= 3 /* Constants.PointerSize */;
targetRect.bottom -= 3 /* Constants.PointerSize */;
this._hoverContainer.style.paddingBottom = `${3 /* Constants.PointerSize */}px`;
this._hoverContainer.style.marginBottom = `${-3 /* Constants.PointerSize */}px`;
break;
}
targetRect.center.x = targetRect.left + (width / 2);
targetRect.center.y = targetRect.top + (height / 2);
}
this.computeXCordinate(targetRect);
this.computeYCordinate(targetRect);
if (this._hoverPointer) {
// reset
this._hoverPointer.classList.remove('top');
this._hoverPointer.classList.remove('left');
this._hoverPointer.classList.remove('right');
this._hoverPointer.classList.remove('bottom');
this.setHoverPointerPosition(targetRect);
}
this._hover.onContentsChanged();
}
computeXCordinate(target) {
const hoverWidth = this._hover.containerDomNode.clientWidth + 2 /* Constants.HoverBorderWidth */;
if (this._target.x !== undefined) {
this._x = this._target.x;
}
else if (this._hoverPosition === 1 /* HoverPosition.RIGHT */) {
this._x = target.right;
}
else if (this._hoverPosition === 0 /* HoverPosition.LEFT */) {
this._x = target.left - hoverWidth;
}
else {
if (this._hoverPointer) {
this._x = target.center.x - (this._hover.containerDomNode.clientWidth / 2);
}
else {
this._x = target.left;
}
// Hover is going beyond window towards right end
if (this._x + hoverWidth >= this._targetDocumentElement.clientWidth) {
this._hover.containerDomNode.classList.add('right-aligned');
this._x = Math.max(this._targetDocumentElement.clientWidth - hoverWidth - 2 /* Constants.HoverWindowEdgeMargin */, this._targetDocumentElement.clientLeft);
}
}
// Hover is going beyond window towards left end
if (this._x < this._targetDocumentElement.clientLeft) {
this._x = target.left + 2 /* Constants.HoverWindowEdgeMargin */;
}
}
computeYCordinate(target) {
if (this._target.y !== undefined) {
this._y = this._target.y;
}
else if (this._hoverPosition === 3 /* HoverPosition.ABOVE */) {
this._y = target.top;
}
else if (this._hoverPosition === 2 /* HoverPosition.BELOW */) {
this._y = target.bottom - 2;
}
else {
if (this._hoverPointer) {
this._y = target.center.y + (this._hover.containerDomNode.clientHeight / 2);
}
else {
this._y = target.bottom;
}
}
// Hover on bottom is going beyond window
if (this._y > this._targetWindow.innerHeight) {
this._y = target.bottom;
}
}
adjustHorizontalHoverPosition(target) {
// Do not adjust horizontal hover position if x cordiante is provided
if (this._target.x !== undefined) {
return;
}
const hoverPointerOffset = (this._hoverPointer ? 3 /* Constants.PointerSize */ : 0);
// When force position is enabled, restrict max width
if (this._forcePosition) {
const padding = hoverPointerOffset + 2 /* Constants.HoverBorderWidth */;
if (this._hoverPosition === 1 /* HoverPosition.RIGHT */) {
this._hover.containerDomNode.style.maxWidth = `${this._targetDocumentElement.clientWidth - target.right - padding}px`;
}
else if (this._hoverPosition === 0 /* HoverPosition.LEFT */) {
this._hover.containerDomNode.style.maxWidth = `${target.left - padding}px`;
}
return;
}
// Position hover on right to target
if (this._hoverPosition === 1 /* HoverPosition.RIGHT */) {
const roomOnRight = this._targetDocumentElement.clientWidth - target.right;
// Hover on the right is going beyond window.
if (roomOnRight < this._hover.containerDomNode.clientWidth + hoverPointerOffset) {
const roomOnLeft = target.left;
// There's enough room on the left, flip the hover position
if (roomOnLeft >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) {
this._hoverPosition = 0 /* HoverPosition.LEFT */;
}
// Hover on the left would go beyond window too
else {
this._hoverPosition = 2 /* HoverPosition.BELOW */;
}
}
}
// Position hover on left to target
else if (this._hoverPosition === 0 /* HoverPosition.LEFT */) {
const roomOnLeft = target.left;
// Hover on the left is going beyond window.
if (roomOnLeft < this._hover.containerDomNode.clientWidth + hoverPointerOffset) {
const roomOnRight = this._targetDocumentElement.clientWidth - target.right;
// There's enough room on the right, flip the hover position
if (roomOnRight >= this._hover.containerDomNode.clientWidth + hoverPointerOffset) {
this._hoverPosition = 1 /* HoverPosition.RIGHT */;
}
// Hover on the right would go beyond window too
else {
this._hoverPosition = 2 /* HoverPosition.BELOW */;
}
}
// Hover on the left is going beyond window.
if (target.left - this._hover.containerDomNode.clientWidth - hoverPointerOffset <= this._targetDocumentElement.clientLeft) {
this._hoverPosition = 1 /* HoverPosition.RIGHT */;
}
}
}
adjustVerticalHoverPosition(target) {
// Do not adjust vertical hover position if the y coordinate is provided
// or the position is forced
if (this._target.y !== undefined || this._forcePosition) {
return;
}
const hoverPointerOffset = (this._hoverPointer ? 3 /* Constants.PointerSize */ : 0);
// Position hover on top of the target
if (this._hoverPosition === 3 /* HoverPosition.ABOVE */) {
// Hover on top is going beyond window
if (target.top - this._hover.containerDomNode.clientHeight - hoverPointerOffset < 0) {
this._hoverPosition = 2 /* HoverPosition.BELOW */;
}
}
// Position hover below the target
else if (this._hoverPosition === 2 /* HoverPosition.BELOW */) {
// Hover on bottom is going beyond window
if (target.bottom + this._hover.containerDomNode.clientHeight + hoverPointerOffset > this._targetWindow.innerHeight) {
this._hoverPosition = 3 /* HoverPosition.ABOVE */;
}
}
}
adjustHoverMaxHeight(target) {
let maxHeight = this._targetWindow.innerHeight / 2;
// When force position is enabled, restrict max height
if (this._forcePosition) {
const padding = (this._hoverPointer ? 3 /* Constants.PointerSize */ : 0) + 2 /* Constants.HoverBorderWidth */;
if (this._hoverPosition === 3 /* HoverPosition.ABOVE */) {
maxHeight = Math.min(maxHeight, target.top - padding);
}
else if (this._hoverPosition === 2 /* HoverPosition.BELOW */) {
maxHeight = Math.min(maxHeight, this._targetWindow.innerHeight - target.bottom - padding);
}
}
this._hover.containerDomNode.style.maxHeight = `${maxHeight}px`;
if (this._hover.contentsDomNode.clientHeight < this._hover.contentsDomNode.scrollHeight) {
// Add padding for a vertical scrollbar
const extraRightPadding = `${this._hover.scrollbar.options.verticalScrollbarSize}px`;
if (this._hover.contentsDomNode.style.paddingRight !== extraRightPadding) {
this._hover.contentsDomNode.style.paddingRight = extraRightPadding;
}
}
}
setHoverPointerPosition(target) {
if (!this._hoverPointer) {
return;
}
switch (this._hoverPosition) {
case 0 /* HoverPosition.LEFT */:
case 1 /* HoverPosition.RIGHT */: {
this._hoverPointer.classList.add(this._hoverPosition === 0 /* HoverPosition.LEFT */ ? 'right' : 'left');
const hoverHeight = this._hover.containerDomNode.clientHeight;
// If hover is taller than target, then show the pointer at the center of target
if (hoverHeight > target.height) {
this._hoverPointer.style.top = `${target.center.y - (this._y - hoverHeight) - 3 /* Constants.PointerSize */}px`;
}
// Otherwise show the pointer at the center of hover
else {
this._hoverPointer.style.top = `${Math.round((hoverHeight / 2)) - 3 /* Constants.PointerSize */}px`;
}
break;
}
case 3 /* HoverPosition.ABOVE */:
case 2 /* HoverPosition.BELOW */: {
this._hoverPointer.classList.add(this._hoverPosition === 3 /* HoverPosition.ABOVE */ ? 'bottom' : 'top');
const hoverWidth = this._hover.containerDomNode.clientWidth;
// Position pointer at the center of the hover
let pointerLeftPosition = Math.round((hoverWidth / 2)) - 3 /* Constants.PointerSize */;
// If pointer goes beyond target then position it at the center of the target
const pointerX = this._x + pointerLeftPosition;
if (pointerX < target.left || pointerX > target.right) {
pointerLeftPosition = target.center.x - this._x - 3 /* Constants.PointerSize */;
}
this._hoverPointer.style.left = `${pointerLeftPosition}px`;
break;
}
}
}
focus() {
this._hover.containerDomNode.focus();
}
dispose() {
if (!this._isDisposed) {
this._onDispose.fire();
this._hoverContainer.remove();
this._messageListeners.dispose();
this._target.dispose();
super.dispose();
}
this._isDisposed = true;
}
};
HoverWidget = __decorate([
__param(1, IKeybindingService),
__param(2, IConfigurationService),
__param(3, IOpenerService),
__param(4, IInstantiationService),
__param(5, IAccessibilityService)
], HoverWidget);
export { HoverWidget };
class CompositeMouseTracker extends Widget {
get onMouseOut() { return this._onMouseOut.event; }
get isMouseIn() { return this._isMouseIn; }
constructor(_elements) {
super();
this._elements = _elements;
this._isMouseIn = true;
this._onMouseOut = this._register(new Emitter());
this._elements.forEach(n => this.onmouseover(n, () => this._onTargetMouseOver(n)));
this._elements.forEach(n => this.onmouseleave(n, () => this._onTargetMouseLeave(n)));
}
_onTargetMouseOver(target) {
this._isMouseIn = true;
this._clearEvaluateMouseStateTimeout(target);
}
_onTargetMouseLeave(target) {
this._isMouseIn = false;
this._evaluateMouseState(target);
}
_evaluateMouseState(target) {
this._clearEvaluateMouseStateTimeout(target);
// Evaluate whether the mouse is still outside asynchronously such that other mouse targets
// have the opportunity to first their mouse in event.
this._mouseTimeout = dom.getWindow(target).setTimeout(() => this._fireIfMouseOutside(), 0);
}
_clearEvaluateMouseStateTimeout(target) {
if (this._mouseTimeout) {
dom.getWindow(target).clearTimeout(this._mouseTimeout);
this._mouseTimeout = undefined;
}
}
_fireIfMouseOutside() {
if (!this._isMouseIn) {
this._onMouseOut.fire();
}
}
}
class ElementHoverTarget {
constructor(_element) {
this._element = _element;
this.targetElements = [this._element];
}
dispose() {
}
}