Version 1.0

This commit is contained in:
Alexander R.
2026-07-21 22:24:25 +00:00
parent f3645fbfbc
commit 9edbfd3f77
4134 changed files with 1448752 additions and 1 deletions
+57
View File
@@ -0,0 +1,57 @@
import { mainWindow } from './window.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class WindowManager {
constructor() {
// --- Zoom Factor
this.mapWindowIdToZoomFactor = new Map();
}
static { this.INSTANCE = new WindowManager(); }
getZoomFactor(targetWindow) {
return this.mapWindowIdToZoomFactor.get(this.getWindowId(targetWindow)) ?? 1;
}
getWindowId(targetWindow) {
return targetWindow.vscodeWindowId;
}
}
function addMatchMediaChangeListener(targetWindow, query, callback) {
if (typeof query === 'string') {
query = targetWindow.matchMedia(query);
}
query.addEventListener('change', callback);
}
/** The zoom scale for an index, e.g. 1, 1.2, 1.4 */
function getZoomFactor(targetWindow) {
return WindowManager.INSTANCE.getZoomFactor(targetWindow);
}
const userAgent = navigator.userAgent;
const isFirefox = (userAgent.indexOf('Firefox') >= 0);
const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0);
const isChrome = (userAgent.indexOf('Chrome') >= 0);
const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0));
const isWebkitWebView = (!isChrome && !isSafari && isWebKit);
(userAgent.indexOf('Electron/') >= 0);
const isAndroid = (userAgent.indexOf('Android') >= 0);
let standalone = false;
if (typeof mainWindow.matchMedia === 'function') {
const standaloneMatchMedia = mainWindow.matchMedia('(display-mode: standalone) or (display-mode: window-controls-overlay)');
const fullScreenMatchMedia = mainWindow.matchMedia('(display-mode: fullscreen)');
standalone = standaloneMatchMedia.matches;
addMatchMediaChangeListener(mainWindow, standaloneMatchMedia, ({ matches }) => {
// entering fullscreen would change standaloneMatchMedia.matches to false
// if standalone is true (running as PWA) and entering fullscreen, skip this change
if (standalone && fullScreenMatchMedia.matches) {
return;
}
// otherwise update standalone (browser to PWA or PWA to browser)
standalone = matches;
});
}
function getMonacoEnvironment() {
return globalThis.MonacoEnvironment;
}
export { addMatchMediaChangeListener, getMonacoEnvironment, getZoomFactor, isAndroid, isChrome, isFirefox, isSafari, isWebKit, isWebkitWebView };
+23
View File
@@ -0,0 +1,23 @@
import './browser.js';
import { mainWindow } from './window.js';
import { isNative } from '../common/platform.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Browser feature we can support in current platform, browser and environment.
*/
const BrowserFeatures = {
clipboard: {
writeText: (isNative
|| (document.queryCommandSupported && document.queryCommandSupported('copy'))
|| !!(navigator && navigator.clipboard && navigator.clipboard.writeText)),
readText: (isNative
|| !!(navigator && navigator.clipboard && navigator.clipboard.readText))
},
pointerEvents: mainWindow.PointerEvent && ('ontouchstart' in mainWindow || navigator.maxTouchPoints > 0)
};
export { BrowserFeatures };
+69
View File
@@ -0,0 +1,69 @@
import { FileAccess } from '../common/network.js';
function asFragment(raw) {
return raw;
}
function asCssValueWithDefault(cssPropertyValue, dflt) {
if (cssPropertyValue !== undefined) {
const variableMatch = cssPropertyValue.match(/^\s*var\((.+)\)$/);
if (variableMatch) {
const varArguments = variableMatch[1].split(',', 2);
if (varArguments.length === 2) {
dflt = asCssValueWithDefault(varArguments[1].trim(), dflt);
}
return `var(${varArguments[0]}, ${dflt})`;
}
return cssPropertyValue;
}
return dflt;
}
function identValue(value) {
const out = value.replaceAll(/[^_\-a-z0-9]/gi, '');
if (out !== value) {
console.warn(`CSS ident value ${value} modified to ${out} to be safe for CSS`);
}
return asFragment(out);
}
function stringValue(value) {
return asFragment(`'${value.replaceAll(/'/g, '\\000027')}'`);
}
/**
* returns url('...')
*/
function asCSSUrl(uri) {
if (!uri) {
return asFragment(`url('')`);
}
return inline `url('${asFragment(CSS.escape(FileAccess.uriToBrowserUri(uri).toString(true)))}')`;
}
function className(value, escapingExpected = false) {
const out = CSS.escape(value);
if (!escapingExpected && out !== value) {
console.warn(`CSS class name ${value} modified to ${out} to be safe for CSS`);
}
return asFragment(out);
}
/**
* Template string tag that that constructs a CSS fragment.
*
* All expressions in the template must be css safe values.
*/
function inline(strings, ...values) {
return asFragment(strings.reduce((result, str, i) => {
const value = values[i] || '';
return result + str + value;
}, ''));
}
class Builder {
constructor() {
this._parts = [];
}
push(...parts) {
this._parts.push(...parts);
}
join(joiner = '\n') {
return asFragment(this._parts.join(joiner));
}
}
export { Builder, asCSSUrl, asCssValueWithDefault, className, identValue, inline, stringValue };
+21
View File
@@ -0,0 +1,21 @@
import { Mimes } from '../common/mime.js';
// Common data transfers
const DataTransfers = {
/**
* Application specific resource transfer type
*/
RESOURCES: 'ResourceURLs',
/**
* Typically transfer type for copy/paste transfers.
*/
TEXT: Mimes.text,
/**
* Internal type used to pass around text/uri-list data.
*
* This is needed to work around https://bugs.chromium.org/p/chromium/issues/detail?id=239745.
*/
INTERNAL_URI_LIST: 'application/vnd.code.uri-list',
};
export { DataTransfers };
File diff suppressed because it is too large Load Diff
+312
View File
@@ -0,0 +1,312 @@
import { Schemas } from '../common/network.js';
import { reset } from './dom.js';
import purify from './dompurify/dompurify.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* List of safe, non-input html tags.
*/
const basicMarkupHtmlTags = Object.freeze([
'a',
'abbr',
'b',
'bdo',
'blockquote',
'br',
'caption',
'cite',
'code',
'col',
'colgroup',
'dd',
'del',
'details',
'dfn',
'div',
'dl',
'dt',
'em',
'figcaption',
'figure',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'hr',
'i',
'img',
'ins',
'kbd',
'label',
'li',
'mark',
'ol',
'p',
'pre',
'q',
'rp',
'rt',
'ruby',
's',
'samp',
'small',
'small',
'source',
'span',
'strike',
'strong',
'sub',
'summary',
'sup',
'table',
'tbody',
'td',
'tfoot',
'th',
'thead',
'time',
'tr',
'tt',
'u',
'ul',
'var',
'video',
'wbr',
]);
const defaultAllowedAttrs = Object.freeze([
'href',
'target',
'src',
'alt',
'title',
'for',
'name',
'role',
'tabindex',
'x-dispatch',
'required',
'checked',
'placeholder',
'type',
'start',
'width',
'height',
'align',
]);
const fakeRelativeUrlProtocol = 'vscode-relative-path';
function validateLink(value, allowedProtocols) {
if (allowedProtocols.override === '*') {
return true; // allow all protocols
}
try {
const url = new URL(value, fakeRelativeUrlProtocol + '://');
if (allowedProtocols.override.includes(url.protocol.replace(/:$/, ''))) {
return true;
}
if (allowedProtocols.allowRelativePaths
&& url.protocol === fakeRelativeUrlProtocol + ':'
&& !value.trim().toLowerCase().startsWith(fakeRelativeUrlProtocol)) {
return true;
}
return false;
}
catch (e) {
return false;
}
}
/**
* Hooks dompurify using `afterSanitizeAttributes` to check that all `href` and `src`
* attributes are valid.
*/
function hookDomPurifyHrefAndSrcSanitizer(allowedLinkProtocols, allowedMediaProtocols) {
purify.addHook('afterSanitizeAttributes', (node) => {
// check all href/src attributes for validity
for (const attr of ['href', 'src']) {
if (node.hasAttribute(attr)) {
const attrValue = node.getAttribute(attr);
if (attr === 'href') {
if (!attrValue.startsWith('#') && !validateLink(attrValue, allowedLinkProtocols)) {
node.removeAttribute(attr);
}
}
else { // 'src'
if (!validateLink(attrValue, allowedMediaProtocols)) {
node.removeAttribute(attr);
}
}
}
}
});
}
const defaultDomPurifyConfig = Object.freeze({
ALLOWED_TAGS: [...basicMarkupHtmlTags],
ALLOWED_ATTR: [...defaultAllowedAttrs],
// We sanitize the src/href attributes later if needed
ALLOW_UNKNOWN_PROTOCOLS: true,
});
/**
* Sanitizes an html string.
*
* @param untrusted The HTML string to sanitize.
* @param config Optional configuration for sanitization. If not provided, defaults to a safe configuration.
*
* @returns A sanitized string of html.
*/
function sanitizeHtml(untrusted, config) {
return doSanitizeHtml(untrusted, config, 'trusted');
}
function doSanitizeHtml(untrusted, config, outputType) {
try {
const resolvedConfig = { ...defaultDomPurifyConfig };
if (config?.allowedTags) {
if (config.allowedTags.override) {
resolvedConfig.ALLOWED_TAGS = [...config.allowedTags.override];
}
if (config.allowedTags.augment) {
resolvedConfig.ALLOWED_TAGS = [...(resolvedConfig.ALLOWED_TAGS ?? []), ...config.allowedTags.augment];
}
}
let resolvedAttributes = [...defaultAllowedAttrs];
if (config?.allowedAttributes) {
if (config.allowedAttributes.override) {
resolvedAttributes = [...config.allowedAttributes.override];
}
if (config.allowedAttributes.augment) {
resolvedAttributes = [...resolvedAttributes, ...config.allowedAttributes.augment];
}
}
// All attr names are lower-case in the sanitizer hooks
resolvedAttributes = resolvedAttributes.map((attr) => {
if (typeof attr === 'string') {
return attr.toLowerCase();
}
return {
attributeName: attr.attributeName.toLowerCase(),
shouldKeep: attr.shouldKeep,
};
});
const allowedAttrNames = new Set(resolvedAttributes.map(attr => typeof attr === 'string' ? attr : attr.attributeName));
const allowedAttrPredicates = new Map();
for (const attr of resolvedAttributes) {
if (typeof attr === 'string') {
// New string attribute value clears previously set predicates
allowedAttrPredicates.delete(attr);
}
else {
allowedAttrPredicates.set(attr.attributeName, attr);
}
}
resolvedConfig.ALLOWED_ATTR = Array.from(allowedAttrNames);
hookDomPurifyHrefAndSrcSanitizer({
override: config?.allowedLinkProtocols?.override ?? [Schemas.http, Schemas.https],
allowRelativePaths: config?.allowRelativeLinkPaths ?? false
}, {
override: config?.allowedMediaProtocols?.override ?? [Schemas.http, Schemas.https],
allowRelativePaths: config?.allowRelativeMediaPaths ?? false
});
if (config?.replaceWithPlaintext) {
purify.addHook('uponSanitizeElement', replaceWithPlainTextHook);
}
if (allowedAttrPredicates.size) {
purify.addHook('uponSanitizeAttribute', (node, e) => {
const predicate = allowedAttrPredicates.get(e.attrName);
if (predicate) {
const result = predicate.shouldKeep(node, e);
if (typeof result === 'string') {
e.keepAttr = true;
e.attrValue = result;
}
else {
e.keepAttr = result;
}
}
else {
e.keepAttr = allowedAttrNames.has(e.attrName);
}
});
}
if (outputType === 'dom') {
return purify.sanitize(untrusted, {
...resolvedConfig,
RETURN_DOM_FRAGMENT: true
});
}
else {
return purify.sanitize(untrusted, {
...resolvedConfig,
RETURN_TRUSTED_TYPE: true
}); // Cast from lib TrustedHTML to global TrustedHTML
}
}
finally {
purify.removeAllHooks();
}
}
const selfClosingTags = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
const replaceWithPlainTextHook = (node, data, _config) => {
if (!data.allowedTags[data.tagName] && data.tagName !== 'body') {
const replacement = convertTagToPlaintext(node);
if (replacement) {
if (node.nodeType === Node.COMMENT_NODE) {
// Workaround for https://github.com/cure53/DOMPurify/issues/1005
// The comment will be deleted in the next phase. However if we try to remove it now, it will cause
// an exception. Instead we insert the text node before the comment.
node.parentElement?.insertBefore(replacement, node);
}
else {
node.parentElement?.replaceChild(replacement, node);
}
}
}
};
function convertTagToPlaintext(node) {
if (!node.ownerDocument) {
return;
}
let startTagText;
let endTagText;
if (node.nodeType === Node.COMMENT_NODE) {
startTagText = `<!--${node.textContent}-->`;
}
else if (node instanceof Element) {
const tagName = node.tagName.toLowerCase();
const isSelfClosing = selfClosingTags.includes(tagName);
const attrString = node.attributes.length ?
' ' + Array.from(node.attributes)
.map(attr => `${attr.name}="${attr.value}"`)
.join(' ')
: '';
startTagText = `<${tagName}${attrString}>`;
if (!isSelfClosing) {
endTagText = `</${tagName}>`;
}
}
else {
return;
}
const fragment = document.createDocumentFragment();
const textNode = node.ownerDocument.createTextNode(startTagText);
fragment.appendChild(textNode);
while (node.firstChild) {
fragment.appendChild(node.firstChild);
}
const endTagTextNode = endTagText ? node.ownerDocument.createTextNode(endTagText) : undefined;
if (endTagTextNode) {
fragment.appendChild(endTagTextNode);
}
return fragment;
}
/**
* Sanitizes the given `value` and reset the given `node` with it.
*/
function safeSetInnerHtml(node, untrusted, config) {
const fragment = doSanitizeHtml(untrusted, config, 'dom');
reset(node, fragment);
}
export { basicMarkupHtmlTags, convertTagToPlaintext, defaultAllowedAttrs, safeSetInnerHtml, sanitizeHtml };
@@ -0,0 +1,145 @@
import { DisposableStore, toDisposable } from '../common/lifecycle.js';
import '../common/observableInternal/index.js';
import { isFirefox } from './browser.js';
import { getWindows, sharedMutationObserver } from './dom.js';
import { mainWindow } from './window.js';
import { autorun } from '../common/observableInternal/reactions/autorun.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const globalStylesheets = new Map();
/**
* A version of createStyleSheet which has a unified API to initialize/set the style content.
*/
function createStyleSheet2() {
return new WrappedStyleElement();
}
class WrappedStyleElement {
constructor() {
this._currentCssStyle = '';
this._styleSheet = undefined;
}
setStyle(cssStyle) {
if (cssStyle === this._currentCssStyle) {
return;
}
this._currentCssStyle = cssStyle;
if (!this._styleSheet) {
this._styleSheet = createStyleSheet(mainWindow.document.head, (s) => s.textContent = cssStyle);
}
else {
this._styleSheet.textContent = cssStyle;
}
}
dispose() {
if (this._styleSheet) {
this._styleSheet.remove();
this._styleSheet = undefined;
}
}
}
function createStyleSheet(container = mainWindow.document.head, beforeAppend, disposableStore) {
const style = document.createElement('style');
style.type = 'text/css';
style.media = 'screen';
beforeAppend?.(style);
container.appendChild(style);
if (disposableStore) {
disposableStore.add(toDisposable(() => style.remove()));
}
// With <head> as container, the stylesheet becomes global and is tracked
// to support auxiliary windows to clone the stylesheet.
if (container === mainWindow.document.head) {
const globalStylesheetClones = new Set();
globalStylesheets.set(style, globalStylesheetClones);
if (disposableStore) {
disposableStore.add(toDisposable(() => globalStylesheets.delete(style)));
}
for (const { window: targetWindow, disposables } of getWindows()) {
if (targetWindow === mainWindow) {
continue; // main window is already tracked
}
const cloneDisposable = disposables.add(cloneGlobalStyleSheet(style, globalStylesheetClones, targetWindow));
disposableStore?.add(cloneDisposable);
}
}
return style;
}
function cloneGlobalStyleSheet(globalStylesheet, globalStylesheetClones, targetWindow) {
const disposables = new DisposableStore();
const clone = globalStylesheet.cloneNode(true);
targetWindow.document.head.appendChild(clone);
disposables.add(toDisposable(() => clone.remove()));
for (const rule of getDynamicStyleSheetRules(globalStylesheet)) {
clone.sheet?.insertRule(rule.cssText, clone.sheet?.cssRules.length);
}
disposables.add(sharedMutationObserver.observe(globalStylesheet, disposables, { childList: true, subtree: isFirefox, characterData: isFirefox })(() => {
clone.textContent = globalStylesheet.textContent;
}));
globalStylesheetClones.add(clone);
disposables.add(toDisposable(() => globalStylesheetClones.delete(clone)));
return disposables;
}
let _sharedStyleSheet = null;
function getSharedStyleSheet() {
if (!_sharedStyleSheet) {
_sharedStyleSheet = createStyleSheet();
}
return _sharedStyleSheet;
}
function getDynamicStyleSheetRules(style) {
if (style?.sheet?.rules) {
// Chrome, IE
return style.sheet.rules;
}
if (style?.sheet?.cssRules) {
// FF
return style.sheet.cssRules;
}
return [];
}
function createCSSRule(selector, cssText, style = getSharedStyleSheet()) {
if (!style || !cssText) {
return;
}
style.sheet?.insertRule(`${selector} {${cssText}}`, 0);
// Apply rule also to all cloned global stylesheets
for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) {
createCSSRule(selector, cssText, clonedGlobalStylesheet);
}
}
function removeCSSRulesContainingSelector(ruleName, style = getSharedStyleSheet()) {
if (!style) {
return;
}
const rules = getDynamicStyleSheetRules(style);
const toDelete = [];
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
if (isCSSStyleRule(rule) && rule.selectorText.indexOf(ruleName) !== -1) {
toDelete.push(i);
}
}
for (let i = toDelete.length - 1; i >= 0; i--) {
style.sheet?.deleteRule(toDelete[i]);
}
// Remove rules also from all cloned global stylesheets
for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) {
removeCSSRulesContainingSelector(ruleName, clonedGlobalStylesheet);
}
}
function isCSSStyleRule(rule) {
return typeof rule.selectorText === 'string';
}
function createStyleSheetFromObservable(css) {
const store = new DisposableStore();
const w = store.add(createStyleSheet2());
store.add(autorun(reader => {
w.setStyle(css.read(reader));
}));
return store;
}
export { createCSSRule, createStyleSheet, createStyleSheet2, createStyleSheetFromObservable, removeCSSRulesContainingSelector };
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
import { Emitter } from '../common/event.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class DomEmitter {
get event() {
return this.emitter.event;
}
constructor(element, type, useCapture) {
const fn = (e) => this.emitter.fire(e);
this.emitter = new Emitter({
onWillAddFirstListener: () => element.addEventListener(type, fn, useCapture),
onDidRemoveLastListener: () => element.removeEventListener(type, fn, useCapture)
});
}
dispose() {
this.emitter.dispose();
}
}
export { DomEmitter };
+255
View File
@@ -0,0 +1,255 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class FastDomNode {
constructor(domNode) {
this.domNode = domNode;
this._maxWidth = '';
this._width = '';
this._height = '';
this._top = '';
this._left = '';
this._bottom = '';
this._right = '';
this._paddingLeft = '';
this._fontFamily = '';
this._fontWeight = '';
this._fontSize = '';
this._fontStyle = '';
this._fontFeatureSettings = '';
this._fontVariationSettings = '';
this._textDecoration = '';
this._lineHeight = '';
this._letterSpacing = '';
this._className = '';
this._display = '';
this._position = '';
this._visibility = '';
this._color = '';
this._backgroundColor = '';
this._layerHint = false;
this._contain = 'none';
this._boxShadow = '';
}
focus() {
this.domNode.focus();
}
setMaxWidth(_maxWidth) {
const maxWidth = numberAsPixels(_maxWidth);
if (this._maxWidth === maxWidth) {
return;
}
this._maxWidth = maxWidth;
this.domNode.style.maxWidth = this._maxWidth;
}
setWidth(_width) {
const width = numberAsPixels(_width);
if (this._width === width) {
return;
}
this._width = width;
this.domNode.style.width = this._width;
}
setHeight(_height) {
const height = numberAsPixels(_height);
if (this._height === height) {
return;
}
this._height = height;
this.domNode.style.height = this._height;
}
setTop(_top) {
const top = numberAsPixels(_top);
if (this._top === top) {
return;
}
this._top = top;
this.domNode.style.top = this._top;
}
setLeft(_left) {
const left = numberAsPixels(_left);
if (this._left === left) {
return;
}
this._left = left;
this.domNode.style.left = this._left;
}
setBottom(_bottom) {
const bottom = numberAsPixels(_bottom);
if (this._bottom === bottom) {
return;
}
this._bottom = bottom;
this.domNode.style.bottom = this._bottom;
}
setRight(_right) {
const right = numberAsPixels(_right);
if (this._right === right) {
return;
}
this._right = right;
this.domNode.style.right = this._right;
}
setPaddingLeft(_paddingLeft) {
const paddingLeft = numberAsPixels(_paddingLeft);
if (this._paddingLeft === paddingLeft) {
return;
}
this._paddingLeft = paddingLeft;
this.domNode.style.paddingLeft = this._paddingLeft;
}
setFontFamily(fontFamily) {
if (this._fontFamily === fontFamily) {
return;
}
this._fontFamily = fontFamily;
this.domNode.style.fontFamily = this._fontFamily;
}
setFontWeight(fontWeight) {
if (this._fontWeight === fontWeight) {
return;
}
this._fontWeight = fontWeight;
this.domNode.style.fontWeight = this._fontWeight;
}
setFontSize(_fontSize) {
const fontSize = numberAsPixels(_fontSize);
if (this._fontSize === fontSize) {
return;
}
this._fontSize = fontSize;
this.domNode.style.fontSize = this._fontSize;
}
setFontStyle(fontStyle) {
if (this._fontStyle === fontStyle) {
return;
}
this._fontStyle = fontStyle;
this.domNode.style.fontStyle = this._fontStyle;
}
setFontFeatureSettings(fontFeatureSettings) {
if (this._fontFeatureSettings === fontFeatureSettings) {
return;
}
this._fontFeatureSettings = fontFeatureSettings;
this.domNode.style.fontFeatureSettings = this._fontFeatureSettings;
}
setFontVariationSettings(fontVariationSettings) {
if (this._fontVariationSettings === fontVariationSettings) {
return;
}
this._fontVariationSettings = fontVariationSettings;
this.domNode.style.fontVariationSettings = this._fontVariationSettings;
}
setTextDecoration(textDecoration) {
if (this._textDecoration === textDecoration) {
return;
}
this._textDecoration = textDecoration;
this.domNode.style.textDecoration = this._textDecoration;
}
setLineHeight(_lineHeight) {
const lineHeight = numberAsPixels(_lineHeight);
if (this._lineHeight === lineHeight) {
return;
}
this._lineHeight = lineHeight;
this.domNode.style.lineHeight = this._lineHeight;
}
setLetterSpacing(_letterSpacing) {
const letterSpacing = numberAsPixels(_letterSpacing);
if (this._letterSpacing === letterSpacing) {
return;
}
this._letterSpacing = letterSpacing;
this.domNode.style.letterSpacing = this._letterSpacing;
}
setClassName(className) {
if (this._className === className) {
return;
}
this._className = className;
this.domNode.className = this._className;
}
toggleClassName(className, shouldHaveIt) {
this.domNode.classList.toggle(className, shouldHaveIt);
this._className = this.domNode.className;
}
setDisplay(display) {
if (this._display === display) {
return;
}
this._display = display;
this.domNode.style.display = this._display;
}
setPosition(position) {
if (this._position === position) {
return;
}
this._position = position;
this.domNode.style.position = this._position;
}
setVisibility(visibility) {
if (this._visibility === visibility) {
return;
}
this._visibility = visibility;
this.domNode.style.visibility = this._visibility;
}
setColor(color) {
if (this._color === color) {
return;
}
this._color = color;
this.domNode.style.color = this._color;
}
setBackgroundColor(backgroundColor) {
if (this._backgroundColor === backgroundColor) {
return;
}
this._backgroundColor = backgroundColor;
this.domNode.style.backgroundColor = this._backgroundColor;
}
setLayerHinting(layerHint) {
if (this._layerHint === layerHint) {
return;
}
this._layerHint = layerHint;
this.domNode.style.transform = this._layerHint ? 'translate3d(0px, 0px, 0px)' : '';
}
setBoxShadow(boxShadow) {
if (this._boxShadow === boxShadow) {
return;
}
this._boxShadow = boxShadow;
this.domNode.style.boxShadow = boxShadow;
}
setContain(contain) {
if (this._contain === contain) {
return;
}
this._contain = contain;
this.domNode.style.contain = this._contain;
}
setAttribute(name, value) {
this.domNode.setAttribute(name, value);
}
removeAttribute(name) {
this.domNode.removeAttribute(name);
}
appendChild(child) {
this.domNode.appendChild(child.domNode);
}
removeChild(child) {
this.domNode.removeChild(child.domNode);
}
}
function numberAsPixels(value) {
return (typeof value === 'number' ? `${value}px` : value);
}
function createFastDomNode(domNode) {
return new FastDomNode(domNode);
}
export { FastDomNode, createFastDomNode };
+17
View File
@@ -0,0 +1,17 @@
import { isWindows, isMacintosh } from '../common/platform.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* The best font-family to be used in CSS based on the platform:
* - Windows: Segoe preferred, fallback to sans-serif
* - macOS: standard system font, fallback to sans-serif
* - Linux: standard system font preferred, fallback to Ubuntu fonts
*
* Note: this currently does not adjust for different locales.
*/
const DEFAULT_FONT_FAMILY = isWindows ? '"Segoe WPC", "Segoe UI", sans-serif' : isMacintosh ? '-apple-system, BlinkMacSystemFont, sans-serif' : 'system-ui, "Ubuntu", "Droid Sans", sans-serif';
export { DEFAULT_FONT_FAMILY };
@@ -0,0 +1,160 @@
import { addStandardDisposableListener } from './dom.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
function renderText(text, _options, target) {
const element = target ?? document.createElement('div');
element.textContent = text;
return element;
}
function renderFormattedText(formattedText, options, target) {
const element = target ?? document.createElement('div');
element.textContent = '';
_renderFormattedText(element, parseFormattedText(formattedText), options?.actionHandler, options?.renderCodeSegments);
return element;
}
class StringStream {
constructor(source) {
this.source = source;
this.index = 0;
}
eos() {
return this.index >= this.source.length;
}
next() {
const next = this.peek();
this.advance();
return next;
}
peek() {
return this.source[this.index];
}
advance() {
this.index++;
}
}
function _renderFormattedText(element, treeNode, actionHandler, renderCodeSegments) {
let child;
if (treeNode.type === 2 /* FormatType.Text */) {
child = document.createTextNode(treeNode.content || '');
}
else if (treeNode.type === 3 /* FormatType.Bold */) {
child = document.createElement('b');
}
else if (treeNode.type === 4 /* FormatType.Italics */) {
child = document.createElement('i');
}
else if (treeNode.type === 7 /* FormatType.Code */ && renderCodeSegments) {
child = document.createElement('code');
}
else if (treeNode.type === 5 /* FormatType.Action */ && actionHandler) {
const a = document.createElement('a');
actionHandler.disposables.add(addStandardDisposableListener(a, 'click', (event) => {
actionHandler.callback(String(treeNode.index), event);
}));
child = a;
}
else if (treeNode.type === 8 /* FormatType.NewLine */) {
child = document.createElement('br');
}
else if (treeNode.type === 1 /* FormatType.Root */) {
child = element;
}
if (child && element !== child) {
element.appendChild(child);
}
if (child && Array.isArray(treeNode.children)) {
treeNode.children.forEach((nodeChild) => {
_renderFormattedText(child, nodeChild, actionHandler, renderCodeSegments);
});
}
}
function parseFormattedText(content, parseCodeSegments) {
const root = {
type: 1 /* FormatType.Root */,
children: []
};
let actionViewItemIndex = 0;
let current = root;
const stack = [];
const stream = new StringStream(content);
while (!stream.eos()) {
let next = stream.next();
const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek()) !== 0 /* FormatType.Invalid */);
if (isEscapedFormatType) {
next = stream.next(); // unread the backslash if it escapes a format tag type
}
if (!isEscapedFormatType && isFormatTag(next) && next === stream.peek()) {
stream.advance();
if (current.type === 2 /* FormatType.Text */) {
current = stack.pop();
}
const type = formatTagType(next);
if (current.type === type || (current.type === 5 /* FormatType.Action */ && type === 6 /* FormatType.ActionClose */)) {
current = stack.pop();
}
else {
const newCurrent = {
type: type,
children: []
};
if (type === 5 /* FormatType.Action */) {
newCurrent.index = actionViewItemIndex;
actionViewItemIndex++;
}
current.children.push(newCurrent);
stack.push(current);
current = newCurrent;
}
}
else if (next === '\n') {
if (current.type === 2 /* FormatType.Text */) {
current = stack.pop();
}
current.children.push({
type: 8 /* FormatType.NewLine */
});
}
else {
if (current.type !== 2 /* FormatType.Text */) {
const textCurrent = {
type: 2 /* FormatType.Text */,
content: next
};
current.children.push(textCurrent);
stack.push(current);
current = textCurrent;
}
else {
current.content += next;
}
}
}
if (current.type === 2 /* FormatType.Text */) {
current = stack.pop();
}
return root;
}
function isFormatTag(char, supportCodeSegments) {
return formatTagType(char) !== 0 /* FormatType.Invalid */;
}
function formatTagType(char, supportCodeSegments) {
switch (char) {
case '*':
return 3 /* FormatType.Bold */;
case '_':
return 4 /* FormatType.Italics */;
case '[':
return 5 /* FormatType.Action */;
case ']':
return 6 /* FormatType.ActionClose */;
case '`':
return 0 /* FormatType.Invalid */;
default:
return 0 /* FormatType.Invalid */;
}
}
export { renderFormattedText, renderText };
@@ -0,0 +1,83 @@
import { getWindow, addDisposableListener, EventType } from './dom.js';
import { DisposableStore, toDisposable } from '../common/lifecycle.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class GlobalPointerMoveMonitor {
constructor() {
this._hooks = new DisposableStore();
this._pointerMoveCallback = null;
this._onStopCallback = null;
}
dispose() {
this.stopMonitoring(false);
this._hooks.dispose();
}
stopMonitoring(invokeStopCallback, browserEvent) {
if (!this.isMonitoring()) {
// Not monitoring
return;
}
// Unhook
this._hooks.clear();
this._pointerMoveCallback = null;
const onStopCallback = this._onStopCallback;
this._onStopCallback = null;
if (invokeStopCallback && onStopCallback) {
onStopCallback(browserEvent);
}
}
isMonitoring() {
return !!this._pointerMoveCallback;
}
startMonitoring(initialElement, pointerId, initialButtons, pointerMoveCallback, onStopCallback) {
if (this.isMonitoring()) {
this.stopMonitoring(false);
}
this._pointerMoveCallback = pointerMoveCallback;
this._onStopCallback = onStopCallback;
let eventSource = initialElement;
try {
initialElement.setPointerCapture(pointerId);
this._hooks.add(toDisposable(() => {
try {
initialElement.releasePointerCapture(pointerId);
}
catch (err) {
// See https://github.com/microsoft/vscode/issues/161731
//
// `releasePointerCapture` sometimes fails when being invoked with the exception:
// DOMException: Failed to execute 'releasePointerCapture' on 'Element':
// No active pointer with the given id is found.
//
// There's no need to do anything in case of failure
}
}));
}
catch (err) {
// See https://github.com/microsoft/vscode/issues/144584
// See https://github.com/microsoft/vscode/issues/146947
// `setPointerCapture` sometimes fails when being invoked
// from a `mousedown` listener on macOS and Windows
// and it always fails on Linux with the exception:
// DOMException: Failed to execute 'setPointerCapture' on 'Element':
// No active pointer with the given id is found.
// In case of failure, we bind the listeners on the window
eventSource = getWindow(initialElement);
}
this._hooks.add(addDisposableListener(eventSource, EventType.POINTER_MOVE, (e) => {
if (e.buttons !== initialButtons) {
// Buttons state has changed in the meantime
this.stopMonitoring(true);
return;
}
e.preventDefault();
this._pointerMoveCallback(e);
}));
this._hooks.add(addDisposableListener(eventSource, EventType.POINTER_UP, (e) => this.stopMonitoring(true)));
}
}
export { GlobalPointerMoveMonitor };
+87
View File
@@ -0,0 +1,87 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const sameOriginWindowChainCache = new WeakMap();
function getParentWindowIfSameOrigin(w) {
if (!w.parent || w.parent === w) {
return null;
}
// Cannot really tell if we have access to the parent window unless we try to access something in it
try {
const location = w.location;
const parentLocation = w.parent.location;
if (location.origin !== 'null' && parentLocation.origin !== 'null' && location.origin !== parentLocation.origin) {
return null;
}
}
catch (e) {
return null;
}
return w.parent;
}
class IframeUtils {
/**
* Returns a chain of embedded windows with the same origin (which can be accessed programmatically).
* Having a chain of length 1 might mean that the current execution environment is running outside of an iframe or inside an iframe embedded in a window with a different origin.
*/
static getSameOriginWindowChain(targetWindow) {
let windowChainCache = sameOriginWindowChainCache.get(targetWindow);
if (!windowChainCache) {
windowChainCache = [];
sameOriginWindowChainCache.set(targetWindow, windowChainCache);
let w = targetWindow;
let parent;
do {
parent = getParentWindowIfSameOrigin(w);
if (parent) {
windowChainCache.push({
window: new WeakRef(w),
iframeElement: w.frameElement || null
});
}
else {
windowChainCache.push({
window: new WeakRef(w),
iframeElement: null
});
}
w = parent;
} while (w);
}
return windowChainCache.slice(0);
}
/**
* Returns the position of `childWindow` relative to `ancestorWindow`
*/
static getPositionOfChildWindowRelativeToAncestorWindow(childWindow, ancestorWindow) {
if (!ancestorWindow || childWindow === ancestorWindow) {
return {
top: 0,
left: 0
};
}
let top = 0, left = 0;
const windowChain = this.getSameOriginWindowChain(childWindow);
for (const windowChainEl of windowChain) {
const windowInChain = windowChainEl.window.deref();
top += windowInChain?.scrollY ?? 0;
left += windowInChain?.scrollX ?? 0;
if (windowInChain === ancestorWindow) {
break;
}
if (!windowChainEl.iframeElement) {
break;
}
const boundingRect = windowChainEl.iframeElement.getBoundingClientRect();
top += boundingRect.top;
left += boundingRect.left;
}
return {
top: top,
left: left
};
}
}
export { IframeUtils };
@@ -0,0 +1,125 @@
import { isFirefox, isWebKit } from './browser.js';
import { KeyCodeUtils, EVENT_KEY_CODE_MAP } from '../common/keyCodes.js';
import { KeyCodeChord } from '../common/keybindings.js';
import { isMacintosh, isLinux } from '../common/platform.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
function extractKeyCode(e) {
if (e.charCode) {
// "keypress" events mostly
const char = String.fromCharCode(e.charCode).toUpperCase();
return KeyCodeUtils.fromString(char);
}
const keyCode = e.keyCode;
// browser quirks
if (keyCode === 3) {
return 7 /* KeyCode.PauseBreak */;
}
else if (isFirefox) {
switch (keyCode) {
case 59: return 85 /* KeyCode.Semicolon */;
case 60:
if (isLinux) {
return 97 /* KeyCode.IntlBackslash */;
}
break;
case 61: return 86 /* KeyCode.Equal */;
// based on: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#numpad_keys
case 107: return 109 /* KeyCode.NumpadAdd */;
case 109: return 111 /* KeyCode.NumpadSubtract */;
case 173: return 88 /* KeyCode.Minus */;
case 224:
if (isMacintosh) {
return 57 /* KeyCode.Meta */;
}
break;
}
}
else if (isWebKit) {
if (isMacintosh && keyCode === 93) {
// the two meta keys in the Mac have different key codes (91 and 93)
return 57 /* KeyCode.Meta */;
}
else if (!isMacintosh && keyCode === 92) {
return 57 /* KeyCode.Meta */;
}
}
// cross browser keycodes:
return EVENT_KEY_CODE_MAP[keyCode] || 0 /* KeyCode.Unknown */;
}
const ctrlKeyMod = (isMacintosh ? 256 /* KeyMod.WinCtrl */ : 2048 /* KeyMod.CtrlCmd */);
const altKeyMod = 512 /* KeyMod.Alt */;
const shiftKeyMod = 1024 /* KeyMod.Shift */;
const metaKeyMod = (isMacintosh ? 2048 /* KeyMod.CtrlCmd */ : 256 /* KeyMod.WinCtrl */);
class StandardKeyboardEvent {
constructor(source) {
this._standardKeyboardEventBrand = true;
const e = source;
this.browserEvent = e;
this.target = e.target;
this.ctrlKey = e.ctrlKey;
this.shiftKey = e.shiftKey;
this.altKey = e.altKey;
this.metaKey = e.metaKey;
this.altGraphKey = e.getModifierState?.('AltGraph');
this.keyCode = extractKeyCode(e);
this.code = e.code;
// console.info(e.type + ": keyCode: " + e.keyCode + ", which: " + e.which + ", charCode: " + e.charCode + ", detail: " + e.detail + " ====> " + this.keyCode + ' -- ' + KeyCode[this.keyCode]);
this.ctrlKey = this.ctrlKey || this.keyCode === 5 /* KeyCode.Ctrl */;
this.altKey = this.altKey || this.keyCode === 6 /* KeyCode.Alt */;
this.shiftKey = this.shiftKey || this.keyCode === 4 /* KeyCode.Shift */;
this.metaKey = this.metaKey || this.keyCode === 57 /* KeyCode.Meta */;
this._asKeybinding = this._computeKeybinding();
this._asKeyCodeChord = this._computeKeyCodeChord();
// console.log(`code: ${e.code}, keyCode: ${e.keyCode}, key: ${e.key}`);
}
preventDefault() {
if (this.browserEvent && this.browserEvent.preventDefault) {
this.browserEvent.preventDefault();
}
}
stopPropagation() {
if (this.browserEvent && this.browserEvent.stopPropagation) {
this.browserEvent.stopPropagation();
}
}
toKeyCodeChord() {
return this._asKeyCodeChord;
}
equals(other) {
return this._asKeybinding === other;
}
_computeKeybinding() {
let key = 0 /* KeyCode.Unknown */;
if (this.keyCode !== 5 /* KeyCode.Ctrl */ && this.keyCode !== 4 /* KeyCode.Shift */ && this.keyCode !== 6 /* KeyCode.Alt */ && this.keyCode !== 57 /* KeyCode.Meta */) {
key = this.keyCode;
}
let result = 0;
if (this.ctrlKey) {
result |= ctrlKeyMod;
}
if (this.altKey) {
result |= altKeyMod;
}
if (this.shiftKey) {
result |= shiftKeyMod;
}
if (this.metaKey) {
result |= metaKeyMod;
}
result |= key;
return result;
}
_computeKeyCodeChord() {
let key = 0 /* KeyCode.Unknown */;
if (this.keyCode !== 5 /* KeyCode.Ctrl */ && this.keyCode !== 4 /* KeyCode.Shift */ && this.keyCode !== 6 /* KeyCode.Alt */ && this.keyCode !== 57 /* KeyCode.Meta */) {
key = this.keyCode;
}
return new KeyCodeChord(this.ctrlKey, this.shiftKey, this.altKey, this.metaKey, key);
}
}
export { StandardKeyboardEvent };
@@ -0,0 +1,901 @@
import { onUnexpectedError } from '../common/errors.js';
import { removeMarkdownEscapes, escapeDoubleQuotes, parseHrefAndDimensions } from '../common/htmlContent.js';
import { markdownEscapeEscapedIcons } from '../common/iconLabels.js';
import { defaultGenerator } from '../common/idGenerator.js';
import { Lazy } from '../common/lazy.js';
import { DisposableStore } from '../common/lifecycle.js';
import { Renderer as _Renderer, Marked, lexer, parse as parse$1 } from '../common/marked/marked.js';
import { parse } from '../common/marshalling.js';
import { Schemas, FileAccess } from '../common/network.js';
import { cloneAndChange } from '../common/objects.js';
import { resolvePath, dirname } from '../common/resources.js';
import { escape } from '../common/strings.js';
import { URI } from '../common/uri.js';
import { reset, addDisposableListener, $, getWindow, isHTMLElement } from './dom.js';
import { basicMarkupHtmlTags, safeSetInnerHtml, convertTagToPlaintext, sanitizeHtml } from './domSanitize.js';
import { StandardKeyboardEvent } from './keyboardEvent.js';
import { StandardMouseEvent } from './mouseEvent.js';
import { renderLabelWithIcons, renderIcon } from './ui/iconLabel/iconLabels.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const defaultMarkedRenderers = Object.freeze({
image: ({ href, title, text }) => {
let dimensions = [];
let attributes = [];
if (href) {
({ href, dimensions } = parseHrefAndDimensions(href));
attributes.push(`src="${escapeDoubleQuotes(href)}"`);
}
if (text) {
attributes.push(`alt="${escapeDoubleQuotes(text)}"`);
}
if (title) {
attributes.push(`title="${escapeDoubleQuotes(title)}"`);
}
if (dimensions.length) {
attributes = attributes.concat(dimensions);
}
return '<img ' + attributes.join(' ') + '>';
},
paragraph({ tokens }) {
return `<p>${this.parser.parseInline(tokens)}</p>`;
},
link({ href, title, tokens }) {
let text = this.parser.parseInline(tokens);
if (typeof href !== 'string') {
return '';
}
// Remove markdown escapes. Workaround for https://github.com/chjj/marked/issues/829
if (href === text) { // raw link case
text = removeMarkdownEscapes(text);
}
title = typeof title === 'string' ? escapeDoubleQuotes(removeMarkdownEscapes(title)) : '';
href = removeMarkdownEscapes(href);
// HTML Encode href
href = href.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
return `<a href="${href}" title="${title || href}" draggable="false">${text}</a>`;
},
});
/**
* Blockquote renderer that processes GitHub-style alert syntax.
* Transforms blockquotes like "> [!NOTE]" into structured alert markup with icons.
*
* Based on GitHub's alert syntax: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts
*/
function createAlertBlockquoteRenderer(fallbackRenderer) {
return function (token) {
const { tokens } = token;
// Check if this blockquote starts with alert syntax [!TYPE]
const firstToken = tokens[0];
if (firstToken?.type !== 'paragraph') {
return fallbackRenderer.call(this, token);
}
const paragraphTokens = firstToken.tokens;
if (!paragraphTokens || paragraphTokens.length === 0) {
return fallbackRenderer.call(this, token);
}
const firstTextToken = paragraphTokens[0];
if (firstTextToken?.type !== 'text') {
return fallbackRenderer.call(this, token);
}
const pattern = /^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*?\n*/i;
const match = firstTextToken.raw.match(pattern);
if (!match) {
return fallbackRenderer.call(this, token);
}
// Remove the alert marker from the token
firstTextToken.raw = firstTextToken.raw.replace(pattern, '');
firstTextToken.text = firstTextToken.text.replace(pattern, '');
const alertIcons = {
'note': 'info',
'tip': 'light-bulb',
'important': 'comment',
'warning': 'alert',
'caution': 'stop'
};
const type = match[1];
const typeCapitalized = type.charAt(0).toUpperCase() + type.slice(1).toLowerCase();
const severity = type.toLowerCase();
const iconHtml = renderIcon({ id: alertIcons[severity] }).outerHTML;
// Render the remaining content
const content = this.parser.parse(tokens);
// Return alert markup with icon and severity (skipping the first 3 characters: `<p>`)
return `<blockquote data-severity="${severity}"><p><span>${iconHtml}${typeCapitalized}</span>${content.substring(3)}</blockquote>\n`;
};
}
/**
* Low-level way create a html element from a markdown string.
*
* **Note** that for most cases you should be using {@link import('../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js').MarkdownRenderer MarkdownRenderer}
* which comes with support for pretty code block rendering and which uses the default way of handling links.
*/
function renderMarkdown(markdown, options = {}, target) {
const disposables = new DisposableStore();
let isDisposed = false;
const markedInstance = new Marked(...(options.markedExtensions ?? []));
const { renderer, codeBlocks, syncCodeBlocks } = createMarkdownRenderer(markedInstance, options, markdown);
const value = preprocessMarkdownString(markdown);
let renderedMarkdown;
if (options.fillInIncompleteTokens) {
// The defaults are applied by parse but not lexer()/parser(), and they need to be present
const opts = {
...markedInstance.defaults,
...options.markedOptions,
renderer
};
const tokens = markedInstance.lexer(value, opts);
const newTokens = fillInIncompleteTokens(tokens);
renderedMarkdown = markedInstance.parser(newTokens, opts);
}
else {
renderedMarkdown = markedInstance.parse(value, { ...options?.markedOptions, renderer, async: false });
}
// Rewrite theme icons
if (markdown.supportThemeIcons) {
const elements = renderLabelWithIcons(renderedMarkdown);
renderedMarkdown = elements.map(e => typeof e === 'string' ? e : e.outerHTML).join('');
}
const renderedContent = document.createElement('div');
const sanitizerConfig = getDomSanitizerConfig(markdown, options.sanitizerConfig ?? {});
safeSetInnerHtml(renderedContent, renderedMarkdown, sanitizerConfig);
// Rewrite links and images before potentially inserting them into the real dom
rewriteRenderedLinks(markdown, options, renderedContent);
let outElement;
if (target) {
outElement = target;
reset(target, ...renderedContent.children);
}
else {
outElement = renderedContent;
}
if (codeBlocks.length > 0) {
Promise.all(codeBlocks).then((tuples) => {
if (isDisposed) {
return;
}
const renderedElements = new Map(tuples);
// eslint-disable-next-line no-restricted-syntax
const placeholderElements = outElement.querySelectorAll(`div[data-code]`);
for (const placeholderElement of placeholderElements) {
const renderedElement = renderedElements.get(placeholderElement.dataset['code'] ?? '');
if (renderedElement) {
reset(placeholderElement, renderedElement);
}
}
options.asyncRenderCallback?.();
});
}
else if (syncCodeBlocks.length > 0) {
const renderedElements = new Map(syncCodeBlocks);
// eslint-disable-next-line no-restricted-syntax
const placeholderElements = outElement.querySelectorAll(`div[data-code]`);
for (const placeholderElement of placeholderElements) {
const renderedElement = renderedElements.get(placeholderElement.dataset['code'] ?? '');
if (renderedElement) {
reset(placeholderElement, renderedElement);
}
}
}
// Signal size changes for image tags
if (options.asyncRenderCallback) {
// eslint-disable-next-line no-restricted-syntax
for (const img of outElement.getElementsByTagName('img')) {
const listener = disposables.add(addDisposableListener(img, 'load', () => {
listener.dispose();
options.asyncRenderCallback();
}));
}
}
// Add event listeners for links
if (options.actionHandler) {
const clickCb = (e) => {
const mouseEvent = new StandardMouseEvent(getWindow(outElement), e);
if (!mouseEvent.leftButton && !mouseEvent.middleButton) {
return;
}
activateLink(markdown, options, mouseEvent);
};
disposables.add(addDisposableListener(outElement, 'click', clickCb));
disposables.add(addDisposableListener(outElement, 'auxclick', clickCb));
disposables.add(addDisposableListener(outElement, 'keydown', (e) => {
const keyboardEvent = new StandardKeyboardEvent(e);
if (!keyboardEvent.equals(10 /* KeyCode.Space */) && !keyboardEvent.equals(3 /* KeyCode.Enter */)) {
return;
}
activateLink(markdown, options, keyboardEvent);
}));
}
// Remove/disable inputs
// eslint-disable-next-line no-restricted-syntax
for (const input of [...outElement.getElementsByTagName('input')]) {
if (input.attributes.getNamedItem('type')?.value === 'checkbox') {
input.setAttribute('disabled', '');
}
else {
if (options.sanitizerConfig?.replaceWithPlaintext) {
const replacement = convertTagToPlaintext(input);
if (replacement) {
input.parentElement?.replaceChild(replacement, input);
}
else {
input.remove();
}
}
else {
input.remove();
}
}
}
return {
element: outElement,
dispose: () => {
isDisposed = true;
disposables.dispose();
}
};
}
function rewriteRenderedLinks(markdown, options, root) {
// eslint-disable-next-line no-restricted-syntax
for (const el of root.querySelectorAll('img, audio, video, source')) {
const src = el.getAttribute('src'); // Get the raw 'src' attribute value as text, not the resolved 'src'
if (src) {
let href = src;
try {
if (markdown.baseUri) { // absolute or relative local path, or file: uri
href = resolveWithBaseUri(URI.from(markdown.baseUri), href);
}
}
catch (err) { }
el.setAttribute('src', massageHref(markdown, href, true));
if (options.sanitizerConfig?.remoteImageIsAllowed) {
const uri = URI.parse(href);
if (uri.scheme !== Schemas.file && uri.scheme !== Schemas.data && !options.sanitizerConfig.remoteImageIsAllowed(uri)) {
el.replaceWith($('', undefined, el.outerHTML));
}
}
}
}
// eslint-disable-next-line no-restricted-syntax
for (const el of root.querySelectorAll('a')) {
const href = el.getAttribute('href'); // Get the raw 'href' attribute value as text, not the resolved 'href'
el.setAttribute('href', ''); // Clear out href. We use the `data-href` for handling clicks instead
if (!href
|| /^data:|javascript:/i.test(href)
|| (/^command:/i.test(href) && !markdown.isTrusted)
|| /^command:(\/\/\/)?_workbench\.downloadResource/i.test(href)) {
// drop the link
el.replaceWith(...el.childNodes);
}
else {
let resolvedHref = massageHref(markdown, href, false);
if (markdown.baseUri) {
resolvedHref = resolveWithBaseUri(URI.from(markdown.baseUri), href);
}
el.dataset.href = resolvedHref;
}
}
}
function createMarkdownRenderer(marked, options, markdown) {
const renderer = new marked.Renderer(options.markedOptions);
renderer.image = defaultMarkedRenderers.image;
renderer.link = defaultMarkedRenderers.link;
renderer.paragraph = defaultMarkedRenderers.paragraph;
if (markdown.supportAlertSyntax) {
renderer.blockquote = createAlertBlockquoteRenderer(renderer.blockquote);
}
// Will collect [id, renderedElement] tuples
const codeBlocks = [];
const syncCodeBlocks = [];
if (options.codeBlockRendererSync) {
renderer.code = ({ text, lang, raw }) => {
const id = defaultGenerator.nextId();
const value = options.codeBlockRendererSync(postProcessCodeBlockLanguageId(lang), text, raw);
syncCodeBlocks.push([id, value]);
return `<div class="code" data-code="${id}">${escape(text)}</div>`;
};
}
else if (options.codeBlockRenderer) {
renderer.code = ({ text, lang }) => {
const id = defaultGenerator.nextId();
const value = options.codeBlockRenderer(postProcessCodeBlockLanguageId(lang), text);
codeBlocks.push(value.then(element => [id, element]));
return `<div class="code" data-code="${id}">${escape(text)}</div>`;
};
}
if (!markdown.supportHtml) {
// Note: we always pass the output through dompurify after this so that we don't rely on
// marked for real sanitization.
renderer.html = ({ text }) => {
if (options.sanitizerConfig?.replaceWithPlaintext) {
return escape(text);
}
const match = markdown.isTrusted ? text.match(/^(<span[^>]+>)|(<\/\s*span>)$/) : undefined;
return match ? text : '';
};
}
return { renderer, codeBlocks, syncCodeBlocks };
}
function preprocessMarkdownString(markdown) {
let value = markdown.value;
// values that are too long will freeze the UI
if (value.length > 100_000) {
value = `${value.substr(0, 100_000)}`;
}
// escape theme icons
if (markdown.supportThemeIcons) {
value = markdownEscapeEscapedIcons(value);
}
return value;
}
function activateLink(mdStr, options, event) {
const target = event.target.closest('a[data-href]');
if (!isHTMLElement(target)) {
return;
}
try {
let href = target.dataset['href'];
if (href) {
if (mdStr.baseUri) {
href = resolveWithBaseUri(URI.from(mdStr.baseUri), href);
}
options.actionHandler?.(href, mdStr);
}
}
catch (err) {
onUnexpectedError(err);
}
finally {
event.preventDefault();
}
}
function uriMassage(markdown, part) {
let data;
try {
data = parse(decodeURIComponent(part));
}
catch (e) {
// ignore
}
if (!data) {
return part;
}
data = cloneAndChange(data, value => {
if (markdown.uris && markdown.uris[value]) {
return URI.revive(markdown.uris[value]);
}
else {
return undefined;
}
});
return encodeURIComponent(JSON.stringify(data));
}
function massageHref(markdown, href, isDomUri) {
const data = markdown.uris && markdown.uris[href];
let uri = URI.revive(data);
if (isDomUri) {
if (href.startsWith(Schemas.data + ':')) {
return href;
}
if (!uri) {
uri = URI.parse(href);
}
// this URI will end up as "src"-attribute of a dom node
// and because of that special rewriting needs to be done
// so that the URI uses a protocol that's understood by
// browsers (like http or https)
return FileAccess.uriToBrowserUri(uri).toString(true);
}
if (!uri) {
return href;
}
if (URI.parse(href).toString() === uri.toString()) {
return href; // no transformation performed
}
if (uri.query) {
uri = uri.with({ query: uriMassage(markdown, uri.query) });
}
return uri.toString();
}
function postProcessCodeBlockLanguageId(lang) {
if (!lang) {
return '';
}
const parts = lang.split(/[\s+|:|,|\{|\?]/, 1);
if (parts.length) {
return parts[0];
}
return lang;
}
function resolveWithBaseUri(baseUri, href) {
const hasScheme = /^\w[\w\d+.-]*:/.test(href);
if (hasScheme) {
return href;
}
if (baseUri.path.endsWith('/')) {
return resolvePath(baseUri, href).toString();
}
else {
return resolvePath(dirname(baseUri), href).toString();
}
}
function sanitizeRenderedMarkdown(renderedMarkdown, originalMdStrConfig, options = {}) {
const sanitizerConfig = getDomSanitizerConfig(originalMdStrConfig, options);
return sanitizeHtml(renderedMarkdown, sanitizerConfig);
}
const allowedMarkdownHtmlTags = Object.freeze([
...basicMarkupHtmlTags,
'input', // Allow inputs for rendering checkboxes. Other types of inputs are removed and the inputs are always disabled
]);
const allowedMarkdownHtmlAttributes = Object.freeze([
'align',
'autoplay',
'alt',
'colspan',
'controls',
'draggable',
'height',
'href',
'loop',
'muted',
'playsinline',
'poster',
'rowspan',
'src',
'target',
'title',
'type',
'width',
'start',
// Input (For disabled inputs)
'checked',
'disabled',
'value',
// Custom markdown attributes
'data-code',
'data-href',
'data-severity',
// Only allow very specific styles
{
attributeName: 'style',
shouldKeep: (element, data) => {
if (element.tagName === 'SPAN') {
if (data.attrName === 'style') {
return /^(color\:(#[0-9a-fA-F]+|var\(--vscode(-[a-zA-Z0-9]+)+\));)?(background-color\:(#[0-9a-fA-F]+|var\(--vscode(-[a-zA-Z0-9]+)+\));)?(border-radius:[0-9]+px;)?$/.test(data.attrValue);
}
}
return false;
}
},
// Only allow codicons for classes
{
attributeName: 'class',
shouldKeep: (element, data) => {
if (element.tagName === 'SPAN') {
if (data.attrName === 'class') {
return /^codicon codicon-[a-z\-]+( codicon-modifier-[a-z\-]+)?$/.test(data.attrValue);
}
}
return false;
},
},
]);
function getDomSanitizerConfig(mdStrConfig, options) {
const isTrusted = mdStrConfig.isTrusted ?? false;
const allowedLinkSchemes = [
Schemas.http,
Schemas.https,
Schemas.mailto,
Schemas.file,
Schemas.vscodeFileResource,
Schemas.vscodeRemote,
Schemas.vscodeRemoteResource,
Schemas.vscodeNotebookCell
];
if (isTrusted) {
allowedLinkSchemes.push(Schemas.command);
}
if (options.allowedLinkSchemes?.augment) {
allowedLinkSchemes.push(...options.allowedLinkSchemes.augment);
}
return {
// allowedTags should included everything that markdown renders to.
// Since we have our own sanitize function for marked, it's possible we missed some tag so let dompurify make sure.
// HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/
// HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension-
allowedTags: {
override: options.allowedTags?.override ?? allowedMarkdownHtmlTags
},
allowedAttributes: {
override: options.allowedAttributes?.override ?? allowedMarkdownHtmlAttributes,
},
allowedLinkProtocols: {
override: allowedLinkSchemes,
},
allowRelativeLinkPaths: !!mdStrConfig.baseUri,
allowedMediaProtocols: {
override: [
Schemas.http,
Schemas.https,
Schemas.data,
Schemas.file,
Schemas.vscodeFileResource,
Schemas.vscodeRemote,
Schemas.vscodeRemoteResource,
]
},
allowRelativeMediaPaths: !!mdStrConfig.baseUri,
replaceWithPlaintext: options.replaceWithPlaintext,
};
}
/**
* Renders `str` as plaintext, stripping out Markdown syntax if it's a {@link IMarkdownString}.
*
* For example `# Header` would be output as `Header`.
*/
function renderAsPlaintext(str, options) {
if (typeof str === 'string') {
return str;
}
// values that are too long will freeze the UI
let value = str.value ?? '';
if (value.length > 100_000) {
value = `${value.substr(0, 100_000)}`;
}
const html = parse$1(value, { async: false, renderer: plainTextRenderer.value });
return sanitizeRenderedMarkdown(html, { isTrusted: false }, {})
.toString()
.replace(/&(#\d+|[a-zA-Z]+);/g, m => unescapeInfo.get(m) ?? m)
.trim();
}
const unescapeInfo = new Map([
['&quot;', '"'],
['&nbsp;', ' '],
['&amp;', '&'],
['&#39;', '\''],
['&lt;', '<'],
['&gt;', '>'],
]);
function createPlainTextRenderer() {
const renderer = new _Renderer();
renderer.code = ({ text }) => {
return escape(text);
};
renderer.blockquote = ({ text }) => {
return text + '\n';
};
renderer.html = (_) => {
return '';
};
renderer.heading = function ({ tokens }) {
return this.parser.parseInline(tokens) + '\n';
};
renderer.hr = () => {
return '';
};
renderer.list = function ({ items }) {
return items.map(x => this.listitem(x)).join('\n') + '\n';
};
renderer.listitem = ({ text }) => {
return text + '\n';
};
renderer.paragraph = function ({ tokens }) {
return this.parser.parseInline(tokens) + '\n';
};
renderer.table = function ({ header, rows }) {
return header.map(cell => this.tablecell(cell)).join(' ') + '\n' + rows.map(cells => cells.map(cell => this.tablecell(cell)).join(' ')).join('\n') + '\n';
};
renderer.tablerow = ({ text }) => {
return text;
};
renderer.tablecell = function ({ tokens }) {
return this.parser.parseInline(tokens);
};
renderer.strong = ({ text }) => {
return text;
};
renderer.em = ({ text }) => {
return text;
};
renderer.codespan = ({ text }) => {
return escape(text);
};
renderer.br = (_) => {
return '\n';
};
renderer.del = ({ text }) => {
return text;
};
renderer.image = (_) => {
return '';
};
renderer.text = ({ text }) => {
return text;
};
renderer.link = ({ text }) => {
return text;
};
return renderer;
}
const plainTextRenderer = new Lazy(createPlainTextRenderer);
new Lazy(() => {
const renderer = createPlainTextRenderer();
renderer.code = ({ text }) => {
return `\n\`\`\`\n${escape(text)}\n\`\`\`\n`;
};
return renderer;
});
function mergeRawTokenText(tokens) {
let mergedTokenText = '';
tokens.forEach(token => {
mergedTokenText += token.raw;
});
return mergedTokenText;
}
function completeSingleLinePattern(token) {
if (!token.tokens) {
return undefined;
}
for (let i = token.tokens.length - 1; i >= 0; i--) {
const subtoken = token.tokens[i];
if (subtoken.type === 'text') {
const lines = subtoken.raw.split('\n');
const lastLine = lines[lines.length - 1];
if (lastLine.includes('`')) {
return completeCodespan(token);
}
else if (lastLine.includes('**')) {
return completeDoublestar(token);
}
else if (lastLine.match(/\*\w/)) {
return completeStar(token);
}
else if (lastLine.match(/(^|\s)__\w/)) {
return completeDoubleUnderscore(token);
}
else if (lastLine.match(/(^|\s)_\w/)) {
return completeUnderscore(token);
}
else if (
// Text with start of link target
hasLinkTextAndStartOfLinkTarget(lastLine) ||
// This token doesn't have the link text, eg if it contains other markdown constructs that are in other subtokens.
// But some preceding token does have an unbalanced [ at least
hasStartOfLinkTargetAndNoLinkText(lastLine) && token.tokens.slice(0, i).some(t => t.type === 'text' && t.raw.match(/\[[^\]]*$/))) {
const nextTwoSubTokens = token.tokens.slice(i + 1);
// A markdown link can look like
// [link text](https://microsoft.com "more text")
// Where "more text" is a title for the link or an argument to a vscode command link
if (
// If the link was parsed as a link, then look for a link token and a text token with a quote
nextTwoSubTokens[0]?.type === 'link' && nextTwoSubTokens[1]?.type === 'text' && nextTwoSubTokens[1].raw.match(/^ *"[^"]*$/) ||
// And if the link was not parsed as a link (eg command link), just look for a single quote in this token
lastLine.match(/^[^"]* +"[^"]*$/)) {
return completeLinkTargetArg(token);
}
return completeLinkTarget(token);
}
// Contains the start of link text, and no following tokens contain the link target
else if (lastLine.match(/(^|\s)\[\w*[^\]]*$/)) {
return completeLinkText(token);
}
}
}
return undefined;
}
function hasLinkTextAndStartOfLinkTarget(str) {
return !!str.match(/(^|\s)\[.*\]\(\w*/);
}
function hasStartOfLinkTargetAndNoLinkText(str) {
return !!str.match(/^[^\[]*\]\([^\)]*$/);
}
function completeListItemPattern(list) {
// Patch up this one list item
const lastListItem = list.items[list.items.length - 1];
const lastListSubToken = lastListItem.tokens ? lastListItem.tokens[lastListItem.tokens.length - 1] : undefined;
/*
Example list token structures:
list
list_item
text
text
codespan
link
list_item
text
code // Complete indented codeblock
list_item
text
space
text
text // Incomplete indented codeblock
list_item
text
list // Nested list
list_item
text
text
Contrast with paragraph:
paragraph
text
codespan
*/
const listEndsInHeading = (list) => {
// A list item can be rendered as a heading for some reason when it has a subitem where we haven't rendered the text yet like this:
// 1. list item
// -
const lastItem = list.items.at(-1);
const lastToken = lastItem?.tokens.at(-1);
return lastToken?.type === 'heading' || lastToken?.type === 'list' && listEndsInHeading(lastToken);
};
let newToken;
if (lastListSubToken?.type === 'text' && !('inRawBlock' in lastListItem)) { // Why does Tag have a type of 'text'
newToken = completeSingleLinePattern(lastListSubToken);
}
else if (listEndsInHeading(list)) {
const newList = lexer(list.raw.trim() + ' &nbsp;')[0];
if (newList.type !== 'list') {
// Something went wrong
return;
}
return newList;
}
if (!newToken || newToken.type !== 'paragraph') { // 'text' item inside the list item turns into paragraph
// Nothing to fix, or not a pattern we were expecting
return;
}
const previousListItemsText = mergeRawTokenText(list.items.slice(0, -1));
// Grabbing the `- ` or `1. ` or `* ` off the list item because I can't find a better way to do this
const lastListItemLead = lastListItem.raw.match(/^(\s*(-|\d+\.|\*) +)/)?.[0];
if (!lastListItemLead) {
// Is badly formatted
return;
}
const newListItemText = lastListItemLead +
mergeRawTokenText(lastListItem.tokens.slice(0, -1)) +
newToken.raw;
const newList = lexer(previousListItemsText + newListItemText)[0];
if (newList.type !== 'list') {
// Something went wrong
return;
}
return newList;
}
function completeHeading(token, fullRawText) {
if (token.raw.match(/-\s*$/)) {
return lexer(fullRawText + ' &nbsp;');
}
}
const maxIncompleteTokensFixRounds = 3;
function fillInIncompleteTokens(tokens) {
for (let i = 0; i < maxIncompleteTokensFixRounds; i++) {
const newTokens = fillInIncompleteTokensOnce(tokens);
if (newTokens) {
tokens = newTokens;
}
else {
break;
}
}
return tokens;
}
function fillInIncompleteTokensOnce(tokens) {
let i;
let newTokens;
for (i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === 'paragraph' && token.raw.match(/(\n|^)\|/)) {
newTokens = completeTable(tokens.slice(i));
break;
}
}
const lastToken = tokens.at(-1);
if (!newTokens && lastToken?.type === 'list') {
const newListToken = completeListItemPattern(lastToken);
if (newListToken) {
newTokens = [newListToken];
i = tokens.length - 1;
}
}
if (!newTokens && lastToken?.type === 'paragraph') {
// Only operates on a single token, because any newline that follows this should break these patterns
const newToken = completeSingleLinePattern(lastToken);
if (newToken) {
newTokens = [newToken];
i = tokens.length - 1;
}
}
if (newTokens) {
const newTokensList = [
...tokens.slice(0, i),
...newTokens
];
newTokensList.links = tokens.links;
return newTokensList;
}
if (lastToken?.type === 'heading') {
const completeTokens = completeHeading(lastToken, mergeRawTokenText(tokens));
if (completeTokens) {
return completeTokens;
}
}
return null;
}
function completeCodespan(token) {
return completeWithString(token, '`');
}
function completeStar(tokens) {
return completeWithString(tokens, '*');
}
function completeUnderscore(tokens) {
return completeWithString(tokens, '_');
}
function completeLinkTarget(tokens) {
return completeWithString(tokens, ')', false);
}
function completeLinkTargetArg(tokens) {
return completeWithString(tokens, '")', false);
}
function completeLinkText(tokens) {
return completeWithString(tokens, '](https://microsoft.com)', false);
}
function completeDoublestar(tokens) {
return completeWithString(tokens, '**');
}
function completeDoubleUnderscore(tokens) {
return completeWithString(tokens, '__');
}
function completeWithString(tokens, closingString, shouldTrim = true) {
const mergedRawText = mergeRawTokenText(Array.isArray(tokens) ? tokens : [tokens]);
// If it was completed correctly, this should be a single token.
// Expecting either a Paragraph or a List
const trimmedRawText = shouldTrim ? mergedRawText.trimEnd() : mergedRawText;
return lexer(trimmedRawText + closingString)[0];
}
function completeTable(tokens) {
const mergedRawText = mergeRawTokenText(tokens);
const lines = mergedRawText.split('\n');
let numCols; // The number of line1 col headers
let hasSeparatorRow = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (typeof numCols === 'undefined' && line.match(/^\s*\|/)) {
const line1Matches = line.match(/(\|[^\|]+)(?=\||$)/g);
if (line1Matches) {
numCols = line1Matches.length;
}
}
else if (typeof numCols === 'number') {
if (line.match(/^\s*\|/)) {
if (i !== lines.length - 1) {
// We got the line1 header row, and the line2 separator row, but there are more lines, and it wasn't parsed as a table!
// That's strange and means that the table is probably malformed in the source, so I won't try to patch it up.
return undefined;
}
// Got a line2 separator row- partial or complete, doesn't matter, we'll replace it with a correct one
hasSeparatorRow = true;
}
else {
// The line after the header row isn't a valid separator row, so the table is malformed, don't fix it up
return undefined;
}
}
}
if (typeof numCols === 'number' && numCols > 0) {
const prefixText = hasSeparatorRow ? lines.slice(0, -1).join('\n') : mergedRawText;
const line1EndsInPipe = !!prefixText.match(/\|\s*$/);
const newRawText = prefixText + (line1EndsInPipe ? '' : '|') + `\n|${' --- |'.repeat(numCols)}`;
return lexer(newRawText);
}
return undefined;
}
export { allowedMarkdownHtmlAttributes, allowedMarkdownHtmlTags, fillInIncompleteTokens, renderAsPlaintext, renderMarkdown };
+153
View File
@@ -0,0 +1,153 @@
import { isChrome, isFirefox, isSafari } from './browser.js';
import { IframeUtils } from './iframe.js';
import { isMacintosh, isWindows } from '../common/platform.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class StandardMouseEvent {
constructor(targetWindow, e) {
this.timestamp = Date.now();
this.browserEvent = e;
this.leftButton = e.button === 0;
this.middleButton = e.button === 1;
this.rightButton = e.button === 2;
this.buttons = e.buttons;
this.defaultPrevented = e.defaultPrevented;
this.target = e.target;
this.detail = e.detail || 1;
if (e.type === 'dblclick') {
this.detail = 2;
}
this.ctrlKey = e.ctrlKey;
this.shiftKey = e.shiftKey;
this.altKey = e.altKey;
this.metaKey = e.metaKey;
if (typeof e.pageX === 'number') {
this.posx = e.pageX;
this.posy = e.pageY;
}
else {
// Probably hit by MSGestureEvent
this.posx = e.clientX + this.target.ownerDocument.body.scrollLeft + this.target.ownerDocument.documentElement.scrollLeft;
this.posy = e.clientY + this.target.ownerDocument.body.scrollTop + this.target.ownerDocument.documentElement.scrollTop;
}
// Find the position of the iframe this code is executing in relative to the iframe where the event was captured.
const iframeOffsets = IframeUtils.getPositionOfChildWindowRelativeToAncestorWindow(targetWindow, e.view);
this.posx -= iframeOffsets.left;
this.posy -= iframeOffsets.top;
}
preventDefault() {
this.browserEvent.preventDefault();
}
stopPropagation() {
this.browserEvent.stopPropagation();
}
}
class StandardWheelEvent {
constructor(e, deltaX = 0, deltaY = 0) {
this.browserEvent = e || null;
// eslint-disable-next-line local/code-no-any-casts
this.target = e ? (e.target || e.targetNode || e.srcElement) : null;
this.deltaY = deltaY;
this.deltaX = deltaX;
let shouldFactorDPR = false;
if (isChrome) {
// Chrome version >= 123 contains the fix to factor devicePixelRatio into the wheel event.
// See https://chromium.googlesource.com/chromium/src.git/+/be51b448441ff0c9d1f17e0f25c4bf1ab3f11f61
const chromeVersionMatch = navigator.userAgent.match(/Chrome\/(\d+)/);
const chromeMajorVersion = chromeVersionMatch ? parseInt(chromeVersionMatch[1]) : 123;
shouldFactorDPR = chromeMajorVersion <= 122;
}
if (e) {
// Old (deprecated) wheel events
// eslint-disable-next-line local/code-no-any-casts
const e1 = e;
// eslint-disable-next-line local/code-no-any-casts
const e2 = e;
const devicePixelRatio = e.view?.devicePixelRatio || 1;
// vertical delta scroll
if (typeof e1.wheelDeltaY !== 'undefined') {
if (shouldFactorDPR) {
// Refs https://github.com/microsoft/vscode/issues/146403#issuecomment-1854538928
this.deltaY = e1.wheelDeltaY / (120 * devicePixelRatio);
}
else {
this.deltaY = e1.wheelDeltaY / 120;
}
}
else if (typeof e2.VERTICAL_AXIS !== 'undefined' && e2.axis === e2.VERTICAL_AXIS) {
this.deltaY = -e2.detail / 3;
}
else if (e.type === 'wheel') {
// Modern wheel event
// https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
const ev = e;
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
// the deltas are expressed in lines
if (isFirefox && !isMacintosh) {
this.deltaY = -e.deltaY / 3;
}
else {
this.deltaY = -e.deltaY;
}
}
else {
this.deltaY = -e.deltaY / 40;
}
}
// horizontal delta scroll
if (typeof e1.wheelDeltaX !== 'undefined') {
if (isSafari && isWindows) {
this.deltaX = -(e1.wheelDeltaX / 120);
}
else if (shouldFactorDPR) {
// Refs https://github.com/microsoft/vscode/issues/146403#issuecomment-1854538928
this.deltaX = e1.wheelDeltaX / (120 * devicePixelRatio);
}
else {
this.deltaX = e1.wheelDeltaX / 120;
}
}
else if (typeof e2.HORIZONTAL_AXIS !== 'undefined' && e2.axis === e2.HORIZONTAL_AXIS) {
this.deltaX = -e.detail / 3;
}
else if (e.type === 'wheel') {
// Modern wheel event
// https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
const ev = e;
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
// the deltas are expressed in lines
if (isFirefox && !isMacintosh) {
this.deltaX = -e.deltaX / 3;
}
else {
this.deltaX = -e.deltaX;
}
}
else {
this.deltaX = -e.deltaX / 40;
}
}
// Assume a vertical scroll if nothing else worked
if (this.deltaY === 0 && this.deltaX === 0 && e.wheelDelta) {
if (shouldFactorDPR) {
// Refs https://github.com/microsoft/vscode/issues/146403#issuecomment-1854538928
this.deltaY = e.wheelDelta / (120 * devicePixelRatio);
}
else {
this.deltaY = e.wheelDelta / 120;
}
}
}
}
preventDefault() {
this.browserEvent?.preventDefault();
}
stopPropagation() {
this.browserEvent?.stopPropagation();
}
}
export { StandardMouseEvent, StandardWheelEvent };
+222
View File
@@ -0,0 +1,222 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var inputLatency;
(function (inputLatency) {
const totalKeydownTime = { total: 0, min: Number.MAX_VALUE, max: 0 };
const totalInputTime = { ...totalKeydownTime };
const totalRenderTime = { ...totalKeydownTime };
const totalInputLatencyTime = { ...totalKeydownTime };
let measurementsCount = 0;
const state = {
keydown: 0 /* EventPhase.Before */,
input: 0 /* EventPhase.Before */,
render: 0 /* EventPhase.Before */,
};
/**
* Record the start of the keydown event.
*/
function onKeyDown() {
/** Direct Check C. See explanation in {@link recordIfFinished} */
recordIfFinished();
performance.mark('inputlatency/start');
performance.mark('keydown/start');
state.keydown = 1 /* EventPhase.InProgress */;
queueMicrotask(markKeyDownEnd);
}
inputLatency.onKeyDown = onKeyDown;
/**
* Mark the end of the keydown event.
*/
function markKeyDownEnd() {
if (state.keydown === 1 /* EventPhase.InProgress */) {
performance.mark('keydown/end');
state.keydown = 2 /* EventPhase.Finished */;
}
}
/**
* Record the start of the beforeinput event.
*/
function onBeforeInput() {
performance.mark('input/start');
state.input = 1 /* EventPhase.InProgress */;
/** Schedule Task A. See explanation in {@link recordIfFinished} */
scheduleRecordIfFinishedTask();
}
inputLatency.onBeforeInput = onBeforeInput;
/**
* Record the start of the input event.
*/
function onInput() {
if (state.input === 0 /* EventPhase.Before */) {
// it looks like we didn't receive a `beforeinput`
onBeforeInput();
}
queueMicrotask(markInputEnd);
}
inputLatency.onInput = onInput;
function markInputEnd() {
if (state.input === 1 /* EventPhase.InProgress */) {
performance.mark('input/end');
state.input = 2 /* EventPhase.Finished */;
}
}
/**
* Record the start of the keyup event.
*/
function onKeyUp() {
/** Direct Check D. See explanation in {@link recordIfFinished} */
recordIfFinished();
}
inputLatency.onKeyUp = onKeyUp;
/**
* Record the start of the selectionchange event.
*/
function onSelectionChange() {
/** Direct Check E. See explanation in {@link recordIfFinished} */
recordIfFinished();
}
inputLatency.onSelectionChange = onSelectionChange;
/**
* Record the start of the animation frame performing the rendering.
*/
function onRenderStart() {
// Render may be triggered during input, but we only measure the following animation frame
if (state.keydown === 2 /* EventPhase.Finished */ && state.input === 2 /* EventPhase.Finished */ && state.render === 0 /* EventPhase.Before */) {
// Only measure the first render after keyboard input
performance.mark('render/start');
state.render = 1 /* EventPhase.InProgress */;
queueMicrotask(markRenderEnd);
/** Schedule Task B. See explanation in {@link recordIfFinished} */
scheduleRecordIfFinishedTask();
}
}
inputLatency.onRenderStart = onRenderStart;
/**
* Mark the end of the animation frame performing the rendering.
*/
function markRenderEnd() {
if (state.render === 1 /* EventPhase.InProgress */) {
performance.mark('render/end');
state.render = 2 /* EventPhase.Finished */;
}
}
function scheduleRecordIfFinishedTask() {
// Here we can safely assume that the `setTimeout` will not be
// artificially delayed by 4ms because we schedule it from
// event handlers
setTimeout(recordIfFinished);
}
/**
* Record the input latency sample if input handling and rendering are finished.
*
* The challenge here is that we want to record the latency in such a way that it includes
* also the layout and painting work the browser does during the animation frame task.
*
* Simply scheduling a new task (via `setTimeout`) from the animation frame task would
* schedule the new task at the end of the task queue (after other code that uses `setTimeout`),
* so we need to use multiple strategies to make sure our task runs before others:
*
* We schedule tasks (A and B):
* - we schedule a task A (via a `setTimeout` call) when the input starts in `markInputStart`.
* If the animation frame task is scheduled quickly by the browser, then task A has a very good
* chance of being the very first task after the animation frame and thus will record the input latency.
* - however, if the animation frame task is scheduled a bit later, then task A might execute
* before the animation frame task. We therefore schedule another task B from `markRenderStart`.
*
* We do direct checks in browser event handlers (C, D, E):
* - if the browser has multiple keydown events queued up, they will be scheduled before the `setTimeout` tasks,
* so we do a direct check in the keydown event handler (C).
* - depending on timing, sometimes the animation frame is scheduled even before the `keyup` event, so we
* do a direct check there too (E).
* - the browser oftentimes emits a `selectionchange` event after an `input`, so we do a direct check there (D).
*/
function recordIfFinished() {
if (state.keydown === 2 /* EventPhase.Finished */ && state.input === 2 /* EventPhase.Finished */ && state.render === 2 /* EventPhase.Finished */) {
performance.mark('inputlatency/end');
performance.measure('keydown', 'keydown/start', 'keydown/end');
performance.measure('input', 'input/start', 'input/end');
performance.measure('render', 'render/start', 'render/end');
performance.measure('inputlatency', 'inputlatency/start', 'inputlatency/end');
addMeasure('keydown', totalKeydownTime);
addMeasure('input', totalInputTime);
addMeasure('render', totalRenderTime);
addMeasure('inputlatency', totalInputLatencyTime);
// console.info(
// `input latency=${performance.getEntriesByName('inputlatency')[0].duration.toFixed(1)} [` +
// `keydown=${performance.getEntriesByName('keydown')[0].duration.toFixed(1)}, ` +
// `input=${performance.getEntriesByName('input')[0].duration.toFixed(1)}, ` +
// `render=${performance.getEntriesByName('render')[0].duration.toFixed(1)}` +
// `]`
// );
measurementsCount++;
reset();
}
}
function addMeasure(entryName, cumulativeMeasurement) {
const duration = performance.getEntriesByName(entryName)[0].duration;
cumulativeMeasurement.total += duration;
cumulativeMeasurement.min = Math.min(cumulativeMeasurement.min, duration);
cumulativeMeasurement.max = Math.max(cumulativeMeasurement.max, duration);
}
/**
* Clear the current sample.
*/
function reset() {
performance.clearMarks('keydown/start');
performance.clearMarks('keydown/end');
performance.clearMarks('input/start');
performance.clearMarks('input/end');
performance.clearMarks('render/start');
performance.clearMarks('render/end');
performance.clearMarks('inputlatency/start');
performance.clearMarks('inputlatency/end');
performance.clearMeasures('keydown');
performance.clearMeasures('input');
performance.clearMeasures('render');
performance.clearMeasures('inputlatency');
state.keydown = 0 /* EventPhase.Before */;
state.input = 0 /* EventPhase.Before */;
state.render = 0 /* EventPhase.Before */;
}
/**
* Gets all input latency samples and clears the internal buffers to start recording a new set
* of samples.
*/
function getAndClearMeasurements() {
if (measurementsCount === 0) {
return undefined;
}
// Assemble the result
const result = {
keydown: cumulativeToFinalMeasurement(totalKeydownTime),
input: cumulativeToFinalMeasurement(totalInputTime),
render: cumulativeToFinalMeasurement(totalRenderTime),
total: cumulativeToFinalMeasurement(totalInputLatencyTime),
sampleCount: measurementsCount
};
// Clear the cumulative measurements
clearCumulativeMeasurement(totalKeydownTime);
clearCumulativeMeasurement(totalInputTime);
clearCumulativeMeasurement(totalRenderTime);
clearCumulativeMeasurement(totalInputLatencyTime);
measurementsCount = 0;
return result;
}
inputLatency.getAndClearMeasurements = getAndClearMeasurements;
function cumulativeToFinalMeasurement(cumulative) {
return {
average: cumulative.total / measurementsCount,
max: cumulative.max,
min: cumulative.min,
};
}
function clearCumulativeMeasurement(cumulative) {
cumulative.total = 0;
cumulative.min = Number.MAX_VALUE;
cumulative.max = 0;
}
})(inputLatency || (inputLatency = {}));
export { inputLatency };
+88
View File
@@ -0,0 +1,88 @@
import { getWindowId, onDidUnregisterWindow } from './dom.js';
import { Event, Emitter } from '../common/event.js';
import { markAsSingleton, Disposable } from '../common/lifecycle.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* See https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#monitoring_screen_resolution_or_zoom_level_changes
*/
class DevicePixelRatioMonitor extends Disposable {
constructor(targetWindow) {
super();
this._onDidChange = this._register(new Emitter());
this.onDidChange = this._onDidChange.event;
this._listener = () => this._handleChange(targetWindow, true);
this._mediaQueryList = null;
this._handleChange(targetWindow, false);
}
_handleChange(targetWindow, fireEvent) {
this._mediaQueryList?.removeEventListener('change', this._listener);
this._mediaQueryList = targetWindow.matchMedia(`(resolution: ${targetWindow.devicePixelRatio}dppx)`);
this._mediaQueryList.addEventListener('change', this._listener);
if (fireEvent) {
this._onDidChange.fire();
}
}
}
class PixelRatioMonitorImpl extends Disposable {
get value() {
return this._value;
}
constructor(targetWindow) {
super();
this._onDidChange = this._register(new Emitter());
this.onDidChange = this._onDidChange.event;
this._value = this._getPixelRatio(targetWindow);
const dprMonitor = this._register(new DevicePixelRatioMonitor(targetWindow));
this._register(dprMonitor.onDidChange(() => {
this._value = this._getPixelRatio(targetWindow);
this._onDidChange.fire(this._value);
}));
}
_getPixelRatio(targetWindow) {
const ctx = document.createElement('canvas').getContext('2d');
const dpr = targetWindow.devicePixelRatio || 1;
const bsr = ctx.webkitBackingStorePixelRatio ||
ctx.mozBackingStorePixelRatio ||
ctx.msBackingStorePixelRatio ||
ctx.oBackingStorePixelRatio ||
ctx.backingStorePixelRatio || 1;
return dpr / bsr;
}
}
class PixelRatioMonitorFacade {
constructor() {
this.mapWindowIdToPixelRatioMonitor = new Map();
}
_getOrCreatePixelRatioMonitor(targetWindow) {
const targetWindowId = getWindowId(targetWindow);
let pixelRatioMonitor = this.mapWindowIdToPixelRatioMonitor.get(targetWindowId);
if (!pixelRatioMonitor) {
pixelRatioMonitor = markAsSingleton(new PixelRatioMonitorImpl(targetWindow));
this.mapWindowIdToPixelRatioMonitor.set(targetWindowId, pixelRatioMonitor);
markAsSingleton(Event.once(onDidUnregisterWindow)(({ vscodeWindowId }) => {
if (vscodeWindowId === targetWindowId) {
pixelRatioMonitor?.dispose();
this.mapWindowIdToPixelRatioMonitor.delete(targetWindowId);
}
}));
}
return pixelRatioMonitor;
}
getInstance(targetWindow) {
return this._getOrCreatePixelRatioMonitor(targetWindow);
}
}
/**
* Returns the pixel ratio.
*
* This is useful for rendering <canvas> elements at native screen resolution or for being used as
* a cache key when storing font measurements. Fonts might render differently depending on resolution
* and any measurements need to be discarded for example when a window is moved from a monitor to another.
*/
const PixelRatio = new PixelRatioMonitorFacade();
export { PixelRatio };
+270
View File
@@ -0,0 +1,270 @@
import { onDidRegisterWindow, addDisposableListener, scheduleAtNextAnimationFrame } from './dom.js';
import { mainWindow } from './window.js';
import { memoize } from '../common/decorators.js';
import { Event } from '../common/event.js';
import { Disposable, markAsSingleton, toDisposable } from '../common/lifecycle.js';
import { LinkedList } from '../common/linkedList.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate = (undefined && undefined.__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 EventType;
(function (EventType) {
EventType.Tap = '-monaco-gesturetap';
EventType.Change = '-monaco-gesturechange';
EventType.Start = '-monaco-gesturestart';
EventType.End = '-monaco-gesturesend';
EventType.Contextmenu = '-monaco-gesturecontextmenu';
})(EventType || (EventType = {}));
class Gesture extends Disposable {
static { this.SCROLL_FRICTION = -5e-3; }
static { this.HOLD_DELAY = 700; }
static { this.CLEAR_TAP_COUNT_TIME = 400; } // ms
constructor() {
super();
this.dispatched = false;
this.targets = new LinkedList();
this.ignoreTargets = new LinkedList();
this.activeTouches = {};
this.handle = null;
this._lastSetTapCountTime = 0;
this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => {
disposables.add(addDisposableListener(window.document, 'touchstart', (e) => this.onTouchStart(e), { passive: false }));
disposables.add(addDisposableListener(window.document, 'touchend', (e) => this.onTouchEnd(window, e)));
disposables.add(addDisposableListener(window.document, 'touchmove', (e) => this.onTouchMove(e), { passive: false }));
}, { window: mainWindow, disposables: this._store }));
}
static addTarget(element) {
if (!Gesture.isTouchDevice()) {
return Disposable.None;
}
if (!Gesture.INSTANCE) {
Gesture.INSTANCE = markAsSingleton(new Gesture());
}
const remove = Gesture.INSTANCE.targets.push(element);
return toDisposable(remove);
}
static ignoreTarget(element) {
if (!Gesture.isTouchDevice()) {
return Disposable.None;
}
if (!Gesture.INSTANCE) {
Gesture.INSTANCE = markAsSingleton(new Gesture());
}
const remove = Gesture.INSTANCE.ignoreTargets.push(element);
return toDisposable(remove);
}
static isTouchDevice() {
// `'ontouchstart' in window` always evaluates to true with typescript's modern typings. This causes `window` to be
// `never` later in `window.navigator`. That's why we need the explicit `window as Window` cast
return 'ontouchstart' in mainWindow || navigator.maxTouchPoints > 0;
}
dispose() {
if (this.handle) {
this.handle.dispose();
this.handle = null;
}
super.dispose();
}
onTouchStart(e) {
const timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
if (this.handle) {
this.handle.dispose();
this.handle = null;
}
for (let i = 0, len = e.targetTouches.length; i < len; i++) {
const touch = e.targetTouches.item(i);
this.activeTouches[touch.identifier] = {
id: touch.identifier,
initialTarget: touch.target,
initialTimeStamp: timestamp,
initialPageX: touch.pageX,
initialPageY: touch.pageY,
rollingTimestamps: [timestamp],
rollingPageX: [touch.pageX],
rollingPageY: [touch.pageY]
};
const evt = this.newGestureEvent(EventType.Start, touch.target);
evt.pageX = touch.pageX;
evt.pageY = touch.pageY;
this.dispatchEvent(evt);
}
if (this.dispatched) {
e.preventDefault();
e.stopPropagation();
this.dispatched = false;
}
}
onTouchEnd(targetWindow, e) {
const timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
const activeTouchCount = Object.keys(this.activeTouches).length;
for (let i = 0, len = e.changedTouches.length; i < len; i++) {
const touch = e.changedTouches.item(i);
if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) {
console.warn('move of an UNKNOWN touch', touch);
continue;
}
const data = this.activeTouches[touch.identifier], holdTime = Date.now() - data.initialTimeStamp;
if (holdTime < Gesture.HOLD_DELAY
&& Math.abs(data.initialPageX - data.rollingPageX.at(-1)) < 30
&& Math.abs(data.initialPageY - data.rollingPageY.at(-1)) < 30) {
const evt = this.newGestureEvent(EventType.Tap, data.initialTarget);
evt.pageX = data.rollingPageX.at(-1);
evt.pageY = data.rollingPageY.at(-1);
this.dispatchEvent(evt);
}
else if (holdTime >= Gesture.HOLD_DELAY
&& Math.abs(data.initialPageX - data.rollingPageX.at(-1)) < 30
&& Math.abs(data.initialPageY - data.rollingPageY.at(-1)) < 30) {
const evt = this.newGestureEvent(EventType.Contextmenu, data.initialTarget);
evt.pageX = data.rollingPageX.at(-1);
evt.pageY = data.rollingPageY.at(-1);
this.dispatchEvent(evt);
}
else if (activeTouchCount === 1) {
const finalX = data.rollingPageX.at(-1);
const finalY = data.rollingPageY.at(-1);
const deltaT = data.rollingTimestamps.at(-1) - data.rollingTimestamps[0];
const deltaX = finalX - data.rollingPageX[0];
const deltaY = finalY - data.rollingPageY[0];
// We need to get all the dispatch targets on the start of the inertia event
const dispatchTo = [...this.targets].filter(t => data.initialTarget instanceof Node && t.contains(data.initialTarget));
this.inertia(targetWindow, dispatchTo, timestamp, // time now
Math.abs(deltaX) / deltaT, // speed
deltaX > 0 ? 1 : -1, // x direction
finalX, // x now
Math.abs(deltaY) / deltaT, // y speed
deltaY > 0 ? 1 : -1, // y direction
finalY // y now
);
}
this.dispatchEvent(this.newGestureEvent(EventType.End, data.initialTarget));
// forget about this touch
delete this.activeTouches[touch.identifier];
}
if (this.dispatched) {
e.preventDefault();
e.stopPropagation();
this.dispatched = false;
}
}
newGestureEvent(type, initialTarget) {
const event = document.createEvent('CustomEvent');
event.initEvent(type, false, true);
event.initialTarget = initialTarget;
event.tapCount = 0;
return event;
}
dispatchEvent(event) {
if (event.type === EventType.Tap) {
const currentTime = (new Date()).getTime();
let setTapCount = 0;
if (currentTime - this._lastSetTapCountTime > Gesture.CLEAR_TAP_COUNT_TIME) {
setTapCount = 1;
}
else {
setTapCount = 2;
}
this._lastSetTapCountTime = currentTime;
event.tapCount = setTapCount;
}
else if (event.type === EventType.Change || event.type === EventType.Contextmenu) {
// tap is canceled by scrolling or context menu
this._lastSetTapCountTime = 0;
}
if (event.initialTarget instanceof Node) {
for (const ignoreTarget of this.ignoreTargets) {
if (ignoreTarget.contains(event.initialTarget)) {
return;
}
}
const targets = [];
for (const target of this.targets) {
if (target.contains(event.initialTarget)) {
let depth = 0;
let now = event.initialTarget;
while (now && now !== target) {
depth++;
now = now.parentElement;
}
targets.push([depth, target]);
}
}
targets.sort((a, b) => a[0] - b[0]);
for (const [_, target] of targets) {
target.dispatchEvent(event);
this.dispatched = true;
}
}
}
inertia(targetWindow, dispatchTo, t1, vX, dirX, x, vY, dirY, y) {
this.handle = scheduleAtNextAnimationFrame(targetWindow, () => {
const now = Date.now();
// velocity: old speed + accel_over_time
const deltaT = now - t1;
let delta_pos_x = 0, delta_pos_y = 0;
let stopped = true;
vX += Gesture.SCROLL_FRICTION * deltaT;
vY += Gesture.SCROLL_FRICTION * deltaT;
if (vX > 0) {
stopped = false;
delta_pos_x = dirX * vX * deltaT;
}
if (vY > 0) {
stopped = false;
delta_pos_y = dirY * vY * deltaT;
}
// dispatch translation event
const evt = this.newGestureEvent(EventType.Change);
evt.translationX = delta_pos_x;
evt.translationY = delta_pos_y;
dispatchTo.forEach(d => d.dispatchEvent(evt));
if (!stopped) {
this.inertia(targetWindow, dispatchTo, now, vX, dirX, x + delta_pos_x, vY, dirY, y + delta_pos_y);
}
});
}
onTouchMove(e) {
const timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
for (let i = 0, len = e.changedTouches.length; i < len; i++) {
const touch = e.changedTouches.item(i);
if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) {
console.warn('end of an UNKNOWN touch', touch);
continue;
}
const data = this.activeTouches[touch.identifier];
const evt = this.newGestureEvent(EventType.Change, data.initialTarget);
evt.translationX = touch.pageX - data.rollingPageX.at(-1);
evt.translationY = touch.pageY - data.rollingPageY.at(-1);
evt.pageX = touch.pageX;
evt.pageY = touch.pageY;
this.dispatchEvent(evt);
// only keep a few data points, to average the final speed
if (data.rollingPageX.length > 3) {
data.rollingPageX.shift();
data.rollingPageY.shift();
data.rollingTimestamps.shift();
}
data.rollingPageX.push(touch.pageX);
data.rollingPageY.push(touch.pageY);
data.rollingTimestamps.push(timestamp);
}
if (this.dispatched) {
e.preventDefault();
e.stopPropagation();
this.dispatched = false;
}
}
}
__decorate([
memoize
], Gesture, "isTouchDevice", null);
export { EventType, Gesture };
@@ -0,0 +1,29 @@
import { onUnexpectedError } from '../common/errors.js';
import { getMonacoEnvironment } from './browser.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
function createTrustedTypesPolicy(policyName, policyOptions) {
const monacoEnvironment = getMonacoEnvironment();
if (monacoEnvironment?.createTrustedTypesPolicy) {
try {
return monacoEnvironment.createTrustedTypesPolicy(policyName, policyOptions);
}
catch (err) {
onUnexpectedError(err);
return undefined;
}
}
try {
// eslint-disable-next-line local/code-no-any-casts
return globalThis.trustedTypes?.createPolicy(policyName, policyOptions);
}
catch (err) {
onUnexpectedError(err);
return undefined;
}
}
export { createTrustedTypesPolicy };
@@ -0,0 +1,376 @@
import { isFirefox } from '../../browser.js';
import { DataTransfers } from '../../dnd.js';
import { addDisposableListener, EventType, EventHelper } from '../../dom.js';
import { Gesture, EventType as EventType$1 } from '../../touch.js';
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
import { SelectBox } from '../selectBox/selectBox.js';
import { Action, ActionRunner, Separator } from '../../../common/actions.js';
import { Disposable } from '../../../common/lifecycle.js';
import { isMacintosh } from '../../../common/platform.js';
import { isUndefinedOrNull, assertType } from '../../../common/types.js';
import './actionbar.css';
import { localize } from '../../../../nls.js';
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class BaseActionViewItem extends Disposable {
get action() {
return this._action;
}
constructor(context, action, options = {}) {
super();
this.options = options;
this._context = context || this;
this._action = action;
if (action instanceof Action) {
this._register(action.onDidChange(event => {
if (!this.element) {
// we have not been rendered yet, so there
// is no point in updating the UI
return;
}
this.handleActionChangeEvent(event);
}));
}
}
handleActionChangeEvent(event) {
if (event.enabled !== undefined) {
this.updateEnabled();
}
if (event.checked !== undefined) {
this.updateChecked();
}
if (event.class !== undefined) {
this.updateClass();
}
if (event.label !== undefined) {
this.updateLabel();
this.updateTooltip();
}
if (event.tooltip !== undefined) {
this.updateTooltip();
}
}
get actionRunner() {
if (!this._actionRunner) {
this._actionRunner = this._register(new ActionRunner());
}
return this._actionRunner;
}
set actionRunner(actionRunner) {
this._actionRunner = actionRunner;
}
isEnabled() {
return this._action.enabled;
}
setActionContext(newContext) {
this._context = newContext;
}
render(container) {
const element = this.element = container;
this._register(Gesture.addTarget(container));
const enableDragging = this.options && this.options.draggable;
if (enableDragging) {
container.draggable = true;
if (isFirefox) {
// Firefox: requires to set a text data transfer to get going
this._register(addDisposableListener(container, EventType.DRAG_START, e => e.dataTransfer?.setData(DataTransfers.TEXT, this._action.label)));
}
}
this._register(addDisposableListener(element, EventType$1.Tap, e => this.onClick(e, true))); // Preserve focus on tap #125470
this._register(addDisposableListener(element, EventType.MOUSE_DOWN, e => {
if (!enableDragging) {
EventHelper.stop(e, true); // do not run when dragging is on because that would disable it
}
if (this._action.enabled && e.button === 0) {
element.classList.add('active');
}
}));
if (isMacintosh) {
// macOS: allow to trigger the button when holding Ctrl+key and pressing the
// main mouse button. This is for scenarios where e.g. some interaction forces
// the Ctrl+key to be pressed and hold but the user still wants to interact
// with the actions (for example quick access in quick navigation mode).
this._register(addDisposableListener(element, EventType.CONTEXT_MENU, e => {
if (e.button === 0 && e.ctrlKey === true) {
this.onClick(e);
}
}));
}
this._register(addDisposableListener(element, EventType.CLICK, e => {
EventHelper.stop(e, true);
// menus do not use the click event
if (!(this.options && this.options.isMenu)) {
this.onClick(e);
}
}));
this._register(addDisposableListener(element, EventType.DBLCLICK, e => {
EventHelper.stop(e, true);
}));
[EventType.MOUSE_UP, EventType.MOUSE_OUT].forEach(event => {
this._register(addDisposableListener(element, event, e => {
EventHelper.stop(e);
element.classList.remove('active');
}));
});
}
onClick(event, preserveFocus = false) {
EventHelper.stop(event, true);
const context = isUndefinedOrNull(this._context) ? this.options?.useEventAsContext ? event : { preserveFocus } : this._context;
this.actionRunner.run(this._action, context);
}
// Only set the tabIndex on the element once it is about to get focused
// That way this element wont be a tab stop when it is not needed #106441
focus() {
if (this.element) {
this.element.tabIndex = 0;
this.element.focus();
this.element.classList.add('focused');
}
}
blur() {
if (this.element) {
this.element.blur();
this.element.tabIndex = -1;
this.element.classList.remove('focused');
}
}
setFocusable(focusable) {
if (this.element) {
this.element.tabIndex = focusable ? 0 : -1;
}
}
get trapsArrowNavigation() {
return false;
}
updateEnabled() {
// implement in subclass
}
updateLabel() {
// implement in subclass
}
getClass() {
return this.action.class;
}
getTooltip() {
return this.action.tooltip;
}
getHoverContents() {
return this.getTooltip();
}
updateTooltip() {
if (!this.element) {
return;
}
const title = this.getHoverContents() ?? '';
this.updateAriaLabel();
if (!this.customHover && title !== '') {
const hoverDelegate = this.options.hoverDelegate ?? getDefaultHoverDelegate('element');
this.customHover = this._store.add(getBaseLayerHoverDelegate().setupManagedHover(hoverDelegate, this.element, title));
}
else if (this.customHover) {
this.customHover.update(title);
}
}
updateAriaLabel() {
if (this.element) {
const title = this.getTooltip() ?? '';
this.element.setAttribute('aria-label', title);
}
}
updateClass() {
// implement in subclass
}
updateChecked() {
// implement in subclass
}
dispose() {
if (this.element) {
this.element.remove();
this.element = undefined;
}
this._context = undefined;
super.dispose();
}
}
class ActionViewItem extends BaseActionViewItem {
constructor(context, action, options) {
options = {
...options,
icon: options.icon !== undefined ? options.icon : false,
label: options.label !== undefined ? options.label : true,
};
super(context, action, options);
this.options = options;
this.cssClass = '';
}
render(container) {
super.render(container);
assertType(this.element);
const label = document.createElement('a');
label.classList.add('action-label');
label.setAttribute('role', this.getDefaultAriaRole());
this.label = label;
this.element.appendChild(label);
if (this.options.label && this.options.keybinding && !this.options.keybindingNotRenderedWithLabel) {
const kbLabel = document.createElement('span');
kbLabel.classList.add('keybinding');
kbLabel.textContent = this.options.keybinding;
this.element.appendChild(kbLabel);
}
this.updateClass();
this.updateLabel();
this.updateTooltip();
this.updateEnabled();
this.updateChecked();
}
getDefaultAriaRole() {
if (this._action.id === Separator.ID) {
return 'presentation'; // A separator is a presentation item
}
else {
if (this.options.isMenu) {
return 'menuitem';
}
else if (this.options.isTabList) {
return 'tab';
}
else {
return 'button';
}
}
}
// Only set the tabIndex on the element once it is about to get focused
// That way this element wont be a tab stop when it is not needed #106441
focus() {
if (this.label) {
this.label.tabIndex = 0;
this.label.focus();
}
}
blur() {
if (this.label) {
this.label.tabIndex = -1;
}
}
setFocusable(focusable) {
if (this.label) {
this.label.tabIndex = focusable ? 0 : -1;
}
}
updateLabel() {
if (this.options.label && this.label) {
this.label.textContent = this.action.label;
}
}
getTooltip() {
let title = null;
if (this.action.tooltip) {
title = this.action.tooltip;
}
else if (this.action.label) {
title = this.action.label;
if (this.options.keybinding) {
title = localize(0, "{0} ({1})", title, this.options.keybinding);
}
}
return title ?? undefined;
}
updateClass() {
if (this.cssClass && this.label) {
this.label.classList.remove(...this.cssClass.split(' '));
}
if (this.options.icon) {
this.cssClass = this.getClass();
if (this.label) {
this.label.classList.add('codicon');
if (this.cssClass) {
this.label.classList.add(...this.cssClass.split(' '));
}
}
this.updateEnabled();
}
else {
this.label?.classList.remove('codicon');
}
}
updateEnabled() {
if (this.action.enabled) {
if (this.label) {
this.label.removeAttribute('aria-disabled');
this.label.classList.remove('disabled');
}
this.element?.classList.remove('disabled');
}
else {
if (this.label) {
this.label.setAttribute('aria-disabled', 'true');
this.label.classList.add('disabled');
}
this.element?.classList.add('disabled');
}
}
updateAriaLabel() {
if (this.label) {
const title = this.getTooltip() ?? '';
this.label.setAttribute('aria-label', title);
}
}
updateChecked() {
if (this.label) {
if (this.action.checked !== undefined) {
this.label.classList.toggle('checked', this.action.checked);
if (this.options.isTabList) {
this.label.setAttribute('aria-selected', this.action.checked ? 'true' : 'false');
}
else {
this.label.setAttribute('aria-checked', this.action.checked ? 'true' : 'false');
this.label.setAttribute('role', 'checkbox');
}
}
else {
this.label.classList.remove('checked');
this.label.removeAttribute(this.options.isTabList ? 'aria-selected' : 'aria-checked');
this.label.setAttribute('role', this.getDefaultAriaRole());
}
}
}
}
class SelectActionViewItem extends BaseActionViewItem {
constructor(ctx, action, options, selected, contextViewProvider, styles, selectBoxOptions) {
super(ctx, action);
this.selectBox = new SelectBox(options, selected, contextViewProvider, styles, selectBoxOptions);
this.selectBox.setFocusable(false);
this._register(this.selectBox);
this.registerListeners();
}
select(index) {
this.selectBox.select(index);
}
registerListeners() {
this._register(this.selectBox.onDidSelect(e => this.runAction(e.selected, e.index)));
}
runAction(option, index) {
this.actionRunner.run(this._action, this.getActionContext(option, index));
}
getActionContext(option, index) {
return option;
}
setFocusable(focusable) {
this.selectBox.setFocusable(focusable);
}
focus() {
this.selectBox?.focus();
}
blur() {
this.selectBox?.blur();
}
render(container) {
this.selectBox.render(container);
}
}
export { ActionViewItem, BaseActionViewItem, SelectActionViewItem };
@@ -0,0 +1,124 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-action-bar {
white-space: nowrap;
height: 100%;
}
.monaco-action-bar .actions-container {
display: flex;
margin: 0 auto;
padding: 0;
height: 100%;
width: 100%;
align-items: center;
}
.monaco-action-bar.vertical .actions-container {
display: inline-block;
}
.monaco-action-bar .action-item {
display: block;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative; /* DO NOT REMOVE - this is the key to preventing the ghosting icon bug in Chrome 42 */
}
.monaco-action-bar .action-item.disabled {
cursor: default;
}
.monaco-action-bar .action-item .icon,
.monaco-action-bar .action-item .codicon {
display: block;
}
.monaco-action-bar .action-item .codicon {
display: flex;
align-items: center;
width: 16px;
height: 16px;
}
.monaco-action-bar .action-label {
display: flex;
font-size: 11px;
padding: 3px;
border-radius: 5px;
}
.monaco-action-bar .action-item.disabled .action-label:not(.icon) ,
.monaco-action-bar .action-item.disabled .action-label:not(.icon)::before,
.monaco-action-bar .action-item.disabled .action-label:not(.icon):hover {
color: var(--vscode-disabledForeground);
}
/* Unable to change color of SVGs, hence opacity is used */
.monaco-action-bar .action-item.disabled .action-label.icon ,
.monaco-action-bar .action-item.disabled .action-label.icon::before,
.monaco-action-bar .action-item.disabled .action-label.icon:hover {
opacity: 0.6;
}
/* Vertical actions */
.monaco-action-bar.vertical {
text-align: left;
}
.monaco-action-bar.vertical .action-item {
display: block;
}
.monaco-action-bar.vertical .action-label.separator {
display: block;
border-bottom: 1px solid var(--vscode-disabledForeground);
padding-top: 1px;
margin-left: .8em;
margin-right: .8em;
}
.monaco-action-bar .action-item .action-label.separator {
width: 1px;
height: 16px;
margin: 5px 4px !important;
cursor: default;
min-width: 1px;
padding: 0;
background-color: var(--vscode-disabledForeground);
}
.secondary-actions .monaco-action-bar .action-label {
margin-left: 6px;
}
/* Action Items */
.monaco-action-bar .action-item.select-container {
overflow: hidden; /* somehow the dropdown overflows its container, we prevent it here to not push */
flex: 1;
max-width: 170px;
min-width: 60px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
}
.monaco-action-bar .action-item.action-dropdown-item {
display: flex;
}
.monaco-action-bar .action-item.action-dropdown-item > .action-dropdown-item-separator {
display: flex;
align-items: center;
cursor: default;
}
.monaco-action-bar .action-item.action-dropdown-item > .action-dropdown-item-separator > div {
width: 1px;
}
@@ -0,0 +1,462 @@
import { addDisposableListener, EventType, trackFocus, getActiveElement, isAncestor, isHTMLElement, EventHelper, clearNode } from '../../dom.js';
import { StandardKeyboardEvent } from '../../keyboardEvent.js';
import { BaseActionViewItem, ActionViewItem } from './actionViewItems.js';
import { createInstantHoverDelegate } from '../hover/hoverDelegateFactory.js';
import { ActionRunner, Separator } from '../../../common/actions.js';
import { Emitter } from '../../../common/event.js';
import { Disposable, DisposableStore, DisposableMap, dispose } from '../../../common/lifecycle.js';
import { isNumber, isFunction } from '../../../common/types.js';
import './actionbar.css';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class ActionBar extends Disposable {
get onDidBlur() { return this._onDidBlur.event; }
get onDidCancel() { return this._onDidCancel.event; }
get onDidRun() { return this._onDidRun.event; }
get onWillRun() { return this._onWillRun.event; }
constructor(container, options = {}) {
super();
this._actionRunnerDisposables = this._register(new DisposableStore());
this.viewItemDisposables = this._register(new DisposableMap());
// Trigger Key Tracking
this.triggerKeyDown = false;
this.focusable = true;
this._onDidBlur = this._register(new Emitter());
this._onDidCancel = this._register(new Emitter({ onWillAddFirstListener: () => this.cancelHasListener = true }));
this.cancelHasListener = false;
this._onDidRun = this._register(new Emitter());
this._onWillRun = this._register(new Emitter());
this.options = options;
this._context = options.context ?? null;
this._orientation = this.options.orientation ?? 0 /* ActionsOrientation.HORIZONTAL */;
this._triggerKeys = {
keyDown: this.options.triggerKeys?.keyDown ?? false,
keys: this.options.triggerKeys?.keys ?? [3 /* KeyCode.Enter */, 10 /* KeyCode.Space */]
};
this._hoverDelegate = options.hoverDelegate ?? this._register(createInstantHoverDelegate());
if (this.options.actionRunner) {
this._actionRunner = this.options.actionRunner;
}
else {
this._actionRunner = new ActionRunner();
this._actionRunnerDisposables.add(this._actionRunner);
}
this._actionRunnerDisposables.add(this._actionRunner.onDidRun(e => this._onDidRun.fire(e)));
this._actionRunnerDisposables.add(this._actionRunner.onWillRun(e => this._onWillRun.fire(e)));
this.viewItems = [];
this.focusedItem = undefined;
this.domNode = document.createElement('div');
this.domNode.className = 'monaco-action-bar';
let previousKeys;
let nextKeys;
switch (this._orientation) {
case 0 /* ActionsOrientation.HORIZONTAL */:
previousKeys = [15 /* KeyCode.LeftArrow */];
nextKeys = [17 /* KeyCode.RightArrow */];
break;
case 1 /* ActionsOrientation.VERTICAL */:
previousKeys = [16 /* KeyCode.UpArrow */];
nextKeys = [18 /* KeyCode.DownArrow */];
this.domNode.className += ' vertical';
break;
}
this._register(addDisposableListener(this.domNode, EventType.KEY_DOWN, e => {
const event = new StandardKeyboardEvent(e);
let eventHandled = true;
const focusedItem = typeof this.focusedItem === 'number' ? this.viewItems[this.focusedItem] : undefined;
if (previousKeys && (event.equals(previousKeys[0]) || event.equals(previousKeys[1]))) {
eventHandled = this.focusPrevious();
}
else if (nextKeys && (event.equals(nextKeys[0]) || event.equals(nextKeys[1]))) {
eventHandled = this.focusNext();
}
else if (event.equals(9 /* KeyCode.Escape */) && this.cancelHasListener) {
this._onDidCancel.fire();
}
else if (event.equals(14 /* KeyCode.Home */)) {
eventHandled = this.focusFirst();
}
else if (event.equals(13 /* KeyCode.End */)) {
eventHandled = this.focusLast();
}
else if (event.equals(2 /* KeyCode.Tab */) && focusedItem instanceof BaseActionViewItem && focusedItem.trapsArrowNavigation) {
// Tab, so forcibly focus next #219199
eventHandled = this.focusNext(undefined, true);
}
else if (this.isTriggerKeyEvent(event)) {
// Staying out of the else branch even if not triggered
if (this._triggerKeys.keyDown) {
this.doTrigger(event);
}
else {
this.triggerKeyDown = true;
}
}
else {
eventHandled = false;
}
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
}
}));
this._register(addDisposableListener(this.domNode, EventType.KEY_UP, e => {
const event = new StandardKeyboardEvent(e);
// Run action on Enter/Space
if (this.isTriggerKeyEvent(event)) {
if (!this._triggerKeys.keyDown && this.triggerKeyDown) {
this.triggerKeyDown = false;
this.doTrigger(event);
}
event.preventDefault();
event.stopPropagation();
}
// Recompute focused item
else if (event.equals(2 /* KeyCode.Tab */) || event.equals(1024 /* KeyMod.Shift */ | 2 /* KeyCode.Tab */) || event.equals(16 /* KeyCode.UpArrow */) || event.equals(18 /* KeyCode.DownArrow */) || event.equals(15 /* KeyCode.LeftArrow */) || event.equals(17 /* KeyCode.RightArrow */)) {
this.updateFocusedItem();
}
}));
this.focusTracker = this._register(trackFocus(this.domNode));
this._register(this.focusTracker.onDidBlur(() => {
if (getActiveElement() === this.domNode || !isAncestor(getActiveElement(), this.domNode)) {
this._onDidBlur.fire();
this.previouslyFocusedItem = this.focusedItem;
this.focusedItem = undefined;
this.triggerKeyDown = false;
}
}));
this._register(this.focusTracker.onDidFocus(() => this.updateFocusedItem()));
this.actionsList = document.createElement('ul');
this.actionsList.className = 'actions-container';
if (this.options.highlightToggledItems) {
this.actionsList.classList.add('highlight-toggled');
}
this.actionsList.setAttribute('role', this.options.ariaRole || 'toolbar');
if (this.options.ariaLabel) {
this.actionsList.setAttribute('aria-label', this.options.ariaLabel);
}
this.domNode.appendChild(this.actionsList);
container.appendChild(this.domNode);
}
refreshRole() {
if (this.length() >= 1) {
this.actionsList.setAttribute('role', this.options.ariaRole || 'toolbar');
}
else {
this.actionsList.setAttribute('role', 'presentation');
}
}
// Some action bars should not be focusable at times
// When an action bar is not focusable make sure to make all the elements inside it not focusable
// When an action bar is focusable again, make sure the first item can be focused
setFocusable(focusable) {
this.focusable = focusable;
if (this.focusable) {
const firstEnabled = this.viewItems.find(vi => vi instanceof BaseActionViewItem && vi.isEnabled());
if (firstEnabled instanceof BaseActionViewItem) {
firstEnabled.setFocusable(true);
}
}
else {
this.viewItems.forEach(vi => {
if (vi instanceof BaseActionViewItem) {
vi.setFocusable(false);
}
});
}
}
isTriggerKeyEvent(event) {
let ret = false;
this._triggerKeys.keys.forEach(keyCode => {
ret = ret || event.equals(keyCode);
});
return ret;
}
updateFocusedItem() {
for (let i = 0; i < this.actionsList.children.length; i++) {
const elem = this.actionsList.children[i];
if (isAncestor(getActiveElement(), elem)) {
this.focusedItem = i;
this.viewItems[this.focusedItem]?.showHover?.();
break;
}
}
}
get context() {
return this._context;
}
set context(context) {
this._context = context;
this.viewItems.forEach(i => i.setActionContext(context));
}
get actionRunner() {
return this._actionRunner;
}
set actionRunner(actionRunner) {
this._actionRunner = actionRunner;
// when setting a new `IActionRunner` make sure to dispose old listeners and
// start to forward events from the new listener
this._actionRunnerDisposables.clear();
this._actionRunnerDisposables.add(this._actionRunner.onDidRun(e => this._onDidRun.fire(e)));
this._actionRunnerDisposables.add(this._actionRunner.onWillRun(e => this._onWillRun.fire(e)));
this.viewItems.forEach(item => item.actionRunner = actionRunner);
}
getContainer() {
return this.domNode;
}
getAction(indexOrElement) {
// by index
if (typeof indexOrElement === 'number') {
return this.viewItems[indexOrElement]?.action;
}
// by element
if (isHTMLElement(indexOrElement)) {
while (indexOrElement.parentElement !== this.actionsList) {
if (!indexOrElement.parentElement) {
return undefined;
}
indexOrElement = indexOrElement.parentElement;
}
for (let i = 0; i < this.actionsList.childNodes.length; i++) {
if (this.actionsList.childNodes[i] === indexOrElement) {
return this.viewItems[i].action;
}
}
}
return undefined;
}
push(arg, options = {}) {
const actions = Array.isArray(arg) ? arg : [arg];
let index = isNumber(options.index) ? options.index : null;
actions.forEach((action) => {
const actionViewItemElement = document.createElement('li');
actionViewItemElement.className = 'action-item';
actionViewItemElement.setAttribute('role', 'presentation');
let item;
const viewItemOptions = { hoverDelegate: this._hoverDelegate, ...options, isTabList: this.options.ariaRole === 'tablist' };
if (this.options.actionViewItemProvider) {
item = this.options.actionViewItemProvider(action, viewItemOptions);
}
if (!item) {
item = new ActionViewItem(this.context, action, viewItemOptions);
}
// Prevent native context menu on actions
if (!this.options.allowContextMenu) {
this.viewItemDisposables.set(item, addDisposableListener(actionViewItemElement, EventType.CONTEXT_MENU, (e) => {
EventHelper.stop(e, true);
}));
}
item.actionRunner = this._actionRunner;
item.setActionContext(this.context);
item.render(actionViewItemElement);
if (index === null || index < 0 || index >= this.actionsList.children.length) {
this.actionsList.appendChild(actionViewItemElement);
this.viewItems.push(item);
}
else {
this.actionsList.insertBefore(actionViewItemElement, this.actionsList.children[index]);
this.viewItems.splice(index, 0, item);
index++;
}
});
// We need to allow for the first enabled item to be focused on using tab navigation #106441
if (this.focusable) {
let didFocus = false;
for (const item of this.viewItems) {
if (!(item instanceof BaseActionViewItem)) {
continue;
}
let focus;
if (didFocus) {
focus = false; // already focused an item
}
else if (item.action.id === Separator.ID) {
focus = false; // never focus a separator
}
else if (!item.isEnabled() && this.options.focusOnlyEnabledItems) {
focus = false; // never focus a disabled item
}
else {
focus = true;
}
if (focus) {
item.setFocusable(true);
didFocus = true;
}
else {
item.setFocusable(false);
}
}
}
if (typeof this.focusedItem === 'number') {
// After a clear actions might be re-added to simply toggle some actions. We should preserve focus #97128
this.focus(this.focusedItem);
}
this.refreshRole();
}
getWidth(index) {
if (index >= 0 && index < this.actionsList.children.length) {
const item = this.actionsList.children.item(index);
if (item) {
return item.clientWidth;
}
}
return 0;
}
pull(index) {
if (index >= 0 && index < this.viewItems.length) {
this.actionsList.childNodes[index].remove();
this.viewItemDisposables.deleteAndDispose(this.viewItems[index]);
dispose(this.viewItems.splice(index, 1));
this.refreshRole();
}
}
clear() {
if (this.isEmpty()) {
return;
}
this.viewItems = dispose(this.viewItems);
this.viewItemDisposables.clearAndDisposeAll();
clearNode(this.actionsList);
this.refreshRole();
}
length() {
return this.viewItems.length;
}
isEmpty() {
return this.viewItems.length === 0;
}
focus(arg) {
let selectFirst = false;
let index = undefined;
if (arg === undefined) {
selectFirst = true;
}
else if (typeof arg === 'number') {
index = arg;
}
else if (typeof arg === 'boolean') {
selectFirst = arg;
}
if (selectFirst && typeof this.focusedItem === 'undefined') {
const firstEnabled = this.viewItems.findIndex(item => item.isEnabled());
// Focus the first enabled item
this.focusedItem = firstEnabled === -1 ? undefined : firstEnabled;
this.updateFocus(undefined, undefined, true);
}
else {
if (index !== undefined) {
this.focusedItem = index;
}
this.updateFocus(undefined, undefined, true);
}
}
focusFirst() {
this.focusedItem = this.length() - 1;
return this.focusNext(true);
}
focusLast() {
this.focusedItem = 0;
return this.focusPrevious(true);
}
focusNext(forceLoop, forceFocus) {
if (typeof this.focusedItem === 'undefined') {
this.focusedItem = this.viewItems.length - 1;
}
else if (this.viewItems.length <= 1) {
return false;
}
const startIndex = this.focusedItem;
let item;
do {
if (!forceLoop && this.options.preventLoopNavigation && this.focusedItem + 1 >= this.viewItems.length) {
this.focusedItem = startIndex;
return false;
}
this.focusedItem = (this.focusedItem + 1) % this.viewItems.length;
item = this.viewItems[this.focusedItem];
} while (this.focusedItem !== startIndex && ((this.options.focusOnlyEnabledItems && !item.isEnabled()) || item.action.id === Separator.ID));
this.updateFocus(undefined, undefined, forceFocus);
return true;
}
focusPrevious(forceLoop) {
if (typeof this.focusedItem === 'undefined') {
this.focusedItem = 0;
}
else if (this.viewItems.length <= 1) {
return false;
}
const startIndex = this.focusedItem;
let item;
do {
this.focusedItem = this.focusedItem - 1;
if (this.focusedItem < 0) {
if (!forceLoop && this.options.preventLoopNavigation) {
this.focusedItem = startIndex;
return false;
}
this.focusedItem = this.viewItems.length - 1;
}
item = this.viewItems[this.focusedItem];
} while (this.focusedItem !== startIndex && ((this.options.focusOnlyEnabledItems && !item.isEnabled()) || item.action.id === Separator.ID));
this.updateFocus(true);
return true;
}
updateFocus(fromRight, preventScroll, forceFocus = false) {
if (typeof this.focusedItem === 'undefined') {
this.actionsList.focus({ preventScroll });
}
if (this.previouslyFocusedItem !== undefined && this.previouslyFocusedItem !== this.focusedItem) {
this.viewItems[this.previouslyFocusedItem]?.blur();
}
const actionViewItem = this.focusedItem !== undefined ? this.viewItems[this.focusedItem] : undefined;
if (actionViewItem) {
let focusItem = true;
if (!isFunction(actionViewItem.focus)) {
focusItem = false;
}
if (this.options.focusOnlyEnabledItems && isFunction(actionViewItem.isEnabled) && !actionViewItem.isEnabled()) {
focusItem = false;
}
if (actionViewItem.action.id === Separator.ID) {
focusItem = false;
}
if (!focusItem) {
this.actionsList.focus({ preventScroll });
this.previouslyFocusedItem = undefined;
}
else if (forceFocus || this.previouslyFocusedItem !== this.focusedItem) {
actionViewItem.focus(fromRight);
this.previouslyFocusedItem = this.focusedItem;
}
if (focusItem) {
actionViewItem.showHover?.();
}
}
}
doTrigger(event) {
if (typeof this.focusedItem === 'undefined') {
return; //nothing to focus
}
// trigger action
const actionViewItem = this.viewItems[this.focusedItem];
if (actionViewItem instanceof BaseActionViewItem) {
const context = (actionViewItem._context === null || actionViewItem._context === undefined) ? event : actionViewItem._context;
this.run(actionViewItem._action, context);
}
}
async run(action, context) {
await this._actionRunner.run(action, context);
}
dispose() {
this._context = undefined;
this.viewItems = dispose(this.viewItems);
this.getContainer().remove();
super.dispose();
}
}
export { ActionBar };
@@ -0,0 +1,9 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-aria-container {
position: absolute; /* try to hide from window but not from screen readers */
left:-999em;
}
@@ -0,0 +1,84 @@
import { clearNode } from '../../dom.js';
import './aria.css';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Use a max length since we are inserting the whole msg in the DOM and that can cause browsers to freeze for long messages #94233
const MAX_MESSAGE_LENGTH = 20000;
let ariaContainer;
let alertContainer;
let alertContainer2;
let statusContainer;
let statusContainer2;
function setARIAContainer(parent) {
ariaContainer = document.createElement('div');
ariaContainer.className = 'monaco-aria-container';
const createAlertContainer = () => {
const element = document.createElement('div');
element.className = 'monaco-alert';
element.setAttribute('role', 'alert');
element.setAttribute('aria-atomic', 'true');
ariaContainer.appendChild(element);
return element;
};
alertContainer = createAlertContainer();
alertContainer2 = createAlertContainer();
const createStatusContainer = () => {
const element = document.createElement('div');
element.className = 'monaco-status';
element.setAttribute('aria-live', 'polite');
element.setAttribute('aria-atomic', 'true');
ariaContainer.appendChild(element);
return element;
};
statusContainer = createStatusContainer();
statusContainer2 = createStatusContainer();
parent.appendChild(ariaContainer);
}
/**
* Given the provided message, will make sure that it is read as alert to screen readers.
*/
function alert(msg) {
if (!ariaContainer) {
return;
}
// Use alternate containers such that duplicated messages get read out by screen readers #99466
if (alertContainer.textContent !== msg) {
clearNode(alertContainer2);
insertMessage(alertContainer, msg);
}
else {
clearNode(alertContainer);
insertMessage(alertContainer2, msg);
}
}
/**
* Given the provided message, will make sure that it is read as status to screen readers.
*/
function status(msg) {
if (!ariaContainer) {
return;
}
if (statusContainer.textContent !== msg) {
clearNode(statusContainer2);
insertMessage(statusContainer, msg);
}
else {
clearNode(statusContainer);
insertMessage(statusContainer2, msg);
}
}
function insertMessage(target, msg) {
clearNode(target);
if (msg.length > MAX_MESSAGE_LENGTH) {
msg = msg.substr(0, MAX_MESSAGE_LENGTH);
}
target.textContent = msg;
// See https://www.paciellogroup.com/blog/2012/06/html5-accessibility-chops-aria-rolealert-browser-support/
target.style.visibility = 'hidden';
target.style.visibility = 'visible';
}
export { alert, setARIAContainer, status };
@@ -0,0 +1,174 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-text-button {
box-sizing: border-box;
display: flex;
width: 100%;
padding: 4px;
border-radius: 2px;
text-align: center;
cursor: pointer;
justify-content: center;
align-items: center;
border: 1px solid var(--vscode-button-border, transparent);
line-height: 18px;
}
.monaco-text-button:focus {
outline-offset: 2px !important;
}
.monaco-text-button:hover {
text-decoration: none !important;
}
.monaco-button.disabled:focus,
.monaco-button.disabled {
opacity: 0.4 !important;
cursor: default;
}
.monaco-text-button .codicon {
margin: 0 0.2em;
color: inherit !important;
}
.monaco-text-button.monaco-text-button-with-short-label {
flex-direction: row;
flex-wrap: wrap;
padding: 0 4px;
overflow: hidden;
height: 28px;
}
.monaco-text-button.monaco-text-button-with-short-label > .monaco-button-label {
flex-basis: 100%;
}
.monaco-text-button.monaco-text-button-with-short-label > .monaco-button-label-short {
flex-grow: 1;
width: 0;
overflow: hidden;
}
.monaco-text-button.monaco-text-button-with-short-label > .monaco-button-label,
.monaco-text-button.monaco-text-button-with-short-label > .monaco-button-label-short {
display: flex;
justify-content: center;
align-items: center;
font-weight: normal;
font-style: inherit;
padding: 4px 0;
}
.monaco-button-dropdown {
display: flex;
cursor: pointer;
}
.monaco-button-dropdown.disabled {
cursor: default;
}
.monaco-button-dropdown > .monaco-button:focus {
outline-offset: -1px !important;
}
.monaco-button-dropdown.disabled > .monaco-button.disabled,
.monaco-button-dropdown.disabled > .monaco-button.disabled:focus,
.monaco-button-dropdown.disabled > .monaco-button-dropdown-separator {
opacity: 0.4 !important;
}
.monaco-button-dropdown > .monaco-button.monaco-text-button {
border-right-width: 0 !important;
}
.monaco-button-dropdown .monaco-button-dropdown-separator {
padding: 4px 0;
cursor: default;
}
.monaco-button-dropdown .monaco-button-dropdown-separator > div {
height: 100%;
width: 1px;
}
.monaco-button-dropdown > .monaco-button.monaco-dropdown-button {
border: 1px solid var(--vscode-button-border, transparent);
border-left-width: 0 !important;
border-radius: 0 2px 2px 0;
display: flex;
align-items: center;
}
.monaco-button-dropdown > .monaco-button.monaco-text-button {
border-radius: 2px 0 0 2px;
}
.monaco-description-button {
display: flex;
flex-direction: column;
align-items: center;
margin: 4px 5px; /* allows button focus outline to be visible */
}
.monaco-description-button .monaco-button-description {
font-style: italic;
font-size: 11px;
padding: 4px 20px;
}
.monaco-description-button .monaco-button-label,
.monaco-description-button .monaco-button-description {
display: flex;
justify-content: center;
align-items: center;
}
.monaco-description-button .monaco-button-label > .codicon,
.monaco-description-button .monaco-button-description > .codicon {
margin: 0 0.2em;
color: inherit !important;
}
/* default color styles - based on CSS variables */
.monaco-button.default-colors,
.monaco-button-dropdown.default-colors > .monaco-button{
color: var(--vscode-button-foreground);
background-color: var(--vscode-button-background);
}
.monaco-button.default-colors:hover,
.monaco-button-dropdown.default-colors > .monaco-button:hover {
background-color: var(--vscode-button-hoverBackground);
}
.monaco-button.default-colors.secondary,
.monaco-button-dropdown.default-colors > .monaco-button.secondary {
color: var(--vscode-button-secondaryForeground);
background-color: var(--vscode-button-secondaryBackground);
}
.monaco-button.default-colors.secondary:hover,
.monaco-button-dropdown.default-colors > .monaco-button.secondary:hover {
background-color: var(--vscode-button-secondaryHoverBackground);
}
.monaco-button-dropdown.default-colors .monaco-button-dropdown-separator {
background-color: var(--vscode-button-background);
border-top: 1px solid var(--vscode-button-border);
border-bottom: 1px solid var(--vscode-button-border);
}
.monaco-button-dropdown.default-colors .monaco-button.secondary + .monaco-button-dropdown-separator {
background-color: var(--vscode-button-secondaryBackground);
}
.monaco-button-dropdown.default-colors .monaco-button-dropdown-separator > div {
background-color: var(--vscode-button-separator);
}
@@ -0,0 +1,226 @@
import { EventType, addDisposableListener, EventHelper, trackFocus, reset } from '../../dom.js';
import { StandardKeyboardEvent } from '../../keyboardEvent.js';
import { renderMarkdown, renderAsPlaintext } from '../../markdownRenderer.js';
import { Gesture, EventType as EventType$1 } from '../../touch.js';
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
import { renderLabelWithIcons } from '../iconLabel/iconLabels.js';
import { Color } from '../../../common/color.js';
import { Emitter } from '../../../common/event.js';
import { isMarkdownString, markdownStringEqual } from '../../../common/htmlContent.js';
import { Disposable } from '../../../common/lifecycle.js';
import { ThemeIcon } from '../../../common/themables.js';
import './button.css';
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
import { safeSetInnerHtml } from '../../domSanitize.js';
({
buttonSeparator: Color.white.toString(),
buttonForeground: Color.white.toString()});
// Only allow a very limited set of inline html tags
const buttonSanitizerConfig = Object.freeze({
allowedTags: {
override: ['b', 'i', 'u', 'code', 'span'],
},
allowedAttributes: {
override: ['class'],
},
});
class Button extends Disposable {
get onDidClick() { return this._onDidClick.event; }
constructor(container, options) {
super();
this._label = '';
this._onDidClick = this._register(new Emitter());
this._onDidEscape = this._register(new Emitter());
this.options = options;
this._element = document.createElement('a');
this._element.classList.add('monaco-button');
this._element.tabIndex = 0;
this._element.setAttribute('role', 'button');
this._element.classList.toggle('secondary', !!options.secondary);
const background = options.secondary ? options.buttonSecondaryBackground : options.buttonBackground;
const foreground = options.secondary ? options.buttonSecondaryForeground : options.buttonForeground;
this._element.style.color = foreground || '';
this._element.style.backgroundColor = background || '';
if (options.supportShortLabel) {
this._labelShortElement = document.createElement('div');
this._labelShortElement.classList.add('monaco-button-label-short');
this._element.appendChild(this._labelShortElement);
this._labelElement = document.createElement('div');
this._labelElement.classList.add('monaco-button-label');
this._element.appendChild(this._labelElement);
this._element.classList.add('monaco-text-button-with-short-label');
}
if (typeof options.title === 'string') {
this.setTitle(options.title);
}
if (typeof options.ariaLabel === 'string') {
this._element.setAttribute('aria-label', options.ariaLabel);
}
container.appendChild(this._element);
this.enabled = !options.disabled;
this._register(Gesture.addTarget(this._element));
[EventType.CLICK, EventType$1.Tap].forEach(eventType => {
this._register(addDisposableListener(this._element, eventType, e => {
if (!this.enabled) {
EventHelper.stop(e);
return;
}
this._onDidClick.fire(e);
}));
});
this._register(addDisposableListener(this._element, EventType.KEY_DOWN, e => {
const event = new StandardKeyboardEvent(e);
let eventHandled = false;
if (this.enabled && (event.equals(3 /* KeyCode.Enter */) || event.equals(10 /* KeyCode.Space */))) {
this._onDidClick.fire(e);
eventHandled = true;
}
else if (event.equals(9 /* KeyCode.Escape */)) {
this._onDidEscape.fire(e);
this._element.blur();
eventHandled = true;
}
if (eventHandled) {
EventHelper.stop(event, true);
}
}));
this._register(addDisposableListener(this._element, EventType.MOUSE_OVER, e => {
if (!this._element.classList.contains('disabled')) {
this.updateBackground(true);
}
}));
this._register(addDisposableListener(this._element, EventType.MOUSE_OUT, e => {
this.updateBackground(false); // restore standard styles
}));
// Also set hover background when button is focused for feedback
this.focusTracker = this._register(trackFocus(this._element));
this._register(this.focusTracker.onDidFocus(() => { if (this.enabled) {
this.updateBackground(true);
} }));
this._register(this.focusTracker.onDidBlur(() => { if (this.enabled) {
this.updateBackground(false);
} }));
}
dispose() {
super.dispose();
this._element.remove();
}
getContentElements(content) {
const elements = [];
for (let segment of renderLabelWithIcons(content)) {
if (typeof (segment) === 'string') {
segment = segment.trim();
// Ignore empty segment
if (segment === '') {
continue;
}
// Convert string segments to <span> nodes
const node = document.createElement('span');
node.textContent = segment;
elements.push(node);
}
else {
elements.push(segment);
}
}
return elements;
}
updateBackground(hover) {
let background;
if (this.options.secondary) {
background = hover ? this.options.buttonSecondaryHoverBackground : this.options.buttonSecondaryBackground;
}
else {
background = hover ? this.options.buttonHoverBackground : this.options.buttonBackground;
}
if (background) {
this._element.style.backgroundColor = background;
}
}
get element() {
return this._element;
}
set label(value) {
if (this._label === value) {
return;
}
if (isMarkdownString(this._label) && isMarkdownString(value) && markdownStringEqual(this._label, value)) {
return;
}
this._element.classList.add('monaco-text-button');
const labelElement = this.options.supportShortLabel ? this._labelElement : this._element;
if (isMarkdownString(value)) {
const rendered = renderMarkdown(value, undefined, document.createElement('span'));
rendered.dispose();
// Don't include outer `<p>`
// eslint-disable-next-line no-restricted-syntax
const root = rendered.element.querySelector('p')?.innerHTML;
if (root) {
safeSetInnerHtml(labelElement, root, buttonSanitizerConfig);
}
else {
reset(labelElement);
}
}
else {
if (this.options.supportIcons) {
reset(labelElement, ...this.getContentElements(value));
}
else {
labelElement.textContent = value;
}
}
let title = '';
if (typeof this.options.title === 'string') {
title = this.options.title;
}
else if (this.options.title) {
title = renderAsPlaintext(value);
}
this.setTitle(title);
this._setAriaLabel();
this._label = value;
}
get label() {
return this._label;
}
_setAriaLabel() {
if (typeof this.options.ariaLabel === 'string') {
this._element.setAttribute('aria-label', this.options.ariaLabel);
}
else if (typeof this.options.title === 'string') {
this._element.setAttribute('aria-label', this.options.title);
}
}
set icon(icon) {
this._setAriaLabel();
const oldIcons = Array.from(this._element.classList).filter(item => item.startsWith('codicon-'));
this._element.classList.remove(...oldIcons);
this._element.classList.add(...ThemeIcon.asClassNameArray(icon));
}
set enabled(value) {
if (value) {
this._element.classList.remove('disabled');
this._element.setAttribute('aria-disabled', String(false));
this._element.tabIndex = 0;
}
else {
this._element.classList.add('disabled');
this._element.setAttribute('aria-disabled', String(true));
}
}
get enabled() {
return !this._element.classList.contains('disabled');
}
setTitle(title) {
if (!this._hover && title !== '') {
this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('element'), this._element, title));
}
else if (this._hover) {
this._hover.update(title);
}
}
}
export { Button };
@@ -0,0 +1,33 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.codicon-wrench-subaction {
opacity: 0.5;
}
@keyframes codicon-spin {
100% {
transform:rotate(360deg);
}
}
.codicon-sync.codicon-modifier-spin,
.codicon-loading.codicon-modifier-spin,
.codicon-gear.codicon-modifier-spin,
.codicon-notebook-state-executing.codicon-modifier-spin {
/* Use steps to throttle FPS to reduce CPU usage */
animation: codicon-spin 1.5s steps(30) infinite;
}
.codicon-modifier-disabled {
opacity: 0.4;
}
/* custom speed & easing for loading icon */
.codicon-loading,
.codicon-tree-item-loading::before {
animation-duration: 1s !important;
animation-timing-function: cubic-bezier(0.53, 0.21, 0.29, 0.67) !important;
}
@@ -0,0 +1 @@
export {}
@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
@font-face {
font-family: "codicon";
font-display: block;
src: url(./codicon.ttf) format("truetype");
}
.codicon[class*='codicon-'] {
font: normal normal normal 16px/1 codicon;
display: inline-block;
text-decoration: none;
text-rendering: auto;
text-align: center;
text-transform: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
-webkit-user-select: none;
}
/* icon rules are dynamically created by the platform theme service (see iconsStyleSheet.ts) */
@@ -0,0 +1 @@
export {}
@@ -0,0 +1,16 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.context-view {
position: absolute;
}
.context-view.fixed {
all: initial;
font-family: inherit;
font-size: 13px;
position: fixed;
color: inherit;
}
@@ -0,0 +1,298 @@
import { BrowserFeatures } from '../../canIUse.js';
import { $, hide, addStandardDisposableListener, clearNode, show, isHTMLElement, getDomNodePagePosition, getDomNodeZoomLevel, getTotalWidth, getTotalHeight, getActiveWindow, getWindow, isAncestor } from '../../dom.js';
import { Disposable, toDisposable, DisposableStore } from '../../../common/lifecycle.js';
import { isIOS } from '../../../common/platform.js';
import { Range } from '../../../common/range.js';
import './contextview.css';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
function isAnchor(obj) {
const anchor = obj;
return !!anchor && typeof anchor.x === 'number' && typeof anchor.y === 'number';
}
var LayoutAnchorMode;
(function (LayoutAnchorMode) {
LayoutAnchorMode[LayoutAnchorMode["AVOID"] = 0] = "AVOID";
LayoutAnchorMode[LayoutAnchorMode["ALIGN"] = 1] = "ALIGN";
})(LayoutAnchorMode || (LayoutAnchorMode = {}));
/**
* Lays out a one dimensional view next to an anchor in a viewport.
*
* @returns The view offset within the viewport.
*/
function layout(viewportSize, viewSize, anchor) {
const layoutAfterAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset : anchor.offset + anchor.size;
const layoutBeforeAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset + anchor.size : anchor.offset;
if (anchor.position === 0 /* LayoutAnchorPosition.Before */) {
if (viewSize <= viewportSize - layoutAfterAnchorBoundary) {
return layoutAfterAnchorBoundary; // happy case, lay it out after the anchor
}
if (viewSize <= layoutBeforeAnchorBoundary) {
return layoutBeforeAnchorBoundary - viewSize; // ok case, lay it out before the anchor
}
return Math.max(viewportSize - viewSize, 0); // sad case, lay it over the anchor
}
else {
if (viewSize <= layoutBeforeAnchorBoundary) {
return layoutBeforeAnchorBoundary - viewSize; // happy case, lay it out before the anchor
}
if (viewSize <= viewportSize - layoutAfterAnchorBoundary) {
return layoutAfterAnchorBoundary; // ok case, lay it out after the anchor
}
return 0; // sad case, lay it over the anchor
}
}
class ContextView extends Disposable {
static { this.BUBBLE_UP_EVENTS = ['click', 'keydown', 'focus', 'blur']; }
static { this.BUBBLE_DOWN_EVENTS = ['click']; }
constructor(container, domPosition) {
super();
this.container = null;
this.useFixedPosition = false;
this.useShadowDOM = false;
this.delegate = null;
this.toDisposeOnClean = Disposable.None;
this.toDisposeOnSetContainer = Disposable.None;
this.shadowRoot = null;
this.shadowRootHostElement = null;
this.view = $('.context-view');
hide(this.view);
this.setContainer(container, domPosition);
this._register(toDisposable(() => this.setContainer(null, 1 /* ContextViewDOMPosition.ABSOLUTE */)));
}
setContainer(container, domPosition) {
this.useFixedPosition = domPosition !== 1 /* ContextViewDOMPosition.ABSOLUTE */;
const usedShadowDOM = this.useShadowDOM;
this.useShadowDOM = domPosition === 3 /* ContextViewDOMPosition.FIXED_SHADOW */;
if (container === this.container && usedShadowDOM === this.useShadowDOM) {
return; // container is the same and no shadow DOM usage has changed
}
if (this.container) {
this.toDisposeOnSetContainer.dispose();
this.view.remove();
if (this.shadowRoot) {
this.shadowRoot = null;
this.shadowRootHostElement?.remove();
this.shadowRootHostElement = null;
}
this.container = null;
}
if (container) {
this.container = container;
if (this.useShadowDOM) {
this.shadowRootHostElement = $('.shadow-root-host');
this.container.appendChild(this.shadowRootHostElement);
this.shadowRoot = this.shadowRootHostElement.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = SHADOW_ROOT_CSS;
this.shadowRoot.appendChild(style);
this.shadowRoot.appendChild(this.view);
this.shadowRoot.appendChild($('slot'));
}
else {
this.container.appendChild(this.view);
}
const toDisposeOnSetContainer = new DisposableStore();
ContextView.BUBBLE_UP_EVENTS.forEach(event => {
toDisposeOnSetContainer.add(addStandardDisposableListener(this.container, event, e => {
this.onDOMEvent(e, false);
}));
});
ContextView.BUBBLE_DOWN_EVENTS.forEach(event => {
toDisposeOnSetContainer.add(addStandardDisposableListener(this.container, event, e => {
this.onDOMEvent(e, true);
}, true));
});
this.toDisposeOnSetContainer = toDisposeOnSetContainer;
}
}
show(delegate) {
if (this.isVisible()) {
this.hide();
}
// Show static box
clearNode(this.view);
this.view.className = 'context-view monaco-component';
this.view.style.top = '0px';
this.view.style.left = '0px';
this.view.style.zIndex = `${2575 + (delegate.layer ?? 0)}`;
this.view.style.position = this.useFixedPosition ? 'fixed' : 'absolute';
show(this.view);
// Render content
this.toDisposeOnClean = delegate.render(this.view) || Disposable.None;
// Set active delegate
this.delegate = delegate;
// Layout
this.doLayout();
// Focus
this.delegate.focus?.();
}
getViewElement() {
return this.view;
}
layout() {
if (!this.isVisible()) {
return;
}
if (this.delegate.canRelayout === false && !(isIOS && BrowserFeatures.pointerEvents)) {
this.hide();
return;
}
this.delegate?.layout?.();
this.doLayout();
}
doLayout() {
// Check that we still have a delegate - this.delegate.layout may have hidden
if (!this.isVisible()) {
return;
}
// Get anchor
const anchor = this.delegate.getAnchor();
// Compute around
let around;
// Get the element's position and size (to anchor the view)
if (isHTMLElement(anchor)) {
const elementPosition = getDomNodePagePosition(anchor);
// In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element
// e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level.
// Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Size Multiplier: 1.5
const zoom = getDomNodeZoomLevel(anchor);
around = {
top: elementPosition.top * zoom,
left: elementPosition.left * zoom,
width: elementPosition.width * zoom,
height: elementPosition.height * zoom
};
}
else if (isAnchor(anchor)) {
around = {
top: anchor.y,
left: anchor.x,
width: anchor.width || 1,
height: anchor.height || 2
};
}
else {
around = {
top: anchor.posy,
left: anchor.posx,
// We are about to position the context view where the mouse
// cursor is. To prevent the view being exactly under the mouse
// when showing and thus potentially triggering an action within,
// we treat the mouse location like a small sized block element.
width: 2,
height: 2
};
}
const viewSizeWidth = getTotalWidth(this.view);
const viewSizeHeight = getTotalHeight(this.view);
const anchorPosition = this.delegate.anchorPosition ?? 0 /* AnchorPosition.BELOW */;
const anchorAlignment = this.delegate.anchorAlignment ?? 0 /* AnchorAlignment.LEFT */;
const anchorAxisAlignment = this.delegate.anchorAxisAlignment ?? 0 /* AnchorAxisAlignment.VERTICAL */;
let top;
let left;
const activeWindow = getActiveWindow();
if (anchorAxisAlignment === 0 /* AnchorAxisAlignment.VERTICAL */) {
const verticalAnchor = { offset: around.top - activeWindow.pageYOffset, size: around.height, position: anchorPosition === 0 /* AnchorPosition.BELOW */ ? 0 /* LayoutAnchorPosition.Before */ : 1 /* LayoutAnchorPosition.After */ };
const horizontalAnchor = { offset: around.left, size: around.width, position: anchorAlignment === 0 /* AnchorAlignment.LEFT */ ? 0 /* LayoutAnchorPosition.Before */ : 1 /* LayoutAnchorPosition.After */, mode: LayoutAnchorMode.ALIGN };
top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset;
// if view intersects vertically with anchor, we must avoid the anchor
if (Range.intersects({ start: top, end: top + viewSizeHeight }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) {
horizontalAnchor.mode = LayoutAnchorMode.AVOID;
}
left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor);
}
else {
const horizontalAnchor = { offset: around.left, size: around.width, position: anchorAlignment === 0 /* AnchorAlignment.LEFT */ ? 0 /* LayoutAnchorPosition.Before */ : 1 /* LayoutAnchorPosition.After */ };
const verticalAnchor = { offset: around.top, size: around.height, position: anchorPosition === 0 /* AnchorPosition.BELOW */ ? 0 /* LayoutAnchorPosition.Before */ : 1 /* LayoutAnchorPosition.After */, mode: LayoutAnchorMode.ALIGN };
left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor);
// if view intersects horizontally with anchor, we must avoid the anchor
if (Range.intersects({ start: left, end: left + viewSizeWidth }, { start: horizontalAnchor.offset, end: horizontalAnchor.offset + horizontalAnchor.size })) {
verticalAnchor.mode = LayoutAnchorMode.AVOID;
}
top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset;
}
this.view.classList.remove('top', 'bottom', 'left', 'right');
this.view.classList.add(anchorPosition === 0 /* AnchorPosition.BELOW */ ? 'bottom' : 'top');
this.view.classList.add(anchorAlignment === 0 /* AnchorAlignment.LEFT */ ? 'left' : 'right');
this.view.classList.toggle('fixed', this.useFixedPosition);
const containerPosition = getDomNodePagePosition(this.container);
// Account for container scroll when positioning the context view
const containerScrollTop = this.container.scrollTop || 0;
const containerScrollLeft = this.container.scrollLeft || 0;
this.view.style.top = `${top - (this.useFixedPosition ? getDomNodePagePosition(this.view).top : containerPosition.top) + containerScrollTop}px`;
this.view.style.left = `${left - (this.useFixedPosition ? getDomNodePagePosition(this.view).left : containerPosition.left) + containerScrollLeft}px`;
this.view.style.width = 'initial';
}
hide(data) {
const delegate = this.delegate;
this.delegate = null;
if (delegate?.onHide) {
delegate.onHide(data);
}
this.toDisposeOnClean.dispose();
hide(this.view);
}
isVisible() {
return !!this.delegate;
}
onDOMEvent(e, onCapture) {
if (this.delegate) {
if (this.delegate.onDOMEvent) {
this.delegate.onDOMEvent(e, getWindow(e).document.activeElement);
}
else if (onCapture && !isAncestor(e.target, this.container)) {
this.hide();
}
}
}
dispose() {
this.hide();
super.dispose();
}
}
const SHADOW_ROOT_CSS = /* css */ `
:host {
all: initial; /* 1st rule so subsequent properties are reset. */
}
.codicon[class*='codicon-'] {
font: normal normal normal 16px/1 codicon;
display: inline-block;
text-decoration: none;
text-rendering: auto;
text-align: center;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
:host {
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", system-ui, "Ubuntu", "Droid Sans", sans-serif;
}
:host-context(.mac) { font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
:host-context(.mac:lang(zh-Hans)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; }
:host-context(.mac:lang(zh-Hant)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; }
:host-context(.mac:lang(ja)) { font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; }
:host-context(.mac:lang(ko)) { font-family: -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Nanum Gothic", "AppleGothic", sans-serif; }
:host-context(.windows) { font-family: "Segoe WPC", "Segoe UI", sans-serif; }
:host-context(.windows:lang(zh-Hans)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; }
:host-context(.windows:lang(zh-Hant)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; }
:host-context(.windows:lang(ja)) { font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; }
:host-context(.windows:lang(ko)) { font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; }
:host-context(.linux) { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; }
:host-context(.linux:lang(zh-Hans)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; }
:host-context(.linux:lang(zh-Hant)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; }
:host-context(.linux:lang(ja)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; }
:host-context(.linux:lang(ko)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; }
`;
export { ContextView, LayoutAnchorMode, isAnchor, layout };
@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-count-badge {
padding: 3px 5px;
border-radius: 11px;
font-size: 11px;
min-width: 18px;
min-height: 18px;
line-height: 11px;
font-weight: normal;
text-align: center;
display: inline-block;
box-sizing: border-box;
}
.monaco-count-badge.long {
padding: 2px 3px;
border-radius: 2px;
min-height: auto;
line-height: normal;
}
@@ -0,0 +1,52 @@
import { append, $ } from '../../dom.js';
import { format } from '../../../common/strings.js';
import './countBadge.css';
import { Disposable, MutableDisposable, toDisposable } from '../../../common/lifecycle.js';
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class CountBadge extends Disposable {
constructor(container, options, styles) {
super();
this.options = options;
this.styles = styles;
this.count = 0;
this.hover = this._register(new MutableDisposable());
this.element = append(container, $('.monaco-count-badge'));
this._register(toDisposable(() => container.removeChild(this.element)));
this.countFormat = this.options.countFormat || '{0}';
this.titleFormat = this.options.titleFormat || '';
this.setCount(this.options.count || 0);
this.updateHover();
}
setCount(count) {
this.count = count;
this.render();
}
setTitleFormat(titleFormat) {
this.titleFormat = titleFormat;
this.updateHover();
this.render();
}
updateHover() {
if (this.titleFormat !== '' && !this.hover.value) {
this.hover.value = getBaseLayerHoverDelegate().setupDelayedHoverAtMouse(this.element, () => ({ content: format(this.titleFormat, this.count), appearance: { compact: true } }));
}
else if (this.titleFormat === '' && this.hover.value) {
this.hover.value = undefined;
}
}
render() {
this.element.textContent = format(this.countFormat, this.count);
this.element.style.backgroundColor = this.styles.badgeBackground ?? '';
this.element.style.color = this.styles.badgeForeground ?? '';
if (this.styles.badgeBorder) {
this.element.style.border = `1px solid ${this.styles.badgeBorder}`;
}
}
}
export { CountBadge };
+28
View File
@@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-drag-image {
display: inline-block;
padding: 1px 7px;
border-radius: 10px;
font-size: 12px;
position: absolute;
z-index: 1000;
/* Default styles */
background-color: var(--vscode-list-activeSelectionBackground);
color: var(--vscode-list-activeSelectionForeground);
outline: 1px solid var(--vscode-list-focusOutline);
outline-offset: -1px;
/*
* Browsers apply an effect to the drag image when the div becomes too
* large which makes them unreadable. Use max width so it does not happen
*/
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
+28
View File
@@ -0,0 +1,28 @@
import { $ } from '../../dom.js';
import './dnd.css';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
function applyDragImage(event, container, label, extraClasses = []) {
if (!event.dataTransfer) {
return;
}
const dragImage = $('.monaco-drag-image');
dragImage.textContent = label;
dragImage.classList.add(...extraClasses);
const getDragImageContainer = (e) => {
while (e && !e.classList.contains('monaco-workbench')) {
e = e.parentElement;
}
return e || container.ownerDocument.body;
};
const dragContainer = getDragImageContainer(container);
dragContainer.appendChild(dragImage);
event.dataTransfer.setDragImage(dragImage, -10, -10);
// Removes the element when the DND operation is done
setTimeout(() => dragImage.remove(), 0);
}
export { applyDragImage };
@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-dropdown {
height: 100%;
padding: 0;
}
.monaco-dropdown > .dropdown-label {
cursor: pointer;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.monaco-dropdown > .dropdown-label > .action-label.disabled {
cursor: default;
}
.monaco-dropdown-with-primary {
display: flex !important;
flex-direction: row;
border-radius: 5px;
}
.monaco-dropdown-with-primary > .action-container > .action-label {
margin-right: 0;
}
.monaco-dropdown-with-primary > .dropdown-action-container > .monaco-dropdown > .dropdown-label .codicon[class*='codicon-'] {
font-size: 12px;
padding-left: 0px;
padding-right: 0px;
line-height: 16px;
margin-left: -3px;
}
.monaco-dropdown-with-primary > .dropdown-action-container > .monaco-dropdown > .dropdown-label > .action-label {
display: block;
background-size: 16px;
background-position: center center;
background-repeat: no-repeat;
}
@@ -0,0 +1,141 @@
import { append, $, EventType, addDisposableListener, EventHelper, isMouseEvent } from '../../dom.js';
import { StandardKeyboardEvent } from '../../keyboardEvent.js';
import { EventType as EventType$1, Gesture } from '../../touch.js';
import { ActionRunner } from '../../../common/actions.js';
import { Emitter } from '../../../common/event.js';
import './dropdown.css';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class BaseDropdown extends ActionRunner {
constructor(container, options) {
super();
this._onDidChangeVisibility = this._register(new Emitter());
this.onDidChangeVisibility = this._onDidChangeVisibility.event;
this._element = append(container, $('.monaco-dropdown'));
this._label = append(this._element, $('.dropdown-label'));
let labelRenderer = options.labelRenderer;
if (!labelRenderer) {
labelRenderer = (container) => {
container.textContent = options.label || '';
return null;
};
}
for (const event of [EventType.CLICK, EventType.MOUSE_DOWN, EventType$1.Tap]) {
this._register(addDisposableListener(this.element, event, e => EventHelper.stop(e, true))); // prevent default click behaviour to trigger
}
for (const event of [EventType.MOUSE_DOWN, EventType$1.Tap]) {
this._register(addDisposableListener(this._label, event, e => {
if (isMouseEvent(e) && e.button !== 0) {
// prevent right click trigger to allow separate context menu (https://github.com/microsoft/vscode/issues/151064)
return;
}
if (this.visible) {
this.hide();
}
else {
this.show();
}
}));
}
this._register(addDisposableListener(this._label, EventType.KEY_DOWN, e => {
const event = new StandardKeyboardEvent(e);
if (event.equals(3 /* KeyCode.Enter */) || event.equals(10 /* KeyCode.Space */)) {
EventHelper.stop(e, true); // https://github.com/microsoft/vscode/issues/57997
if (this.visible) {
this.hide();
}
else {
this.show();
}
}
}));
const cleanupFn = labelRenderer(this._label);
if (cleanupFn) {
this._register(cleanupFn);
}
this._register(Gesture.addTarget(this._label));
}
get element() {
return this._element;
}
show() {
if (!this.visible) {
this.visible = true;
this._onDidChangeVisibility.fire(true);
}
}
hide() {
if (this.visible) {
this.visible = false;
this._onDidChangeVisibility.fire(false);
}
}
dispose() {
super.dispose();
this.hide();
if (this.boxContainer) {
this.boxContainer.remove();
this.boxContainer = undefined;
}
if (this.contents) {
this.contents.remove();
this.contents = undefined;
}
if (this._label) {
this._label.remove();
this._label = undefined;
}
}
}
class DropdownMenu extends BaseDropdown {
constructor(container, _options) {
super(container, _options);
this._options = _options;
this._actions = [];
this.actions = _options.actions || [];
}
set menuOptions(options) {
this._menuOptions = options;
}
get menuOptions() {
return this._menuOptions;
}
get actions() {
if (this._options.actionProvider) {
return this._options.actionProvider.getActions();
}
return this._actions;
}
set actions(actions) {
this._actions = actions;
}
show() {
super.show();
this.element.classList.add('active');
this._options.contextMenuProvider.showContextMenu({
getAnchor: () => this.element,
getActions: () => this.actions,
getActionsContext: () => this.menuOptions ? this.menuOptions.context : null,
getActionViewItem: (action, options) => this.menuOptions && this.menuOptions.actionViewItemProvider ? this.menuOptions.actionViewItemProvider(action, options) : undefined,
getKeyBinding: action => this.menuOptions && this.menuOptions.getKeyBinding ? this.menuOptions.getKeyBinding(action) : undefined,
getMenuClassName: () => this._options.menuClassName || '',
onHide: () => this.onHide(),
actionRunner: this.menuOptions ? this.menuOptions.actionRunner : undefined,
anchorAlignment: this.menuOptions ? this.menuOptions.anchorAlignment : 0 /* AnchorAlignment.LEFT */,
domForShadowRoot: this._options.menuAsChild ? this.element : undefined,
skipTelemetry: this._options.skipTelemetry
});
}
hide() {
super.hide();
}
onHide() {
this.hide();
this.element.classList.remove('active');
}
}
export { BaseDropdown, DropdownMenu };
@@ -0,0 +1,109 @@
import { Emitter } from '../../../common/event.js';
import { append, $ } from '../../dom.js';
import { BaseActionViewItem } from '../actionbar/actionViewItems.js';
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
import './dropdown.css';
import { DropdownMenu } from './dropdown.js';
class DropdownMenuActionViewItem extends BaseActionViewItem {
get onDidChangeVisibility() { return this._onDidChangeVisibility.event; }
constructor(action, menuActionsOrProvider, contextMenuProvider, options = Object.create(null)) {
super(null, action, options);
this.actionItem = null;
this._onDidChangeVisibility = this._register(new Emitter());
this.menuActionsOrProvider = menuActionsOrProvider;
this.contextMenuProvider = contextMenuProvider;
this.options = options;
if (this.options.actionRunner) {
this.actionRunner = this.options.actionRunner;
}
}
render(container) {
this.actionItem = container;
const labelRenderer = (el) => {
this.element = append(el, $('a.action-label'));
return this.renderLabel(this.element);
};
const isActionsArray = Array.isArray(this.menuActionsOrProvider);
const options = {
contextMenuProvider: this.contextMenuProvider,
labelRenderer: labelRenderer,
menuAsChild: this.options.menuAsChild,
actions: isActionsArray ? this.menuActionsOrProvider : undefined,
actionProvider: isActionsArray ? undefined : this.menuActionsOrProvider,
skipTelemetry: this.options.skipTelemetry
};
this.dropdownMenu = this._register(new DropdownMenu(container, options));
this._register(this.dropdownMenu.onDidChangeVisibility(visible => {
this.element?.setAttribute('aria-expanded', `${visible}`);
this._onDidChangeVisibility.fire(visible);
}));
this.dropdownMenu.menuOptions = {
actionViewItemProvider: this.options.actionViewItemProvider,
actionRunner: this.actionRunner,
getKeyBinding: this.options.keybindingProvider,
context: this._context
};
if (this.options.anchorAlignmentProvider) {
const that = this;
this.dropdownMenu.menuOptions = {
...this.dropdownMenu.menuOptions,
get anchorAlignment() {
return that.options.anchorAlignmentProvider();
}
};
}
this.updateTooltip();
this.updateEnabled();
}
renderLabel(element) {
let classNames = [];
if (typeof this.options.classNames === 'string') {
classNames = this.options.classNames.split(/\s+/g).filter(s => !!s);
}
else if (this.options.classNames) {
classNames = this.options.classNames;
}
// todo@aeschli: remove codicon, should come through `this.options.classNames`
if (!classNames.find(c => c === 'icon')) {
classNames.push('codicon');
}
element.classList.add(...classNames);
if (this._action.label) {
this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), element, this._action.label));
}
return null;
}
getTooltip() {
let title = null;
if (this.action.tooltip) {
title = this.action.tooltip;
}
else if (this.action.label) {
title = this.action.label;
}
return title ?? undefined;
}
setActionContext(newContext) {
super.setActionContext(newContext);
if (this.dropdownMenu) {
if (this.dropdownMenu.menuOptions) {
this.dropdownMenu.menuOptions.context = newContext;
}
else {
this.dropdownMenu.menuOptions = { context: newContext };
}
}
}
show() {
this.dropdownMenu?.show();
}
updateEnabled() {
const disabled = !this.action.enabled;
this.actionItem?.classList.toggle('disabled', disabled);
this.element?.classList.toggle('disabled', disabled);
}
}
export { DropdownMenuActionViewItem };
@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* ---------- Find input ---------- */
.monaco-findInput {
position: relative;
}
.monaco-findInput .monaco-inputbox {
font-size: 13px;
width: 100%;
}
.monaco-findInput > .controls {
position: absolute;
top: 3px;
right: 2px;
}
.vs .monaco-findInput.disabled {
background-color: #E1E1E1;
}
/* Theming */
.vs-dark .monaco-findInput.disabled {
background-color: #333;
}
/* Highlighting */
.monaco-findInput.highlight-0 .controls,
.hc-light .monaco-findInput.highlight-0 .controls {
animation: monaco-findInput-highlight-0 100ms linear 0s;
}
.monaco-findInput.highlight-1 .controls,
.hc-light .monaco-findInput.highlight-1 .controls {
animation: monaco-findInput-highlight-1 100ms linear 0s;
}
.hc-black .monaco-findInput.highlight-0 .controls,
.vs-dark .monaco-findInput.highlight-0 .controls {
animation: monaco-findInput-highlight-dark-0 100ms linear 0s;
}
.hc-black .monaco-findInput.highlight-1 .controls,
.vs-dark .monaco-findInput.highlight-1 .controls {
animation: monaco-findInput-highlight-dark-1 100ms linear 0s;
}
@keyframes monaco-findInput-highlight-0 {
0% { background: rgba(253, 255, 0, 0.8); }
100% { background: transparent; }
}
@keyframes monaco-findInput-highlight-1 {
0% { background: rgba(253, 255, 0, 0.8); }
/* Made intentionally different such that the CSS minifier does not collapse the two animations into a single one*/
99% { background: transparent; }
}
@keyframes monaco-findInput-highlight-dark-0 {
0% { background: rgba(255, 255, 255, 0.44); }
100% { background: transparent; }
}
@keyframes monaco-findInput-highlight-dark-1 {
0% { background: rgba(255, 255, 255, 0.44); }
/* Made intentionally different such that the CSS minifier does not collapse the two animations into a single one*/
99% { background: transparent; }
}
@@ -0,0 +1,294 @@
import { EventHelper, addDisposableListener } from '../../dom.js';
import { RegexToggle, WholeWordsToggle, CaseSensitiveToggle } from './findInputToggles.js';
import { HistoryInputBox } from '../inputbox/inputBox.js';
import { Widget } from '../widget.js';
import { Emitter } from '../../../common/event.js';
import './findInput.css';
import { localize } from '../../../../nls.js';
import { MutableDisposable, DisposableStore } from '../../../common/lifecycle.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const NLS_DEFAULT_LABEL = localize(1, "input");
class FindInput extends Widget {
get onDidOptionChange() { return this._onDidOptionChange.event; }
get onKeyDown() { return this._onKeyDown.event; }
get onMouseDown() { return this._onMouseDown.event; }
get onCaseSensitiveKeyDown() { return this._onCaseSensitiveKeyDown.event; }
get onRegexKeyDown() { return this._onRegexKeyDown.event; }
constructor(parent, contextViewProvider, options) {
super();
this.fixFocusOnOptionClickEnabled = true;
this.imeSessionInProgress = false;
this.additionalTogglesDisposables = this._register(new MutableDisposable());
this.additionalToggles = [];
this._onDidOptionChange = this._register(new Emitter());
this._onKeyDown = this._register(new Emitter());
this._onMouseDown = this._register(new Emitter());
this._onInput = this._register(new Emitter());
this._onKeyUp = this._register(new Emitter());
this._onCaseSensitiveKeyDown = this._register(new Emitter());
this._onRegexKeyDown = this._register(new Emitter());
this._lastHighlightFindOptions = 0;
this.placeholder = options.placeholder || '';
this.validation = options.validation;
this.label = options.label || NLS_DEFAULT_LABEL;
this.showCommonFindToggles = !!options.showCommonFindToggles;
const appendCaseSensitiveLabel = options.appendCaseSensitiveLabel || '';
const appendWholeWordsLabel = options.appendWholeWordsLabel || '';
const appendRegexLabel = options.appendRegexLabel || '';
const flexibleHeight = !!options.flexibleHeight;
const flexibleWidth = !!options.flexibleWidth;
const flexibleMaxHeight = options.flexibleMaxHeight;
this.domNode = document.createElement('div');
this.domNode.classList.add('monaco-findInput');
this.inputBox = this._register(new HistoryInputBox(this.domNode, contextViewProvider, {
placeholder: this.placeholder || '',
ariaLabel: this.label || '',
validationOptions: {
validation: this.validation
},
showHistoryHint: options.showHistoryHint,
flexibleHeight,
flexibleWidth,
flexibleMaxHeight,
inputBoxStyles: options.inputBoxStyles,
history: options.history
}));
if (this.showCommonFindToggles) {
const hoverLifecycleOptions = options?.hoverLifecycleOptions || { groupId: 'find-input' };
this.regex = this._register(new RegexToggle({
appendTitle: appendRegexLabel,
isChecked: false,
hoverLifecycleOptions,
...options.toggleStyles
}));
this._register(this.regex.onChange(viaKeyboard => {
this._onDidOptionChange.fire(viaKeyboard);
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
this.inputBox.focus();
}
this.validate();
}));
this._register(this.regex.onKeyDown(e => {
this._onRegexKeyDown.fire(e);
}));
this.wholeWords = this._register(new WholeWordsToggle({
appendTitle: appendWholeWordsLabel,
isChecked: false,
hoverLifecycleOptions,
...options.toggleStyles
}));
this._register(this.wholeWords.onChange(viaKeyboard => {
this._onDidOptionChange.fire(viaKeyboard);
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
this.inputBox.focus();
}
this.validate();
}));
this.caseSensitive = this._register(new CaseSensitiveToggle({
appendTitle: appendCaseSensitiveLabel,
isChecked: false,
hoverLifecycleOptions,
...options.toggleStyles
}));
this._register(this.caseSensitive.onChange(viaKeyboard => {
this._onDidOptionChange.fire(viaKeyboard);
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
this.inputBox.focus();
}
this.validate();
}));
this._register(this.caseSensitive.onKeyDown(e => {
this._onCaseSensitiveKeyDown.fire(e);
}));
// Arrow-Key support to navigate between options
const indexes = [this.caseSensitive.domNode, this.wholeWords.domNode, this.regex.domNode];
this.onkeydown(this.domNode, (event) => {
if (event.equals(15 /* KeyCode.LeftArrow */) || event.equals(17 /* KeyCode.RightArrow */) || event.equals(9 /* KeyCode.Escape */)) {
const index = indexes.indexOf(this.domNode.ownerDocument.activeElement);
if (index >= 0) {
let newIndex = -1;
if (event.equals(17 /* KeyCode.RightArrow */)) {
newIndex = (index + 1) % indexes.length;
}
else if (event.equals(15 /* KeyCode.LeftArrow */)) {
if (index === 0) {
newIndex = indexes.length - 1;
}
else {
newIndex = index - 1;
}
}
if (event.equals(9 /* KeyCode.Escape */)) {
indexes[index].blur();
this.inputBox.focus();
}
else if (newIndex >= 0) {
indexes[newIndex].focus();
}
EventHelper.stop(event, true);
}
}
});
}
this.controls = document.createElement('div');
this.controls.className = 'controls';
this.controls.style.display = this.showCommonFindToggles ? '' : 'none';
if (this.caseSensitive) {
this.controls.append(this.caseSensitive.domNode);
}
if (this.wholeWords) {
this.controls.appendChild(this.wholeWords.domNode);
}
if (this.regex) {
this.controls.appendChild(this.regex.domNode);
}
this.setAdditionalToggles(options?.additionalToggles);
if (this.controls) {
this.domNode.appendChild(this.controls);
}
parent?.appendChild(this.domNode);
this._register(addDisposableListener(this.inputBox.inputElement, 'compositionstart', (e) => {
this.imeSessionInProgress = true;
}));
this._register(addDisposableListener(this.inputBox.inputElement, 'compositionend', (e) => {
this.imeSessionInProgress = false;
this._onInput.fire();
}));
this.onkeydown(this.inputBox.inputElement, (e) => this._onKeyDown.fire(e));
this.onkeyup(this.inputBox.inputElement, (e) => this._onKeyUp.fire(e));
this.oninput(this.inputBox.inputElement, (e) => this._onInput.fire());
this.onmousedown(this.inputBox.inputElement, (e) => this._onMouseDown.fire(e));
}
get onDidChange() {
return this.inputBox.onDidChange;
}
layout(style) {
this.inputBox.layout();
this.updateInputBoxPadding(style.collapsedFindWidget);
}
enable() {
this.domNode.classList.remove('disabled');
this.inputBox.enable();
this.regex?.enable();
this.wholeWords?.enable();
this.caseSensitive?.enable();
for (const toggle of this.additionalToggles) {
toggle.enable();
}
}
disable() {
this.domNode.classList.add('disabled');
this.inputBox.disable();
this.regex?.disable();
this.wholeWords?.disable();
this.caseSensitive?.disable();
for (const toggle of this.additionalToggles) {
toggle.disable();
}
}
setFocusInputOnOptionClick(value) {
this.fixFocusOnOptionClickEnabled = value;
}
setEnabled(enabled) {
if (enabled) {
this.enable();
}
else {
this.disable();
}
}
setAdditionalToggles(toggles) {
for (const currentToggle of this.additionalToggles) {
currentToggle.domNode.remove();
}
this.additionalToggles = [];
this.additionalTogglesDisposables.value = new DisposableStore();
for (const toggle of toggles ?? []) {
this.additionalTogglesDisposables.value.add(toggle);
this.controls.appendChild(toggle.domNode);
this.additionalTogglesDisposables.value.add(toggle.onChange(viaKeyboard => {
this._onDidOptionChange.fire(viaKeyboard);
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
this.inputBox.focus();
}
}));
this.additionalToggles.push(toggle);
}
if (this.additionalToggles.length > 0) {
this.controls.style.display = '';
}
this.updateInputBoxPadding();
}
updateInputBoxPadding(controlsHidden = false) {
if (controlsHidden) {
this.inputBox.paddingRight = 0;
}
else {
this.inputBox.paddingRight =
((this.caseSensitive?.width() ?? 0) + (this.wholeWords?.width() ?? 0) + (this.regex?.width() ?? 0))
+ this.additionalToggles.reduce((r, t) => r + t.width(), 0);
}
}
getValue() {
return this.inputBox.value;
}
setValue(value) {
if (this.inputBox.value !== value) {
this.inputBox.value = value;
}
}
select() {
this.inputBox.select();
}
focus() {
this.inputBox.focus();
}
getCaseSensitive() {
return this.caseSensitive?.checked ?? false;
}
setCaseSensitive(value) {
if (this.caseSensitive) {
this.caseSensitive.checked = value;
}
}
getWholeWords() {
return this.wholeWords?.checked ?? false;
}
setWholeWords(value) {
if (this.wholeWords) {
this.wholeWords.checked = value;
}
}
getRegex() {
return this.regex?.checked ?? false;
}
setRegex(value) {
if (this.regex) {
this.regex.checked = value;
this.validate();
}
}
focusOnCaseSensitive() {
this.caseSensitive?.focus();
}
highlightFindOptions() {
this.domNode.classList.remove('highlight-' + (this._lastHighlightFindOptions));
this._lastHighlightFindOptions = 1 - this._lastHighlightFindOptions;
this.domNode.classList.add('highlight-' + (this._lastHighlightFindOptions));
}
validate() {
this.inputBox.validate();
}
showMessage(message) {
this.inputBox.showMessage(message);
}
clearMessage() {
this.inputBox.hideMessage();
}
}
export { FindInput };
@@ -0,0 +1,52 @@
import { Toggle } from '../toggle/toggle.js';
import { Codicon } from '../../../common/codicons.js';
import { localize } from '../../../../nls.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const NLS_CASE_SENSITIVE_TOGGLE_LABEL = localize(2, "Match Case");
const NLS_WHOLE_WORD_TOGGLE_LABEL = localize(3, "Match Whole Word");
const NLS_REGEX_TOGGLE_LABEL = localize(4, "Use Regular Expression");
class CaseSensitiveToggle extends Toggle {
constructor(opts) {
super({
icon: Codicon.caseSensitive,
title: NLS_CASE_SENSITIVE_TOGGLE_LABEL + opts.appendTitle,
isChecked: opts.isChecked,
hoverLifecycleOptions: opts.hoverLifecycleOptions,
inputActiveOptionBorder: opts.inputActiveOptionBorder,
inputActiveOptionForeground: opts.inputActiveOptionForeground,
inputActiveOptionBackground: opts.inputActiveOptionBackground
});
}
}
class WholeWordsToggle extends Toggle {
constructor(opts) {
super({
icon: Codicon.wholeWord,
title: NLS_WHOLE_WORD_TOGGLE_LABEL + opts.appendTitle,
isChecked: opts.isChecked,
hoverLifecycleOptions: opts.hoverLifecycleOptions,
inputActiveOptionBorder: opts.inputActiveOptionBorder,
inputActiveOptionForeground: opts.inputActiveOptionForeground,
inputActiveOptionBackground: opts.inputActiveOptionBackground
});
}
}
class RegexToggle extends Toggle {
constructor(opts) {
super({
icon: Codicon.regex,
title: NLS_REGEX_TOGGLE_LABEL + opts.appendTitle,
isChecked: opts.isChecked,
hoverLifecycleOptions: opts.hoverLifecycleOptions,
inputActiveOptionBorder: opts.inputActiveOptionBorder,
inputActiveOptionForeground: opts.inputActiveOptionForeground,
inputActiveOptionBackground: opts.inputActiveOptionBackground
});
}
}
export { CaseSensitiveToggle, RegexToggle, WholeWordsToggle };
@@ -0,0 +1,176 @@
import { EventHelper } from '../../dom.js';
import { Toggle } from '../toggle/toggle.js';
import { HistoryInputBox } from '../inputbox/inputBox.js';
import { Widget } from '../widget.js';
import { Codicon } from '../../../common/codicons.js';
import { Emitter } from '../../../common/event.js';
import './findInput.css';
import { localize } from '../../../../nls.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const NLS_DEFAULT_LABEL = localize(5, "input");
const NLS_PRESERVE_CASE_LABEL = localize(6, "Preserve Case");
class PreserveCaseToggle extends Toggle {
constructor(opts) {
super({
// TODO: does this need its own icon?
icon: Codicon.preserveCase,
title: NLS_PRESERVE_CASE_LABEL + opts.appendTitle,
isChecked: opts.isChecked,
hoverLifecycleOptions: opts.hoverLifecycleOptions,
inputActiveOptionBorder: opts.inputActiveOptionBorder,
inputActiveOptionForeground: opts.inputActiveOptionForeground,
inputActiveOptionBackground: opts.inputActiveOptionBackground,
});
}
}
class ReplaceInput extends Widget {
get onDidOptionChange() { return this._onDidOptionChange.event; }
get onKeyDown() { return this._onKeyDown.event; }
get onPreserveCaseKeyDown() { return this._onPreserveCaseKeyDown.event; }
constructor(parent, contextViewProvider, _showOptionButtons, options) {
super();
this._showOptionButtons = _showOptionButtons;
this.fixFocusOnOptionClickEnabled = true;
this.cachedOptionsWidth = 0;
this._onDidOptionChange = this._register(new Emitter());
this._onKeyDown = this._register(new Emitter());
this._onMouseDown = this._register(new Emitter());
this._onInput = this._register(new Emitter());
this._onKeyUp = this._register(new Emitter());
this._onPreserveCaseKeyDown = this._register(new Emitter());
this.contextViewProvider = contextViewProvider;
this.placeholder = options.placeholder || '';
this.validation = options.validation;
this.label = options.label || NLS_DEFAULT_LABEL;
const appendPreserveCaseLabel = options.appendPreserveCaseLabel || '';
const history = options.history || new Set([]);
const flexibleHeight = !!options.flexibleHeight;
const flexibleWidth = !!options.flexibleWidth;
const flexibleMaxHeight = options.flexibleMaxHeight;
this.domNode = document.createElement('div');
this.domNode.classList.add('monaco-findInput');
this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, {
ariaLabel: this.label || '',
placeholder: this.placeholder || '',
validationOptions: {
validation: this.validation
},
history,
showHistoryHint: options.showHistoryHint,
flexibleHeight,
flexibleWidth,
flexibleMaxHeight,
inputBoxStyles: options.inputBoxStyles
}));
this.preserveCase = this._register(new PreserveCaseToggle({
appendTitle: appendPreserveCaseLabel,
isChecked: false,
hoverLifecycleOptions: options.hoverLifecycleOptions,
...options.toggleStyles
}));
this._register(this.preserveCase.onChange(viaKeyboard => {
this._onDidOptionChange.fire(viaKeyboard);
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
this.inputBox.focus();
}
this.validate();
}));
this._register(this.preserveCase.onKeyDown(e => {
this._onPreserveCaseKeyDown.fire(e);
}));
if (this._showOptionButtons) {
this.cachedOptionsWidth = this.preserveCase.width();
}
else {
this.cachedOptionsWidth = 0;
}
// Arrow-Key support to navigate between options
const indexes = [this.preserveCase.domNode];
this.onkeydown(this.domNode, (event) => {
if (event.equals(15 /* KeyCode.LeftArrow */) || event.equals(17 /* KeyCode.RightArrow */) || event.equals(9 /* KeyCode.Escape */)) {
const index = indexes.indexOf(this.domNode.ownerDocument.activeElement);
if (index >= 0) {
let newIndex = -1;
if (event.equals(17 /* KeyCode.RightArrow */)) {
newIndex = (index + 1) % indexes.length;
}
else if (event.equals(15 /* KeyCode.LeftArrow */)) {
if (index === 0) {
newIndex = indexes.length - 1;
}
else {
newIndex = index - 1;
}
}
if (event.equals(9 /* KeyCode.Escape */)) {
indexes[index].blur();
this.inputBox.focus();
}
else if (newIndex >= 0) {
indexes[newIndex].focus();
}
EventHelper.stop(event, true);
}
}
});
const controls = document.createElement('div');
controls.className = 'controls';
controls.style.display = this._showOptionButtons ? 'block' : 'none';
controls.appendChild(this.preserveCase.domNode);
this.domNode.appendChild(controls);
parent?.appendChild(this.domNode);
this.onkeydown(this.inputBox.inputElement, (e) => this._onKeyDown.fire(e));
this.onkeyup(this.inputBox.inputElement, (e) => this._onKeyUp.fire(e));
this.oninput(this.inputBox.inputElement, (e) => this._onInput.fire());
this.onmousedown(this.inputBox.inputElement, (e) => this._onMouseDown.fire(e));
}
enable() {
this.domNode.classList.remove('disabled');
this.inputBox.enable();
this.preserveCase.enable();
}
disable() {
this.domNode.classList.add('disabled');
this.inputBox.disable();
this.preserveCase.disable();
}
setEnabled(enabled) {
if (enabled) {
this.enable();
}
else {
this.disable();
}
}
select() {
this.inputBox.select();
}
focus() {
this.inputBox.focus();
}
getPreserveCase() {
return this.preserveCase.checked;
}
setPreserveCase(value) {
this.preserveCase.checked = value;
}
focusOnPreserve() {
this.preserveCase.focus();
}
validate() {
this.inputBox?.validate();
}
set width(newWidth) {
this.inputBox.paddingRight = this.cachedOptionsWidth;
this.domNode.style.width = newWidth + 'px';
}
dispose() {
super.dispose();
}
}
export { ReplaceInput };
@@ -0,0 +1,129 @@
import { append, $, reset } from '../../dom.js';
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
import { renderLabelWithIcons } from '../iconLabel/iconLabels.js';
import { Disposable } from '../../../common/lifecycle.js';
import { equals } from '../../../common/objects.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* A widget which can render a label with substring highlights, often
* originating from a filter function like the fuzzy matcher.
*/
class HighlightedLabel extends Disposable {
/**
* Create a new {@link HighlightedLabel}.
*
* @param container The parent container to append to.
*/
constructor(container, options) {
super();
this.options = options;
this.text = '';
this.title = '';
this.highlights = [];
this.didEverRender = false;
this.domNode = append(container, $('span.monaco-highlighted-label'));
}
/**
* The label's DOM node.
*/
get element() {
return this.domNode;
}
/**
* Set the label and highlights.
*
* @param text The label to display.
* @param highlights The ranges to highlight.
* @param title An optional title for the hover tooltip.
* @param escapeNewLines Whether to escape new lines.
* @returns
*/
set(text, highlights = [], title = '', escapeNewLines, supportIcons) {
if (!text) {
text = '';
}
if (escapeNewLines) {
// adjusts highlights inplace
text = HighlightedLabel.escapeNewLines(text, highlights);
}
if (this.didEverRender && this.text === text && this.title === title && equals(this.highlights, highlights)) {
return;
}
this.text = text;
this.title = title;
this.highlights = highlights;
this.render(supportIcons);
}
render(supportIcons) {
const children = [];
let pos = 0;
for (const highlight of this.highlights) {
if (highlight.end === highlight.start) {
continue;
}
if (pos < highlight.start) {
const substring = this.text.substring(pos, highlight.start);
if (supportIcons) {
children.push(...renderLabelWithIcons(substring));
}
else {
children.push(substring);
}
pos = highlight.start;
}
const substring = this.text.substring(pos, highlight.end);
const element = $('span.highlight', undefined, ...supportIcons ? renderLabelWithIcons(substring) : [substring]);
if (highlight.extraClasses) {
element.classList.add(...highlight.extraClasses);
}
children.push(element);
pos = highlight.end;
}
if (pos < this.text.length) {
const substring = this.text.substring(pos);
if (supportIcons) {
children.push(...renderLabelWithIcons(substring));
}
else {
children.push(substring);
}
}
reset(this.domNode, ...children);
if (!this.customHover && this.title !== '') {
const hoverDelegate = this.options?.hoverDelegate ?? getDefaultHoverDelegate('mouse');
this.customHover = this._register(getBaseLayerHoverDelegate().setupManagedHover(hoverDelegate, this.domNode, this.title));
}
else if (this.customHover) {
this.customHover.update(this.title);
}
this.didEverRender = true;
}
static escapeNewLines(text, highlights) {
let total = 0;
let extra = 0;
return text.replace(/\r\n|\r|\n/g, (match, offset) => {
extra = match === '\r\n' ? -1 : 0;
offset += total;
for (const highlight of highlights) {
if (highlight.end <= offset) {
continue;
}
if (highlight.start >= offset) {
highlight.start += extra;
}
if (highlight.end >= offset) {
highlight.end += extra;
}
}
total += extra;
return '\u23CE';
});
}
}
export { HighlightedLabel };
@@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
function isManagedHoverTooltipMarkdownString(obj) {
const candidate = obj;
return typeof candidate === 'object' && 'markdown' in candidate && 'markdownNotSupportedFallback' in candidate;
}
// #endregion Managed hover
export { isManagedHoverTooltipMarkdownString };
@@ -0,0 +1,39 @@
import { Disposable } from '../../../common/lifecycle.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
let baseHoverDelegate = {
showInstantHover: () => undefined,
showDelayedHover: () => undefined,
setupDelayedHover: () => Disposable.None,
setupDelayedHoverAtMouse: () => Disposable.None,
hideHover: () => undefined,
showAndFocusLastHover: () => undefined,
setupManagedHover: () => ({
dispose: () => undefined,
show: () => undefined,
hide: () => undefined,
update: () => undefined,
}),
showManagedHover: () => undefined
};
/**
* Sets the hover delegate for use **only in the `base/` layer**.
*/
function setBaseLayerHoverDelegate(hoverDelegate) {
baseHoverDelegate = hoverDelegate;
}
/**
* Gets the hover delegate for use **only in the `base/` layer**.
*
* Since the hover service depends on various platform services, this delegate essentially bypasses
* the standard dependency injection mechanism by injecting a global hover service at start up. The
* only reason this should be used is if `IHoverService` is not available.
*/
function getBaseLayerHoverDelegate() {
return baseHoverDelegate;
}
export { getBaseLayerHoverDelegate, setBaseLayerHoverDelegate };
@@ -0,0 +1,34 @@
import { Lazy } from '../../../common/lazy.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const nullHoverDelegateFactory = () => ({
get delay() { return -1; },
dispose: () => { },
showHover: () => { return undefined; },
});
let hoverDelegateFactory = nullHoverDelegateFactory;
const defaultHoverDelegateMouse = new Lazy(() => hoverDelegateFactory('mouse', false));
const defaultHoverDelegateElement = new Lazy(() => hoverDelegateFactory('element', false));
// TODO: Remove when getDefaultHoverDelegate is no longer used
function setHoverDelegateFactory(hoverDelegateProvider) {
hoverDelegateFactory = hoverDelegateProvider;
}
// TODO: Refine type for use in new IHoverService interface
function getDefaultHoverDelegate(placement) {
if (placement === 'element') {
return defaultHoverDelegateElement.value;
}
return defaultHoverDelegateMouse.value;
}
// TODO: Create equivalent in IHoverService
function createInstantHoverDelegate() {
// Creates a hover delegate with instant hover enabled.
// This hover belongs to the consumer and requires the them to dispose it.
// Instant hover only makes sense for 'element' placement.
return hoverDelegateFactory('element', true);
}
export { createInstantHoverDelegate, getDefaultHoverDelegate, setHoverDelegateFactory };
@@ -0,0 +1,225 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-hover {
cursor: default;
position: absolute;
overflow: hidden;
user-select: text;
-webkit-user-select: text;
box-sizing: border-box;
line-height: 1.5em;
white-space: var(--vscode-hover-whiteSpace, normal);
}
.monaco-hover.fade-in {
animation: fadein 100ms linear;
}
.monaco-hover.hidden {
display: none;
}
.monaco-hover a:hover:not(.disabled) {
cursor: pointer;
}
.monaco-hover .hover-contents:not(.html-hover-contents) {
padding: 4px 8px;
}
.monaco-hover .markdown-hover > .hover-contents:not(.code-hover-contents) {
max-width: var(--vscode-hover-maxWidth, 500px);
word-wrap: break-word;
}
.monaco-hover .markdown-hover > .hover-contents:not(.code-hover-contents) hr {
min-width: 100%;
}
.monaco-hover p,
.monaco-hover .code,
.monaco-hover ul,
.monaco-hover h1,
.monaco-hover h2,
.monaco-hover h3,
.monaco-hover h4,
.monaco-hover h5,
.monaco-hover h6 {
margin: 8px 0;
}
.monaco-hover h1,
.monaco-hover h2,
.monaco-hover h3,
.monaco-hover h4,
.monaco-hover h5,
.monaco-hover h6 {
line-height: 1.1;
}
.monaco-hover code {
font-family: var(--monaco-monospace-font);
}
.monaco-hover hr {
box-sizing: border-box;
border-left: 0px;
border-right: 0px;
margin-top: 4px;
margin-bottom: -4px;
margin-left: -8px;
margin-right: -8px;
height: 1px;
}
.monaco-hover p:first-child,
.monaco-hover .code:first-child,
.monaco-hover ul:first-child {
margin-top: 0;
}
.monaco-hover p:last-child,
.monaco-hover .code:last-child,
.monaco-hover ul:last-child {
margin-bottom: 0;
}
/* MarkupContent Layout */
.monaco-hover ul {
padding-left: 20px;
}
.monaco-hover ol {
padding-left: 20px;
}
.monaco-hover li > p {
margin-bottom: 0;
}
.monaco-hover li > ul {
margin-top: 0;
}
.monaco-hover code {
border-radius: 3px;
padding: 0 0.4em;
}
.monaco-hover .monaco-tokenized-source {
white-space: var(--vscode-hover-sourceWhiteSpace, pre-wrap);
}
.monaco-hover .hover-row.status-bar {
font-size: 12px;
line-height: 22px;
}
.monaco-hover .hover-row.status-bar .info {
font-style: italic;
padding: 0px 8px;
}
.monaco-hover .hover-row.status-bar .actions {
display: flex;
padding: 0px 8px;
width: 100%;
}
.monaco-hover .hover-row.status-bar .actions .action-container {
margin-right: 16px;
cursor: pointer;
overflow: hidden;
text-wrap: nowrap;
text-overflow: ellipsis;
}
.monaco-hover .hover-row.status-bar .actions .action-container .action .icon {
padding-right: 4px;
vertical-align: middle;
}
.monaco-hover .hover-row.status-bar .actions .action-container a {
color: var(--vscode-textLink-foreground);
text-decoration: var(--text-link-decoration);
}
.monaco-hover .hover-row.status-bar .actions .action-container a .icon.codicon {
color: var(--vscode-textLink-foreground);
}
.monaco-hover .markdown-hover .hover-contents .codicon {
color: inherit;
font-size: inherit;
vertical-align: middle;
}
.monaco-hover .hover-contents a.code-link:hover,
.monaco-hover .hover-contents a.code-link {
color: inherit;
}
.monaco-hover .hover-contents a.code-link:before {
content: '(';
}
.monaco-hover .hover-contents a.code-link:after {
content: ')';
}
.monaco-hover .hover-contents a.code-link > span {
text-decoration: underline;
/** Hack to force underline to show **/
border-bottom: 1px solid transparent;
text-underline-position: under;
color: var(--vscode-textLink-foreground);
}
.monaco-hover .hover-contents a.code-link > span:hover {
color: var(--vscode-textLink-activeForeground);
}
/**
* Spans in markdown hovers need a margin-bottom to avoid looking cramped:
* https://github.com/microsoft/vscode/issues/101496
* This was later refined to only apply when the last child of a rendered markdown block (before the
* border or a `hr`) uses background color:
* https://github.com/microsoft/vscode/issues/228136
*/
.monaco-hover .markdown-hover .hover-contents:not(.code-hover-contents):not(.html-hover-contents) p:last-child [style*="background-color"] {
margin-bottom: 4px;
display: inline-block;
}
/**
* Add a slight margin to try vertically align codicons with any text
* https://github.com/microsoft/vscode/issues/221359
*/
.monaco-hover .markdown-hover .hover-contents:not(.code-hover-contents):not(.html-hover-contents) span.codicon {
margin-bottom: 2px;
}
.monaco-hover-content .action-container a {
-webkit-user-select: none;
user-select: none;
}
.monaco-hover-content .action-container.disabled {
pointer-events: none;
opacity: 0.4;
cursor: default;
}
/* Prevent text selection in all button-like elements within hovers */
.monaco-hover .action-container,
.monaco-hover .action,
.monaco-hover button,
.monaco-hover .monaco-button,
.monaco-hover .monaco-text-button,
.monaco-hover [role="button"] {
-webkit-user-select: none;
user-select: none;
}
@@ -0,0 +1,93 @@
import { append, $ as $$1, addDisposableListener, EventType } from '../../dom.js';
import { StandardKeyboardEvent } from '../../keyboardEvent.js';
import { DomScrollableElement } from '../scrollbar/scrollableElement.js';
import { Disposable } from '../../../common/lifecycle.js';
import './hoverWidget.css';
import { localize } from '../../../../nls.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const $ = $$1;
class HoverWidget extends Disposable {
constructor(fadeIn) {
super();
this.containerDomNode = document.createElement('div');
this.containerDomNode.className = 'monaco-hover';
this.containerDomNode.classList.toggle('fade-in', !!fadeIn);
this.containerDomNode.tabIndex = 0;
this.containerDomNode.setAttribute('role', 'tooltip');
this.contentsDomNode = document.createElement('div');
this.contentsDomNode.className = 'monaco-hover-content';
this.scrollbar = this._register(new DomScrollableElement(this.contentsDomNode, {
consumeMouseWheelIfScrollbarIsNeeded: true
}));
this.containerDomNode.appendChild(this.scrollbar.getDomNode());
}
onContentsChanged() {
this.scrollbar.scanDomNode();
}
}
class HoverAction extends Disposable {
static render(parent, actionOptions, keybindingLabel) {
return new HoverAction(parent, actionOptions, keybindingLabel);
}
constructor(parent, actionOptions, keybindingLabel) {
super();
this.actionLabel = actionOptions.label;
this.actionKeybindingLabel = keybindingLabel;
this.actionContainer = append(parent, $('div.action-container'));
this.actionContainer.setAttribute('tabindex', '0');
this.action = append(this.actionContainer, $('a.action'));
this.action.setAttribute('role', 'button');
if (actionOptions.iconClass) {
const iconElement = append(this.action, $(`span.icon`));
iconElement.classList.add(...actionOptions.iconClass.split(' '));
}
this.actionRenderedLabel = keybindingLabel ? `${actionOptions.label} (${keybindingLabel})` : actionOptions.label;
const label = append(this.action, $('span'));
label.textContent = this.actionRenderedLabel;
this._store.add(new ClickAction(this.actionContainer, actionOptions.run));
this._store.add(new KeyDownAction(this.actionContainer, actionOptions.run, [3 /* KeyCode.Enter */, 10 /* KeyCode.Space */]));
this.setEnabled(true);
}
setEnabled(enabled) {
if (enabled) {
this.actionContainer.classList.remove('disabled');
this.actionContainer.removeAttribute('aria-disabled');
}
else {
this.actionContainer.classList.add('disabled');
this.actionContainer.setAttribute('aria-disabled', 'true');
}
}
}
function getHoverAccessibleViewHint(shouldHaveHint, keybinding) {
return shouldHaveHint && keybinding ? localize(7, "Inspect this in the accessible view with {0}.", keybinding) : shouldHaveHint ? localize(8, "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding.") : '';
}
class ClickAction extends Disposable {
constructor(container, run) {
super();
this._register(addDisposableListener(container, EventType.CLICK, e => {
e.stopPropagation();
e.preventDefault();
run(container);
}));
}
}
class KeyDownAction extends Disposable {
constructor(container, run, keyCodes) {
super();
this._register(addDisposableListener(container, EventType.KEY_DOWN, e => {
const event = new StandardKeyboardEvent(e);
if (keyCodes.some(keyCode => event.equals(keyCode))) {
e.stopPropagation();
e.preventDefault();
run(container);
}
}));
}
}
export { ClickAction, HoverAction, HoverWidget, KeyDownAction, getHoverAccessibleViewHint };
@@ -0,0 +1,291 @@
import './iconlabel.css';
import { append, $, isHTMLElement, isAncestor, after } from '../../dom.js';
import { asCSSUrl } from '../../cssValue.js';
import { HighlightedLabel } from '../highlightedlabel/highlightedLabel.js';
import { Disposable } from '../../../common/lifecycle.js';
import { equals } from '../../../common/objects.js';
import { Range } from '../../../common/range.js';
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
import { ThemeIcon } from '../../../common/themables.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class FastLabelNode {
constructor(_element) {
this._element = _element;
}
get element() {
return this._element;
}
set textContent(content) {
if (this.disposed || content === this._textContent) {
return;
}
this._textContent = content;
this._element.textContent = content;
}
set classNames(classNames) {
if (this.disposed || equals(classNames, this._classNames)) {
return;
}
this._classNames = classNames;
this._element.classList.value = '';
this._element.classList.add(...classNames);
}
set empty(empty) {
if (this.disposed || empty === this._empty) {
return;
}
this._empty = empty;
this._element.style.marginLeft = empty ? '0' : '';
}
dispose() {
this.disposed = true;
}
}
class IconLabel extends Disposable {
constructor(container, options) {
super();
this.customHovers = new Map();
this.creationOptions = options;
this.domNode = this._register(new FastLabelNode(append(container, $('.monaco-icon-label'))));
this.labelContainer = append(this.domNode.element, $('.monaco-icon-label-container'));
this.nameContainer = append(this.labelContainer, $('span.monaco-icon-name-container'));
if (options?.supportHighlights || options?.supportIcons) {
this.nameNode = this._register(new LabelWithHighlights(this.nameContainer, !!options.supportIcons));
}
else {
this.nameNode = new Label(this.nameContainer);
}
this.hoverDelegate = options?.hoverDelegate ?? getDefaultHoverDelegate('mouse');
}
get element() {
return this.domNode.element;
}
setLabel(label, description, options) {
const labelClasses = ['monaco-icon-label'];
const containerClasses = ['monaco-icon-label-container'];
let ariaLabel = '';
if (options) {
if (options.extraClasses) {
labelClasses.push(...options.extraClasses);
}
if (options.bold) {
labelClasses.push('bold');
}
if (options.italic) {
labelClasses.push('italic');
}
if (options.strikethrough) {
labelClasses.push('strikethrough');
}
if (options.disabledCommand) {
containerClasses.push('disabled');
}
if (options.title) {
if (typeof options.title === 'string') {
ariaLabel += options.title;
}
else {
ariaLabel += label;
}
}
}
// eslint-disable-next-line no-restricted-syntax
const existingIconNode = this.domNode.element.querySelector('.monaco-icon-label-iconpath');
if (options?.iconPath) {
let iconNode;
if (!existingIconNode || !(isHTMLElement(existingIconNode))) {
iconNode = $('.monaco-icon-label-iconpath');
this.domNode.element.prepend(iconNode);
}
else {
iconNode = existingIconNode;
}
if (ThemeIcon.isThemeIcon(options.iconPath)) {
const iconClass = ThemeIcon.asClassName(options.iconPath);
iconNode.className = `monaco-icon-label-iconpath ${iconClass}`;
iconNode.style.backgroundImage = '';
}
else {
iconNode.style.backgroundImage = asCSSUrl(options?.iconPath);
}
iconNode.style.backgroundRepeat = 'no-repeat';
iconNode.style.backgroundPosition = 'center';
iconNode.style.backgroundSize = 'contain';
}
else if (existingIconNode) {
existingIconNode.remove();
}
this.domNode.classNames = labelClasses;
this.domNode.element.setAttribute('aria-label', ariaLabel);
this.labelContainer.classList.value = '';
this.labelContainer.classList.add(...containerClasses);
this.setupHover(options?.descriptionTitle ? this.labelContainer : this.element, options?.title);
this.nameNode.setLabel(label, options);
if (description || this.descriptionNode) {
const descriptionNode = this.getOrCreateDescriptionNode();
if (descriptionNode instanceof HighlightedLabel) {
const supportIcons = options?.supportIcons ?? this.creationOptions?.supportIcons;
descriptionNode.set(description || '', options ? options.descriptionMatches : undefined, undefined, options?.labelEscapeNewLines, supportIcons);
this.setupHover(descriptionNode.element, options?.descriptionTitle);
}
else {
descriptionNode.textContent = description && options?.labelEscapeNewLines ? HighlightedLabel.escapeNewLines(description, []) : (description || '');
this.setupHover(descriptionNode.element, options?.descriptionTitle || '');
descriptionNode.empty = !description;
}
}
if (options?.suffix || this.suffixNode) {
const suffixNode = this.getOrCreateSuffixNode();
suffixNode.textContent = options?.suffix ?? '';
}
}
setupHover(htmlElement, tooltip) {
const previousCustomHover = this.customHovers.get(htmlElement);
if (previousCustomHover) {
previousCustomHover.dispose();
this.customHovers.delete(htmlElement);
}
if (!tooltip) {
htmlElement.removeAttribute('title');
return;
}
let hoverTarget = htmlElement;
if (this.creationOptions?.hoverTargetOverride) {
if (!isAncestor(htmlElement, this.creationOptions.hoverTargetOverride)) {
throw new Error('hoverTargetOverrride must be an ancestor of the htmlElement');
}
hoverTarget = this.creationOptions.hoverTargetOverride;
}
const hoverDisposable = getBaseLayerHoverDelegate().setupManagedHover(this.hoverDelegate, hoverTarget, tooltip);
if (hoverDisposable) {
this.customHovers.set(htmlElement, hoverDisposable);
}
}
dispose() {
super.dispose();
for (const disposable of this.customHovers.values()) {
disposable.dispose();
}
this.customHovers.clear();
}
getOrCreateSuffixNode() {
if (!this.suffixNode) {
const suffixContainer = this._register(new FastLabelNode(after(this.nameContainer, $('span.monaco-icon-suffix-container'))));
this.suffixNode = this._register(new FastLabelNode(append(suffixContainer.element, $('span.label-suffix'))));
}
return this.suffixNode;
}
getOrCreateDescriptionNode() {
if (!this.descriptionNode) {
const descriptionContainer = this._register(new FastLabelNode(append(this.labelContainer, $('span.monaco-icon-description-container'))));
if (this.creationOptions?.supportDescriptionHighlights) {
this.descriptionNode = this._register(new HighlightedLabel(append(descriptionContainer.element, $('span.label-description'))));
}
else {
this.descriptionNode = this._register(new FastLabelNode(append(descriptionContainer.element, $('span.label-description'))));
}
}
return this.descriptionNode;
}
}
class Label {
constructor(container) {
this.container = container;
this.label = undefined;
this.singleLabel = undefined;
}
setLabel(label, options) {
if (this.label === label && equals(this.options, options)) {
return;
}
this.label = label;
this.options = options;
if (typeof label === 'string') {
if (!this.singleLabel) {
this.container.textContent = '';
this.container.classList.remove('multiple');
this.singleLabel = append(this.container, $('a.label-name', { id: options?.domId }));
}
this.singleLabel.textContent = label;
}
else {
this.container.textContent = '';
this.container.classList.add('multiple');
this.singleLabel = undefined;
for (let i = 0; i < label.length; i++) {
const l = label[i];
const id = options?.domId && `${options?.domId}_${i}`;
append(this.container, $('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' }, l));
if (i < label.length - 1) {
append(this.container, $('span.label-separator', undefined, options?.separator || '/'));
}
}
}
}
}
function splitMatches(labels, separator, matches) {
if (!matches) {
return undefined;
}
let labelStart = 0;
return labels.map(label => {
const labelRange = { start: labelStart, end: labelStart + label.length };
const result = matches
.map(match => Range.intersect(labelRange, match))
.filter(range => !Range.isEmpty(range))
.map(({ start, end }) => ({ start: start - labelStart, end: end - labelStart }));
labelStart = labelRange.end + separator.length;
return result;
});
}
class LabelWithHighlights extends Disposable {
constructor(container, supportIcons) {
super();
this.container = container;
this.supportIcons = supportIcons;
this.label = undefined;
this.singleLabel = undefined;
}
setLabel(label, options) {
if (this.label === label && equals(this.options, options)) {
return;
}
this.label = label;
this.options = options;
// Determine supportIcons: use option if provided, otherwise use constructor value
const supportIcons = options?.supportIcons ?? this.supportIcons;
if (typeof label === 'string') {
if (!this.singleLabel) {
this.container.textContent = '';
this.container.classList.remove('multiple');
this.singleLabel = this._register(new HighlightedLabel(append(this.container, $('a.label-name', { id: options?.domId }))));
}
this.singleLabel.set(label, options?.matches, undefined, options?.labelEscapeNewLines, supportIcons);
}
else {
this.container.textContent = '';
this.container.classList.add('multiple');
this.singleLabel = undefined;
const separator = options?.separator || '/';
const matches = splitMatches(label, separator, options?.matches);
for (let i = 0; i < label.length; i++) {
const l = label[i];
const m = matches ? matches[i] : undefined;
const id = options?.domId && `${options?.domId}_${i}`;
const name = $('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' });
const highlightedLabel = this._register(new HighlightedLabel(append(this.container, name)));
highlightedLabel.set(l, m, undefined, options?.labelEscapeNewLines, supportIcons);
if (i < label.length - 1) {
append(name, $('span.label-separator', undefined, separator));
}
}
}
}
}
export { IconLabel };
@@ -0,0 +1,33 @@
import { $ } from '../../dom.js';
import { ThemeIcon } from '../../../common/themables.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const labelWithIconsRegex = new RegExp(`(\\\\)?\\$\\((${ThemeIcon.iconNameExpression}(?:${ThemeIcon.iconModifierExpression})?)\\)`, 'g');
function renderLabelWithIcons(text) {
const elements = new Array();
let match;
let textStart = 0, textStop = 0;
while ((match = labelWithIconsRegex.exec(text)) !== null) {
textStop = match.index || 0;
if (textStart < textStop) {
elements.push(text.substring(textStart, textStop));
}
textStart = (match.index || 0) + match[0].length;
const [, escaped, codicon] = match;
elements.push(escaped ? `$(${codicon})` : renderIcon({ id: codicon }));
}
if (textStart < text.length) {
elements.push(text.substring(textStart));
}
return elements;
}
function renderIcon(icon) {
const node = $(`span`);
node.classList.add(...ThemeIcon.asClassNameArray(icon));
return node;
}
export { renderIcon, renderLabelWithIcons };
@@ -0,0 +1,119 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* ---------- Icon label ---------- */
.monaco-icon-label {
display: flex; /* required for icons support :before rule */
overflow: hidden;
text-overflow: ellipsis;
}
.monaco-icon-label::before {
/* svg icons rendered as background image */
background-size: 16px;
background-position: left center;
background-repeat: no-repeat;
padding-right: 6px;
width: 16px;
height: 22px;
line-height: inherit !important;
display: inline-block;
/* fonts icons */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
vertical-align: top;
flex-shrink: 0; /* fix for https://github.com/microsoft/vscode/issues/13787 */
}
.monaco-icon-label-iconpath {
width: 16px;
height: 22px;
margin-right: 6px;
display: flex;
}
.monaco-icon-label-container.disabled {
color: var(--vscode-disabledForeground);
}
.monaco-icon-label > .monaco-icon-label-container {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container > .label-name {
color: inherit;
white-space: pre; /* enable to show labels that include multiple whitespaces */
}
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container > .label-name > .label-separator {
margin: 0 2px;
opacity: 0.5;
}
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-suffix-container > .label-suffix {
opacity: .7;
white-space: pre;
}
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
opacity: .7;
margin-left: 0.5em;
font-size: 0.9em;
white-space: pre; /* enable to show labels that include multiple whitespaces */
}
.monaco-icon-label.nowrap > .monaco-icon-label-container > .monaco-icon-description-container > .label-description{
white-space: nowrap
}
.vs .monaco-icon-label > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
opacity: .95;
}
.monaco-icon-label.bold > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
.monaco-icon-label.bold > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
font-weight: bold;
}
.monaco-icon-label.italic > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
.monaco-icon-label.italic > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
font-style: italic;
}
.monaco-icon-label.deprecated {
text-decoration: line-through;
opacity: 0.66;
}
.monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
.monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
text-decoration: line-through;
}
.monaco-icon-label::after {
opacity: 0.75;
font-size: 90%;
font-weight: 600;
margin: auto 16px 0 5px; /* https://github.com/microsoft/vscode/issues/113223 */
text-align: center;
}
/* make sure selection color wins when a label is being selected */
.monaco-list:focus .selected .monaco-icon-label, /* list */
.monaco-list:focus .selected .monaco-icon-label::after
{
color: inherit !important;
}
.monaco-list-row.focused.selected .label-description,
.monaco-list-row.selected .label-description {
opacity: .8;
}
@@ -0,0 +1,106 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-inputbox {
position: relative;
display: block;
padding: 0;
box-sizing: border-box;
border-radius: 2px;
/* Customizable */
font-size: inherit;
}
.monaco-inputbox > .ibwrapper > .input,
.monaco-inputbox > .ibwrapper > .mirror {
/* Customizable */
padding: 4px 6px;
}
.monaco-inputbox > .ibwrapper {
position: relative;
width: 100%;
}
.monaco-inputbox > .ibwrapper > .input {
display: inline-block;
box-sizing: border-box;
width: 100%;
height: 100%;
line-height: inherit;
border: none;
font-family: inherit;
font-size: inherit;
resize: none;
color: inherit;
}
.monaco-inputbox > .ibwrapper > input {
text-overflow: ellipsis;
}
.monaco-inputbox > .ibwrapper > textarea.input {
display: block;
scrollbar-width: none; /* Firefox: hide scrollbars */
outline: none;
}
.monaco-inputbox > .ibwrapper > textarea.input::-webkit-scrollbar {
display: none; /* Chrome + Safari: hide scrollbar */
}
.monaco-inputbox > .ibwrapper > textarea.input.empty {
white-space: nowrap;
}
.monaco-inputbox > .ibwrapper > .mirror {
position: absolute;
display: inline-block;
width: 100%;
top: 0;
left: 0;
box-sizing: border-box;
white-space: pre-wrap;
visibility: hidden;
word-wrap: break-word;
}
/* Context view */
.monaco-inputbox-container {
text-align: right;
}
.monaco-inputbox-container .monaco-inputbox-message {
display: inline-block;
overflow: hidden;
text-align: left;
width: 100%;
box-sizing: border-box;
padding: 0.4em;
font-size: 12px;
line-height: 17px;
margin-top: -1px;
word-wrap: break-word;
}
/* Action bar support */
.monaco-inputbox .monaco-action-bar {
position: absolute;
right: 2px;
top: 4px;
}
.monaco-inputbox .monaco-action-bar .action-item {
margin-left: 2px;
}
.monaco-inputbox .monaco-action-bar .action-item .codicon {
background-repeat: no-repeat;
width: 16px;
height: 16px;
}
@@ -0,0 +1,510 @@
import { append, $ as $$1, getTotalHeight, isActiveElement, getTotalWidth } from '../../dom.js';
import { asCssValueWithDefault } from '../../cssValue.js';
import { DomEmitter } from '../../event.js';
import { renderFormattedText, renderText } from '../../formattedTextRenderer.js';
import { ActionBar } from '../actionbar/actionbar.js';
import { status, alert } from '../aria/aria.js';
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
import { ScrollableElement } from '../scrollbar/scrollableElement.js';
import { Widget } from '../widget.js';
import { Emitter, Event } from '../../../common/event.js';
import { HistoryNavigator } from '../../../common/history.js';
import { equals } from '../../../common/objects.js';
import './inputBox.css';
import { localize } from '../../../../nls.js';
import { MutableDisposable } from '../../../common/lifecycle.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const $ = $$1;
class InputBox extends Widget {
get onDidChange() { return this._onDidChange.event; }
get onDidHeightChange() { return this._onDidHeightChange.event; }
constructor(container, contextViewProvider, options) {
super();
this.state = 'idle';
this.maxHeight = Number.POSITIVE_INFINITY;
this.hover = this._register(new MutableDisposable());
this._onDidChange = this._register(new Emitter());
this._onDidHeightChange = this._register(new Emitter());
this.contextViewProvider = contextViewProvider;
this.options = options;
this.message = null;
this.placeholder = this.options.placeholder || '';
this.tooltip = this.options.tooltip ?? (this.placeholder || '');
this.ariaLabel = this.options.ariaLabel || '';
if (this.options.validationOptions) {
this.validation = this.options.validationOptions.validation;
}
this.element = append(container, $('.monaco-inputbox.idle'));
const tagName = this.options.flexibleHeight ? 'textarea' : 'input';
const wrapper = append(this.element, $('.ibwrapper'));
this.input = append(wrapper, $(tagName + '.input.empty'));
this.input.setAttribute('autocorrect', 'off');
this.input.setAttribute('autocapitalize', 'off');
this.input.setAttribute('spellcheck', 'false');
this.onfocus(this.input, () => this.element.classList.add('synthetic-focus'));
this.onblur(this.input, () => this.element.classList.remove('synthetic-focus'));
if (this.options.flexibleHeight) {
this.maxHeight = typeof this.options.flexibleMaxHeight === 'number' ? this.options.flexibleMaxHeight : Number.POSITIVE_INFINITY;
this.mirror = append(wrapper, $('div.mirror'));
this.mirror.innerText = '\u00a0';
this.scrollableElement = new ScrollableElement(this.element, { vertical: 1 /* ScrollbarVisibility.Auto */ });
if (this.options.flexibleWidth) {
this.input.setAttribute('wrap', 'off');
this.mirror.style.whiteSpace = 'pre';
this.mirror.style.wordWrap = 'initial';
}
append(container, this.scrollableElement.getDomNode());
this._register(this.scrollableElement);
// from ScrollableElement to DOM
this._register(this.scrollableElement.onScroll(e => this.input.scrollTop = e.scrollTop));
const onSelectionChange = this._register(new DomEmitter(container.ownerDocument, 'selectionchange'));
const onAnchoredSelectionChange = Event.filter(onSelectionChange.event, () => {
const selection = container.ownerDocument.getSelection();
return selection?.anchorNode === wrapper;
});
// from DOM to ScrollableElement
this._register(onAnchoredSelectionChange(this.updateScrollDimensions, this));
this._register(this.onDidHeightChange(this.updateScrollDimensions, this));
}
else {
this.input.type = this.options.type || 'text';
this.input.setAttribute('wrap', 'off');
}
if (this.ariaLabel) {
this.input.setAttribute('aria-label', this.ariaLabel);
}
if (this.placeholder && !this.options.showPlaceholderOnFocus) {
this.setPlaceHolder(this.placeholder);
}
if (this.tooltip) {
this.setTooltip(this.tooltip);
}
this.oninput(this.input, () => this.onValueChange());
this.onblur(this.input, () => this.onBlur());
this.onfocus(this.input, () => this.onFocus());
this._register(this.ignoreGesture(this.input));
setTimeout(() => this.updateMirror(), 0);
// Support actions
if (this.options.actions) {
this.actionbar = this._register(new ActionBar(this.element));
this.actionbar.push(this.options.actions, { icon: true, label: false });
}
this.applyStyles();
}
onBlur() {
this._hideMessage();
if (this.options.showPlaceholderOnFocus) {
this.input.setAttribute('placeholder', '');
}
}
onFocus() {
this._showMessage();
if (this.options.showPlaceholderOnFocus) {
this.input.setAttribute('placeholder', this.placeholder || '');
}
}
setPlaceHolder(placeHolder) {
this.placeholder = placeHolder;
this.input.setAttribute('placeholder', placeHolder);
}
setTooltip(tooltip) {
this.tooltip = tooltip;
if (!this.hover.value) {
this.hover.value = this._register(getBaseLayerHoverDelegate().setupDelayedHoverAtMouse(this.input, () => ({
content: this.tooltip,
appearance: {
compact: true,
}
})));
}
}
get inputElement() {
return this.input;
}
get value() {
return this.input.value;
}
set value(newValue) {
if (this.input.value !== newValue) {
this.input.value = newValue;
this.onValueChange();
}
}
get height() {
return typeof this.cachedHeight === 'number' ? this.cachedHeight : getTotalHeight(this.element);
}
focus() {
this.input.focus();
}
blur() {
this.input.blur();
}
hasFocus() {
return isActiveElement(this.input);
}
select(range = null) {
this.input.select();
if (range) {
this.input.setSelectionRange(range.start, range.end);
if (range.end === this.input.value.length) {
this.input.scrollLeft = this.input.scrollWidth;
}
}
}
isSelectionAtEnd() {
return this.input.selectionEnd === this.input.value.length && this.input.selectionStart === this.input.selectionEnd;
}
getSelection() {
const selectionStart = this.input.selectionStart;
if (selectionStart === null) {
return null;
}
const selectionEnd = this.input.selectionEnd ?? selectionStart;
return {
start: selectionStart,
end: selectionEnd,
};
}
enable() {
this.input.removeAttribute('disabled');
}
disable() {
this.blur();
this.input.disabled = true;
this._hideMessage();
}
set paddingRight(paddingRight) {
// Set width to avoid hint text overlapping buttons
this.input.style.width = `calc(100% - ${paddingRight}px)`;
if (this.mirror) {
this.mirror.style.paddingRight = paddingRight + 'px';
}
}
updateScrollDimensions() {
if (typeof this.cachedContentHeight !== 'number' || typeof this.cachedHeight !== 'number' || !this.scrollableElement) {
return;
}
const scrollHeight = this.cachedContentHeight;
const height = this.cachedHeight;
const scrollTop = this.input.scrollTop;
this.scrollableElement.setScrollDimensions({ scrollHeight, height });
this.scrollableElement.setScrollPosition({ scrollTop });
}
showMessage(message, force) {
if (this.state === 'open' && equals(this.message, message)) {
// Already showing
return;
}
this.message = message;
this.element.classList.remove('idle');
this.element.classList.remove('info');
this.element.classList.remove('warning');
this.element.classList.remove('error');
this.element.classList.add(this.classForType(message.type));
const styles = this.stylesForType(this.message.type);
this.element.style.border = `1px solid ${asCssValueWithDefault(styles.border, 'transparent')}`;
if (this.message.content && (this.hasFocus() || force)) {
this._showMessage();
}
}
hideMessage() {
this.message = null;
this.element.classList.remove('info');
this.element.classList.remove('warning');
this.element.classList.remove('error');
this.element.classList.add('idle');
this._hideMessage();
this.applyStyles();
}
validate() {
let errorMsg = null;
if (this.validation) {
errorMsg = this.validation(this.value);
if (errorMsg) {
this.inputElement.setAttribute('aria-invalid', 'true');
this.showMessage(errorMsg);
}
else if (this.inputElement.hasAttribute('aria-invalid')) {
this.inputElement.removeAttribute('aria-invalid');
this.hideMessage();
}
}
return errorMsg?.type;
}
stylesForType(type) {
const styles = this.options.inputBoxStyles;
switch (type) {
case 1 /* MessageType.INFO */: return { border: styles.inputValidationInfoBorder, background: styles.inputValidationInfoBackground, foreground: styles.inputValidationInfoForeground };
case 2 /* MessageType.WARNING */: return { border: styles.inputValidationWarningBorder, background: styles.inputValidationWarningBackground, foreground: styles.inputValidationWarningForeground };
default: return { border: styles.inputValidationErrorBorder, background: styles.inputValidationErrorBackground, foreground: styles.inputValidationErrorForeground };
}
}
classForType(type) {
switch (type) {
case 1 /* MessageType.INFO */: return 'info';
case 2 /* MessageType.WARNING */: return 'warning';
default: return 'error';
}
}
_showMessage() {
if (!this.contextViewProvider || !this.message) {
return;
}
let div;
const layout = () => div.style.width = getTotalWidth(this.element) + 'px';
this.contextViewProvider.showContextView({
getAnchor: () => this.element,
anchorAlignment: 1 /* AnchorAlignment.RIGHT */,
render: (container) => {
if (!this.message) {
return null;
}
div = append(container, $('.monaco-inputbox-container'));
layout();
const spanElement = $('span.monaco-inputbox-message');
if (this.message.formatContent) {
renderFormattedText(this.message.content, undefined, spanElement);
}
else {
renderText(this.message.content, undefined, spanElement);
}
spanElement.classList.add(this.classForType(this.message.type));
const styles = this.stylesForType(this.message.type);
spanElement.style.backgroundColor = styles.background ?? '';
spanElement.style.color = styles.foreground ?? '';
spanElement.style.border = styles.border ? `1px solid ${styles.border}` : '';
append(div, spanElement);
return null;
},
onHide: () => {
this.state = 'closed';
},
layout: layout
});
// ARIA Support
let alertText;
if (this.message.type === 3 /* MessageType.ERROR */) {
alertText = localize(9, "Error: {0}", this.message.content);
}
else if (this.message.type === 2 /* MessageType.WARNING */) {
alertText = localize(10, "Warning: {0}", this.message.content);
}
else {
alertText = localize(11, "Info: {0}", this.message.content);
}
alert(alertText);
this.state = 'open';
}
_hideMessage() {
if (!this.contextViewProvider) {
return;
}
if (this.state === 'open') {
this.contextViewProvider.hideContextView();
}
this.state = 'idle';
}
onValueChange() {
this._onDidChange.fire(this.value);
this.validate();
this.updateMirror();
this.input.classList.toggle('empty', !this.value);
if (this.state === 'open' && this.contextViewProvider) {
this.contextViewProvider.layout();
}
}
updateMirror() {
if (!this.mirror) {
return;
}
const value = this.value;
const lastCharCode = value.charCodeAt(value.length - 1);
const suffix = lastCharCode === 10 ? ' ' : '';
const mirrorTextContent = (value + suffix)
.replace(/\u000c/g, ''); // Don't measure with the form feed character, which messes up sizing
if (mirrorTextContent) {
this.mirror.textContent = value + suffix;
}
else {
this.mirror.innerText = '\u00a0';
}
this.layout();
}
applyStyles() {
const styles = this.options.inputBoxStyles;
const background = styles.inputBackground ?? '';
const foreground = styles.inputForeground ?? '';
const border = styles.inputBorder ?? '';
this.element.style.backgroundColor = background;
this.element.style.color = foreground;
this.input.style.backgroundColor = 'inherit';
this.input.style.color = foreground;
// there's always a border, even if the color is not set.
this.element.style.border = `1px solid ${asCssValueWithDefault(border, 'transparent')}`;
}
layout() {
if (!this.mirror) {
return;
}
const previousHeight = this.cachedContentHeight;
this.cachedContentHeight = getTotalHeight(this.mirror);
if (previousHeight !== this.cachedContentHeight) {
this.cachedHeight = Math.min(this.cachedContentHeight, this.maxHeight);
this.input.style.height = this.cachedHeight + 'px';
this._onDidHeightChange.fire(this.cachedContentHeight);
}
}
insertAtCursor(text) {
const inputElement = this.inputElement;
const start = inputElement.selectionStart;
const end = inputElement.selectionEnd;
const content = inputElement.value;
if (start !== null && end !== null) {
this.value = content.substr(0, start) + text + content.substr(end);
inputElement.setSelectionRange(start + 1, start + 1);
this.layout();
}
}
dispose() {
this._hideMessage();
this.message = null;
this.actionbar?.dispose();
super.dispose();
}
}
class HistoryInputBox extends InputBox {
constructor(container, contextViewProvider, options) {
const NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_NO_PARENS = localize(12, ' or {0} for history', `\u21C5`);
const NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS = localize(13, ' ({0} for history)', `\u21C5`);
super(container, contextViewProvider, options);
this._onDidFocus = this._register(new Emitter());
this.onDidFocus = this._onDidFocus.event;
this._onDidBlur = this._register(new Emitter());
this.onDidBlur = this._onDidBlur.event;
this.history = this._register(new HistoryNavigator(options.history, 100));
// Function to append the history suffix to the placeholder if necessary
const addSuffix = () => {
if (options.showHistoryHint && options.showHistoryHint() && !this.placeholder.endsWith(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_NO_PARENS) && !this.placeholder.endsWith(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS) && this.history.getHistory().length) {
const suffix = this.placeholder.endsWith(')') ? NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_NO_PARENS : NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS;
const suffixedPlaceholder = this.placeholder + suffix;
if (options.showPlaceholderOnFocus && !isActiveElement(this.input)) {
this.placeholder = suffixedPlaceholder;
}
else {
this.setPlaceHolder(suffixedPlaceholder);
}
}
};
// Spot the change to the textarea class attribute which occurs when it changes between non-empty and empty,
// and add the history suffix to the placeholder if not yet present
this.observer = new MutationObserver((mutationList, observer) => {
mutationList.forEach((mutation) => {
if (!mutation.target.textContent) {
addSuffix();
}
});
});
this.observer.observe(this.input, { attributeFilter: ['class'] });
this.onfocus(this.input, () => addSuffix());
this.onblur(this.input, () => {
const resetPlaceholder = (historyHint) => {
if (!this.placeholder.endsWith(historyHint)) {
return false;
}
else {
const revertedPlaceholder = this.placeholder.slice(0, this.placeholder.length - historyHint.length);
if (options.showPlaceholderOnFocus) {
this.placeholder = revertedPlaceholder;
}
else {
this.setPlaceHolder(revertedPlaceholder);
}
return true;
}
};
if (!resetPlaceholder(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS)) {
resetPlaceholder(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_NO_PARENS);
}
});
}
dispose() {
super.dispose();
if (this.observer) {
this.observer.disconnect();
this.observer = undefined;
}
}
addToHistory(always) {
if (this.value && (always || this.value !== this.getCurrentValue())) {
this.history.add(this.value);
}
}
isAtLastInHistory() {
return this.history.isLast();
}
isNowhereInHistory() {
return this.history.isNowhere();
}
showNextValue() {
if (!this.history.has(this.value)) {
this.addToHistory();
}
let next = this.getNextValue();
if (next) {
next = next === this.value ? this.getNextValue() : next;
}
this.value = next ?? '';
status(this.value ? this.value : localize(14, "Cleared Input"));
}
showPreviousValue() {
if (!this.history.has(this.value)) {
this.addToHistory();
}
let previous = this.getPreviousValue();
if (previous) {
previous = previous === this.value ? this.getPreviousValue() : previous;
}
if (previous) {
this.value = previous;
status(this.value);
}
}
setPlaceHolder(placeHolder) {
super.setPlaceHolder(placeHolder);
this.setTooltip(placeHolder);
}
onBlur() {
super.onBlur();
this._onDidBlur.fire();
}
onFocus() {
super.onFocus();
this._onDidFocus.fire();
}
getCurrentValue() {
let currentValue = this.history.current();
if (!currentValue) {
currentValue = this.history.last();
this.history.next();
}
return currentValue;
}
getPreviousValue() {
return this.history.previous() || this.history.first();
}
getNextValue() {
return this.history.next();
}
}
export { HistoryInputBox, InputBox };
@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-keybinding {
display: flex;
align-items: center;
line-height: 10px;
}
.monaco-keybinding > .monaco-keybinding-key {
display: inline-block;
border-style: solid;
border-width: 1px;
border-radius: 3px;
vertical-align: middle;
font-size: 11px;
padding: 3px 5px;
margin: 0 2px;
}
.monaco-keybinding > .monaco-keybinding-key:first-child {
margin-left: 0;
}
.monaco-keybinding > .monaco-keybinding-key:last-child {
margin-right: 0;
}
.monaco-keybinding > .monaco-keybinding-key-separator {
display: inline-block;
}
.monaco-keybinding > .monaco-keybinding-key-chord-separator {
width: 6px;
}
@@ -0,0 +1,122 @@
import { append, $ as $$1, clearNode } from '../../dom.js';
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
import { UILabelProvider } from '../../../common/keybindingLabels.js';
import { Disposable } from '../../../common/lifecycle.js';
import { equals } from '../../../common/objects.js';
import './keybindingLabel.css';
import { localize } from '../../../../nls.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const $ = $$1;
const unthemedKeybindingLabelOptions = {
keybindingLabelBackground: undefined,
keybindingLabelForeground: undefined,
keybindingLabelBorder: undefined,
keybindingLabelBottomBorder: undefined,
keybindingLabelShadow: undefined
};
class KeybindingLabel extends Disposable {
constructor(container, os, options) {
super();
this.os = os;
this.keyElements = new Set();
this.options = options || Object.create(null);
const labelForeground = this.options.keybindingLabelForeground;
this.domNode = append(container, $('.monaco-keybinding'));
if (labelForeground) {
this.domNode.style.color = labelForeground;
}
this.hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.domNode, ''));
this.didEverRender = false;
container.appendChild(this.domNode);
}
set(keybinding, matches) {
if (this.didEverRender && this.keybinding === keybinding && KeybindingLabel.areSame(this.matches, matches)) {
return;
}
this.keybinding = keybinding;
this.matches = matches;
this.render();
}
render() {
this.clear();
if (this.keybinding) {
const chords = this.keybinding.getChords();
if (chords[0]) {
this.renderChord(this.domNode, chords[0], this.matches ? this.matches.firstPart : null);
}
for (let i = 1; i < chords.length; i++) {
append(this.domNode, $('span.monaco-keybinding-key-chord-separator', undefined, ' '));
this.renderChord(this.domNode, chords[i], this.matches ? this.matches.chordPart : null);
}
const title = (this.options.disableTitle ?? false) ? undefined : this.keybinding.getAriaLabel() || undefined;
this.hover.update(title);
this.domNode.setAttribute('aria-label', title || '');
}
else if (this.options && this.options.renderUnboundKeybindings) {
this.renderUnbound(this.domNode);
}
this.didEverRender = true;
}
clear() {
clearNode(this.domNode);
this.keyElements.clear();
}
renderChord(parent, chord, match) {
const modifierLabels = UILabelProvider.modifierLabels[this.os];
if (chord.ctrlKey) {
this.renderKey(parent, modifierLabels.ctrlKey, Boolean(match?.ctrlKey), modifierLabels.separator);
}
if (chord.shiftKey) {
this.renderKey(parent, modifierLabels.shiftKey, Boolean(match?.shiftKey), modifierLabels.separator);
}
if (chord.altKey) {
this.renderKey(parent, modifierLabels.altKey, Boolean(match?.altKey), modifierLabels.separator);
}
if (chord.metaKey) {
this.renderKey(parent, modifierLabels.metaKey, Boolean(match?.metaKey), modifierLabels.separator);
}
const keyLabel = chord.keyLabel;
if (keyLabel) {
this.renderKey(parent, keyLabel, Boolean(match?.keyCode), '');
}
}
renderKey(parent, label, highlight, separator) {
append(parent, this.createKeyElement(label, highlight ? '.highlight' : ''));
if (separator) {
append(parent, $('span.monaco-keybinding-key-separator', undefined, separator));
}
}
renderUnbound(parent) {
append(parent, this.createKeyElement(localize(15, "Unbound")));
}
createKeyElement(label, extraClass = '') {
const keyElement = $('span.monaco-keybinding-key' + extraClass, undefined, label);
this.keyElements.add(keyElement);
if (this.options.keybindingLabelBackground) {
keyElement.style.backgroundColor = this.options.keybindingLabelBackground;
}
if (this.options.keybindingLabelBorder) {
keyElement.style.borderColor = this.options.keybindingLabelBorder;
}
if (this.options.keybindingLabelBottomBorder) {
keyElement.style.borderBottomColor = this.options.keybindingLabelBottomBorder;
}
if (this.options.keybindingLabelShadow) {
keyElement.style.boxShadow = `inset 0 -1px 0 ${this.options.keybindingLabelShadow}`;
}
return keyElement;
}
static areSame(a, b) {
if (a === b || (!a && !b)) {
return true;
}
return !!a && !!b && equals(a.firstPart, b.firstPart) && equals(a.chordPart, b.chordPart);
}
}
export { KeybindingLabel, unthemedKeybindingLabelOptions };
@@ -0,0 +1,81 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-list {
position: relative;
height: 100%;
width: 100%;
white-space: nowrap;
}
.monaco-list.mouse-support {
user-select: none;
-webkit-user-select: none;
}
.monaco-list > .monaco-scrollable-element {
height: 100%;
}
.monaco-list-rows {
position: relative;
width: 100%;
height: 100%;
}
.monaco-list.horizontal-scrolling .monaco-list-rows {
width: auto;
min-width: 100%;
}
.monaco-list-row {
position: absolute;
box-sizing: border-box;
overflow: hidden;
width: 100%;
}
.monaco-list.mouse-support .monaco-list-row {
cursor: pointer;
touch-action: none;
}
/* Make sure the scrollbar renders above overlays (sticky scroll) */
.monaco-list .monaco-scrollable-element > .scrollbar.vertical,
.monaco-pane-view > .monaco-split-view2.vertical > .monaco-scrollable-element > .scrollbar.vertical {
z-index: 14;
}
/* for OS X ballistic scrolling */
.monaco-list-row.scrolling {
display: none !important;
}
/* Focus */
.monaco-list.element-focused,
.monaco-list.selection-single,
.monaco-list.selection-multiple {
outline: 0 !important;
}
/* Filter */
.monaco-list-type-filter-message {
position: absolute;
box-sizing: border-box;
width: 100%;
height: 100%;
top: 0;
left: 0;
padding: 40px 1em 1em 1em;
text-align: center;
white-space: normal;
opacity: 0.7;
pointer-events: none;
}
.monaco-list-type-filter-message:empty {
display: none;
}
@@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class ListError extends Error {
constructor(user, message) {
super(`ListError [${user}] ${message}`);
}
}
export { ListError };
@@ -0,0 +1,128 @@
import { range } from '../../../common/arrays.js';
import { CancellationTokenSource } from '../../../common/cancellation.js';
import { Event } from '../../../common/event.js';
import { DisposableStore, Disposable } from '../../../common/lifecycle.js';
import './list.css';
import { List } from './listWidget.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class PagedRenderer {
get templateId() { return this.renderer.templateId; }
constructor(renderer, modelProvider) {
this.renderer = renderer;
this.modelProvider = modelProvider;
}
renderTemplate(container) {
const data = this.renderer.renderTemplate(container);
return { data, disposable: Disposable.None };
}
renderElement(index, _, data, details) {
data.disposable?.dispose();
if (!data.data) {
return;
}
const model = this.modelProvider();
if (model.isResolved(index)) {
return this.renderer.renderElement(model.get(index), index, data.data, details);
}
const cts = new CancellationTokenSource();
const promise = model.resolve(index, cts.token);
data.disposable = { dispose: () => cts.cancel() };
this.renderer.renderPlaceholder(index, data.data);
promise.then(entry => this.renderer.renderElement(entry, index, data.data, details));
}
disposeTemplate(data) {
if (data.disposable) {
data.disposable.dispose();
data.disposable = undefined;
}
if (data.data) {
this.renderer.disposeTemplate(data.data);
data.data = undefined;
}
}
}
class PagedAccessibilityProvider {
constructor(modelProvider, accessibilityProvider) {
this.modelProvider = modelProvider;
this.accessibilityProvider = accessibilityProvider;
}
getWidgetAriaLabel() {
return this.accessibilityProvider.getWidgetAriaLabel();
}
getAriaLabel(index) {
const model = this.modelProvider();
if (!model.isResolved(index)) {
return null;
}
return this.accessibilityProvider.getAriaLabel(model.get(index));
}
}
function fromPagedListOptions(modelProvider, options) {
return {
...options,
accessibilityProvider: options.accessibilityProvider && new PagedAccessibilityProvider(modelProvider, options.accessibilityProvider)
};
}
class PagedList {
constructor(user, container, virtualDelegate, renderers, options = {}) {
this.modelDisposables = new DisposableStore();
const modelProvider = () => this.model;
const pagedRenderers = renderers.map(r => new PagedRenderer(r, modelProvider));
this.list = new List(user, container, virtualDelegate, pagedRenderers, fromPagedListOptions(modelProvider, options));
}
updateOptions(options) {
this.list.updateOptions(options);
}
getHTMLElement() {
return this.list.getHTMLElement();
}
get onDidFocus() {
return this.list.onDidFocus;
}
get widget() {
return this.list;
}
get onDidDispose() {
return this.list.onDidDispose;
}
get onMouseDblClick() {
return Event.map(this.list.onMouseDblClick, ({ element, index, browserEvent }) => ({ element: element === undefined ? undefined : this._model.get(element), index, browserEvent }));
}
get onPointer() {
return Event.map(this.list.onPointer, ({ element, index, browserEvent }) => ({ element: element === undefined ? undefined : this._model.get(element), index, browserEvent }));
}
get onDidChangeSelection() {
return Event.map(this.list.onDidChangeSelection, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent }));
}
get model() {
return this._model;
}
set model(model) {
this.modelDisposables.clear();
this._model = model;
this.list.splice(0, this.list.length, range(model.length));
this.modelDisposables.add(model.onDidIncrementLength(newLength => this.list.splice(this.list.length, 0, range(this.list.length, newLength))));
}
getFocus() {
return this.list.getFocus();
}
getSelection() {
return this.list.getSelection();
}
getSelectedElements() {
return this.getSelection().map(i => this.model.get(i));
}
style(styles) {
this.list.style(styles);
}
dispose() {
this.list.dispose();
this.modelDisposables.dispose();
}
}
export { PagedList };
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,161 @@
import { Range } from '../../../common/range.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Returns the intersection between a ranged group and a range.
* Returns `[]` if the intersection is empty.
*/
function groupIntersect(range, groups) {
const result = [];
for (const r of groups) {
if (range.start >= r.range.end) {
continue;
}
if (range.end < r.range.start) {
break;
}
const intersection = Range.intersect(range, r.range);
if (Range.isEmpty(intersection)) {
continue;
}
result.push({
range: intersection,
size: r.size
});
}
return result;
}
/**
* Shifts a range by that `much`.
*/
function shift({ start, end }, much) {
return { start: start + much, end: end + much };
}
/**
* Consolidates a collection of ranged groups.
*
* Consolidation is the process of merging consecutive ranged groups
* that share the same `size`.
*/
function consolidate(groups) {
const result = [];
let previousGroup = null;
for (const group of groups) {
const start = group.range.start;
const end = group.range.end;
const size = group.size;
if (previousGroup && size === previousGroup.size) {
previousGroup.range.end = end;
continue;
}
previousGroup = { range: { start, end }, size };
result.push(previousGroup);
}
return result;
}
/**
* Concatenates several collections of ranged groups into a single
* collection.
*/
function concat(...groups) {
return consolidate(groups.reduce((r, g) => r.concat(g), []));
}
class RangeMap {
get paddingTop() {
return this._paddingTop;
}
set paddingTop(paddingTop) {
this._size = this._size + paddingTop - this._paddingTop;
this._paddingTop = paddingTop;
}
constructor(topPadding) {
this.groups = [];
this._size = 0;
this._paddingTop = 0;
this._paddingTop = topPadding ?? 0;
this._size = this._paddingTop;
}
splice(index, deleteCount, items = []) {
const diff = items.length - deleteCount;
const before = groupIntersect({ start: 0, end: index }, this.groups);
const after = groupIntersect({ start: index + deleteCount, end: Number.POSITIVE_INFINITY }, this.groups)
.map(g => ({ range: shift(g.range, diff), size: g.size }));
const middle = items.map((item, i) => ({
range: { start: index + i, end: index + i + 1 },
size: item.size
}));
this.groups = concat(before, middle, after);
this._size = this._paddingTop + this.groups.reduce((t, g) => t + (g.size * (g.range.end - g.range.start)), 0);
}
/**
* Returns the number of items in the range map.
*/
get count() {
const len = this.groups.length;
if (!len) {
return 0;
}
return this.groups[len - 1].range.end;
}
/**
* Returns the sum of the sizes of all items in the range map.
*/
get size() {
return this._size;
}
/**
* Returns the index of the item at the given position.
*/
indexAt(position) {
if (position < 0) {
return -1;
}
if (position < this._paddingTop) {
return 0;
}
let index = 0;
let size = this._paddingTop;
for (const group of this.groups) {
const count = group.range.end - group.range.start;
const newSize = size + (count * group.size);
if (position < newSize) {
return index + Math.floor((position - size) / group.size);
}
index += count;
size = newSize;
}
return index;
}
/**
* Returns the index of the item right after the item at the
* index of the given position.
*/
indexAfter(position) {
return Math.min(this.indexAt(position) + 1, this.count);
}
/**
* Returns the start position of the item at the given index.
*/
positionAt(index) {
if (index < 0) {
return -1;
}
let position = 0;
let count = 0;
for (const group of this.groups) {
const groupCount = group.range.end - group.range.start;
const newCount = count + groupCount;
if (index < newCount) {
return this._paddingTop + position + ((index - count) * group.size);
}
position += groupCount * group.size;
count = newCount;
}
return -1;
}
}
export { RangeMap, consolidate, groupIntersect, shift };
@@ -0,0 +1,110 @@
import { $ } from '../../dom.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class RowCache {
constructor(renderers) {
this.renderers = renderers;
this.cache = new Map();
this.transactionNodesPendingRemoval = new Set();
this.inTransaction = false;
}
/**
* Returns a row either by creating a new one or reusing
* a previously released row which shares the same templateId.
*
* @returns A row and `isReusingConnectedDomNode` if the row's node is already in the dom in a stale position.
*/
alloc(templateId) {
let result = this.getTemplateCache(templateId).pop();
let isStale = false;
if (result) {
isStale = this.transactionNodesPendingRemoval.has(result.domNode);
if (isStale) {
this.transactionNodesPendingRemoval.delete(result.domNode);
}
}
else {
const domNode = $('.monaco-list-row');
const renderer = this.getRenderer(templateId);
const templateData = renderer.renderTemplate(domNode);
result = { domNode, templateId, templateData };
}
return { row: result, isReusingConnectedDomNode: isStale };
}
/**
* Releases the row for eventual reuse.
*/
release(row) {
if (!row) {
return;
}
this.releaseRow(row);
}
/**
* Begin a set of changes that use the cache. This lets us skip work when a row is removed and then inserted again.
*/
transact(makeChanges) {
if (this.inTransaction) {
throw new Error('Already in transaction');
}
this.inTransaction = true;
try {
makeChanges();
}
finally {
for (const domNode of this.transactionNodesPendingRemoval) {
this.doRemoveNode(domNode);
}
this.transactionNodesPendingRemoval.clear();
this.inTransaction = false;
}
}
releaseRow(row) {
const { domNode, templateId } = row;
if (domNode) {
if (this.inTransaction) {
this.transactionNodesPendingRemoval.add(domNode);
}
else {
this.doRemoveNode(domNode);
}
}
const cache = this.getTemplateCache(templateId);
cache.push(row);
}
doRemoveNode(domNode) {
domNode.classList.remove('scrolling');
domNode.remove();
}
getTemplateCache(templateId) {
let result = this.cache.get(templateId);
if (!result) {
result = [];
this.cache.set(templateId, result);
}
return result;
}
dispose() {
this.cache.forEach((cachedRows, templateId) => {
for (const cachedRow of cachedRows) {
const renderer = this.getRenderer(templateId);
renderer.disposeTemplate(cachedRow.templateData);
cachedRow.templateData = null;
}
});
this.cache.clear();
this.transactionNodesPendingRemoval.clear();
}
getRenderer(templateId) {
const renderer = this.renderers.get(templateId);
if (!renderer) {
throw new Error(`No renderer found for ${templateId}`);
}
return renderer;
}
}
export { RowCache };
@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class CombinedSpliceable {
constructor(spliceables) {
this.spliceables = spliceables;
}
splice(start, deleteCount, elements) {
this.spliceables.forEach(s => s.splice(start, deleteCount, elements));
}
}
export { CombinedSpliceable };
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-mouse-cursor-text {
cursor: text;
}
@@ -0,0 +1,9 @@
import './mouseCursor.css';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const MOUSE_CURSOR_TEXT_CSS_CLASS_NAME = `monaco-mouse-cursor-text`;
export { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME };
@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-progress-container {
width: 100%;
height: 2px;
overflow: hidden; /* keep progress bit in bounds */
}
.monaco-progress-container .progress-bit {
width: 2%;
height: 2px;
position: absolute;
left: 0;
display: none;
}
.monaco-progress-container.active .progress-bit {
display: inherit;
}
.monaco-progress-container.discrete .progress-bit {
left: 0;
transition: width 100ms linear;
}
.monaco-progress-container.discrete.done .progress-bit {
width: 100%;
}
.monaco-progress-container.infinite .progress-bit {
animation-name: progress;
animation-duration: 4s;
animation-iteration-count: infinite;
transform: translate3d(0px, 0px, 0px);
animation-timing-function: linear;
}
.monaco-progress-container.infinite.infinite-long-running .progress-bit {
/*
The more smooth `linear` timing function can cause
higher GPU consumption as indicated in
https://github.com/microsoft/vscode/issues/97900 &
https://github.com/microsoft/vscode/issues/138396
*/
animation-timing-function: steps(100);
}
/**
* The progress bit has a width: 2% (1/50) of the parent container. The animation moves it from 0% to 100% of
* that container. Since translateX is relative to the progress bit size, we have to multiple it with
* its relative size to the parent container:
* parent width: 5000%
* bit width: 100%
* translateX should be as follow:
* 50%: 5000% * 50% - 50% (set to center) = 2450%
* 100%: 5000% * 100% - 100% (do not overflow) = 4900%
*/
@keyframes progress { from { transform: translateX(0%) scaleX(1) } 50% { transform: translateX(2500%) scaleX(3) } to { transform: translateX(4900%) scaleX(1) } }
@@ -0,0 +1,105 @@
import { show } from '../../dom.js';
import { RunOnceScheduler } from '../../../common/async.js';
import { Disposable, MutableDisposable } from '../../../common/lifecycle.js';
import './progressbar.css';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const CSS_DONE = 'done';
const CSS_ACTIVE = 'active';
const CSS_INFINITE = 'infinite';
const CSS_INFINITE_LONG_RUNNING = 'infinite-long-running';
const CSS_DISCRETE = 'discrete';
/**
* A progress bar with support for infinite or discrete progress.
*/
class ProgressBar extends Disposable {
/**
* After a certain time of showing the progress bar, switch
* to long-running mode and throttle animations to reduce
* the pressure on the GPU process.
*
* https://github.com/microsoft/vscode/issues/97900
* https://github.com/microsoft/vscode/issues/138396
*/
static { this.LONG_RUNNING_INFINITE_THRESHOLD = 10000; }
constructor(container, options) {
super();
this.progressSignal = this._register(new MutableDisposable());
this.workedVal = 0;
this.showDelayedScheduler = this._register(new RunOnceScheduler(() => show(this.element), 0));
this.longRunningScheduler = this._register(new RunOnceScheduler(() => this.infiniteLongRunning(), ProgressBar.LONG_RUNNING_INFINITE_THRESHOLD));
this.create(container, options);
}
create(container, options) {
this.element = document.createElement('div');
this.element.classList.add('monaco-progress-container');
this.element.setAttribute('role', 'progressbar');
this.element.setAttribute('aria-valuemin', '0');
container.appendChild(this.element);
this.bit = document.createElement('div');
this.bit.classList.add('progress-bit');
this.bit.style.backgroundColor = options?.progressBarBackground || '#0E70C0';
this.element.appendChild(this.bit);
}
off() {
this.bit.style.width = 'inherit';
this.bit.style.opacity = '1';
this.element.classList.remove(CSS_ACTIVE, CSS_INFINITE, CSS_INFINITE_LONG_RUNNING, CSS_DISCRETE);
this.workedVal = 0;
this.totalWork = undefined;
this.longRunningScheduler.cancel();
this.progressSignal.clear();
}
/**
* Stops the progressbar from showing any progress instantly without fading out.
*/
stop() {
return this.doDone(false);
}
doDone(delayed) {
this.element.classList.add(CSS_DONE);
// discrete: let it grow to 100% width and hide afterwards
if (!this.element.classList.contains(CSS_INFINITE)) {
this.bit.style.width = 'inherit';
if (delayed) {
setTimeout(() => this.off(), 200);
}
else {
this.off();
}
}
// infinite: let it fade out and hide afterwards
else {
this.bit.style.opacity = '0';
if (delayed) {
setTimeout(() => this.off(), 200);
}
else {
this.off();
}
}
return this;
}
/**
* Use this mode to indicate progress that has no total number of work units.
*/
infinite() {
this.bit.style.width = '2%';
this.bit.style.opacity = '1';
this.element.classList.remove(CSS_DISCRETE, CSS_DONE, CSS_INFINITE_LONG_RUNNING);
this.element.classList.add(CSS_ACTIVE, CSS_INFINITE);
this.longRunningScheduler.schedule();
return this;
}
infiniteLongRunning() {
this.element.classList.add(CSS_INFINITE_LONG_RUNNING);
}
getContainer() {
return this.element;
}
}
export { ProgressBar };
@@ -0,0 +1,150 @@
import { Dimension } from '../../dom.js';
import { Sash, OrthogonalEdge } from '../sash/sash.js';
import { Emitter, Event } from '../../../common/event.js';
import { DisposableStore } from '../../../common/lifecycle.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class ResizableHTMLElement {
get onDidWillResize() { return this._onDidWillResize.event; }
get onDidResize() { return this._onDidResize.event; }
constructor() {
this._onDidWillResize = new Emitter();
this._onDidResize = new Emitter();
this._sashListener = new DisposableStore();
this._size = new Dimension(0, 0);
this._minSize = new Dimension(0, 0);
this._maxSize = new Dimension(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
this.domNode = document.createElement('div');
this._eastSash = new Sash(this.domNode, { getVerticalSashLeft: () => this._size.width }, { orientation: 0 /* Orientation.VERTICAL */ });
this._westSash = new Sash(this.domNode, { getVerticalSashLeft: () => 0 }, { orientation: 0 /* Orientation.VERTICAL */ });
this._northSash = new Sash(this.domNode, { getHorizontalSashTop: () => 0 }, { orientation: 1 /* Orientation.HORIZONTAL */, orthogonalEdge: OrthogonalEdge.North });
this._southSash = new Sash(this.domNode, { getHorizontalSashTop: () => this._size.height }, { orientation: 1 /* Orientation.HORIZONTAL */, orthogonalEdge: OrthogonalEdge.South });
this._northSash.orthogonalStartSash = this._westSash;
this._northSash.orthogonalEndSash = this._eastSash;
this._southSash.orthogonalStartSash = this._westSash;
this._southSash.orthogonalEndSash = this._eastSash;
let currentSize;
let deltaY = 0;
let deltaX = 0;
this._sashListener.add(Event.any(this._northSash.onDidStart, this._eastSash.onDidStart, this._southSash.onDidStart, this._westSash.onDidStart)(() => {
if (currentSize === undefined) {
this._onDidWillResize.fire();
currentSize = this._size;
deltaY = 0;
deltaX = 0;
}
}));
this._sashListener.add(Event.any(this._northSash.onDidEnd, this._eastSash.onDidEnd, this._southSash.onDidEnd, this._westSash.onDidEnd)(() => {
if (currentSize !== undefined) {
currentSize = undefined;
deltaY = 0;
deltaX = 0;
this._onDidResize.fire({ dimension: this._size, done: true });
}
}));
this._sashListener.add(this._eastSash.onDidChange(e => {
if (currentSize) {
deltaX = e.currentX - e.startX;
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
this._onDidResize.fire({ dimension: this._size, done: false, east: true });
}
}));
this._sashListener.add(this._westSash.onDidChange(e => {
if (currentSize) {
deltaX = -(e.currentX - e.startX);
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
this._onDidResize.fire({ dimension: this._size, done: false, west: true });
}
}));
this._sashListener.add(this._northSash.onDidChange(e => {
if (currentSize) {
deltaY = -(e.currentY - e.startY);
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
this._onDidResize.fire({ dimension: this._size, done: false, north: true });
}
}));
this._sashListener.add(this._southSash.onDidChange(e => {
if (currentSize) {
deltaY = e.currentY - e.startY;
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
this._onDidResize.fire({ dimension: this._size, done: false, south: true });
}
}));
this._sashListener.add(Event.any(this._eastSash.onDidReset, this._westSash.onDidReset)(e => {
if (this._preferredSize) {
this.layout(this._size.height, this._preferredSize.width);
this._onDidResize.fire({ dimension: this._size, done: true });
}
}));
this._sashListener.add(Event.any(this._northSash.onDidReset, this._southSash.onDidReset)(e => {
if (this._preferredSize) {
this.layout(this._preferredSize.height, this._size.width);
this._onDidResize.fire({ dimension: this._size, done: true });
}
}));
}
dispose() {
this._northSash.dispose();
this._southSash.dispose();
this._eastSash.dispose();
this._westSash.dispose();
this._sashListener.dispose();
this._onDidResize.dispose();
this._onDidWillResize.dispose();
this.domNode.remove();
}
enableSashes(north, east, south, west) {
this._northSash.state = north ? 3 /* SashState.Enabled */ : 0 /* SashState.Disabled */;
this._eastSash.state = east ? 3 /* SashState.Enabled */ : 0 /* SashState.Disabled */;
this._southSash.state = south ? 3 /* SashState.Enabled */ : 0 /* SashState.Disabled */;
this._westSash.state = west ? 3 /* SashState.Enabled */ : 0 /* SashState.Disabled */;
}
layout(height = this.size.height, width = this.size.width) {
const { height: minHeight, width: minWidth } = this._minSize;
const { height: maxHeight, width: maxWidth } = this._maxSize;
height = Math.max(minHeight, Math.min(maxHeight, height));
width = Math.max(minWidth, Math.min(maxWidth, width));
const newSize = new Dimension(width, height);
if (!Dimension.equals(newSize, this._size)) {
this.domNode.style.height = height + 'px';
this.domNode.style.width = width + 'px';
this._size = newSize;
this._northSash.layout();
this._eastSash.layout();
this._southSash.layout();
this._westSash.layout();
}
}
clearSashHoverState() {
this._eastSash.clearSashHoverState();
this._westSash.clearSashHoverState();
this._northSash.clearSashHoverState();
this._southSash.clearSashHoverState();
}
get size() {
return this._size;
}
set maxSize(value) {
this._maxSize = value;
}
get maxSize() {
return this._maxSize;
}
set minSize(value) {
this._minSize = value;
}
get minSize() {
return this._minSize;
}
set preferredSize(value) {
this._preferredSize = value;
}
get preferredSize() {
return this._preferredSize;
}
}
export { ResizableHTMLElement };
@@ -0,0 +1,149 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
:root {
--vscode-sash-size: 4px;
--vscode-sash-hover-size: 4px;
}
.monaco-sash {
position: absolute;
z-index: 35;
touch-action: none;
}
.monaco-sash.disabled {
pointer-events: none;
}
.monaco-sash.mac.vertical {
cursor: col-resize;
}
.monaco-sash.vertical.minimum {
cursor: e-resize;
}
.monaco-sash.vertical.maximum {
cursor: w-resize;
}
.monaco-sash.mac.horizontal {
cursor: row-resize;
}
.monaco-sash.horizontal.minimum {
cursor: s-resize;
}
.monaco-sash.horizontal.maximum {
cursor: n-resize;
}
.monaco-sash.disabled {
cursor: default !important;
pointer-events: none !important;
}
.monaco-sash.vertical {
cursor: ew-resize;
top: 0;
width: var(--vscode-sash-size);
height: 100%;
}
.monaco-sash.horizontal {
cursor: ns-resize;
left: 0;
width: 100%;
height: var(--vscode-sash-size);
}
.monaco-sash:not(.disabled) > .orthogonal-drag-handle {
content: " ";
height: calc(var(--vscode-sash-size) * 2);
width: calc(var(--vscode-sash-size) * 2);
z-index: 100;
display: block;
cursor: all-scroll;
position: absolute;
}
.monaco-sash.horizontal.orthogonal-edge-north:not(.disabled)
> .orthogonal-drag-handle.start,
.monaco-sash.horizontal.orthogonal-edge-south:not(.disabled)
> .orthogonal-drag-handle.end {
cursor: nwse-resize;
}
.monaco-sash.horizontal.orthogonal-edge-north:not(.disabled)
> .orthogonal-drag-handle.end,
.monaco-sash.horizontal.orthogonal-edge-south:not(.disabled)
> .orthogonal-drag-handle.start {
cursor: nesw-resize;
}
.monaco-sash.vertical > .orthogonal-drag-handle.start {
left: calc(var(--vscode-sash-size) * -0.5);
top: calc(var(--vscode-sash-size) * -1);
}
.monaco-sash.vertical > .orthogonal-drag-handle.end {
left: calc(var(--vscode-sash-size) * -0.5);
bottom: calc(var(--vscode-sash-size) * -1);
}
.monaco-sash.horizontal > .orthogonal-drag-handle.start {
top: calc(var(--vscode-sash-size) * -0.5);
left: calc(var(--vscode-sash-size) * -1);
}
.monaco-sash.horizontal > .orthogonal-drag-handle.end {
top: calc(var(--vscode-sash-size) * -0.5);
right: calc(var(--vscode-sash-size) * -1);
}
.monaco-sash:before {
content: '';
pointer-events: none;
position: absolute;
width: 100%;
height: 100%;
background: transparent;
}
.monaco-enable-motion .monaco-sash:before {
transition: background-color 0.1s ease-out;
}
.monaco-sash.hover:before,
.monaco-sash.active:before {
background: var(--vscode-sash-hoverBorder);
}
.monaco-sash.vertical:before {
width: var(--vscode-sash-hover-size);
left: calc(50% - (var(--vscode-sash-hover-size) / 2));
}
.monaco-sash.horizontal:before {
height: var(--vscode-sash-hover-size);
top: calc(50% - (var(--vscode-sash-hover-size) / 2));
}
.pointer-events-disabled {
pointer-events: none !important;
}
/** Debug **/
.monaco-sash.debug {
background: cyan;
}
.monaco-sash.debug.disabled {
background: rgba(0, 255, 255, 0.2);
}
.monaco-sash.debug:not(.disabled) > .orthogonal-drag-handle {
background: red;
}
+451
View File
@@ -0,0 +1,451 @@
import { getWindow, append, $, addDisposableListener, EventHelper, isHTMLElement } from '../../dom.js';
import { createStyleSheet } from '../../domStylesheets.js';
import { DomEmitter } from '../../event.js';
import { EventType, Gesture } from '../../touch.js';
import { Delayer } from '../../../common/async.js';
import { memoize } from '../../../common/decorators.js';
import { Emitter } from '../../../common/event.js';
import { DisposableStore, Disposable, toDisposable } from '../../../common/lifecycle.js';
import { isMacintosh } from '../../../common/platform.js';
import './sash.css';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate = (undefined && undefined.__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;
};
/**
* Allow the sashes to be visible at runtime.
* @remark Use for development purposes only.
*/
const DEBUG = false;
var OrthogonalEdge;
(function (OrthogonalEdge) {
OrthogonalEdge["North"] = "north";
OrthogonalEdge["South"] = "south";
OrthogonalEdge["East"] = "east";
OrthogonalEdge["West"] = "west";
})(OrthogonalEdge || (OrthogonalEdge = {}));
let globalSize = 4;
const onDidChangeGlobalSize = new Emitter();
let globalHoverDelay = 300;
const onDidChangeHoverDelay = new Emitter();
class MouseEventFactory {
constructor(el) {
this.el = el;
this.disposables = new DisposableStore();
}
get onPointerMove() {
return this.disposables.add(new DomEmitter(getWindow(this.el), 'mousemove')).event;
}
get onPointerUp() {
return this.disposables.add(new DomEmitter(getWindow(this.el), 'mouseup')).event;
}
dispose() {
this.disposables.dispose();
}
}
__decorate([
memoize
], MouseEventFactory.prototype, "onPointerMove", null);
__decorate([
memoize
], MouseEventFactory.prototype, "onPointerUp", null);
class GestureEventFactory {
get onPointerMove() {
return this.disposables.add(new DomEmitter(this.el, EventType.Change)).event;
}
get onPointerUp() {
return this.disposables.add(new DomEmitter(this.el, EventType.End)).event;
}
constructor(el) {
this.el = el;
this.disposables = new DisposableStore();
}
dispose() {
this.disposables.dispose();
}
}
__decorate([
memoize
], GestureEventFactory.prototype, "onPointerMove", null);
__decorate([
memoize
], GestureEventFactory.prototype, "onPointerUp", null);
class OrthogonalPointerEventFactory {
get onPointerMove() {
return this.factory.onPointerMove;
}
get onPointerUp() {
return this.factory.onPointerUp;
}
constructor(factory) {
this.factory = factory;
}
dispose() {
// noop
}
}
__decorate([
memoize
], OrthogonalPointerEventFactory.prototype, "onPointerMove", null);
__decorate([
memoize
], OrthogonalPointerEventFactory.prototype, "onPointerUp", null);
const PointerEventsDisabledCssClass = 'pointer-events-disabled';
/**
* The {@link Sash} is the UI component which allows the user to resize other
* components. It's usually an invisible horizontal or vertical line which, when
* hovered, becomes highlighted and can be dragged along the perpendicular dimension
* to its direction.
*
* Features:
* - Touch event handling
* - Corner sash support
* - Hover with different mouse cursor support
* - Configurable hover size
* - Linked sash support, for 2x2 corner sashes
*/
class Sash extends Disposable {
get state() { return this._state; }
get orthogonalStartSash() { return this._orthogonalStartSash; }
get orthogonalEndSash() { return this._orthogonalEndSash; }
/**
* The state of a sash defines whether it can be interacted with by the user
* as well as what mouse cursor to use, when hovered.
*/
set state(state) {
if (this._state === state) {
return;
}
this.el.classList.toggle('disabled', state === 0 /* SashState.Disabled */);
this.el.classList.toggle('minimum', state === 1 /* SashState.AtMinimum */);
this.el.classList.toggle('maximum', state === 2 /* SashState.AtMaximum */);
this._state = state;
this.onDidEnablementChange.fire(state);
}
/**
* An event which fires whenever the user starts dragging this sash.
*/
get onDidStart() { return this._onDidStart.event; }
/**
* An event which fires whenever the user moves the mouse while
* dragging this sash.
*/
get onDidChange() { return this._onDidChange.event; }
/**
* An event which fires whenever the user double clicks this sash.
*/
get onDidReset() { return this._onDidReset.event; }
/**
* An event which fires whenever the user stops dragging this sash.
*/
get onDidEnd() { return this._onDidEnd.event; }
/**
* A reference to another sash, perpendicular to this one, which
* aligns at the start of this one. A corner sash will be created
* automatically at that location.
*
* The start of a horizontal sash is its left-most position.
* The start of a vertical sash is its top-most position.
*/
set orthogonalStartSash(sash) {
if (this._orthogonalStartSash === sash) {
return;
}
this.orthogonalStartDragHandleDisposables.clear();
this.orthogonalStartSashDisposables.clear();
if (sash) {
const onChange = (state) => {
this.orthogonalStartDragHandleDisposables.clear();
if (state !== 0 /* SashState.Disabled */) {
this._orthogonalStartDragHandle = append(this.el, $('.orthogonal-drag-handle.start'));
this.orthogonalStartDragHandleDisposables.add(toDisposable(() => this._orthogonalStartDragHandle.remove()));
this.orthogonalStartDragHandleDisposables.add(addDisposableListener(this._orthogonalStartDragHandle, 'mouseenter', () => Sash.onMouseEnter(sash)));
this.orthogonalStartDragHandleDisposables.add(addDisposableListener(this._orthogonalStartDragHandle, 'mouseleave', () => Sash.onMouseLeave(sash)));
}
};
this.orthogonalStartSashDisposables.add(sash.onDidEnablementChange.event(onChange, this));
onChange(sash.state);
}
this._orthogonalStartSash = sash;
}
/**
* A reference to another sash, perpendicular to this one, which
* aligns at the end of this one. A corner sash will be created
* automatically at that location.
*
* The end of a horizontal sash is its right-most position.
* The end of a vertical sash is its bottom-most position.
*/
set orthogonalEndSash(sash) {
if (this._orthogonalEndSash === sash) {
return;
}
this.orthogonalEndDragHandleDisposables.clear();
this.orthogonalEndSashDisposables.clear();
if (sash) {
const onChange = (state) => {
this.orthogonalEndDragHandleDisposables.clear();
if (state !== 0 /* SashState.Disabled */) {
this._orthogonalEndDragHandle = append(this.el, $('.orthogonal-drag-handle.end'));
this.orthogonalEndDragHandleDisposables.add(toDisposable(() => this._orthogonalEndDragHandle.remove()));
this.orthogonalEndDragHandleDisposables.add(addDisposableListener(this._orthogonalEndDragHandle, 'mouseenter', () => Sash.onMouseEnter(sash)));
this.orthogonalEndDragHandleDisposables.add(addDisposableListener(this._orthogonalEndDragHandle, 'mouseleave', () => Sash.onMouseLeave(sash)));
}
};
this.orthogonalEndSashDisposables.add(sash.onDidEnablementChange.event(onChange, this));
onChange(sash.state);
}
this._orthogonalEndSash = sash;
}
constructor(container, layoutProvider, options) {
super();
this.hoverDelay = globalHoverDelay;
this.hoverDelayer = this._register(new Delayer(this.hoverDelay));
this._state = 3 /* SashState.Enabled */;
this.onDidEnablementChange = this._register(new Emitter());
this._onDidStart = this._register(new Emitter());
this._onDidChange = this._register(new Emitter());
this._onDidReset = this._register(new Emitter());
this._onDidEnd = this._register(new Emitter());
this.orthogonalStartSashDisposables = this._register(new DisposableStore());
this.orthogonalStartDragHandleDisposables = this._register(new DisposableStore());
this.orthogonalEndSashDisposables = this._register(new DisposableStore());
this.orthogonalEndDragHandleDisposables = this._register(new DisposableStore());
/**
* A linked sash will be forwarded the same user interactions and events
* so it moves exactly the same way as this sash.
*
* Useful in 2x2 grids. Not meant for widespread usage.
*/
this.linkedSash = undefined;
this.el = append(container, $('.monaco-sash'));
if (options.orthogonalEdge) {
this.el.classList.add(`orthogonal-edge-${options.orthogonalEdge}`);
}
if (isMacintosh) {
this.el.classList.add('mac');
}
this._register(addDisposableListener(this.el, 'mousedown', e => this.onPointerStart(e, new MouseEventFactory(container))));
this._register(addDisposableListener(this.el, 'dblclick', e => this.onPointerDoublePress(e)));
this._register(addDisposableListener(this.el, 'mouseenter', () => Sash.onMouseEnter(this)));
this._register(addDisposableListener(this.el, 'mouseleave', () => Sash.onMouseLeave(this)));
this._register(Gesture.addTarget(this.el));
this._register(addDisposableListener(this.el, EventType.Start, e => this.onPointerStart(e, new GestureEventFactory(this.el))));
let doubleTapTimeout = undefined;
this._register(addDisposableListener(this.el, EventType.Tap, event => {
if (doubleTapTimeout) {
clearTimeout(doubleTapTimeout);
doubleTapTimeout = undefined;
this.onPointerDoublePress(event);
return;
}
clearTimeout(doubleTapTimeout);
doubleTapTimeout = setTimeout(() => doubleTapTimeout = undefined, 250);
}));
if (typeof options.size === 'number') {
this.size = options.size;
if (options.orientation === 0 /* Orientation.VERTICAL */) {
this.el.style.width = `${this.size}px`;
}
else {
this.el.style.height = `${this.size}px`;
}
}
else {
this.size = globalSize;
this._register(onDidChangeGlobalSize.event(size => {
this.size = size;
this.layout();
}));
}
this._register(onDidChangeHoverDelay.event(delay => this.hoverDelay = delay));
this.layoutProvider = layoutProvider;
this.orthogonalStartSash = options.orthogonalStartSash;
this.orthogonalEndSash = options.orthogonalEndSash;
this.orientation = options.orientation || 0 /* Orientation.VERTICAL */;
if (this.orientation === 1 /* Orientation.HORIZONTAL */) {
this.el.classList.add('horizontal');
this.el.classList.remove('vertical');
}
else {
this.el.classList.remove('horizontal');
this.el.classList.add('vertical');
}
this.el.classList.toggle('debug', DEBUG);
this.layout();
}
onPointerStart(event, pointerEventFactory) {
EventHelper.stop(event);
let isMultisashResize = false;
// eslint-disable-next-line local/code-no-any-casts
if (!event.__orthogonalSashEvent) {
const orthogonalSash = this.getOrthogonalSash(event);
if (orthogonalSash) {
isMultisashResize = true;
// eslint-disable-next-line local/code-no-any-casts
event.__orthogonalSashEvent = true;
orthogonalSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory));
}
}
// eslint-disable-next-line local/code-no-any-casts
if (this.linkedSash && !event.__linkedSashEvent) {
// eslint-disable-next-line local/code-no-any-casts
event.__linkedSashEvent = true;
this.linkedSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory));
}
if (!this.state) {
return;
}
// eslint-disable-next-line no-restricted-syntax
const iframes = this.el.ownerDocument.getElementsByTagName('iframe');
for (const iframe of iframes) {
iframe.classList.add(PointerEventsDisabledCssClass); // disable mouse events on iframes as long as we drag the sash
}
const startX = event.pageX;
const startY = event.pageY;
const altKey = event.altKey;
const startEvent = { startX, currentX: startX, startY, currentY: startY, altKey };
this.el.classList.add('active');
this._onDidStart.fire(startEvent);
// fix https://github.com/microsoft/vscode/issues/21675
const style = createStyleSheet(this.el);
const updateStyle = () => {
let cursor = '';
if (isMultisashResize) {
cursor = 'all-scroll';
}
else if (this.orientation === 1 /* Orientation.HORIZONTAL */) {
if (this.state === 1 /* SashState.AtMinimum */) {
cursor = 's-resize';
}
else if (this.state === 2 /* SashState.AtMaximum */) {
cursor = 'n-resize';
}
else {
cursor = isMacintosh ? 'row-resize' : 'ns-resize';
}
}
else {
if (this.state === 1 /* SashState.AtMinimum */) {
cursor = 'e-resize';
}
else if (this.state === 2 /* SashState.AtMaximum */) {
cursor = 'w-resize';
}
else {
cursor = isMacintosh ? 'col-resize' : 'ew-resize';
}
}
style.textContent = `* { cursor: ${cursor} !important; }`;
};
const disposables = new DisposableStore();
updateStyle();
if (!isMultisashResize) {
this.onDidEnablementChange.event(updateStyle, null, disposables);
}
const onPointerMove = (e) => {
EventHelper.stop(e, false);
const event = { startX, currentX: e.pageX, startY, currentY: e.pageY, altKey };
this._onDidChange.fire(event);
};
const onPointerUp = (e) => {
EventHelper.stop(e, false);
style.remove();
this.el.classList.remove('active');
this._onDidEnd.fire();
disposables.dispose();
for (const iframe of iframes) {
iframe.classList.remove(PointerEventsDisabledCssClass);
}
};
pointerEventFactory.onPointerMove(onPointerMove, null, disposables);
pointerEventFactory.onPointerUp(onPointerUp, null, disposables);
disposables.add(pointerEventFactory);
}
onPointerDoublePress(e) {
const orthogonalSash = this.getOrthogonalSash(e);
if (orthogonalSash) {
orthogonalSash._onDidReset.fire();
}
if (this.linkedSash) {
this.linkedSash._onDidReset.fire();
}
this._onDidReset.fire();
}
static onMouseEnter(sash, fromLinkedSash = false) {
if (sash.el.classList.contains('active')) {
sash.hoverDelayer.cancel();
sash.el.classList.add('hover');
}
else {
sash.hoverDelayer.trigger(() => sash.el.classList.add('hover'), sash.hoverDelay).then(undefined, () => { });
}
if (!fromLinkedSash && sash.linkedSash) {
Sash.onMouseEnter(sash.linkedSash, true);
}
}
static onMouseLeave(sash, fromLinkedSash = false) {
sash.hoverDelayer.cancel();
sash.el.classList.remove('hover');
if (!fromLinkedSash && sash.linkedSash) {
Sash.onMouseLeave(sash.linkedSash, true);
}
}
/**
* Forcefully stop any user interactions with this sash.
* Useful when hiding a parent component, while the user is still
* interacting with the sash.
*/
clearSashHoverState() {
Sash.onMouseLeave(this);
}
/**
* Layout the sash. The sash will size and position itself
* based on its provided {@link ISashLayoutProvider layout provider}.
*/
layout() {
if (this.orientation === 0 /* Orientation.VERTICAL */) {
const verticalProvider = this.layoutProvider;
this.el.style.left = verticalProvider.getVerticalSashLeft(this) - (this.size / 2) + 'px';
if (verticalProvider.getVerticalSashTop) {
this.el.style.top = verticalProvider.getVerticalSashTop(this) + 'px';
}
if (verticalProvider.getVerticalSashHeight) {
this.el.style.height = verticalProvider.getVerticalSashHeight(this) + 'px';
}
}
else {
const horizontalProvider = this.layoutProvider;
this.el.style.top = horizontalProvider.getHorizontalSashTop(this) - (this.size / 2) + 'px';
if (horizontalProvider.getHorizontalSashLeft) {
this.el.style.left = horizontalProvider.getHorizontalSashLeft(this) + 'px';
}
if (horizontalProvider.getHorizontalSashWidth) {
this.el.style.width = horizontalProvider.getHorizontalSashWidth(this) + 'px';
}
}
}
getOrthogonalSash(e) {
const target = e.initialTarget ?? e.target;
if (!target || !(isHTMLElement(target))) {
return undefined;
}
if (target.classList.contains('orthogonal-drag-handle')) {
return target.classList.contains('start') ? this.orthogonalStartSash : this.orthogonalEndSash;
}
return undefined;
}
dispose() {
super.dispose();
this.el.remove();
}
}
export { OrthogonalEdge, Sash };
@@ -0,0 +1,214 @@
import { addDisposableListener, EventType, getDomNodePagePosition } from '../../dom.js';
import { createFastDomNode } from '../../fastDomNode.js';
import { GlobalPointerMoveMonitor } from '../../globalPointerMoveMonitor.js';
import { ScrollbarArrow } from './scrollbarArrow.js';
import { ScrollbarVisibilityController } from './scrollbarVisibilityController.js';
import { Widget } from '../widget.js';
import { isWindows } from '../../../common/platform.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
*/
const POINTER_DRAG_RESET_DISTANCE = 140;
class AbstractScrollbar extends Widget {
constructor(opts) {
super();
this._lazyRender = opts.lazyRender;
this._host = opts.host;
this._scrollable = opts.scrollable;
this._scrollByPage = opts.scrollByPage;
this._scrollbarState = opts.scrollbarState;
this._visibilityController = this._register(new ScrollbarVisibilityController(opts.visibility, 'visible scrollbar ' + opts.extraScrollbarClassName, 'invisible scrollbar ' + opts.extraScrollbarClassName));
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._pointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
this._shouldRender = true;
this.domNode = createFastDomNode(document.createElement('div'));
this.domNode.setAttribute('role', 'presentation');
this.domNode.setAttribute('aria-hidden', 'true');
this._visibilityController.setDomNode(this.domNode);
this.domNode.setPosition('absolute');
this._register(addDisposableListener(this.domNode.domNode, EventType.POINTER_DOWN, (e) => this._domNodePointerDown(e)));
}
// ----------------- creation
/**
* Creates the dom node for an arrow & adds it to the container
*/
_createArrow(opts) {
const arrow = this._register(new ScrollbarArrow(opts));
this.domNode.domNode.appendChild(arrow.bgDomNode);
this.domNode.domNode.appendChild(arrow.domNode);
}
/**
* Creates the slider dom node, adds it to the container & hooks up the events
*/
_createSlider(top, left, width, height) {
this.slider = createFastDomNode(document.createElement('div'));
this.slider.setClassName('slider');
this.slider.setPosition('absolute');
this.slider.setTop(top);
this.slider.setLeft(left);
if (typeof width === 'number') {
this.slider.setWidth(width);
}
if (typeof height === 'number') {
this.slider.setHeight(height);
}
this.slider.setLayerHinting(true);
this.slider.setContain('strict');
this.domNode.domNode.appendChild(this.slider.domNode);
this._register(addDisposableListener(this.slider.domNode, EventType.POINTER_DOWN, (e) => {
if (e.button === 0) {
e.preventDefault();
this._sliderPointerDown(e);
}
}));
this.onclick(this.slider.domNode, e => {
if (e.leftButton) {
e.stopPropagation();
}
});
}
// ----------------- Update state
_onElementSize(visibleSize) {
if (this._scrollbarState.setVisibleSize(visibleSize)) {
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
return this._shouldRender;
}
_onElementScrollSize(elementScrollSize) {
if (this._scrollbarState.setScrollSize(elementScrollSize)) {
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
return this._shouldRender;
}
_onElementScrollPosition(elementScrollPosition) {
if (this._scrollbarState.setScrollPosition(elementScrollPosition)) {
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
return this._shouldRender;
}
// ----------------- rendering
beginReveal() {
this._visibilityController.setShouldBeVisible(true);
}
beginHide() {
this._visibilityController.setShouldBeVisible(false);
}
render() {
if (!this._shouldRender) {
return;
}
this._shouldRender = false;
this._renderDomNode(this._scrollbarState.getRectangleLargeSize(), this._scrollbarState.getRectangleSmallSize());
this._updateSlider(this._scrollbarState.getSliderSize(), this._scrollbarState.getArrowSize() + this._scrollbarState.getSliderPosition());
}
// ----------------- DOM events
_domNodePointerDown(e) {
if (e.target !== this.domNode.domNode) {
return;
}
this._onPointerDown(e);
}
delegatePointerDown(e) {
const domTop = this.domNode.domNode.getClientRects()[0].top;
const sliderStart = domTop + this._scrollbarState.getSliderPosition();
const sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize();
const pointerPos = this._sliderPointerPosition(e);
if (sliderStart <= pointerPos && pointerPos <= sliderStop) {
// Act as if it was a pointer down on the slider
if (e.button === 0) {
e.preventDefault();
this._sliderPointerDown(e);
}
}
else {
// Act as if it was a pointer down on the scrollbar
this._onPointerDown(e);
}
}
_onPointerDown(e) {
let offsetX;
let offsetY;
if (e.target === this.domNode.domNode && typeof e.offsetX === 'number' && typeof e.offsetY === 'number') {
offsetX = e.offsetX;
offsetY = e.offsetY;
}
else {
const domNodePosition = getDomNodePagePosition(this.domNode.domNode);
offsetX = e.pageX - domNodePosition.left;
offsetY = e.pageY - domNodePosition.top;
}
const isMouse = (e.pointerType === 'mouse');
const isLeftClick = (e.button === 0);
if (isLeftClick || !isMouse) {
const offset = this._pointerDownRelativePosition(offsetX, offsetY);
this._setDesiredScrollPositionNow(this._scrollByPage
? this._scrollbarState.getDesiredScrollPositionFromOffsetPaged(offset)
: this._scrollbarState.getDesiredScrollPositionFromOffset(offset));
}
if (isLeftClick) {
// left button
e.preventDefault();
this._sliderPointerDown(e);
}
}
_sliderPointerDown(e) {
if (!e.target || !(e.target instanceof Element)) {
return;
}
const initialPointerPosition = this._sliderPointerPosition(e);
const initialPointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(e);
const initialScrollbarState = this._scrollbarState.clone();
this.slider.toggleClassName('active', true);
this._pointerMoveMonitor.startMonitoring(e.target, e.pointerId, e.buttons, (pointerMoveData) => {
const pointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(pointerMoveData);
const pointerOrthogonalDelta = Math.abs(pointerOrthogonalPosition - initialPointerOrthogonalPosition);
if (isWindows && pointerOrthogonalDelta > POINTER_DRAG_RESET_DISTANCE) {
// The pointer has wondered away from the scrollbar => reset dragging
this._setDesiredScrollPositionNow(initialScrollbarState.getScrollPosition());
return;
}
const pointerPosition = this._sliderPointerPosition(pointerMoveData);
const pointerDelta = pointerPosition - initialPointerPosition;
this._setDesiredScrollPositionNow(initialScrollbarState.getDesiredScrollPositionFromDelta(pointerDelta));
}, () => {
this.slider.toggleClassName('active', false);
this._host.onDragEnd();
});
this._host.onDragStart();
}
_setDesiredScrollPositionNow(_desiredScrollPosition) {
const desiredScrollPosition = {};
this.writeScrollPosition(desiredScrollPosition, _desiredScrollPosition);
this._scrollable.setScrollPositionNow(desiredScrollPosition);
}
updateScrollbarSize(scrollbarSize) {
this._updateScrollbarSize(scrollbarSize);
this._scrollbarState.setScrollbarSize(scrollbarSize);
this._shouldRender = true;
if (!this._lazyRender) {
this.render();
}
}
isNeeded() {
return this._scrollbarState.isNeeded();
}
}
export { AbstractScrollbar };
@@ -0,0 +1,91 @@
import { StandardWheelEvent } from '../../mouseEvent.js';
import { AbstractScrollbar } from './abstractScrollbar.js';
import { ARROW_IMG_SIZE } from './scrollbarArrow.js';
import { ScrollbarState } from './scrollbarState.js';
import { Codicon } from '../../../common/codicons.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class HorizontalScrollbar extends AbstractScrollbar {
constructor(scrollable, options, host) {
const scrollDimensions = scrollable.getScrollDimensions();
const scrollPosition = scrollable.getCurrentScrollPosition();
super({
lazyRender: options.lazyRender,
host: host,
scrollbarState: new ScrollbarState((options.horizontalHasArrows ? options.arrowSize : 0), (options.horizontal === 2 /* ScrollbarVisibility.Hidden */ ? 0 : options.horizontalScrollbarSize), (options.vertical === 2 /* ScrollbarVisibility.Hidden */ ? 0 : options.verticalScrollbarSize), scrollDimensions.width, scrollDimensions.scrollWidth, scrollPosition.scrollLeft),
visibility: options.horizontal,
extraScrollbarClassName: 'horizontal',
scrollable: scrollable,
scrollByPage: options.scrollByPage
});
if (options.horizontalHasArrows) {
const arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
const scrollbarDelta = (options.horizontalScrollbarSize - ARROW_IMG_SIZE) / 2;
this._createArrow({
className: 'scra',
icon: Codicon.scrollbarButtonLeft,
top: scrollbarDelta,
left: arrowDelta,
bottom: undefined,
right: undefined,
bgWidth: options.arrowSize,
bgHeight: options.horizontalScrollbarSize,
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 1, 0)),
});
this._createArrow({
className: 'scra',
icon: Codicon.scrollbarButtonRight,
top: scrollbarDelta,
left: undefined,
bottom: undefined,
right: arrowDelta,
bgWidth: options.arrowSize,
bgHeight: options.horizontalScrollbarSize,
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, -1, 0)),
});
}
this._createSlider(Math.floor((options.horizontalScrollbarSize - options.horizontalSliderSize) / 2), 0, undefined, options.horizontalSliderSize);
}
_updateSlider(sliderSize, sliderPosition) {
this.slider.setWidth(sliderSize);
this.slider.setLeft(sliderPosition);
}
_renderDomNode(largeSize, smallSize) {
this.domNode.setWidth(largeSize);
this.domNode.setHeight(smallSize);
this.domNode.setLeft(0);
this.domNode.setBottom(0);
}
onDidScroll(e) {
this._shouldRender = this._onElementScrollSize(e.scrollWidth) || this._shouldRender;
this._shouldRender = this._onElementScrollPosition(e.scrollLeft) || this._shouldRender;
this._shouldRender = this._onElementSize(e.width) || this._shouldRender;
return this._shouldRender;
}
_pointerDownRelativePosition(offsetX, offsetY) {
return offsetX;
}
_sliderPointerPosition(e) {
return e.pageX;
}
_sliderOrthogonalPointerPosition(e) {
return e.pageY;
}
_updateScrollbarSize(size) {
this.slider.setHeight(size);
}
writeScrollPosition(target, scrollPosition) {
target.scrollLeft = scrollPosition;
}
updateOptions(options) {
this.updateScrollbarSize(options.horizontal === 2 /* ScrollbarVisibility.Hidden */ ? 0 : options.horizontalScrollbarSize);
this._scrollbarState.setOppositeScrollbarSize(options.vertical === 2 /* ScrollbarVisibility.Hidden */ ? 0 : options.verticalScrollbarSize);
this._visibilityController.setVisibility(options.horizontal);
this._scrollByPage = options.scrollByPage;
}
}
export { HorizontalScrollbar };
@@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* Arrows */
.monaco-scrollable-element > .scrollbar > .scra {
cursor: pointer;
font-size: 11px !important;
}
.monaco-scrollable-element > .visible {
opacity: 1;
/* Background rule added for IE9 - to allow clicks on dom node */
background:rgba(0,0,0,0);
transition: opacity 100ms linear;
/* In front of peek view */
z-index: 11;
}
.monaco-scrollable-element > .invisible {
opacity: 0;
pointer-events: none;
}
.monaco-scrollable-element > .invisible.fade {
transition: opacity 800ms linear;
}
/* Scrollable Content Inset Shadow */
.monaco-scrollable-element > .shadow {
position: absolute;
display: none;
}
.monaco-scrollable-element > .shadow.top {
display: block;
top: 0;
left: 3px;
height: 3px;
width: 100%;
box-shadow: var(--vscode-scrollbar-shadow) 0 6px 6px -6px inset;
}
.monaco-scrollable-element > .shadow.left {
display: block;
top: 3px;
left: 0;
height: 100%;
width: 3px;
box-shadow: var(--vscode-scrollbar-shadow) 6px 0 6px -6px inset;
}
.monaco-scrollable-element > .shadow.top-left-corner {
display: block;
top: 0;
left: 0;
height: 3px;
width: 3px;
}
.monaco-scrollable-element > .shadow.top.left {
box-shadow: var(--vscode-scrollbar-shadow) 6px 0 6px -6px inset;
}
.monaco-scrollable-element > .scrollbar {
background: var(--vscode-scrollbar-background);
}
.monaco-scrollable-element > .scrollbar > .slider {
background: var(--vscode-scrollbarSlider-background);
}
.monaco-scrollable-element > .scrollbar > .slider:hover {
background: var(--vscode-scrollbarSlider-hoverBackground);
}
.monaco-scrollable-element > .scrollbar > .slider.active {
background: var(--vscode-scrollbarSlider-activeBackground);
}
@@ -0,0 +1,603 @@
import { isChrome, getZoomFactor } from '../../browser.js';
import { getWindow, addDisposableListener, EventType, scheduleAtNextAnimationFrame } from '../../dom.js';
import { createFastDomNode } from '../../fastDomNode.js';
import { StandardWheelEvent } from '../../mouseEvent.js';
import { HorizontalScrollbar } from './horizontalScrollbar.js';
import { VerticalScrollbar } from './verticalScrollbar.js';
import { Widget } from '../widget.js';
import { TimeoutTimer } from '../../../common/async.js';
import { Emitter } from '../../../common/event.js';
import { dispose } from '../../../common/lifecycle.js';
import { isMacintosh } from '../../../common/platform.js';
import { Scrollable } from '../../../common/scrollable.js';
import './media/scrollbars.css';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const HIDE_TIMEOUT = 500;
const SCROLL_WHEEL_SENSITIVITY = 50;
class MouseWheelClassifierItem {
constructor(timestamp, deltaX, deltaY) {
this.timestamp = timestamp;
this.deltaX = deltaX;
this.deltaY = deltaY;
this.score = 0;
}
}
class MouseWheelClassifier {
static { this.INSTANCE = new MouseWheelClassifier(); }
constructor() {
this._capacity = 5;
this._memory = [];
this._front = -1;
this._rear = -1;
}
isPhysicalMouseWheel() {
if (this._front === -1 && this._rear === -1) {
// no elements
return false;
}
// 0.5 * last + 0.25 * 2nd last + 0.125 * 3rd last + ...
let remainingInfluence = 1;
let score = 0;
let iteration = 1;
let index = this._rear;
do {
const influence = (index === this._front ? remainingInfluence : Math.pow(2, -iteration));
remainingInfluence -= influence;
score += this._memory[index].score * influence;
if (index === this._front) {
break;
}
index = (this._capacity + index - 1) % this._capacity;
iteration++;
} while (true);
return (score <= 0.5);
}
acceptStandardWheelEvent(e) {
if (isChrome) {
const targetWindow = getWindow(e.browserEvent);
const pageZoomFactor = getZoomFactor(targetWindow);
// On Chrome, the incoming delta events are multiplied with the OS zoom factor.
// The OS zoom factor can be reverse engineered by using the device pixel ratio and the configured zoom factor into account.
this.accept(Date.now(), e.deltaX * pageZoomFactor, e.deltaY * pageZoomFactor);
}
else {
this.accept(Date.now(), e.deltaX, e.deltaY);
}
}
accept(timestamp, deltaX, deltaY) {
let previousItem = null;
const item = new MouseWheelClassifierItem(timestamp, deltaX, deltaY);
if (this._front === -1 && this._rear === -1) {
this._memory[0] = item;
this._front = 0;
this._rear = 0;
}
else {
previousItem = this._memory[this._rear];
this._rear = (this._rear + 1) % this._capacity;
if (this._rear === this._front) {
// Drop oldest
this._front = (this._front + 1) % this._capacity;
}
this._memory[this._rear] = item;
}
item.score = this._computeScore(item, previousItem);
}
/**
* A score between 0 and 1 for `item`.
* - a score towards 0 indicates that the source appears to be a physical mouse wheel
* - a score towards 1 indicates that the source appears to be a touchpad or magic mouse, etc.
*/
_computeScore(item, previousItem) {
if (Math.abs(item.deltaX) > 0 && Math.abs(item.deltaY) > 0) {
// both axes exercised => definitely not a physical mouse wheel
return 1;
}
let score = 0.5;
if (!this._isAlmostInt(item.deltaX) || !this._isAlmostInt(item.deltaY)) {
// non-integer deltas => indicator that this is not a physical mouse wheel
score += 0.25;
}
// Non-accelerating scroll => indicator that this is a physical mouse wheel
// These can be identified by seeing whether they are the module of one another.
if (previousItem) {
const absDeltaX = Math.abs(item.deltaX);
const absDeltaY = Math.abs(item.deltaY);
const absPreviousDeltaX = Math.abs(previousItem.deltaX);
const absPreviousDeltaY = Math.abs(previousItem.deltaY);
// Min 1 to avoid division by zero, module 1 will still be 0.
const minDeltaX = Math.max(Math.min(absDeltaX, absPreviousDeltaX), 1);
const minDeltaY = Math.max(Math.min(absDeltaY, absPreviousDeltaY), 1);
const maxDeltaX = Math.max(absDeltaX, absPreviousDeltaX);
const maxDeltaY = Math.max(absDeltaY, absPreviousDeltaY);
const isSameModulo = (maxDeltaX % minDeltaX === 0 && maxDeltaY % minDeltaY === 0);
if (isSameModulo) {
score -= 0.5;
}
}
return Math.min(Math.max(score, 0), 1);
}
_isAlmostInt(value) {
const epsilon = Number.EPSILON * 100; // Use a small tolerance factor for floating-point errors
const delta = Math.abs(Math.round(value) - value);
return (delta < 0.01 + epsilon);
}
}
class AbstractScrollableElement extends Widget {
get onScroll() { return this._onScroll.event; }
get options() {
return this._options;
}
constructor(element, options, scrollable) {
super();
this._inertialTimeout = null;
this._inertialSpeed = { X: 0, Y: 0 };
this._onScroll = this._register(new Emitter());
this._onWillScroll = this._register(new Emitter());
element.style.overflow = 'hidden';
this._options = resolveOptions(options);
this._scrollable = scrollable;
this._register(this._scrollable.onScroll((e) => {
this._onWillScroll.fire(e);
this._onDidScroll(e);
this._onScroll.fire(e);
}));
const scrollbarHost = {
onMouseWheel: (mouseWheelEvent) => this._onMouseWheel(mouseWheelEvent),
onDragStart: () => this._onDragStart(),
onDragEnd: () => this._onDragEnd(),
};
this._verticalScrollbar = this._register(new VerticalScrollbar(this._scrollable, this._options, scrollbarHost));
this._horizontalScrollbar = this._register(new HorizontalScrollbar(this._scrollable, this._options, scrollbarHost));
this._domNode = document.createElement('div');
this._domNode.className = 'monaco-scrollable-element ' + this._options.className;
this._domNode.setAttribute('role', 'presentation');
this._domNode.style.position = 'relative';
this._domNode.style.overflow = 'hidden';
this._domNode.appendChild(element);
this._domNode.appendChild(this._horizontalScrollbar.domNode.domNode);
this._domNode.appendChild(this._verticalScrollbar.domNode.domNode);
if (this._options.useShadows) {
this._leftShadowDomNode = createFastDomNode(document.createElement('div'));
this._leftShadowDomNode.setClassName('shadow');
this._domNode.appendChild(this._leftShadowDomNode.domNode);
this._topShadowDomNode = createFastDomNode(document.createElement('div'));
this._topShadowDomNode.setClassName('shadow');
this._domNode.appendChild(this._topShadowDomNode.domNode);
this._topLeftShadowDomNode = createFastDomNode(document.createElement('div'));
this._topLeftShadowDomNode.setClassName('shadow');
this._domNode.appendChild(this._topLeftShadowDomNode.domNode);
}
else {
this._leftShadowDomNode = null;
this._topShadowDomNode = null;
this._topLeftShadowDomNode = null;
}
this._listenOnDomNode = this._options.listenOnDomNode || this._domNode;
this._mouseWheelToDispose = [];
this._setListeningToMouseWheel(this._options.handleMouseWheel);
this.onmouseover(this._listenOnDomNode, (e) => this._onMouseOver(e));
this.onmouseleave(this._listenOnDomNode, (e) => this._onMouseLeave(e));
this._hideTimeout = this._register(new TimeoutTimer());
this._isDragging = false;
this._mouseIsOver = false;
this._shouldRender = true;
this._revealOnScroll = true;
}
dispose() {
this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
if (this._inertialTimeout) {
this._inertialTimeout.dispose();
this._inertialTimeout = null;
}
super.dispose();
}
/**
* Get the generated 'scrollable' dom node
*/
getDomNode() {
return this._domNode;
}
getOverviewRulerLayoutInfo() {
return {
parent: this._domNode,
insertBefore: this._verticalScrollbar.domNode.domNode,
};
}
/**
* Delegate a pointer down event to the vertical scrollbar.
* This is to help with clicking somewhere else and having the scrollbar react.
*/
delegateVerticalScrollbarPointerDown(browserEvent) {
this._verticalScrollbar.delegatePointerDown(browserEvent);
}
getScrollDimensions() {
return this._scrollable.getScrollDimensions();
}
setScrollDimensions(dimensions) {
this._scrollable.setScrollDimensions(dimensions, false);
}
/**
* Update the class name of the scrollable element.
*/
updateClassName(newClassName) {
this._options.className = newClassName;
// Defaults are different on Macs
if (isMacintosh) {
this._options.className += ' mac';
}
this._domNode.className = 'monaco-scrollable-element ' + this._options.className;
}
/**
* Update configuration options for the scrollbar.
*/
updateOptions(newOptions) {
if (typeof newOptions.handleMouseWheel !== 'undefined') {
this._options.handleMouseWheel = newOptions.handleMouseWheel;
this._setListeningToMouseWheel(this._options.handleMouseWheel);
}
if (typeof newOptions.mouseWheelScrollSensitivity !== 'undefined') {
this._options.mouseWheelScrollSensitivity = newOptions.mouseWheelScrollSensitivity;
}
if (typeof newOptions.fastScrollSensitivity !== 'undefined') {
this._options.fastScrollSensitivity = newOptions.fastScrollSensitivity;
}
if (typeof newOptions.scrollPredominantAxis !== 'undefined') {
this._options.scrollPredominantAxis = newOptions.scrollPredominantAxis;
}
if (typeof newOptions.horizontal !== 'undefined') {
this._options.horizontal = newOptions.horizontal;
}
if (typeof newOptions.vertical !== 'undefined') {
this._options.vertical = newOptions.vertical;
}
if (typeof newOptions.horizontalScrollbarSize !== 'undefined') {
this._options.horizontalScrollbarSize = newOptions.horizontalScrollbarSize;
}
if (typeof newOptions.verticalScrollbarSize !== 'undefined') {
this._options.verticalScrollbarSize = newOptions.verticalScrollbarSize;
}
if (typeof newOptions.scrollByPage !== 'undefined') {
this._options.scrollByPage = newOptions.scrollByPage;
}
this._horizontalScrollbar.updateOptions(this._options);
this._verticalScrollbar.updateOptions(this._options);
if (!this._options.lazyRender) {
this._render();
}
}
delegateScrollFromMouseWheelEvent(browserEvent) {
this._onMouseWheel(new StandardWheelEvent(browserEvent));
}
async _periodicSync() {
let scheduleAgain = false;
if (this._inertialSpeed.X !== 0 || this._inertialSpeed.Y !== 0) {
this._scrollable.setScrollPositionNow({
scrollTop: this._scrollable.getCurrentScrollPosition().scrollTop - this._inertialSpeed.Y * 100,
scrollLeft: this._scrollable.getCurrentScrollPosition().scrollLeft - this._inertialSpeed.X * 100
});
this._inertialSpeed.X *= 0.9;
this._inertialSpeed.Y *= 0.9;
if (Math.abs(this._inertialSpeed.X) < 0.01) {
this._inertialSpeed.X = 0;
}
if (Math.abs(this._inertialSpeed.Y) < 0.01) {
this._inertialSpeed.Y = 0;
}
scheduleAgain = (this._inertialSpeed.X !== 0 || this._inertialSpeed.Y !== 0);
}
if (scheduleAgain) {
if (!this._inertialTimeout) {
this._inertialTimeout = new TimeoutTimer();
}
this._inertialTimeout.cancelAndSet(() => this._periodicSync(), 1000 / 60);
}
else {
this._inertialTimeout?.dispose();
this._inertialTimeout = null;
}
}
// -------------------- mouse wheel scrolling --------------------
_setListeningToMouseWheel(shouldListen) {
const isListening = (this._mouseWheelToDispose.length > 0);
if (isListening === shouldListen) {
// No change
return;
}
// Stop listening (if necessary)
this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
// Start listening (if necessary)
if (shouldListen) {
const onMouseWheel = (browserEvent) => {
this._onMouseWheel(new StandardWheelEvent(browserEvent));
};
this._mouseWheelToDispose.push(addDisposableListener(this._listenOnDomNode, EventType.MOUSE_WHEEL, onMouseWheel, { passive: false }));
}
}
_onMouseWheel(e) {
if (e.browserEvent?.defaultPrevented) {
return;
}
const classifier = MouseWheelClassifier.INSTANCE;
{
classifier.acceptStandardWheelEvent(e);
}
// useful for creating unit tests:
// console.log(`${Date.now()}, ${e.deltaY}, ${e.deltaX}`);
let didScroll = false;
if (e.deltaY || e.deltaX) {
let deltaY = e.deltaY * this._options.mouseWheelScrollSensitivity;
let deltaX = e.deltaX * this._options.mouseWheelScrollSensitivity;
if (this._options.scrollPredominantAxis) {
if (this._options.scrollYToX && deltaX + deltaY === 0) {
// when configured to map Y to X and we both see
// no dominant axis and X and Y are competing with
// identical values into opposite directions, we
// ignore the delta as we cannot make a decision then
deltaX = deltaY = 0;
}
else if (Math.abs(deltaY) >= Math.abs(deltaX)) {
deltaX = 0;
}
else {
deltaY = 0;
}
}
if (this._options.flipAxes) {
[deltaY, deltaX] = [deltaX, deltaY];
}
// Convert vertical scrolling to horizontal if shift is held, this
// is handled at a higher level on Mac
const shiftConvert = !isMacintosh && e.browserEvent && e.browserEvent.shiftKey;
if ((this._options.scrollYToX || shiftConvert) && !deltaX) {
deltaX = deltaY;
deltaY = 0;
}
if (e.browserEvent && e.browserEvent.altKey) {
// fastScrolling
deltaX = deltaX * this._options.fastScrollSensitivity;
deltaY = deltaY * this._options.fastScrollSensitivity;
}
const futureScrollPosition = this._scrollable.getFutureScrollPosition();
let desiredScrollPosition = {};
if (deltaY) {
const deltaScrollTop = SCROLL_WHEEL_SENSITIVITY * deltaY;
// Here we convert values such as -0.3 to -1 or 0.3 to 1, otherwise low speed scrolling will never scroll
const desiredScrollTop = futureScrollPosition.scrollTop - (deltaScrollTop < 0 ? Math.floor(deltaScrollTop) : Math.ceil(deltaScrollTop));
this._verticalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollTop);
}
if (deltaX) {
const deltaScrollLeft = SCROLL_WHEEL_SENSITIVITY * deltaX;
// Here we convert values such as -0.3 to -1 or 0.3 to 1, otherwise low speed scrolling will never scroll
const desiredScrollLeft = futureScrollPosition.scrollLeft - (deltaScrollLeft < 0 ? Math.floor(deltaScrollLeft) : Math.ceil(deltaScrollLeft));
this._horizontalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollLeft);
}
// Check that we are scrolling towards a location which is valid
desiredScrollPosition = this._scrollable.validateScrollPosition(desiredScrollPosition);
if (this._options.inertialScroll && (deltaX || deltaY) && !classifier.isPhysicalMouseWheel()) {
let startPeriodic = false;
// Only start periodic if it's not running
if (this._inertialSpeed.X === 0 && this._inertialSpeed.Y === 0) {
startPeriodic = true;
}
this._inertialSpeed.Y = (deltaY < 0 ? -1 : 1) * (Math.abs(deltaY) ** 1.02);
this._inertialSpeed.X = (deltaX < 0 ? -1 : 1) * (Math.abs(deltaX) ** 1.02);
if (startPeriodic) {
this._periodicSync();
}
}
if (futureScrollPosition.scrollLeft !== desiredScrollPosition.scrollLeft || futureScrollPosition.scrollTop !== desiredScrollPosition.scrollTop) {
const canPerformSmoothScroll = (this._options.mouseWheelSmoothScroll
&& classifier.isPhysicalMouseWheel());
if (canPerformSmoothScroll) {
this._scrollable.setScrollPositionSmooth(desiredScrollPosition);
}
else {
this._scrollable.setScrollPositionNow(desiredScrollPosition);
}
didScroll = true;
}
}
let consumeMouseWheel = didScroll;
if (!consumeMouseWheel && this._options.alwaysConsumeMouseWheel) {
consumeMouseWheel = true;
}
if (!consumeMouseWheel && this._options.consumeMouseWheelIfScrollbarIsNeeded && (this._verticalScrollbar.isNeeded() || this._horizontalScrollbar.isNeeded())) {
consumeMouseWheel = true;
}
if (consumeMouseWheel) {
e.preventDefault();
e.stopPropagation();
}
}
_onDidScroll(e) {
this._shouldRender = this._horizontalScrollbar.onDidScroll(e) || this._shouldRender;
this._shouldRender = this._verticalScrollbar.onDidScroll(e) || this._shouldRender;
if (this._options.useShadows) {
this._shouldRender = true;
}
if (this._revealOnScroll) {
this._reveal();
}
if (!this._options.lazyRender) {
this._render();
}
}
/**
* Render / mutate the DOM now.
* Should be used together with the ctor option `lazyRender`.
*/
renderNow() {
if (!this._options.lazyRender) {
throw new Error('Please use `lazyRender` together with `renderNow`!');
}
this._render();
}
_render() {
if (!this._shouldRender) {
return;
}
this._shouldRender = false;
this._horizontalScrollbar.render();
this._verticalScrollbar.render();
if (this._options.useShadows) {
const scrollState = this._scrollable.getCurrentScrollPosition();
const enableTop = scrollState.scrollTop > 0;
const enableLeft = scrollState.scrollLeft > 0;
const leftClassName = (enableLeft ? ' left' : '');
const topClassName = (enableTop ? ' top' : '');
const topLeftClassName = (enableLeft || enableTop ? ' top-left-corner' : '');
this._leftShadowDomNode.setClassName(`shadow${leftClassName}`);
this._topShadowDomNode.setClassName(`shadow${topClassName}`);
this._topLeftShadowDomNode.setClassName(`shadow${topLeftClassName}${topClassName}${leftClassName}`);
}
}
// -------------------- fade in / fade out --------------------
_onDragStart() {
this._isDragging = true;
this._reveal();
}
_onDragEnd() {
this._isDragging = false;
this._hide();
}
_onMouseLeave(e) {
this._mouseIsOver = false;
this._hide();
}
_onMouseOver(e) {
this._mouseIsOver = true;
this._reveal();
}
_reveal() {
this._verticalScrollbar.beginReveal();
this._horizontalScrollbar.beginReveal();
this._scheduleHide();
}
_hide() {
if (!this._mouseIsOver && !this._isDragging) {
this._verticalScrollbar.beginHide();
this._horizontalScrollbar.beginHide();
}
}
_scheduleHide() {
if (!this._mouseIsOver && !this._isDragging) {
this._hideTimeout.cancelAndSet(() => this._hide(), HIDE_TIMEOUT);
}
}
}
class ScrollableElement extends AbstractScrollableElement {
constructor(element, options) {
options = options || {};
options.mouseWheelSmoothScroll = false;
const scrollable = new Scrollable({
forceIntegerValues: true,
smoothScrollDuration: 0,
scheduleAtNextAnimationFrame: (callback) => scheduleAtNextAnimationFrame(getWindow(element), callback)
});
super(element, options, scrollable);
this._register(scrollable);
}
setScrollPosition(update) {
this._scrollable.setScrollPositionNow(update);
}
}
class SmoothScrollableElement extends AbstractScrollableElement {
constructor(element, options, scrollable) {
super(element, options, scrollable);
}
setScrollPosition(update) {
if (update.reuseAnimation) {
this._scrollable.setScrollPositionSmooth(update, update.reuseAnimation);
}
else {
this._scrollable.setScrollPositionNow(update);
}
}
getScrollPosition() {
return this._scrollable.getCurrentScrollPosition();
}
}
class DomScrollableElement extends AbstractScrollableElement {
constructor(element, options) {
options = options || {};
options.mouseWheelSmoothScroll = false;
const scrollable = new Scrollable({
forceIntegerValues: false, // See https://github.com/microsoft/vscode/issues/139877
smoothScrollDuration: 0,
scheduleAtNextAnimationFrame: (callback) => scheduleAtNextAnimationFrame(getWindow(element), callback)
});
super(element, options, scrollable);
this._register(scrollable);
this._element = element;
this._register(this.onScroll((e) => {
if (e.scrollTopChanged) {
this._element.scrollTop = e.scrollTop;
}
if (e.scrollLeftChanged) {
this._element.scrollLeft = e.scrollLeft;
}
}));
this.scanDomNode();
}
setScrollPosition(update) {
this._scrollable.setScrollPositionNow(update);
}
getScrollPosition() {
return this._scrollable.getCurrentScrollPosition();
}
scanDomNode() {
// width, scrollLeft, scrollWidth, height, scrollTop, scrollHeight
this.setScrollDimensions({
width: this._element.clientWidth,
scrollWidth: this._element.scrollWidth,
height: this._element.clientHeight,
scrollHeight: this._element.scrollHeight
});
this.setScrollPosition({
scrollLeft: this._element.scrollLeft,
scrollTop: this._element.scrollTop,
});
}
}
function resolveOptions(opts) {
const result = {
lazyRender: (typeof opts.lazyRender !== 'undefined' ? opts.lazyRender : false),
className: (typeof opts.className !== 'undefined' ? opts.className : ''),
useShadows: (typeof opts.useShadows !== 'undefined' ? opts.useShadows : true),
handleMouseWheel: (typeof opts.handleMouseWheel !== 'undefined' ? opts.handleMouseWheel : true),
flipAxes: (typeof opts.flipAxes !== 'undefined' ? opts.flipAxes : false),
consumeMouseWheelIfScrollbarIsNeeded: (typeof opts.consumeMouseWheelIfScrollbarIsNeeded !== 'undefined' ? opts.consumeMouseWheelIfScrollbarIsNeeded : false),
alwaysConsumeMouseWheel: (typeof opts.alwaysConsumeMouseWheel !== 'undefined' ? opts.alwaysConsumeMouseWheel : false),
scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false),
mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1),
fastScrollSensitivity: (typeof opts.fastScrollSensitivity !== 'undefined' ? opts.fastScrollSensitivity : 5),
scrollPredominantAxis: (typeof opts.scrollPredominantAxis !== 'undefined' ? opts.scrollPredominantAxis : true),
mouseWheelSmoothScroll: (typeof opts.mouseWheelSmoothScroll !== 'undefined' ? opts.mouseWheelSmoothScroll : true),
inertialScroll: (typeof opts.inertialScroll !== 'undefined' ? opts.inertialScroll : false),
arrowSize: (typeof opts.arrowSize !== 'undefined' ? opts.arrowSize : 11),
listenOnDomNode: (typeof opts.listenOnDomNode !== 'undefined' ? opts.listenOnDomNode : null),
horizontal: (typeof opts.horizontal !== 'undefined' ? opts.horizontal : 1 /* ScrollbarVisibility.Auto */),
horizontalScrollbarSize: (typeof opts.horizontalScrollbarSize !== 'undefined' ? opts.horizontalScrollbarSize : 10),
horizontalSliderSize: (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : 0),
horizontalHasArrows: (typeof opts.horizontalHasArrows !== 'undefined' ? opts.horizontalHasArrows : false),
vertical: (typeof opts.vertical !== 'undefined' ? opts.vertical : 1 /* ScrollbarVisibility.Auto */),
verticalScrollbarSize: (typeof opts.verticalScrollbarSize !== 'undefined' ? opts.verticalScrollbarSize : 10),
verticalHasArrows: (typeof opts.verticalHasArrows !== 'undefined' ? opts.verticalHasArrows : false),
verticalSliderSize: (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : 0),
scrollByPage: (typeof opts.scrollByPage !== 'undefined' ? opts.scrollByPage : false)
};
result.horizontalSliderSize = (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : result.horizontalScrollbarSize);
result.verticalSliderSize = (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : result.verticalScrollbarSize);
// Defaults are different on Macs
if (isMacintosh) {
result.className += ' mac';
}
return result;
}
export { AbstractScrollableElement, DomScrollableElement, MouseWheelClassifier, ScrollableElement, SmoothScrollableElement };
@@ -0,0 +1,78 @@
import { GlobalPointerMoveMonitor } from '../../globalPointerMoveMonitor.js';
import { Widget } from '../widget.js';
import { TimeoutTimer } from '../../../common/async.js';
import { ThemeIcon } from '../../../common/themables.js';
import { addStandardDisposableListener, EventType, WindowIntervalTimer, getWindow } from '../../dom.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* The arrow image size.
*/
const ARROW_IMG_SIZE = 11;
class ScrollbarArrow extends Widget {
constructor(opts) {
super();
this._onActivate = opts.onActivate;
this.bgDomNode = document.createElement('div');
this.bgDomNode.className = 'arrow-background';
this.bgDomNode.style.position = 'absolute';
this.bgDomNode.style.width = opts.bgWidth + 'px';
this.bgDomNode.style.height = opts.bgHeight + 'px';
if (typeof opts.top !== 'undefined') {
this.bgDomNode.style.top = '0px';
}
if (typeof opts.left !== 'undefined') {
this.bgDomNode.style.left = '0px';
}
if (typeof opts.bottom !== 'undefined') {
this.bgDomNode.style.bottom = '0px';
}
if (typeof opts.right !== 'undefined') {
this.bgDomNode.style.right = '0px';
}
this.domNode = document.createElement('div');
this.domNode.className = opts.className;
this.domNode.classList.add(...ThemeIcon.asClassNameArray(opts.icon));
this.domNode.style.position = 'absolute';
this.domNode.style.width = ARROW_IMG_SIZE + 'px';
this.domNode.style.height = ARROW_IMG_SIZE + 'px';
if (typeof opts.top !== 'undefined') {
this.domNode.style.top = opts.top + 'px';
}
if (typeof opts.left !== 'undefined') {
this.domNode.style.left = opts.left + 'px';
}
if (typeof opts.bottom !== 'undefined') {
this.domNode.style.bottom = opts.bottom + 'px';
}
if (typeof opts.right !== 'undefined') {
this.domNode.style.right = opts.right + 'px';
}
this._pointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
this._register(addStandardDisposableListener(this.bgDomNode, EventType.POINTER_DOWN, (e) => this._arrowPointerDown(e)));
this._register(addStandardDisposableListener(this.domNode, EventType.POINTER_DOWN, (e) => this._arrowPointerDown(e)));
this._pointerdownRepeatTimer = this._register(new WindowIntervalTimer());
this._pointerdownScheduleRepeatTimer = this._register(new TimeoutTimer());
}
_arrowPointerDown(e) {
if (!e.target || !(e.target instanceof Element)) {
return;
}
const scheduleRepeater = () => {
this._pointerdownRepeatTimer.cancelAndSet(() => this._onActivate(), 1000 / 24, getWindow(e));
};
this._onActivate();
this._pointerdownRepeatTimer.cancel();
this._pointerdownScheduleRepeatTimer.cancelAndSet(scheduleRepeater, 200);
this._pointerMoveMonitor.startMonitoring(e.target, e.pointerId, e.buttons, (pointerMoveData) => { }, () => {
this._pointerdownRepeatTimer.cancel();
this._pointerdownScheduleRepeatTimer.cancel();
});
e.preventDefault();
}
}
export { ARROW_IMG_SIZE, ScrollbarArrow };
@@ -0,0 +1,163 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* The minimal size of the slider (such that it can still be clickable) -- it is artificially enlarged.
*/
const MINIMUM_SLIDER_SIZE = 20;
class ScrollbarState {
constructor(arrowSize, scrollbarSize, oppositeScrollbarSize, visibleSize, scrollSize, scrollPosition) {
this._scrollbarSize = Math.round(scrollbarSize);
this._oppositeScrollbarSize = Math.round(oppositeScrollbarSize);
this._arrowSize = Math.round(arrowSize);
this._visibleSize = visibleSize;
this._scrollSize = scrollSize;
this._scrollPosition = scrollPosition;
this._computedAvailableSize = 0;
this._computedIsNeeded = false;
this._computedSliderSize = 0;
this._computedSliderRatio = 0;
this._computedSliderPosition = 0;
this._refreshComputedValues();
}
clone() {
return new ScrollbarState(this._arrowSize, this._scrollbarSize, this._oppositeScrollbarSize, this._visibleSize, this._scrollSize, this._scrollPosition);
}
setVisibleSize(visibleSize) {
const iVisibleSize = Math.round(visibleSize);
if (this._visibleSize !== iVisibleSize) {
this._visibleSize = iVisibleSize;
this._refreshComputedValues();
return true;
}
return false;
}
setScrollSize(scrollSize) {
const iScrollSize = Math.round(scrollSize);
if (this._scrollSize !== iScrollSize) {
this._scrollSize = iScrollSize;
this._refreshComputedValues();
return true;
}
return false;
}
setScrollPosition(scrollPosition) {
const iScrollPosition = Math.round(scrollPosition);
if (this._scrollPosition !== iScrollPosition) {
this._scrollPosition = iScrollPosition;
this._refreshComputedValues();
return true;
}
return false;
}
setScrollbarSize(scrollbarSize) {
this._scrollbarSize = Math.round(scrollbarSize);
}
setOppositeScrollbarSize(oppositeScrollbarSize) {
this._oppositeScrollbarSize = Math.round(oppositeScrollbarSize);
}
static _computeValues(oppositeScrollbarSize, arrowSize, visibleSize, scrollSize, scrollPosition) {
const computedAvailableSize = Math.max(0, visibleSize - oppositeScrollbarSize);
const computedRepresentableSize = Math.max(0, computedAvailableSize - 2 * arrowSize);
const computedIsNeeded = (scrollSize > 0 && scrollSize > visibleSize);
if (!computedIsNeeded) {
// There is no need for a slider
return {
computedAvailableSize: Math.round(computedAvailableSize),
computedIsNeeded: computedIsNeeded,
computedSliderSize: Math.round(computedRepresentableSize),
computedSliderRatio: 0,
computedSliderPosition: 0,
};
}
// We must artificially increase the size of the slider if needed, since the slider would be too small to grab with the mouse otherwise
const computedSliderSize = Math.round(Math.max(MINIMUM_SLIDER_SIZE, Math.floor(visibleSize * computedRepresentableSize / scrollSize)));
// The slider can move from 0 to `computedRepresentableSize` - `computedSliderSize`
// in the same way `scrollPosition` can move from 0 to `scrollSize` - `visibleSize`.
const computedSliderRatio = (computedRepresentableSize - computedSliderSize) / (scrollSize - visibleSize);
const computedSliderPosition = (scrollPosition * computedSliderRatio);
return {
computedAvailableSize: Math.round(computedAvailableSize),
computedIsNeeded: computedIsNeeded,
computedSliderSize: Math.round(computedSliderSize),
computedSliderRatio: computedSliderRatio,
computedSliderPosition: Math.round(computedSliderPosition),
};
}
_refreshComputedValues() {
const r = ScrollbarState._computeValues(this._oppositeScrollbarSize, this._arrowSize, this._visibleSize, this._scrollSize, this._scrollPosition);
this._computedAvailableSize = r.computedAvailableSize;
this._computedIsNeeded = r.computedIsNeeded;
this._computedSliderSize = r.computedSliderSize;
this._computedSliderRatio = r.computedSliderRatio;
this._computedSliderPosition = r.computedSliderPosition;
}
getArrowSize() {
return this._arrowSize;
}
getScrollPosition() {
return this._scrollPosition;
}
getRectangleLargeSize() {
return this._computedAvailableSize;
}
getRectangleSmallSize() {
return this._scrollbarSize;
}
isNeeded() {
return this._computedIsNeeded;
}
getSliderSize() {
return this._computedSliderSize;
}
getSliderPosition() {
return this._computedSliderPosition;
}
/**
* Compute a desired `scrollPosition` such that `offset` ends up in the center of the slider.
* `offset` is based on the same coordinate system as the `sliderPosition`.
*/
getDesiredScrollPositionFromOffset(offset) {
if (!this._computedIsNeeded) {
// no need for a slider
return 0;
}
const desiredSliderPosition = offset - this._arrowSize - this._computedSliderSize / 2;
return Math.round(desiredSliderPosition / this._computedSliderRatio);
}
/**
* Compute a desired `scrollPosition` from if offset is before or after the slider position.
* If offset is before slider, treat as a page up (or left). If after, page down (or right).
* `offset` and `_computedSliderPosition` are based on the same coordinate system.
* `_visibleSize` corresponds to a "page" of lines in the returned coordinate system.
*/
getDesiredScrollPositionFromOffsetPaged(offset) {
if (!this._computedIsNeeded) {
// no need for a slider
return 0;
}
const correctedOffset = offset - this._arrowSize; // compensate if has arrows
let desiredScrollPosition = this._scrollPosition;
if (correctedOffset < this._computedSliderPosition) {
desiredScrollPosition -= this._visibleSize; // page up/left
}
else {
desiredScrollPosition += this._visibleSize; // page down/right
}
return desiredScrollPosition;
}
/**
* Compute a desired `scrollPosition` such that the slider moves by `delta`.
*/
getDesiredScrollPositionFromDelta(delta) {
if (!this._computedIsNeeded) {
// no need for a slider
return 0;
}
const desiredSliderPosition = this._computedSliderPosition + delta;
return Math.round(desiredSliderPosition / this._computedSliderRatio);
}
}
export { ScrollbarState };
@@ -0,0 +1,93 @@
import { TimeoutTimer } from '../../../common/async.js';
import { Disposable } from '../../../common/lifecycle.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class ScrollbarVisibilityController extends Disposable {
constructor(visibility, visibleClassName, invisibleClassName) {
super();
this._visibility = visibility;
this._visibleClassName = visibleClassName;
this._invisibleClassName = invisibleClassName;
this._domNode = null;
this._isVisible = false;
this._isNeeded = false;
this._rawShouldBeVisible = false;
this._shouldBeVisible = false;
this._revealTimer = this._register(new TimeoutTimer());
}
setVisibility(visibility) {
if (this._visibility !== visibility) {
this._visibility = visibility;
this._updateShouldBeVisible();
}
}
// ----------------- Hide / Reveal
setShouldBeVisible(rawShouldBeVisible) {
this._rawShouldBeVisible = rawShouldBeVisible;
this._updateShouldBeVisible();
}
_applyVisibilitySetting() {
if (this._visibility === 2 /* ScrollbarVisibility.Hidden */) {
return false;
}
if (this._visibility === 3 /* ScrollbarVisibility.Visible */) {
return true;
}
return this._rawShouldBeVisible;
}
_updateShouldBeVisible() {
const shouldBeVisible = this._applyVisibilitySetting();
if (this._shouldBeVisible !== shouldBeVisible) {
this._shouldBeVisible = shouldBeVisible;
this.ensureVisibility();
}
}
setIsNeeded(isNeeded) {
if (this._isNeeded !== isNeeded) {
this._isNeeded = isNeeded;
this.ensureVisibility();
}
}
setDomNode(domNode) {
this._domNode = domNode;
this._domNode.setClassName(this._invisibleClassName);
// Now that the flags & the dom node are in a consistent state, ensure the Hidden/Visible configuration
this.setShouldBeVisible(false);
}
ensureVisibility() {
if (!this._isNeeded) {
// Nothing to be rendered
this._hide(false);
return;
}
if (this._shouldBeVisible) {
this._reveal();
}
else {
this._hide(true);
}
}
_reveal() {
if (this._isVisible) {
return;
}
this._isVisible = true;
// The CSS animation doesn't play otherwise
this._revealTimer.setIfNotSet(() => {
this._domNode?.setClassName(this._visibleClassName);
}, 0);
}
_hide(withFadeAway) {
this._revealTimer.cancel();
if (!this._isVisible) {
return;
}
this._isVisible = false;
this._domNode?.setClassName(this._invisibleClassName + (withFadeAway ? ' fade' : ''));
}
}
export { ScrollbarVisibilityController };
@@ -0,0 +1,94 @@
import { StandardWheelEvent } from '../../mouseEvent.js';
import { AbstractScrollbar } from './abstractScrollbar.js';
import { ARROW_IMG_SIZE } from './scrollbarArrow.js';
import { ScrollbarState } from './scrollbarState.js';
import { Codicon } from '../../../common/codicons.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class VerticalScrollbar extends AbstractScrollbar {
constructor(scrollable, options, host) {
const scrollDimensions = scrollable.getScrollDimensions();
const scrollPosition = scrollable.getCurrentScrollPosition();
super({
lazyRender: options.lazyRender,
host: host,
scrollbarState: new ScrollbarState((options.verticalHasArrows ? options.arrowSize : 0), (options.vertical === 2 /* ScrollbarVisibility.Hidden */ ? 0 : options.verticalScrollbarSize),
// give priority to vertical scroll bar over horizontal and let it scroll all the way to the bottom
0, scrollDimensions.height, scrollDimensions.scrollHeight, scrollPosition.scrollTop),
visibility: options.vertical,
extraScrollbarClassName: 'vertical',
scrollable: scrollable,
scrollByPage: options.scrollByPage
});
if (options.verticalHasArrows) {
const arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
const scrollbarDelta = (options.verticalScrollbarSize - ARROW_IMG_SIZE) / 2;
this._createArrow({
className: 'scra',
icon: Codicon.scrollbarButtonUp,
top: arrowDelta,
left: scrollbarDelta,
bottom: undefined,
right: undefined,
bgWidth: options.verticalScrollbarSize,
bgHeight: options.arrowSize,
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 0, 1)),
});
this._createArrow({
className: 'scra',
icon: Codicon.scrollbarButtonDown,
top: undefined,
left: scrollbarDelta,
bottom: arrowDelta,
right: undefined,
bgWidth: options.verticalScrollbarSize,
bgHeight: options.arrowSize,
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 0, -1)),
});
}
this._createSlider(0, Math.floor((options.verticalScrollbarSize - options.verticalSliderSize) / 2), options.verticalSliderSize, undefined);
}
_updateSlider(sliderSize, sliderPosition) {
this.slider.setHeight(sliderSize);
this.slider.setTop(sliderPosition);
}
_renderDomNode(largeSize, smallSize) {
this.domNode.setWidth(smallSize);
this.domNode.setHeight(largeSize);
this.domNode.setRight(0);
this.domNode.setTop(0);
}
onDidScroll(e) {
this._shouldRender = this._onElementScrollSize(e.scrollHeight) || this._shouldRender;
this._shouldRender = this._onElementScrollPosition(e.scrollTop) || this._shouldRender;
this._shouldRender = this._onElementSize(e.height) || this._shouldRender;
return this._shouldRender;
}
_pointerDownRelativePosition(offsetX, offsetY) {
return offsetY;
}
_sliderPointerPosition(e) {
return e.pageY;
}
_sliderOrthogonalPointerPosition(e) {
return e.pageX;
}
_updateScrollbarSize(size) {
this.slider.setWidth(size);
}
writeScrollPosition(target, scrollPosition) {
target.scrollTop = scrollPosition;
}
updateOptions(options) {
this.updateScrollbarSize(options.vertical === 2 /* ScrollbarVisibility.Hidden */ ? 0 : options.verticalScrollbarSize);
// give priority to vertical scroll bar over horizontal and let it scroll all the way to the bottom
this._scrollbarState.setOppositeScrollbarSize(0);
this._visibilityController.setVisibility(options.vertical);
this._scrollByPage = options.scrollByPage;
}
}
export { VerticalScrollbar };
@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-select-box {
width: 100%;
cursor: pointer;
border-radius: 2px;
}
.monaco-select-box-dropdown-container {
font-size: 13px;
font-weight: normal;
text-transform: none;
}
/** Actions */
.monaco-action-bar .action-item.select-container {
cursor: default;
}
.monaco-action-bar .action-item .monaco-select-box {
cursor: pointer;
min-width: 100px;
min-height: 18px;
padding: 2px 23px 2px 8px;
}
.mac .monaco-action-bar .action-item .monaco-select-box {
font-size: 11px;
border-radius: 3px;
min-height: 24px;
}
@@ -0,0 +1,47 @@
import { isMacintosh } from '../../../common/platform.js';
import { Widget } from '../widget.js';
import './selectBox.css';
import { SelectBoxList } from './selectBoxCustom.js';
import { SelectBoxNative } from './selectBoxNative.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class SelectBox extends Widget {
constructor(options, selected, contextViewProvider, styles, selectBoxOptions) {
super();
// Default to native SelectBox for OSX unless overridden
if (isMacintosh && !selectBoxOptions?.useCustomDrawn) {
this.selectBoxDelegate = new SelectBoxNative(options, selected, styles, selectBoxOptions);
}
else {
this.selectBoxDelegate = new SelectBoxList(options, selected, contextViewProvider, styles, selectBoxOptions);
}
this._register(this.selectBoxDelegate);
}
// Public SelectBox Methods - routed through delegate interface
get onDidSelect() {
return this.selectBoxDelegate.onDidSelect;
}
setOptions(options, selected) {
this.selectBoxDelegate.setOptions(options, selected);
}
select(index) {
this.selectBoxDelegate.select(index);
}
focus() {
this.selectBoxDelegate.focus();
}
blur() {
this.selectBoxDelegate.blur();
}
setFocusable(focusable) {
this.selectBoxDelegate.setFocusable(focusable);
}
render(container) {
this.selectBoxDelegate.render(container);
}
}
export { SelectBox };
@@ -0,0 +1,126 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* Use custom CSS vars to expose padding into parent select for padding calculation */
.monaco-select-box-dropdown-padding {
--dropdown-padding-top: 1px;
--dropdown-padding-bottom: 1px;
}
.hc-black .monaco-select-box-dropdown-padding,
.hc-light .monaco-select-box-dropdown-padding {
--dropdown-padding-top: 3px;
--dropdown-padding-bottom: 4px;
}
.monaco-select-box-dropdown-container {
display: none;
box-sizing: border-box;
}
.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown * {
margin: 0;
}
.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown a:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}
.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown code {
line-height: 15px; /** For some reason, this is needed, otherwise <code> will take up 20px height */
font-family: var(--monaco-monospace-font);
}
.monaco-select-box-dropdown-container.visible {
display: flex;
flex-direction: column;
text-align: left;
width: 1px;
overflow: hidden;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
.monaco-select-box-dropdown-container > .select-box-dropdown-list-container {
flex: 0 0 auto;
align-self: flex-start;
padding-top: var(--dropdown-padding-top);
padding-bottom: var(--dropdown-padding-bottom);
padding-left: 1px;
padding-right: 1px;
width: 100%;
overflow: hidden;
box-sizing: border-box;
}
.monaco-select-box-dropdown-container > .select-box-details-pane {
padding: 5px;
}
.hc-black .monaco-select-box-dropdown-container > .select-box-dropdown-list-container {
padding-top: var(--dropdown-padding-top);
padding-bottom: var(--dropdown-padding-bottom);
}
.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row {
cursor: pointer;
}
.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row > .option-text {
text-overflow: ellipsis;
overflow: hidden;
padding-left: 3.5px;
white-space: nowrap;
float: left;
}
.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row > .option-detail {
text-overflow: ellipsis;
overflow: hidden;
padding-left: 3.5px;
white-space: nowrap;
float: left;
opacity: 0.7;
}
.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row > .option-decorator-right {
text-overflow: ellipsis;
overflow: hidden;
padding-right: 10px;
white-space: nowrap;
float: right;
}
/* Accepted CSS hiding technique for accessibility reader text */
/* https://webaim.org/techniques/css/invisiblecontent/ */
.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row > .visually-hidden {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.monaco-select-box-dropdown-container > .select-box-dropdown-container-width-control {
flex: 1 1 auto;
align-self: flex-start;
opacity: 0;
}
.monaco-select-box-dropdown-container > .select-box-dropdown-container-width-control > .width-control-div {
overflow: hidden;
max-height: 0px;
}
.monaco-select-box-dropdown-container > .select-box-dropdown-container-width-control > .width-control-div > .option-text-width-control {
padding-left: 4px;
padding-right: 8px;
white-space: nowrap;
}
@@ -0,0 +1,858 @@
import { localize } from '../../../../nls.js';
import { equals } from '../../../common/arrays.js';
import { Emitter, Event } from '../../../common/event.js';
import { KeyCodeUtils } from '../../../common/keyCodes.js';
import { Disposable, DisposableStore } from '../../../common/lifecycle.js';
import { isMacintosh } from '../../../common/platform.js';
import { asCssValueWithDefault } from '../../cssValue.js';
import { $ as $$1, append, addDisposableListener, EventHelper, EventType, addStandardDisposableListener, getWindow, getDomNodePagePosition, getTotalWidth, isAncestor } from '../../dom.js';
import { createStyleSheet } from '../../domStylesheets.js';
import { DomEmitter } from '../../event.js';
import { StandardKeyboardEvent } from '../../keyboardEvent.js';
import { renderMarkdown } from '../../markdownRenderer.js';
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
import { List } from '../list/listWidget.js';
import './selectBoxCustom.css';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const $ = $$1;
const SELECT_OPTION_ENTRY_TEMPLATE_ID = 'selectOption.entry.template';
class SelectListRenderer {
get templateId() { return SELECT_OPTION_ENTRY_TEMPLATE_ID; }
renderTemplate(container) {
const data = Object.create(null);
data.root = container;
data.text = append(container, $('.option-text'));
data.detail = append(container, $('.option-detail'));
data.decoratorRight = append(container, $('.option-decorator-right'));
return data;
}
renderElement(element, index, templateData) {
const data = templateData;
const text = element.text;
const detail = element.detail;
const decoratorRight = element.decoratorRight;
const isDisabled = element.isDisabled;
data.text.textContent = text;
data.detail.textContent = !!detail ? detail : '';
data.decoratorRight.textContent = !!decoratorRight ? decoratorRight : '';
// pseudo-select disabled option
if (isDisabled) {
data.root.classList.add('option-disabled');
}
else {
// Make sure we do class removal from prior template rendering
data.root.classList.remove('option-disabled');
}
}
disposeTemplate(_templateData) {
// noop
}
}
class SelectBoxList extends Disposable {
static { this.DEFAULT_DROPDOWN_MINIMUM_BOTTOM_MARGIN = 32; }
static { this.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN = 2; }
static { this.DEFAULT_MINIMUM_VISIBLE_OPTIONS = 3; }
constructor(options, selected, contextViewProvider, styles, selectBoxOptions) {
super();
this.options = [];
this._currentSelection = 0;
this._hasDetails = false;
this._selectionDetailsDisposables = this._register(new DisposableStore());
this._skipLayout = false;
this._sticky = false; // for dev purposes only
this._isVisible = false;
this.styles = styles;
this.selectBoxOptions = selectBoxOptions || Object.create(null);
if (typeof this.selectBoxOptions.minBottomMargin !== 'number') {
this.selectBoxOptions.minBottomMargin = SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_BOTTOM_MARGIN;
}
else if (this.selectBoxOptions.minBottomMargin < 0) {
this.selectBoxOptions.minBottomMargin = 0;
}
this.selectElement = document.createElement('select');
// Use custom CSS vars for padding calculation
this.selectElement.className = 'monaco-select-box monaco-select-box-dropdown-padding';
if (typeof this.selectBoxOptions.ariaLabel === 'string') {
this.selectElement.setAttribute('aria-label', this.selectBoxOptions.ariaLabel);
}
if (typeof this.selectBoxOptions.ariaDescription === 'string') {
this.selectElement.setAttribute('aria-description', this.selectBoxOptions.ariaDescription);
}
this._onDidSelect = new Emitter();
this._register(this._onDidSelect);
this.registerListeners();
this.constructSelectDropDown(contextViewProvider);
this.selected = selected || 0;
if (options) {
this.setOptions(options, selected);
}
this.initStyleSheet();
}
setTitle(title) {
if (!this._hover && title) {
this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.selectElement, title));
}
else if (this._hover) {
this._hover.update(title);
}
}
// IDelegate - List renderer
getHeight() {
return 22;
}
getTemplateId() {
return SELECT_OPTION_ENTRY_TEMPLATE_ID;
}
constructSelectDropDown(contextViewProvider) {
// SetUp ContextView container to hold select Dropdown
this.contextViewProvider = contextViewProvider;
this.selectDropDownContainer = $$1('.monaco-select-box-dropdown-container');
// Use custom CSS vars for padding calculation (shared with parent select)
this.selectDropDownContainer.classList.add('monaco-select-box-dropdown-padding');
// Setup container for select option details
this.selectionDetailsPane = append(this.selectDropDownContainer, $('.select-box-details-pane'));
// Create span flex box item/div we can measure and control
const widthControlOuterDiv = append(this.selectDropDownContainer, $('.select-box-dropdown-container-width-control'));
const widthControlInnerDiv = append(widthControlOuterDiv, $('.width-control-div'));
this.widthControlElement = document.createElement('span');
this.widthControlElement.className = 'option-text-width-control';
append(widthControlInnerDiv, this.widthControlElement);
// Always default to below position
this._dropDownPosition = 0 /* AnchorPosition.BELOW */;
// Inline stylesheet for themes
this.styleElement = createStyleSheet(this.selectDropDownContainer);
// Prevent dragging of dropdown #114329
this.selectDropDownContainer.setAttribute('draggable', 'true');
this._register(addDisposableListener(this.selectDropDownContainer, EventType.DRAG_START, (e) => {
EventHelper.stop(e, true);
}));
}
registerListeners() {
// Parent native select keyboard listeners
this._register(addStandardDisposableListener(this.selectElement, 'change', (e) => {
this.selected = e.target.selectedIndex;
this._onDidSelect.fire({
index: e.target.selectedIndex,
selected: e.target.value
});
if (!!this.options[this.selected] && !!this.options[this.selected].text) {
this.setTitle(this.options[this.selected].text);
}
}));
// Have to implement both keyboard and mouse controllers to handle disabled options
// Intercept mouse events to override normal select actions on parents
this._register(addDisposableListener(this.selectElement, EventType.CLICK, (e) => {
EventHelper.stop(e);
if (this._isVisible) {
this.hideSelectDropDown(true);
}
else {
this.showSelectDropDown();
}
}));
this._register(addDisposableListener(this.selectElement, EventType.MOUSE_DOWN, (e) => {
EventHelper.stop(e);
}));
// Intercept touch events
// The following implementation is slightly different from the mouse event handlers above.
// Use the following helper variable, otherwise the list flickers.
let listIsVisibleOnTouchStart;
this._register(addDisposableListener(this.selectElement, 'touchstart', (e) => {
listIsVisibleOnTouchStart = this._isVisible;
}));
this._register(addDisposableListener(this.selectElement, 'touchend', (e) => {
EventHelper.stop(e);
if (listIsVisibleOnTouchStart) {
this.hideSelectDropDown(true);
}
else {
this.showSelectDropDown();
}
}));
// Intercept keyboard handling
this._register(addDisposableListener(this.selectElement, EventType.KEY_DOWN, (e) => {
const event = new StandardKeyboardEvent(e);
let showDropDown = false;
// Create and drop down select list on keyboard select
if (isMacintosh) {
if (event.keyCode === 18 /* KeyCode.DownArrow */ || event.keyCode === 16 /* KeyCode.UpArrow */ || event.keyCode === 10 /* KeyCode.Space */ || event.keyCode === 3 /* KeyCode.Enter */) {
showDropDown = true;
}
}
else {
if (event.keyCode === 18 /* KeyCode.DownArrow */ && event.altKey || event.keyCode === 16 /* KeyCode.UpArrow */ && event.altKey || event.keyCode === 10 /* KeyCode.Space */ || event.keyCode === 3 /* KeyCode.Enter */) {
showDropDown = true;
}
}
if (showDropDown) {
this.showSelectDropDown();
EventHelper.stop(e, true);
}
}));
}
get onDidSelect() {
return this._onDidSelect.event;
}
setOptions(options, selected) {
if (!equals(this.options, options)) {
this.options = options;
this.selectElement.options.length = 0;
this._hasDetails = false;
this._cachedMaxDetailsHeight = undefined;
this.options.forEach((option, index) => {
this.selectElement.add(this.createOption(option.text, index, option.isDisabled));
if (typeof option.description === 'string') {
this._hasDetails = true;
}
});
}
if (selected !== undefined) {
this.select(selected);
// Set current = selected since this is not necessarily a user exit
this._currentSelection = this.selected;
}
}
setOptionsList() {
// Mirror options in drop-down
// Populate select list for non-native select mode
this.selectList?.splice(0, this.selectList.length, this.options);
}
select(index) {
if (index >= 0 && index < this.options.length) {
this.selected = index;
}
else if (index > this.options.length - 1) {
// Adjust index to end of list
// This could make client out of sync with the select
this.select(this.options.length - 1);
}
else if (this.selected < 0) {
this.selected = 0;
}
this.selectElement.selectedIndex = this.selected;
if (!!this.options[this.selected] && !!this.options[this.selected].text) {
this.setTitle(this.options[this.selected].text);
}
}
focus() {
if (this.selectElement) {
this.selectElement.tabIndex = 0;
this.selectElement.focus();
}
}
blur() {
if (this.selectElement) {
this.selectElement.tabIndex = -1;
this.selectElement.blur();
}
}
setFocusable(focusable) {
this.selectElement.tabIndex = focusable ? 0 : -1;
}
render(container) {
this.container = container;
container.classList.add('select-container');
container.appendChild(this.selectElement);
this.styleSelectElement();
}
initStyleSheet() {
const content = [];
// Style non-native select mode
if (this.styles.listFocusBackground) {
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { background-color: ${this.styles.listFocusBackground} !important; }`);
}
if (this.styles.listFocusForeground) {
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { color: ${this.styles.listFocusForeground} !important; }`);
}
if (this.styles.decoratorRightForeground) {
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:not(.focused) .option-decorator-right { color: ${this.styles.decoratorRightForeground}; }`);
}
if (this.styles.selectBackground && this.styles.selectBorder && this.styles.selectBorder !== this.styles.selectBackground) {
content.push(`.monaco-select-box-dropdown-container { border: 1px solid ${this.styles.selectBorder} } `);
content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-top { border-top: 1px solid ${this.styles.selectBorder} } `);
content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-bottom { border-bottom: 1px solid ${this.styles.selectBorder} } `);
}
else if (this.styles.selectListBorder) {
content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-top { border-top: 1px solid ${this.styles.selectListBorder} } `);
content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-bottom { border-bottom: 1px solid ${this.styles.selectListBorder} } `);
}
// Hover foreground - ignore for disabled options
if (this.styles.listHoverForeground) {
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:not(.option-disabled):not(.focused):hover { color: ${this.styles.listHoverForeground} !important; }`);
}
// Hover background - ignore for disabled options
if (this.styles.listHoverBackground) {
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:not(.option-disabled):not(.focused):hover { background-color: ${this.styles.listHoverBackground} !important; }`);
}
// Match quick input outline styles - ignore for disabled options
if (this.styles.listFocusOutline) {
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { outline: 1.6px dotted ${this.styles.listFocusOutline} !important; outline-offset: -1.6px !important; }`);
}
if (this.styles.listHoverOutline) {
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:not(.option-disabled):not(.focused):hover { outline: 1.6px dashed ${this.styles.listHoverOutline} !important; outline-offset: -1.6px !important; }`);
}
// Clear list styles on focus and on hover for disabled options
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.option-disabled.focused { background-color: transparent !important; color: inherit !important; outline: none !important; }`);
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.option-disabled:hover { background-color: transparent !important; color: inherit !important; outline: none !important; }`);
this.styleElement.textContent = content.join('\n');
}
styleSelectElement() {
const background = this.styles.selectBackground ?? '';
const foreground = this.styles.selectForeground ?? '';
const border = this.styles.selectBorder ?? '';
this.selectElement.style.backgroundColor = background;
this.selectElement.style.color = foreground;
this.selectElement.style.borderColor = border;
}
styleList() {
const background = this.styles.selectBackground ?? '';
const listBackground = asCssValueWithDefault(this.styles.selectListBackground, background);
this.selectDropDownListContainer.style.backgroundColor = listBackground;
this.selectionDetailsPane.style.backgroundColor = listBackground;
const optionsBorder = this.styles.focusBorder ?? '';
this.selectDropDownContainer.style.outlineColor = optionsBorder;
this.selectDropDownContainer.style.outlineOffset = '-1px';
this.selectList.style(this.styles);
}
createOption(value, index, disabled) {
const option = document.createElement('option');
option.value = value;
option.text = value;
option.disabled = !!disabled;
return option;
}
// ContextView dropdown methods
showSelectDropDown() {
this.selectionDetailsPane.textContent = '';
if (!this.contextViewProvider || this._isVisible) {
return;
}
// Lazily create and populate list only at open, moved from constructor
this.createSelectList(this.selectDropDownContainer);
this.setOptionsList();
// This allows us to flip the position based on measurement
// Set drop-down position above/below from required height and margins
// If pre-layout cannot fit at least one option do not show drop-down
this.contextViewProvider.showContextView({
getAnchor: () => this.selectElement,
render: (container) => this.renderSelectDropDown(container, true),
layout: () => {
this.layoutSelectDropDown();
},
onHide: () => {
this.selectDropDownContainer.classList.remove('visible');
this.selectElement.classList.remove('synthetic-focus');
},
anchorPosition: this._dropDownPosition
}, this.selectBoxOptions.optionsAsChildren ? this.container : undefined);
// Hide so we can relay out
this._isVisible = true;
this.hideSelectDropDown(false);
this.contextViewProvider.showContextView({
getAnchor: () => this.selectElement,
render: (container) => this.renderSelectDropDown(container),
layout: () => this.layoutSelectDropDown(),
onHide: () => {
this.selectDropDownContainer.classList.remove('visible');
this.selectElement.classList.remove('synthetic-focus');
},
anchorPosition: this._dropDownPosition
}, this.selectBoxOptions.optionsAsChildren ? this.container : undefined);
// Track initial selection the case user escape, blur
this._currentSelection = this.selected;
this._isVisible = true;
this.selectElement.setAttribute('aria-expanded', 'true');
}
hideSelectDropDown(focusSelect) {
if (!this.contextViewProvider || !this._isVisible) {
return;
}
this._isVisible = false;
this.selectElement.setAttribute('aria-expanded', 'false');
if (focusSelect) {
this.selectElement.focus();
}
this.contextViewProvider.hideContextView();
}
renderSelectDropDown(container, preLayoutPosition) {
container.appendChild(this.selectDropDownContainer);
// Pre-Layout allows us to change position
this.layoutSelectDropDown(preLayoutPosition);
return {
dispose: () => {
// contextView will dispose itself if moving from one View to another
this.selectDropDownContainer.remove(); // remove to take out the CSS rules we add
}
};
}
// Iterate over detailed descriptions, find max height
measureMaxDetailsHeight() {
let maxDetailsPaneHeight = 0;
this.options.forEach((_option, index) => {
this.updateDetail(index);
if (this.selectionDetailsPane.offsetHeight > maxDetailsPaneHeight) {
maxDetailsPaneHeight = this.selectionDetailsPane.offsetHeight;
}
});
return maxDetailsPaneHeight;
}
layoutSelectDropDown(preLayoutPosition) {
// Avoid recursion from layout called in onListFocus
if (this._skipLayout) {
return false;
}
// Layout ContextView drop down select list and container
// Have to manage our vertical overflow, sizing, position below or above
// Position has to be determined and set prior to contextView instantiation
if (this.selectList) {
// Make visible to enable measurements
this.selectDropDownContainer.classList.add('visible');
const window = getWindow(this.selectElement);
const selectPosition = getDomNodePagePosition(this.selectElement);
const styles = getWindow(this.selectElement).getComputedStyle(this.selectElement);
const verticalPadding = parseFloat(styles.getPropertyValue('--dropdown-padding-top')) + parseFloat(styles.getPropertyValue('--dropdown-padding-bottom'));
const maxSelectDropDownHeightBelow = (window.innerHeight - selectPosition.top - selectPosition.height - (this.selectBoxOptions.minBottomMargin || 0));
const maxSelectDropDownHeightAbove = (selectPosition.top - SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN);
// Determine optimal width - min(longest option), opt(parent select, excluding margins), max(ContextView controlled)
const selectWidth = this.selectElement.offsetWidth;
const selectMinWidth = this.setWidthControlElement(this.widthControlElement);
const selectOptimalWidth = Math.max(selectMinWidth, Math.round(selectWidth)).toString() + 'px';
this.selectDropDownContainer.style.width = selectOptimalWidth;
// Get initial list height and determine space above and below
this.selectList.getHTMLElement().style.height = '';
this.selectList.layout();
let listHeight = this.selectList.contentHeight;
if (this._hasDetails && this._cachedMaxDetailsHeight === undefined) {
this._cachedMaxDetailsHeight = this.measureMaxDetailsHeight();
}
const maxDetailsPaneHeight = this._hasDetails ? this._cachedMaxDetailsHeight : 0;
const minRequiredDropDownHeight = listHeight + verticalPadding + maxDetailsPaneHeight;
const maxVisibleOptionsBelow = ((Math.floor((maxSelectDropDownHeightBelow - verticalPadding - maxDetailsPaneHeight) / this.getHeight())));
const maxVisibleOptionsAbove = ((Math.floor((maxSelectDropDownHeightAbove - verticalPadding - maxDetailsPaneHeight) / this.getHeight())));
// If we are only doing pre-layout check/adjust position only
// Calculate vertical space available, flip up if insufficient
// Use reflected padding on parent select, ContextView style
// properties not available before DOM attachment
if (preLayoutPosition) {
// Check if select moved out of viewport , do not open
// If at least one option cannot be shown, don't open the drop-down or hide/remove if open
if ((selectPosition.top + selectPosition.height) > (window.innerHeight - 22)
|| selectPosition.top < SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN
|| ((maxVisibleOptionsBelow < 1) && (maxVisibleOptionsAbove < 1))) {
// Indicate we cannot open
return false;
}
// Determine if we have to flip up
// Always show complete list items - never more than Max available vertical height
if (maxVisibleOptionsBelow < SelectBoxList.DEFAULT_MINIMUM_VISIBLE_OPTIONS
&& maxVisibleOptionsAbove > maxVisibleOptionsBelow
&& this.options.length > maxVisibleOptionsBelow) {
this._dropDownPosition = 1 /* AnchorPosition.ABOVE */;
this.selectDropDownListContainer.remove();
this.selectionDetailsPane.remove();
this.selectDropDownContainer.appendChild(this.selectionDetailsPane);
this.selectDropDownContainer.appendChild(this.selectDropDownListContainer);
this.selectionDetailsPane.classList.remove('border-top');
this.selectionDetailsPane.classList.add('border-bottom');
}
else {
this._dropDownPosition = 0 /* AnchorPosition.BELOW */;
this.selectDropDownListContainer.remove();
this.selectionDetailsPane.remove();
this.selectDropDownContainer.appendChild(this.selectDropDownListContainer);
this.selectDropDownContainer.appendChild(this.selectionDetailsPane);
this.selectionDetailsPane.classList.remove('border-bottom');
this.selectionDetailsPane.classList.add('border-top');
}
// Do full layout on showSelectDropDown only
return true;
}
// Check if select out of viewport or cutting into status bar
if ((selectPosition.top + selectPosition.height) > (window.innerHeight - 22)
|| selectPosition.top < SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN
|| (this._dropDownPosition === 0 /* AnchorPosition.BELOW */ && maxVisibleOptionsBelow < 1)
|| (this._dropDownPosition === 1 /* AnchorPosition.ABOVE */ && maxVisibleOptionsAbove < 1)) {
// Cannot properly layout, close and hide
this.hideSelectDropDown(true);
return false;
}
// SetUp list dimensions and layout - account for container padding
// Use position to check above or below available space
if (this._dropDownPosition === 0 /* AnchorPosition.BELOW */) {
if (this._isVisible && maxVisibleOptionsBelow + maxVisibleOptionsAbove < 1) {
// If drop-down is visible, must be doing a DOM re-layout, hide since we don't fit
// Hide drop-down, hide contextview, focus on parent select
this.hideSelectDropDown(true);
return false;
}
// Adjust list height to max from select bottom to margin (default/minBottomMargin)
if (minRequiredDropDownHeight > maxSelectDropDownHeightBelow) {
listHeight = (maxVisibleOptionsBelow * this.getHeight());
}
}
else {
if (minRequiredDropDownHeight > maxSelectDropDownHeightAbove) {
listHeight = (maxVisibleOptionsAbove * this.getHeight());
}
}
// Set adjusted list height and relayout
this.selectList.layout(listHeight);
this.selectList.domFocus();
// Finally set focus on selected item
if (this.selectList.length > 0) {
this.selectList.setFocus([this.selected || 0]);
this.selectList.reveal(this.selectList.getFocus()[0] || 0);
}
if (this._hasDetails) {
// Leave the selectDropDownContainer to size itself according to children (list + details) - #57447
this.selectList.getHTMLElement().style.height = (listHeight + verticalPadding) + 'px';
this.selectDropDownContainer.style.height = '';
}
else {
this.selectDropDownContainer.style.height = (listHeight + verticalPadding) + 'px';
}
this.updateDetail(this.selected);
this.selectDropDownContainer.style.width = selectOptimalWidth;
// Maintain focus outline on parent select as well as list container - tabindex for focus
this.selectDropDownListContainer.setAttribute('tabindex', '0');
this.selectElement.classList.add('synthetic-focus');
this.selectDropDownContainer.classList.add('synthetic-focus');
return true;
}
else {
return false;
}
}
setWidthControlElement(container) {
let elementWidth = 0;
if (container) {
let longest = 0;
let longestLength = 0;
this.options.forEach((option, index) => {
const detailLength = !!option.detail ? option.detail.length : 0;
const rightDecoratorLength = !!option.decoratorRight ? option.decoratorRight.length : 0;
const len = option.text.length + detailLength + rightDecoratorLength;
if (len > longestLength) {
longest = index;
longestLength = len;
}
});
container.textContent = this.options[longest].text + (!!this.options[longest].decoratorRight ? (this.options[longest].decoratorRight + ' ') : '');
elementWidth = getTotalWidth(container);
}
return elementWidth;
}
createSelectList(parent) {
// If we have already constructive list on open, skip
if (this.selectList) {
return;
}
// SetUp container for list
this.selectDropDownListContainer = append(parent, $('.select-box-dropdown-list-container'));
this.listRenderer = new SelectListRenderer();
this.selectList = this._register(new List('SelectBoxCustom', this.selectDropDownListContainer, this, [this.listRenderer], {
useShadows: false,
verticalScrollMode: 3 /* ScrollbarVisibility.Visible */,
keyboardSupport: false,
mouseSupport: false,
accessibilityProvider: {
getAriaLabel: element => {
let label = element.text;
if (element.detail) {
label += `. ${element.detail}`;
}
if (element.decoratorRight) {
label += `. ${element.decoratorRight}`;
}
if (element.description) {
label += `. ${element.description}`;
}
return label;
},
getWidgetAriaLabel: () => localize(16, "Select Box"),
getRole: () => isMacintosh ? '' : 'option',
getWidgetRole: () => 'listbox'
}
}));
if (this.selectBoxOptions.ariaLabel) {
this.selectList.ariaLabel = this.selectBoxOptions.ariaLabel;
}
// SetUp list keyboard controller - control navigation, disabled items, focus
const onKeyDown = this._register(new DomEmitter(this.selectDropDownListContainer, 'keydown'));
const onSelectDropDownKeyDown = Event.chain(onKeyDown.event, $ => $.filter(() => this.selectList.length > 0)
.map(e => new StandardKeyboardEvent(e)));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 3 /* KeyCode.Enter */))(this.onEnter, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 2 /* KeyCode.Tab */))(this.onEnter, this)); // Tab should behave the same as enter, #79339
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 9 /* KeyCode.Escape */))(this.onEscape, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 16 /* KeyCode.UpArrow */))(this.onUpArrow, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 18 /* KeyCode.DownArrow */))(this.onDownArrow, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 12 /* KeyCode.PageDown */))(this.onPageDown, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 11 /* KeyCode.PageUp */))(this.onPageUp, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 14 /* KeyCode.Home */))(this.onHome, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 13 /* KeyCode.End */))(this.onEnd, this));
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => (e.keyCode >= 21 /* KeyCode.Digit0 */ && e.keyCode <= 56 /* KeyCode.KeyZ */) || (e.keyCode >= 85 /* KeyCode.Semicolon */ && e.keyCode <= 113 /* KeyCode.NumpadDivide */)))(this.onCharacter, this));
// SetUp list mouse controller - control navigation, disabled items, focus
this._register(addDisposableListener(this.selectList.getHTMLElement(), EventType.POINTER_UP, e => this.onPointerUp(e)));
this._register(this.selectList.onMouseOver(e => typeof e.index !== 'undefined' && this.selectList.setFocus([e.index])));
this._register(this.selectList.onDidChangeFocus(e => this.onListFocus(e)));
this._register(addDisposableListener(this.selectDropDownContainer, EventType.FOCUS_OUT, e => {
if (!this._isVisible || isAncestor(e.relatedTarget, this.selectDropDownContainer)) {
return;
}
this.onListBlur();
}));
this.selectList.getHTMLElement().setAttribute('aria-label', this.selectBoxOptions.ariaLabel || '');
this.selectList.getHTMLElement().setAttribute('aria-expanded', 'true');
this.styleList();
}
// List methods
// List mouse controller - active exit, select option, fire onDidSelect if change, return focus to parent select
// Also takes in touchend events
onPointerUp(e) {
if (!this.selectList.length) {
return;
}
EventHelper.stop(e);
const target = e.target;
if (!target) {
return;
}
// Check our mouse event is on an option (not scrollbar)
if (target.classList.contains('slider')) {
return;
}
const listRowElement = target.closest('.monaco-list-row');
if (!listRowElement) {
return;
}
const index = Number(listRowElement.getAttribute('data-index'));
const disabled = listRowElement.classList.contains('option-disabled');
// Ignore mouse selection of disabled options
if (index >= 0 && index < this.options.length && !disabled) {
this.selected = index;
this.select(this.selected);
this.selectList.setFocus([this.selected]);
this.selectList.reveal(this.selectList.getFocus()[0]);
// Only fire if selection change
if (this.selected !== this._currentSelection) {
// Set current = selected
this._currentSelection = this.selected;
this._onDidSelect.fire({
index: this.selectElement.selectedIndex,
selected: this.options[this.selected].text
});
if (!!this.options[this.selected] && !!this.options[this.selected].text) {
this.setTitle(this.options[this.selected].text);
}
}
this.hideSelectDropDown(true);
}
}
// List Exit - passive - implicit no selection change, hide drop-down
onListBlur() {
if (this._sticky) {
return;
}
if (this.selected !== this._currentSelection) {
// Reset selected to current if no change
this.select(this._currentSelection);
}
this.hideSelectDropDown(false);
}
renderDescriptionMarkdown(text, actionHandler) {
const cleanRenderedMarkdown = (element) => {
for (let i = 0; i < element.childNodes.length; i++) {
const child = element.childNodes.item(i);
const tagName = child.tagName && child.tagName.toLowerCase();
if (tagName === 'img') {
child.remove();
}
else {
cleanRenderedMarkdown(child);
}
}
};
const rendered = renderMarkdown({ value: text, supportThemeIcons: true }, { actionHandler });
rendered.element.classList.add('select-box-description-markdown');
cleanRenderedMarkdown(rendered.element);
return rendered;
}
// List Focus Change - passive - update details pane with newly focused element's data
onListFocus(e) {
// Skip during initial layout
if (!this._isVisible || !this._hasDetails) {
return;
}
this.updateDetail(e.indexes[0]);
}
updateDetail(selectedIndex) {
// Reset
this._selectionDetailsDisposables.clear();
this.selectionDetailsPane.textContent = '';
const option = this.options[selectedIndex];
const description = option?.description ?? '';
const descriptionIsMarkdown = option?.descriptionIsMarkdown ?? false;
if (description) {
if (descriptionIsMarkdown) {
const actionHandler = option.descriptionMarkdownActionHandler;
const result = this._selectionDetailsDisposables.add(this.renderDescriptionMarkdown(description, actionHandler));
this.selectionDetailsPane.appendChild(result.element);
}
else {
this.selectionDetailsPane.textContent = description;
}
this.selectionDetailsPane.style.display = 'block';
}
else {
this.selectionDetailsPane.style.display = 'none';
}
// Avoid recursion
this._skipLayout = true;
this.contextViewProvider.layout();
this._skipLayout = false;
}
// List keyboard controller
// List exit - active - hide ContextView dropdown, reset selection, return focus to parent select
onEscape(e) {
EventHelper.stop(e);
// Reset selection to value when opened
this.select(this._currentSelection);
this.hideSelectDropDown(true);
}
// List exit - active - hide ContextView dropdown, return focus to parent select, fire onDidSelect if change
onEnter(e) {
EventHelper.stop(e);
// Only fire if selection change
if (this.selected !== this._currentSelection) {
this._currentSelection = this.selected;
this._onDidSelect.fire({
index: this.selectElement.selectedIndex,
selected: this.options[this.selected].text
});
if (!!this.options[this.selected] && !!this.options[this.selected].text) {
this.setTitle(this.options[this.selected].text);
}
}
this.hideSelectDropDown(true);
}
// List navigation - have to handle a disabled option (jump over)
onDownArrow(e) {
if (this.selected < this.options.length - 1) {
EventHelper.stop(e, true);
// Skip disabled options
const nextOptionDisabled = this.options[this.selected + 1].isDisabled;
if (nextOptionDisabled && this.options.length > this.selected + 2) {
this.selected += 2;
}
else if (nextOptionDisabled) {
return;
}
else {
this.selected++;
}
// Set focus/selection - only fire event when closing drop-down or on blur
this.select(this.selected);
this.selectList.setFocus([this.selected]);
this.selectList.reveal(this.selectList.getFocus()[0]);
}
}
onUpArrow(e) {
if (this.selected > 0) {
EventHelper.stop(e, true);
// Skip disabled options
const previousOptionDisabled = this.options[this.selected - 1].isDisabled;
if (previousOptionDisabled && this.selected > 1) {
this.selected -= 2;
}
else {
this.selected--;
}
// Set focus/selection - only fire event when closing drop-down or on blur
this.select(this.selected);
this.selectList.setFocus([this.selected]);
this.selectList.reveal(this.selectList.getFocus()[0]);
}
}
onPageUp(e) {
EventHelper.stop(e);
this.selectList.focusPreviousPage();
// Allow scrolling to settle
setTimeout(() => {
this.selected = this.selectList.getFocus()[0];
// Shift selection down if we land on a disabled option
if (this.options[this.selected].isDisabled && this.selected < this.options.length - 1) {
this.selected++;
this.selectList.setFocus([this.selected]);
}
this.selectList.reveal(this.selected);
this.select(this.selected);
}, 1);
}
onPageDown(e) {
EventHelper.stop(e);
this.selectList.focusNextPage();
// Allow scrolling to settle
setTimeout(() => {
this.selected = this.selectList.getFocus()[0];
// Shift selection up if we land on a disabled option
if (this.options[this.selected].isDisabled && this.selected > 0) {
this.selected--;
this.selectList.setFocus([this.selected]);
}
this.selectList.reveal(this.selected);
this.select(this.selected);
}, 1);
}
onHome(e) {
EventHelper.stop(e);
if (this.options.length < 2) {
return;
}
this.selected = 0;
if (this.options[this.selected].isDisabled && this.selected > 1) {
this.selected++;
}
this.selectList.setFocus([this.selected]);
this.selectList.reveal(this.selected);
this.select(this.selected);
}
onEnd(e) {
EventHelper.stop(e);
if (this.options.length < 2) {
return;
}
this.selected = this.options.length - 1;
if (this.options[this.selected].isDisabled && this.selected > 1) {
this.selected--;
}
this.selectList.setFocus([this.selected]);
this.selectList.reveal(this.selected);
this.select(this.selected);
}
// Mimic option first character navigation of native select
onCharacter(e) {
const ch = KeyCodeUtils.toString(e.keyCode);
let optionIndex = -1;
for (let i = 0; i < this.options.length - 1; i++) {
optionIndex = (i + this.selected + 1) % this.options.length;
if (this.options[optionIndex].text.charAt(0).toUpperCase() === ch && !this.options[optionIndex].isDisabled) {
this.select(optionIndex);
this.selectList.setFocus([optionIndex]);
this.selectList.reveal(this.selectList.getFocus()[0]);
EventHelper.stop(e);
break;
}
}
}
dispose() {
this.hideSelectDropDown(false);
super.dispose();
}
}
export { SelectBoxList };
@@ -0,0 +1,142 @@
import { addDisposableListener, addStandardDisposableListener, EventHelper } from '../../dom.js';
import { Gesture, EventType } from '../../touch.js';
import { equals } from '../../../common/arrays.js';
import { Emitter } from '../../../common/event.js';
import { Disposable } from '../../../common/lifecycle.js';
import { isMacintosh } from '../../../common/platform.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class SelectBoxNative extends Disposable {
constructor(options, selected, styles, selectBoxOptions) {
super();
this.selected = 0;
this.selectBoxOptions = selectBoxOptions || Object.create(null);
this.options = [];
this.selectElement = document.createElement('select');
this.selectElement.className = 'monaco-select-box';
if (typeof this.selectBoxOptions.ariaLabel === 'string') {
this.selectElement.setAttribute('aria-label', this.selectBoxOptions.ariaLabel);
}
if (typeof this.selectBoxOptions.ariaDescription === 'string') {
this.selectElement.setAttribute('aria-description', this.selectBoxOptions.ariaDescription);
}
this._onDidSelect = this._register(new Emitter());
this.styles = styles;
this.registerListeners();
this.setOptions(options, selected);
}
registerListeners() {
this._register(Gesture.addTarget(this.selectElement));
[EventType.Tap].forEach(eventType => {
this._register(addDisposableListener(this.selectElement, eventType, (e) => {
this.selectElement.focus();
}));
});
this._register(addStandardDisposableListener(this.selectElement, 'click', (e) => {
EventHelper.stop(e, true);
}));
this._register(addStandardDisposableListener(this.selectElement, 'change', (e) => {
this.selectElement.title = e.target.value;
this._onDidSelect.fire({
index: e.target.selectedIndex,
selected: e.target.value
});
}));
this._register(addStandardDisposableListener(this.selectElement, 'keydown', (e) => {
let showSelect = false;
if (isMacintosh) {
if (e.keyCode === 18 /* KeyCode.DownArrow */ || e.keyCode === 16 /* KeyCode.UpArrow */ || e.keyCode === 10 /* KeyCode.Space */) {
showSelect = true;
}
}
else {
if (e.keyCode === 18 /* KeyCode.DownArrow */ && e.altKey || e.keyCode === 10 /* KeyCode.Space */ || e.keyCode === 3 /* KeyCode.Enter */) {
showSelect = true;
}
}
if (showSelect) {
// Space, Enter, is used to expand select box, do not propagate it (prevent action bar action run)
e.stopPropagation();
}
}));
}
get onDidSelect() {
return this._onDidSelect.event;
}
setOptions(options, selected) {
if (!this.options || !equals(this.options, options)) {
this.options = options;
this.selectElement.options.length = 0;
this.options.forEach((option, index) => {
this.selectElement.add(this.createOption(option.text, index, option.isDisabled));
});
}
if (selected !== undefined) {
this.select(selected);
}
}
select(index) {
if (this.options.length === 0) {
this.selected = 0;
}
else if (index >= 0 && index < this.options.length) {
this.selected = index;
}
else if (index > this.options.length - 1) {
// Adjust index to end of list
// This could make client out of sync with the select
this.select(this.options.length - 1);
}
else if (this.selected < 0) {
this.selected = 0;
}
this.selectElement.selectedIndex = this.selected;
if ((this.selected < this.options.length) && typeof this.options[this.selected].text === 'string') {
this.selectElement.title = this.options[this.selected].text;
}
else {
this.selectElement.title = '';
}
}
focus() {
if (this.selectElement) {
this.selectElement.tabIndex = 0;
this.selectElement.focus();
}
}
blur() {
if (this.selectElement) {
this.selectElement.tabIndex = -1;
this.selectElement.blur();
}
}
setFocusable(focusable) {
this.selectElement.tabIndex = focusable ? 0 : -1;
}
render(container) {
container.classList.add('select-container');
container.appendChild(this.selectElement);
this.setOptions(this.options, this.selected);
this.applyStyles();
}
applyStyles() {
// Style native select
if (this.selectElement) {
this.selectElement.style.backgroundColor = this.styles.selectBackground ?? '';
this.selectElement.style.color = this.styles.selectForeground ?? '';
this.selectElement.style.borderColor = this.styles.selectBorder ?? '';
}
}
createOption(value, index, disabled) {
const option = document.createElement('option');
option.value = value;
option.text = value;
option.disabled = !!disabled;
return option;
}
}
export { SelectBoxNative };
@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .zone-widget .codicon.codicon-error,
.markers-panel .marker-icon.error, .markers-panel .marker-icon .codicon.codicon-error,
.text-search-provider-messages .providerMessage .codicon.codicon-error,
.extensions-viewlet > .extensions .codicon.codicon-error,
.extension-editor .codicon.codicon-error,
.chat-attached-context-attachment .codicon.codicon-error {
color: var(--vscode-problemsErrorIcon-foreground);
}
.monaco-editor .zone-widget .codicon.codicon-warning,
.markers-panel .marker-icon.warning, .markers-panel .marker-icon .codicon.codicon-warning,
.text-search-provider-messages .providerMessage .codicon.codicon-warning,
.extensions-viewlet > .extensions .codicon.codicon-warning,
.extension-editor .codicon.codicon-warning,
.preferences-editor .codicon.codicon-warning {
color: var(--vscode-problemsWarningIcon-foreground);
}
.monaco-editor .zone-widget .codicon.codicon-info,
.markers-panel .marker-icon.info, .markers-panel .marker-icon .codicon.codicon-info,
.text-search-provider-messages .providerMessage .codicon.codicon-info,
.extensions-viewlet > .extensions .codicon.codicon-info,
.extension-editor .codicon.codicon-info {
color: var(--vscode-problemsInfoIcon-foreground);
}
@@ -0,0 +1,29 @@
import './media/severityIcon.css';
import { Codicon } from '../../../common/codicons.js';
import { ThemeIcon } from '../../../common/themables.js';
import Severity from '../../../common/severity.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var SeverityIcon;
(function (SeverityIcon) {
function className(severity) {
switch (severity) {
case Severity.Ignore:
return 'severity-ignore ' + ThemeIcon.asClassName(Codicon.info);
case Severity.Info:
return ThemeIcon.asClassName(Codicon.info);
case Severity.Warning:
return ThemeIcon.asClassName(Codicon.warning);
case Severity.Error:
return ThemeIcon.asClassName(Codicon.error);
default:
return '';
}
}
SeverityIcon.className = className;
})(SeverityIcon || (SeverityIcon = {}));
export { SeverityIcon };
@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-split-view2 {
position: relative;
width: 100%;
height: 100%;
}
.monaco-split-view2 > .sash-container {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
}
.monaco-split-view2 > .sash-container > .monaco-sash {
pointer-events: initial;
}
.monaco-split-view2 > .monaco-scrollable-element {
width: 100%;
height: 100%;
}
.monaco-split-view2 > .monaco-scrollable-element > .split-view-container {
width: 100%;
height: 100%;
white-space: nowrap;
position: relative;
}
.monaco-split-view2 > .monaco-scrollable-element > .split-view-container > .split-view-view {
white-space: initial;
position: absolute;
}
.monaco-split-view2 > .monaco-scrollable-element > .split-view-container > .split-view-view:not(.visible) {
display: none;
}
.monaco-split-view2.vertical > .monaco-scrollable-element > .split-view-container > .split-view-view {
width: 100%;
}
.monaco-split-view2.horizontal > .monaco-scrollable-element > .split-view-container > .split-view-view {
height: 100%;
}
.monaco-split-view2.separator-border > .monaco-scrollable-element > .split-view-container > .split-view-view:not(:first-child)::before {
content: ' ';
position: absolute;
top: 0;
left: 0;
z-index: 5;
pointer-events: none;
background-color: var(--separator-border);
}
.monaco-split-view2.separator-border.horizontal > .monaco-scrollable-element > .split-view-container > .split-view-view:not(:first-child)::before {
height: 100%;
width: 1px;
}
.monaco-split-view2.separator-border.vertical > .monaco-scrollable-element > .split-view-container > .split-view-view:not(:first-child)::before {
height: 1px;
width: 100%;
}
@@ -0,0 +1,829 @@
import { append, $, scheduleAtNextAnimationFrame, getWindow, addDisposableListener } from '../../dom.js';
import { DomEmitter } from '../../event.js';
import { Sash } from '../sash/sash.js';
import { SmoothScrollableElement } from '../scrollbar/scrollableElement.js';
import { range, pushToStart, pushToEnd } from '../../../common/arrays.js';
import { Color } from '../../../common/color.js';
import { Emitter, Event } from '../../../common/event.js';
import { Disposable, combinedDisposable, toDisposable, dispose } from '../../../common/lifecycle.js';
import { clamp } from '../../../common/numbers.js';
import { Scrollable } from '../../../common/scrollable.js';
import { isUndefined } from '../../../common/types.js';
import './splitview.css';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const defaultStyles = {
separatorBorder: Color.transparent
};
class ViewItem {
set size(size) {
this._size = size;
}
get size() {
return this._size;
}
get visible() {
return typeof this._cachedVisibleSize === 'undefined';
}
setVisible(visible, size) {
if (visible === this.visible) {
return;
}
if (visible) {
this.size = clamp(this._cachedVisibleSize, this.viewMinimumSize, this.viewMaximumSize);
this._cachedVisibleSize = undefined;
}
else {
this._cachedVisibleSize = typeof size === 'number' ? size : this.size;
this.size = 0;
}
this.container.classList.toggle('visible', visible);
try {
this.view.setVisible?.(visible);
}
catch (e) {
console.error('Splitview: Failed to set visible view');
console.error(e);
}
}
get minimumSize() { return this.visible ? this.view.minimumSize : 0; }
get viewMinimumSize() { return this.view.minimumSize; }
get maximumSize() { return this.visible ? this.view.maximumSize : 0; }
get viewMaximumSize() { return this.view.maximumSize; }
get priority() { return this.view.priority; }
get proportionalLayout() { return this.view.proportionalLayout ?? true; }
get snap() { return !!this.view.snap; }
set enabled(enabled) {
this.container.style.pointerEvents = enabled ? '' : 'none';
}
constructor(container, view, size, disposable) {
this.container = container;
this.view = view;
this.disposable = disposable;
this._cachedVisibleSize = undefined;
if (typeof size === 'number') {
this._size = size;
this._cachedVisibleSize = undefined;
container.classList.add('visible');
}
else {
this._size = 0;
this._cachedVisibleSize = size.cachedVisibleSize;
}
}
layout(offset, layoutContext) {
this.layoutContainer(offset);
try {
this.view.layout(this.size, offset, layoutContext);
}
catch (e) {
console.error('Splitview: Failed to layout view');
console.error(e);
}
}
dispose() {
this.disposable.dispose();
}
}
class VerticalViewItem extends ViewItem {
layoutContainer(offset) {
this.container.style.top = `${offset}px`;
this.container.style.height = `${this.size}px`;
}
}
class HorizontalViewItem extends ViewItem {
layoutContainer(offset) {
this.container.style.left = `${offset}px`;
this.container.style.width = `${this.size}px`;
}
}
var State;
(function (State) {
State[State["Idle"] = 0] = "Idle";
State[State["Busy"] = 1] = "Busy";
})(State || (State = {}));
var Sizing;
(function (Sizing) {
/**
* When adding or removing views, distribute the delta space among
* all other views.
*/
Sizing.Distribute = { type: 'distribute' };
/**
* When adding or removing views, split the delta space with another
* specific view, indexed by the provided `index`.
*/
function Split(index) { return { type: 'split', index }; }
Sizing.Split = Split;
/**
* When adding a view, use DistributeSizing when all pre-existing views are
* distributed evenly, otherwise use SplitSizing.
*/
function Auto(index) { return { type: 'auto', index }; }
Sizing.Auto = Auto;
/**
* When adding or removing views, assume the view is invisible.
*/
function Invisible(cachedVisibleSize) { return { type: 'invisible', cachedVisibleSize }; }
Sizing.Invisible = Invisible;
})(Sizing || (Sizing = {}));
/**
* The {@link SplitView} is the UI component which implements a one dimensional
* flex-like layout algorithm for a collection of {@link IView} instances, which
* are essentially HTMLElement instances with the following size constraints:
*
* - {@link IView.minimumSize}
* - {@link IView.maximumSize}
* - {@link IView.priority}
* - {@link IView.snap}
*
* In case the SplitView doesn't have enough size to fit all views, it will overflow
* its content with a scrollbar.
*
* In between each pair of views there will be a {@link Sash} allowing the user
* to resize the views, making sure the constraints are respected.
*
* An optional {@link TLayoutContext layout context type} may be used in order to
* pass along layout contextual data from the {@link SplitView.layout} method down
* to each view's {@link IView.layout} calls.
*
* Features:
* - Flex-like layout algorithm
* - Snap support
* - Orthogonal sash support, for corner sashes
* - View hide/show support
* - View swap/move support
* - Alt key modifier behavior, macOS style
*/
class SplitView extends Disposable {
get orthogonalStartSash() { return this._orthogonalStartSash; }
get orthogonalEndSash() { return this._orthogonalEndSash; }
get startSnappingEnabled() { return this._startSnappingEnabled; }
get endSnappingEnabled() { return this._endSnappingEnabled; }
/**
* A reference to a sash, perpendicular to all sashes in this {@link SplitView},
* located at the left- or top-most side of the SplitView.
* Corner sashes will be created automatically at the intersections.
*/
set orthogonalStartSash(sash) {
for (const sashItem of this.sashItems) {
sashItem.sash.orthogonalStartSash = sash;
}
this._orthogonalStartSash = sash;
}
/**
* A reference to a sash, perpendicular to all sashes in this {@link SplitView},
* located at the right- or bottom-most side of the SplitView.
* Corner sashes will be created automatically at the intersections.
*/
set orthogonalEndSash(sash) {
for (const sashItem of this.sashItems) {
sashItem.sash.orthogonalEndSash = sash;
}
this._orthogonalEndSash = sash;
}
/**
* Enable/disable snapping at the beginning of this {@link SplitView}.
*/
set startSnappingEnabled(startSnappingEnabled) {
if (this._startSnappingEnabled === startSnappingEnabled) {
return;
}
this._startSnappingEnabled = startSnappingEnabled;
this.updateSashEnablement();
}
/**
* Enable/disable snapping at the end of this {@link SplitView}.
*/
set endSnappingEnabled(endSnappingEnabled) {
if (this._endSnappingEnabled === endSnappingEnabled) {
return;
}
this._endSnappingEnabled = endSnappingEnabled;
this.updateSashEnablement();
}
/**
* Create a new {@link SplitView} instance.
*/
constructor(container, options = {}) {
super();
this.size = 0;
this._contentSize = 0;
this.proportions = undefined;
this.viewItems = [];
this.sashItems = []; // used in tests
this.state = State.Idle;
this._onDidSashChange = this._register(new Emitter());
this._onDidSashReset = this._register(new Emitter());
this._startSnappingEnabled = true;
this._endSnappingEnabled = true;
/**
* Fires whenever the user resizes a {@link Sash sash}.
*/
this.onDidSashChange = this._onDidSashChange.event;
/**
* Fires whenever the user double clicks a {@link Sash sash}.
*/
this.onDidSashReset = this._onDidSashReset.event;
this.orientation = options.orientation ?? 0 /* Orientation.VERTICAL */;
this.inverseAltBehavior = options.inverseAltBehavior ?? false;
this.proportionalLayout = options.proportionalLayout ?? true;
this.getSashOrthogonalSize = options.getSashOrthogonalSize;
this.el = document.createElement('div');
this.el.classList.add('monaco-split-view2');
this.el.classList.add(this.orientation === 0 /* Orientation.VERTICAL */ ? 'vertical' : 'horizontal');
container.appendChild(this.el);
this.sashContainer = append(this.el, $('.sash-container'));
this.viewContainer = $('.split-view-container');
this.scrollable = this._register(new Scrollable({
forceIntegerValues: true,
smoothScrollDuration: 125,
scheduleAtNextAnimationFrame: callback => scheduleAtNextAnimationFrame(getWindow(this.el), callback),
}));
this.scrollableElement = this._register(new SmoothScrollableElement(this.viewContainer, {
vertical: this.orientation === 0 /* Orientation.VERTICAL */ ? (options.scrollbarVisibility ?? 1 /* ScrollbarVisibility.Auto */) : 2 /* ScrollbarVisibility.Hidden */,
horizontal: this.orientation === 1 /* Orientation.HORIZONTAL */ ? (options.scrollbarVisibility ?? 1 /* ScrollbarVisibility.Auto */) : 2 /* ScrollbarVisibility.Hidden */
}, this.scrollable));
// https://github.com/microsoft/vscode/issues/157737
const onDidScrollViewContainer = this._register(new DomEmitter(this.viewContainer, 'scroll')).event;
this._register(onDidScrollViewContainer(_ => {
const position = this.scrollableElement.getScrollPosition();
const scrollLeft = Math.abs(this.viewContainer.scrollLeft - position.scrollLeft) <= 1 ? undefined : this.viewContainer.scrollLeft;
const scrollTop = Math.abs(this.viewContainer.scrollTop - position.scrollTop) <= 1 ? undefined : this.viewContainer.scrollTop;
if (scrollLeft !== undefined || scrollTop !== undefined) {
this.scrollableElement.setScrollPosition({ scrollLeft, scrollTop });
}
}));
this.onDidScroll = this.scrollableElement.onScroll;
this._register(this.onDidScroll(e => {
if (e.scrollTopChanged) {
this.viewContainer.scrollTop = e.scrollTop;
}
if (e.scrollLeftChanged) {
this.viewContainer.scrollLeft = e.scrollLeft;
}
}));
append(this.el, this.scrollableElement.getDomNode());
this.style(options.styles || defaultStyles);
// We have an existing set of view, add them now
if (options.descriptor) {
this.size = options.descriptor.size;
options.descriptor.views.forEach((viewDescriptor, index) => {
const sizing = isUndefined(viewDescriptor.visible) || viewDescriptor.visible ? viewDescriptor.size : { type: 'invisible', cachedVisibleSize: viewDescriptor.size };
const view = viewDescriptor.view;
this.doAddView(view, sizing, index, true);
});
// Initialize content size and proportions for first layout
this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
this.saveProportions();
}
}
style(styles) {
if (styles.separatorBorder.isTransparent()) {
this.el.classList.remove('separator-border');
this.el.style.removeProperty('--separator-border');
}
else {
this.el.classList.add('separator-border');
this.el.style.setProperty('--separator-border', styles.separatorBorder.toString());
}
}
/**
* Add a {@link IView view} to this {@link SplitView}.
*
* @param view The view to add.
* @param size Either a fixed size, or a dynamic {@link Sizing} strategy.
* @param index The index to insert the view on.
* @param skipLayout Whether layout should be skipped.
*/
addView(view, size, index = this.viewItems.length, skipLayout) {
this.doAddView(view, size, index, skipLayout);
}
/**
* Layout the {@link SplitView}.
*
* @param size The entire size of the {@link SplitView}.
* @param layoutContext An optional layout context to pass along to {@link IView views}.
*/
layout(size, layoutContext) {
const previousSize = Math.max(this.size, this._contentSize);
this.size = size;
this.layoutContext = layoutContext;
if (!this.proportions) {
const indexes = range(this.viewItems.length);
const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */);
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
this.resize(this.viewItems.length - 1, size - previousSize, undefined, lowPriorityIndexes, highPriorityIndexes);
}
else {
let total = 0;
for (let i = 0; i < this.viewItems.length; i++) {
const item = this.viewItems[i];
const proportion = this.proportions[i];
if (typeof proportion === 'number') {
total += proportion;
}
else {
size -= item.size;
}
}
for (let i = 0; i < this.viewItems.length; i++) {
const item = this.viewItems[i];
const proportion = this.proportions[i];
if (typeof proportion === 'number' && total > 0) {
item.size = clamp(Math.round(proportion * size / total), item.minimumSize, item.maximumSize);
}
}
}
this.distributeEmptySpace();
this.layoutViews();
}
saveProportions() {
if (this.proportionalLayout && this._contentSize > 0) {
this.proportions = this.viewItems.map(v => v.proportionalLayout && v.visible ? v.size / this._contentSize : undefined);
}
}
onSashStart({ sash, start, alt }) {
for (const item of this.viewItems) {
item.enabled = false;
}
const index = this.sashItems.findIndex(item => item.sash === sash);
// This way, we can press Alt while we resize a sash, macOS style!
const disposable = combinedDisposable(addDisposableListener(this.el.ownerDocument.body, 'keydown', e => resetSashDragState(this.sashDragState.current, e.altKey)), addDisposableListener(this.el.ownerDocument.body, 'keyup', () => resetSashDragState(this.sashDragState.current, false)));
const resetSashDragState = (start, alt) => {
const sizes = this.viewItems.map(i => i.size);
let minDelta = Number.NEGATIVE_INFINITY;
let maxDelta = Number.POSITIVE_INFINITY;
if (this.inverseAltBehavior) {
alt = !alt;
}
if (alt) {
// When we're using the last sash with Alt, we're resizing
// the view to the left/up, instead of right/down as usual
// Thus, we must do the inverse of the usual
const isLastSash = index === this.sashItems.length - 1;
if (isLastSash) {
const viewItem = this.viewItems[index];
minDelta = (viewItem.minimumSize - viewItem.size) / 2;
maxDelta = (viewItem.maximumSize - viewItem.size) / 2;
}
else {
const viewItem = this.viewItems[index + 1];
minDelta = (viewItem.size - viewItem.maximumSize) / 2;
maxDelta = (viewItem.size - viewItem.minimumSize) / 2;
}
}
let snapBefore;
let snapAfter;
if (!alt) {
const upIndexes = range(index, -1);
const downIndexes = range(index + 1, this.viewItems.length);
const minDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].minimumSize - sizes[i]), 0);
const maxDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].viewMaximumSize - sizes[i]), 0);
const maxDeltaDown = downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].minimumSize), 0);
const minDeltaDown = downIndexes.length === 0 ? Number.NEGATIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].viewMaximumSize), 0);
const minDelta = Math.max(minDeltaUp, minDeltaDown);
const maxDelta = Math.min(maxDeltaDown, maxDeltaUp);
const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
if (typeof snapBeforeIndex === 'number') {
const viewItem = this.viewItems[snapBeforeIndex];
const halfSize = Math.floor(viewItem.viewMinimumSize / 2);
snapBefore = {
index: snapBeforeIndex,
limitDelta: viewItem.visible ? minDelta - halfSize : minDelta + halfSize,
size: viewItem.size
};
}
if (typeof snapAfterIndex === 'number') {
const viewItem = this.viewItems[snapAfterIndex];
const halfSize = Math.floor(viewItem.viewMinimumSize / 2);
snapAfter = {
index: snapAfterIndex,
limitDelta: viewItem.visible ? maxDelta + halfSize : maxDelta - halfSize,
size: viewItem.size
};
}
}
this.sashDragState = { start, current: start, index, sizes, minDelta, maxDelta, alt, snapBefore, snapAfter, disposable };
};
resetSashDragState(start, alt);
}
onSashChange({ current }) {
const { index, start, sizes, alt, minDelta, maxDelta, snapBefore, snapAfter } = this.sashDragState;
this.sashDragState.current = current;
const delta = current - start;
const newDelta = this.resize(index, delta, sizes, undefined, undefined, minDelta, maxDelta, snapBefore, snapAfter);
if (alt) {
const isLastSash = index === this.sashItems.length - 1;
const newSizes = this.viewItems.map(i => i.size);
const viewItemIndex = isLastSash ? index : index + 1;
const viewItem = this.viewItems[viewItemIndex];
const newMinDelta = viewItem.size - viewItem.maximumSize;
const newMaxDelta = viewItem.size - viewItem.minimumSize;
const resizeIndex = isLastSash ? index - 1 : index + 1;
this.resize(resizeIndex, -newDelta, newSizes, undefined, undefined, newMinDelta, newMaxDelta);
}
this.distributeEmptySpace();
this.layoutViews();
}
onSashEnd(index) {
this._onDidSashChange.fire(index);
this.sashDragState.disposable.dispose();
this.saveProportions();
for (const item of this.viewItems) {
item.enabled = true;
}
}
onViewChange(item, size) {
const index = this.viewItems.indexOf(item);
if (index < 0 || index >= this.viewItems.length) {
return;
}
size = typeof size === 'number' ? size : item.size;
size = clamp(size, item.minimumSize, item.maximumSize);
if (this.inverseAltBehavior && index > 0) {
// In this case, we want the view to grow or shrink both sides equally
// so we just resize the "left" side by half and let `resize` do the clamping magic
this.resize(index - 1, Math.floor((item.size - size) / 2));
this.distributeEmptySpace();
this.layoutViews();
}
else {
item.size = size;
this.relayout([index], undefined);
}
}
/**
* Resize a {@link IView view} within the {@link SplitView}.
*
* @param index The {@link IView view} index.
* @param size The {@link IView view} size.
*/
resizeView(index, size) {
if (index < 0 || index >= this.viewItems.length) {
return;
}
if (this.state !== State.Idle) {
throw new Error('Cant modify splitview');
}
this.state = State.Busy;
try {
const indexes = range(this.viewItems.length).filter(i => i !== index);
const lowPriorityIndexes = [...indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */), index];
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
const item = this.viewItems[index];
size = Math.round(size);
size = clamp(size, item.minimumSize, Math.min(item.maximumSize, this.size));
item.size = size;
this.relayout(lowPriorityIndexes, highPriorityIndexes);
}
finally {
this.state = State.Idle;
}
}
/**
* Distribute the entire {@link SplitView} size among all {@link IView views}.
*/
distributeViewSizes() {
const flexibleViewItems = [];
let flexibleSize = 0;
for (const item of this.viewItems) {
if (item.maximumSize - item.minimumSize > 0) {
flexibleViewItems.push(item);
flexibleSize += item.size;
}
}
const size = Math.floor(flexibleSize / flexibleViewItems.length);
for (const item of flexibleViewItems) {
item.size = clamp(size, item.minimumSize, item.maximumSize);
}
const indexes = range(this.viewItems.length);
const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */);
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
this.relayout(lowPriorityIndexes, highPriorityIndexes);
}
/**
* Returns the size of a {@link IView view}.
*/
getViewSize(index) {
if (index < 0 || index >= this.viewItems.length) {
return -1;
}
return this.viewItems[index].size;
}
doAddView(view, size, index = this.viewItems.length, skipLayout) {
if (this.state !== State.Idle) {
throw new Error('Cant modify splitview');
}
this.state = State.Busy;
try {
// Add view
const container = $('.split-view-view');
if (index === this.viewItems.length) {
this.viewContainer.appendChild(container);
}
else {
this.viewContainer.insertBefore(container, this.viewContainer.children.item(index));
}
const onChangeDisposable = view.onDidChange(size => this.onViewChange(item, size));
const containerDisposable = toDisposable(() => container.remove());
const disposable = combinedDisposable(onChangeDisposable, containerDisposable);
let viewSize;
if (typeof size === 'number') {
viewSize = size;
}
else {
if (size.type === 'auto') {
if (this.areViewsDistributed()) {
size = { type: 'distribute' };
}
else {
size = { type: 'split', index: size.index };
}
}
if (size.type === 'split') {
viewSize = this.getViewSize(size.index) / 2;
}
else if (size.type === 'invisible') {
viewSize = { cachedVisibleSize: size.cachedVisibleSize };
}
else {
viewSize = view.minimumSize;
}
}
const item = this.orientation === 0 /* Orientation.VERTICAL */
? new VerticalViewItem(container, view, viewSize, disposable)
: new HorizontalViewItem(container, view, viewSize, disposable);
this.viewItems.splice(index, 0, item);
// Add sash
if (this.viewItems.length > 1) {
const opts = { orthogonalStartSash: this.orthogonalStartSash, orthogonalEndSash: this.orthogonalEndSash };
const sash = this.orientation === 0 /* Orientation.VERTICAL */
? new Sash(this.sashContainer, { getHorizontalSashTop: s => this.getSashPosition(s), getHorizontalSashWidth: this.getSashOrthogonalSize }, { ...opts, orientation: 1 /* Orientation.HORIZONTAL */ })
: new Sash(this.sashContainer, { getVerticalSashLeft: s => this.getSashPosition(s), getVerticalSashHeight: this.getSashOrthogonalSize }, { ...opts, orientation: 0 /* Orientation.VERTICAL */ });
const sashEventMapper = this.orientation === 0 /* Orientation.VERTICAL */
? (e) => ({ sash, start: e.startY, current: e.currentY, alt: e.altKey })
: (e) => ({ sash, start: e.startX, current: e.currentX, alt: e.altKey });
const onStart = Event.map(sash.onDidStart, sashEventMapper);
const onStartDisposable = onStart(this.onSashStart, this);
const onChange = Event.map(sash.onDidChange, sashEventMapper);
const onChangeDisposable = onChange(this.onSashChange, this);
const onEnd = Event.map(sash.onDidEnd, () => this.sashItems.findIndex(item => item.sash === sash));
const onEndDisposable = onEnd(this.onSashEnd, this);
const onDidResetDisposable = sash.onDidReset(() => {
const index = this.sashItems.findIndex(item => item.sash === sash);
const upIndexes = range(index, -1);
const downIndexes = range(index + 1, this.viewItems.length);
const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
if (typeof snapBeforeIndex === 'number' && !this.viewItems[snapBeforeIndex].visible) {
return;
}
if (typeof snapAfterIndex === 'number' && !this.viewItems[snapAfterIndex].visible) {
return;
}
this._onDidSashReset.fire(index);
});
const disposable = combinedDisposable(onStartDisposable, onChangeDisposable, onEndDisposable, onDidResetDisposable, sash);
const sashItem = { sash, disposable };
this.sashItems.splice(index - 1, 0, sashItem);
}
container.appendChild(view.element);
let highPriorityIndexes;
if (typeof size !== 'number' && size.type === 'split') {
highPriorityIndexes = [size.index];
}
if (!skipLayout) {
this.relayout([index], highPriorityIndexes);
}
if (!skipLayout && typeof size !== 'number' && size.type === 'distribute') {
this.distributeViewSizes();
}
}
finally {
this.state = State.Idle;
}
}
relayout(lowPriorityIndexes, highPriorityIndexes) {
const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
this.resize(this.viewItems.length - 1, this.size - contentSize, undefined, lowPriorityIndexes, highPriorityIndexes);
this.distributeEmptySpace();
this.layoutViews();
this.saveProportions();
}
resize(index, delta, sizes = this.viewItems.map(i => i.size), lowPriorityIndexes, highPriorityIndexes, overloadMinDelta = Number.NEGATIVE_INFINITY, overloadMaxDelta = Number.POSITIVE_INFINITY, snapBefore, snapAfter) {
if (index < 0 || index >= this.viewItems.length) {
return 0;
}
const upIndexes = range(index, -1);
const downIndexes = range(index + 1, this.viewItems.length);
if (highPriorityIndexes) {
for (const index of highPriorityIndexes) {
pushToStart(upIndexes, index);
pushToStart(downIndexes, index);
}
}
if (lowPriorityIndexes) {
for (const index of lowPriorityIndexes) {
pushToEnd(upIndexes, index);
pushToEnd(downIndexes, index);
}
}
const upItems = upIndexes.map(i => this.viewItems[i]);
const upSizes = upIndexes.map(i => sizes[i]);
const downItems = downIndexes.map(i => this.viewItems[i]);
const downSizes = downIndexes.map(i => sizes[i]);
const minDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].minimumSize - sizes[i]), 0);
const maxDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].maximumSize - sizes[i]), 0);
const maxDeltaDown = downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].minimumSize), 0);
const minDeltaDown = downIndexes.length === 0 ? Number.NEGATIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].maximumSize), 0);
const minDelta = Math.max(minDeltaUp, minDeltaDown, overloadMinDelta);
const maxDelta = Math.min(maxDeltaDown, maxDeltaUp, overloadMaxDelta);
let snapped = false;
if (snapBefore) {
const snapView = this.viewItems[snapBefore.index];
const visible = delta >= snapBefore.limitDelta;
snapped = visible !== snapView.visible;
snapView.setVisible(visible, snapBefore.size);
}
if (!snapped && snapAfter) {
const snapView = this.viewItems[snapAfter.index];
const visible = delta < snapAfter.limitDelta;
snapped = visible !== snapView.visible;
snapView.setVisible(visible, snapAfter.size);
}
if (snapped) {
return this.resize(index, delta, sizes, lowPriorityIndexes, highPriorityIndexes, overloadMinDelta, overloadMaxDelta);
}
delta = clamp(delta, minDelta, maxDelta);
for (let i = 0, deltaUp = delta; i < upItems.length; i++) {
const item = upItems[i];
const size = clamp(upSizes[i] + deltaUp, item.minimumSize, item.maximumSize);
const viewDelta = size - upSizes[i];
deltaUp -= viewDelta;
item.size = size;
}
for (let i = 0, deltaDown = delta; i < downItems.length; i++) {
const item = downItems[i];
const size = clamp(downSizes[i] - deltaDown, item.minimumSize, item.maximumSize);
const viewDelta = size - downSizes[i];
deltaDown += viewDelta;
item.size = size;
}
return delta;
}
distributeEmptySpace(lowPriorityIndex) {
const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
let emptyDelta = this.size - contentSize;
const indexes = range(this.viewItems.length - 1, -1);
const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */);
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
for (const index of highPriorityIndexes) {
pushToStart(indexes, index);
}
for (const index of lowPriorityIndexes) {
pushToEnd(indexes, index);
}
if (typeof lowPriorityIndex === 'number') {
pushToEnd(indexes, lowPriorityIndex);
}
for (let i = 0; emptyDelta !== 0 && i < indexes.length; i++) {
const item = this.viewItems[indexes[i]];
const size = clamp(item.size + emptyDelta, item.minimumSize, item.maximumSize);
const viewDelta = size - item.size;
emptyDelta -= viewDelta;
item.size = size;
}
}
layoutViews() {
// Save new content size
this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
// Layout views
let offset = 0;
for (const viewItem of this.viewItems) {
viewItem.layout(offset, this.layoutContext);
offset += viewItem.size;
}
// Layout sashes
this.sashItems.forEach(item => item.sash.layout());
this.updateSashEnablement();
this.updateScrollableElement();
}
updateScrollableElement() {
if (this.orientation === 0 /* Orientation.VERTICAL */) {
this.scrollableElement.setScrollDimensions({
height: this.size,
scrollHeight: this._contentSize
});
}
else {
this.scrollableElement.setScrollDimensions({
width: this.size,
scrollWidth: this._contentSize
});
}
}
updateSashEnablement() {
let previous = false;
const collapsesDown = this.viewItems.map(i => previous = (i.size - i.minimumSize > 0) || previous);
previous = false;
const expandsDown = this.viewItems.map(i => previous = (i.maximumSize - i.size > 0) || previous);
const reverseViews = [...this.viewItems].reverse();
previous = false;
const collapsesUp = reverseViews.map(i => previous = (i.size - i.minimumSize > 0) || previous).reverse();
previous = false;
const expandsUp = reverseViews.map(i => previous = (i.maximumSize - i.size > 0) || previous).reverse();
let position = 0;
for (let index = 0; index < this.sashItems.length; index++) {
const { sash } = this.sashItems[index];
const viewItem = this.viewItems[index];
position += viewItem.size;
const min = !(collapsesDown[index] && expandsUp[index + 1]);
const max = !(expandsDown[index] && collapsesUp[index + 1]);
if (min && max) {
const upIndexes = range(index, -1);
const downIndexes = range(index + 1, this.viewItems.length);
const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
const snappedBefore = typeof snapBeforeIndex === 'number' && !this.viewItems[snapBeforeIndex].visible;
const snappedAfter = typeof snapAfterIndex === 'number' && !this.viewItems[snapAfterIndex].visible;
if (snappedBefore && collapsesUp[index] && (position > 0 || this.startSnappingEnabled)) {
sash.state = 1 /* SashState.AtMinimum */;
}
else if (snappedAfter && collapsesDown[index] && (position < this._contentSize || this.endSnappingEnabled)) {
sash.state = 2 /* SashState.AtMaximum */;
}
else {
sash.state = 0 /* SashState.Disabled */;
}
}
else if (min && !max) {
sash.state = 1 /* SashState.AtMinimum */;
}
else if (!min && max) {
sash.state = 2 /* SashState.AtMaximum */;
}
else {
sash.state = 3 /* SashState.Enabled */;
}
}
}
getSashPosition(sash) {
let position = 0;
for (let i = 0; i < this.sashItems.length; i++) {
position += this.viewItems[i].size;
if (this.sashItems[i].sash === sash) {
return position;
}
}
return 0;
}
findFirstSnapIndex(indexes) {
// visible views first
for (const index of indexes) {
const viewItem = this.viewItems[index];
if (!viewItem.visible) {
continue;
}
if (viewItem.snap) {
return index;
}
}
// then, hidden views
for (const index of indexes) {
const viewItem = this.viewItems[index];
if (viewItem.visible && viewItem.maximumSize - viewItem.minimumSize > 0) {
return undefined;
}
if (!viewItem.visible && viewItem.snap) {
return index;
}
}
return undefined;
}
areViewsDistributed() {
let min = undefined, max = undefined;
for (const view of this.viewItems) {
min = min === undefined ? view.size : Math.min(min, view.size);
max = max === undefined ? view.size : Math.max(max, view.size);
if (max - min > 2) {
return false;
}
}
return true;
}
dispose() {
this.sashDragState?.disposable.dispose();
dispose(this.viewItems);
this.viewItems = [];
this.sashItems.forEach(i => i.disposable.dispose());
this.sashItems = [];
super.dispose();
}
}
export { Sizing, SplitView };
@@ -0,0 +1,57 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-table {
display: flex;
flex-direction: column;
position: relative;
height: 100%;
width: 100%;
white-space: nowrap;
overflow: hidden;
}
.monaco-table > .monaco-split-view2 {
border-bottom: 1px solid transparent;
}
.monaco-table > .monaco-list {
flex: 1;
}
.monaco-table-tr {
display: flex;
height: 100%;
}
.monaco-table-th {
width: 100%;
height: 100%;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
}
.monaco-table-th,
.monaco-table-td {
box-sizing: border-box;
flex-shrink: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.monaco-table > .monaco-split-view2 .monaco-sash.vertical::before {
content: "";
position: absolute;
left: calc(var(--vscode-sash-size) / 2);
width: 0;
border-left: 1px solid transparent;
}
.monaco-enable-motion .monaco-table > .monaco-split-view2,
.monaco-enable-motion .monaco-table > .monaco-split-view2 .monaco-sash.vertical::before {
transition: border-color 0.2s ease-out;
}
@@ -0,0 +1,181 @@
import { append, $, clearNode } from '../../dom.js';
import { createStyleSheet } from '../../domStylesheets.js';
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
import { List, unthemedListStyles } from '../list/listWidget.js';
import { SplitView } from '../splitview/splitview.js';
import { Event, Emitter } from '../../../common/event.js';
import { DisposableStore, Disposable } from '../../../common/lifecycle.js';
import './table.css';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class TableListRenderer {
static { this.TemplateId = 'row'; }
constructor(columns, renderers, getColumnSize) {
this.columns = columns;
this.getColumnSize = getColumnSize;
this.templateId = TableListRenderer.TemplateId;
this.renderedTemplates = new Set();
const rendererMap = new Map(renderers.map(r => [r.templateId, r]));
this.renderers = [];
for (const column of columns) {
const renderer = rendererMap.get(column.templateId);
if (!renderer) {
throw new Error(`Table cell renderer for template id ${column.templateId} not found.`);
}
this.renderers.push(renderer);
}
}
renderTemplate(container) {
const rowContainer = append(container, $('.monaco-table-tr'));
const cellContainers = [];
const cellTemplateData = [];
for (let i = 0; i < this.columns.length; i++) {
const renderer = this.renderers[i];
const cellContainer = append(rowContainer, $('.monaco-table-td', { 'data-col-index': i }));
cellContainer.style.width = `${this.getColumnSize(i)}px`;
cellContainers.push(cellContainer);
cellTemplateData.push(renderer.renderTemplate(cellContainer));
}
const result = { container, cellContainers, cellTemplateData };
this.renderedTemplates.add(result);
return result;
}
renderElement(element, index, templateData, renderDetails) {
for (let i = 0; i < this.columns.length; i++) {
const column = this.columns[i];
const cell = column.project(element);
const renderer = this.renderers[i];
renderer.renderElement(cell, index, templateData.cellTemplateData[i], renderDetails);
}
}
disposeElement(element, index, templateData, renderDetails) {
for (let i = 0; i < this.columns.length; i++) {
const renderer = this.renderers[i];
if (renderer.disposeElement) {
const column = this.columns[i];
const cell = column.project(element);
renderer.disposeElement(cell, index, templateData.cellTemplateData[i], renderDetails);
}
}
}
disposeTemplate(templateData) {
for (let i = 0; i < this.columns.length; i++) {
const renderer = this.renderers[i];
renderer.disposeTemplate(templateData.cellTemplateData[i]);
}
clearNode(templateData.container);
this.renderedTemplates.delete(templateData);
}
layoutColumn(index, size) {
for (const { cellContainers } of this.renderedTemplates) {
cellContainers[index].style.width = `${size}px`;
}
}
}
function asListVirtualDelegate(delegate) {
return {
getHeight(row) { return delegate.getHeight(row); },
getTemplateId() { return TableListRenderer.TemplateId; },
};
}
class ColumnHeader extends Disposable {
get minimumSize() { return this.column.minimumWidth ?? 120; }
get maximumSize() { return this.column.maximumWidth ?? Number.POSITIVE_INFINITY; }
get onDidChange() { return this.column.onDidChangeWidthConstraints ?? Event.None; }
constructor(column, index) {
super();
this.column = column;
this.index = index;
this._onDidLayout = new Emitter();
this.onDidLayout = this._onDidLayout.event;
this.element = $('.monaco-table-th', { 'data-col-index': index }, column.label);
if (column.tooltip) {
this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, column.tooltip));
}
}
layout(size) {
this._onDidLayout.fire([this.index, size]);
}
}
class Table {
static { this.InstanceCount = 0; }
get onDidChangeFocus() { return this.list.onDidChangeFocus; }
get onDidChangeSelection() { return this.list.onDidChangeSelection; }
get onDidScroll() { return this.list.onDidScroll; }
get onMouseDblClick() { return this.list.onMouseDblClick; }
get onPointer() { return this.list.onPointer; }
get onDidFocus() { return this.list.onDidFocus; }
get scrollTop() { return this.list.scrollTop; }
set scrollTop(scrollTop) { this.list.scrollTop = scrollTop; }
get scrollHeight() { return this.list.scrollHeight; }
get renderHeight() { return this.list.renderHeight; }
get onDidDispose() { return this.list.onDidDispose; }
constructor(user, container, virtualDelegate, columns, renderers, _options) {
this.virtualDelegate = virtualDelegate;
this.columns = columns;
this.domId = `table_id_${++Table.InstanceCount}`;
this.disposables = new DisposableStore();
this.cachedWidth = 0;
this.cachedHeight = 0;
this.domNode = append(container, $(`.monaco-table.${this.domId}`));
const headers = columns.map((c, i) => this.disposables.add(new ColumnHeader(c, i)));
const descriptor = {
size: headers.reduce((a, b) => a + b.column.weight, 0),
views: headers.map(view => ({ size: view.column.weight, view }))
};
this.splitview = this.disposables.add(new SplitView(this.domNode, {
orientation: 1 /* Orientation.HORIZONTAL */,
scrollbarVisibility: 2 /* ScrollbarVisibility.Hidden */,
getSashOrthogonalSize: () => this.cachedHeight,
descriptor
}));
this.splitview.el.style.height = `${virtualDelegate.headerRowHeight}px`;
this.splitview.el.style.lineHeight = `${virtualDelegate.headerRowHeight}px`;
const renderer = new TableListRenderer(columns, renderers, i => this.splitview.getViewSize(i));
this.list = this.disposables.add(new List(user, this.domNode, asListVirtualDelegate(virtualDelegate), [renderer], _options));
Event.any(...headers.map(h => h.onDidLayout))(([index, size]) => renderer.layoutColumn(index, size), null, this.disposables);
this.splitview.onDidSashReset(index => {
const totalWeight = columns.reduce((r, c) => r + c.weight, 0);
const size = columns[index].weight / totalWeight * this.cachedWidth;
this.splitview.resizeView(index, size);
}, null, this.disposables);
this.styleElement = createStyleSheet(this.domNode);
this.style(unthemedListStyles);
}
updateOptions(options) {
this.list.updateOptions(options);
}
splice(start, deleteCount, elements = []) {
this.list.splice(start, deleteCount, elements);
}
getHTMLElement() {
return this.domNode;
}
style(styles) {
const content = [];
content.push(`.monaco-table.${this.domId} > .monaco-split-view2 .monaco-sash.vertical::before {
top: ${this.virtualDelegate.headerRowHeight + 1}px;
height: calc(100% - ${this.virtualDelegate.headerRowHeight}px);
}`);
this.styleElement.textContent = content.join('\n');
this.list.style(styles);
}
getSelectedElements() {
return this.list.getSelectedElements();
}
getSelection() {
return this.list.getSelection();
}
getFocus() {
return this.list.getFocus();
}
dispose() {
this.disposables.dispose();
}
}
export { Table };
@@ -0,0 +1,69 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-custom-toggle {
margin-left: 2px;
float: left;
cursor: pointer;
overflow: hidden;
width: 20px;
height: 20px;
border-radius: 3px;
border: 1px solid transparent;
padding: 1px;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
}
.monaco-custom-toggle:hover {
background-color: var(--vscode-inputOption-hoverBackground);
}
.hc-black .monaco-custom-toggle:hover,
.hc-light .monaco-custom-toggle:hover {
border: 1px dashed var(--vscode-focusBorder);
}
.hc-black .monaco-custom-toggle,
.hc-light .monaco-custom-toggle {
background: none;
}
.hc-black .monaco-custom-toggle:hover,
.hc-light .monaco-custom-toggle:hover {
background: none;
}
.monaco-custom-toggle.monaco-checkbox {
height: 18px;
width: 18px;
border: 1px solid transparent;
border-radius: 3px;
margin-right: 9px;
margin-left: 0px;
padding: 0px;
opacity: 1;
background-size: 16px !important;
}
.monaco-action-bar .checkbox-action-item {
display: flex;
align-items: center;
border-radius: 2px;
padding-right: 2px;
}
.monaco-action-bar .checkbox-action-item:hover {
background-color: var(--vscode-toolbar-hoverBackground);
}
.monaco-action-bar .checkbox-action-item > .monaco-custom-toggle.monaco-checkbox {
margin-right: 4px;
}
.monaco-action-bar .checkbox-action-item > .checkbox-label {
font-size: 12px;
}
@@ -0,0 +1,242 @@
import { Codicon } from '../../../common/codicons.js';
import { Emitter } from '../../../common/event.js';
import { ThemeIcon } from '../../../common/themables.js';
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
import { Widget } from '../widget.js';
import './toggle.css';
const unthemedToggleStyles = {
inputActiveOptionBorder: '#007ACC00',
inputActiveOptionForeground: '#FFFFFF',
inputActiveOptionBackground: '#0E639C50'
};
class Toggle extends Widget {
get onChange() { return this._onChange.event; }
get onKeyDown() { return this._onKeyDown.event; }
constructor(opts) {
super();
this._onChange = this._register(new Emitter());
this._onKeyDown = this._register(new Emitter());
this._opts = opts;
this._title = this._opts.title;
this._checked = this._opts.isChecked;
const classes = ['monaco-custom-toggle'];
if (this._opts.icon) {
this._icon = this._opts.icon;
classes.push(...ThemeIcon.asClassNameArray(this._icon));
}
if (this._opts.actionClassName) {
classes.push(...this._opts.actionClassName.split(' '));
}
if (this._checked) {
classes.push('checked');
}
this.domNode = document.createElement('div');
this._register(getBaseLayerHoverDelegate().setupDelayedHover(this.domNode, () => ({
content: this._title,
style: 1 /* HoverStyle.Pointer */,
}), this._opts.hoverLifecycleOptions));
this.domNode.classList.add(...classes);
if (!this._opts.notFocusable) {
this.domNode.tabIndex = 0;
}
this.domNode.setAttribute('role', 'checkbox');
this.domNode.setAttribute('aria-checked', String(this._checked));
this.setTitle(this._opts.title);
this.applyStyles();
this.onclick(this.domNode, (ev) => {
if (this.enabled) {
this.checked = !this._checked;
this._onChange.fire(false);
ev.preventDefault();
}
});
this._register(this.ignoreGesture(this.domNode));
this.onkeydown(this.domNode, (keyboardEvent) => {
if (!this.enabled) {
return;
}
if (keyboardEvent.keyCode === 10 /* KeyCode.Space */ || keyboardEvent.keyCode === 3 /* KeyCode.Enter */) {
this.checked = !this._checked;
this._onChange.fire(true);
keyboardEvent.preventDefault();
keyboardEvent.stopPropagation();
return;
}
this._onKeyDown.fire(keyboardEvent);
});
}
get enabled() {
return this.domNode.getAttribute('aria-disabled') !== 'true';
}
focus() {
this.domNode.focus();
}
get checked() {
return this._checked;
}
set checked(newIsChecked) {
this._checked = newIsChecked;
this.domNode.setAttribute('aria-checked', String(this._checked));
this.domNode.classList.toggle('checked', this._checked);
this.applyStyles();
}
setIcon(icon) {
if (this._icon) {
this.domNode.classList.remove(...ThemeIcon.asClassNameArray(this._icon));
}
this._icon = icon;
if (this._icon) {
this.domNode.classList.add(...ThemeIcon.asClassNameArray(this._icon));
}
}
width() {
return 2 /*margin left*/ + 2 /*border*/ + 2 /*padding*/ + 16 /* icon width */;
}
applyStyles() {
if (this.domNode) {
this.domNode.style.borderColor = (this._checked && this._opts.inputActiveOptionBorder) || '';
this.domNode.style.color = (this._checked && this._opts.inputActiveOptionForeground) || 'inherit';
this.domNode.style.backgroundColor = (this._checked && this._opts.inputActiveOptionBackground) || '';
}
}
enable() {
this.domNode.setAttribute('aria-disabled', String(false));
this.domNode.classList.remove('disabled');
}
disable() {
this.domNode.setAttribute('aria-disabled', String(true));
this.domNode.classList.add('disabled');
}
setTitle(newTitle) {
this._title = newTitle;
this.domNode.setAttribute('aria-label', newTitle);
}
set visible(visible) {
this.domNode.style.display = visible ? '' : 'none';
}
get visible() {
return this.domNode.style.display !== 'none';
}
}
class BaseCheckbox extends Widget {
static { this.CLASS_NAME = 'monaco-checkbox'; }
constructor(checkbox, domNode, styles) {
super();
this.checkbox = checkbox;
this.domNode = domNode;
this.styles = styles;
this._onChange = this._register(new Emitter());
this.onChange = this._onChange.event;
this.applyStyles();
}
get enabled() {
return this.checkbox.enabled;
}
enable() {
this.checkbox.enable();
this.applyStyles(true);
}
disable() {
this.checkbox.disable();
this.applyStyles(false);
}
setTitle(newTitle) {
this.checkbox.setTitle(newTitle);
}
applyStyles(enabled = this.enabled) {
this.domNode.style.color = (enabled ? this.styles.checkboxForeground : this.styles.checkboxDisabledForeground) || '';
this.domNode.style.backgroundColor = (enabled ? this.styles.checkboxBackground : this.styles.checkboxDisabledBackground) || '';
this.domNode.style.borderColor = (enabled ? this.styles.checkboxBorder : this.styles.checkboxDisabledBackground) || '';
const size = this.styles.size || 18;
this.domNode.style.width =
this.domNode.style.height =
this.domNode.style.fontSize = `${size}px`;
this.domNode.style.fontSize = `${size - 2}px`;
}
}
class Checkbox extends BaseCheckbox {
constructor(title, isChecked, styles) {
const toggle = new Toggle({ title, isChecked, icon: Codicon.check, actionClassName: BaseCheckbox.CLASS_NAME, hoverLifecycleOptions: styles.hoverLifecycleOptions, ...unthemedToggleStyles });
super(toggle, toggle.domNode, styles);
this._register(toggle);
this._register(this.checkbox.onChange(keyboard => {
this.applyStyles();
this._onChange.fire(keyboard);
}));
}
get checked() {
return this.checkbox.checked;
}
set checked(newIsChecked) {
this.checkbox.checked = newIsChecked;
this.applyStyles();
}
applyStyles(enabled) {
if (this.checkbox.checked) {
this.checkbox.setIcon(Codicon.check);
}
else {
this.checkbox.setIcon(undefined);
}
super.applyStyles(enabled);
}
}
class TriStateCheckbox extends BaseCheckbox {
constructor(title, _state, styles) {
let icon;
switch (_state) {
case true:
icon = Codicon.check;
break;
case 'mixed':
icon = Codicon.dash;
break;
case false:
icon = undefined;
break;
}
const checkbox = new Toggle({
title,
isChecked: _state === true,
icon,
actionClassName: Checkbox.CLASS_NAME,
hoverLifecycleOptions: styles.hoverLifecycleOptions,
...unthemedToggleStyles
});
super(checkbox, checkbox.domNode, styles);
this._state = _state;
this._register(checkbox);
this._register(this.checkbox.onChange(keyboard => {
this._state = this.checkbox.checked;
this.applyStyles();
this._onChange.fire(keyboard);
}));
}
get checked() {
return this._state;
}
set checked(newState) {
if (this._state !== newState) {
this._state = newState;
this.checkbox.checked = newState === true;
this.applyStyles();
}
}
applyStyles(enabled) {
switch (this._state) {
case true:
this.checkbox.setIcon(Codicon.check);
break;
case 'mixed':
this.checkbox.setIcon(Codicon.dash);
break;
case false:
this.checkbox.setIcon(undefined);
break;
}
super.applyStyles(enabled);
}
}
export { Checkbox, Toggle, TriStateCheckbox, unthemedToggleStyles };
@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-toolbar {
height: 100%;
}
.monaco-toolbar .toolbar-toggle-more {
display: inline-block;
padding: 0;
}
.monaco-toolbar.responsive {
.monaco-action-bar > .actions-container > .action-item {
flex-shrink: 1;
min-width: 20px;
}
}
@@ -0,0 +1,244 @@
import { ActionBar } from '../actionbar/actionbar.js';
import { DropdownMenuActionViewItem } from '../dropdown/dropdownActionViewItem.js';
import { Action, SubmenuAction, Separator } from '../../../common/actions.js';
import { Codicon } from '../../../common/codicons.js';
import { ThemeIcon } from '../../../common/themables.js';
import { EventMultiplexer } from '../../../common/event.js';
import { Disposable, DisposableStore, toDisposable } from '../../../common/lifecycle.js';
import './toolbar.css';
import { localize } from '../../../../nls.js';
import { createInstantHoverDelegate } from '../hover/hoverDelegateFactory.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const ACTION_MIN_WIDTH = 24; /* 20px codicon + 4px left padding*/
/**
* A widget that combines an action bar for primary actions and a dropdown for secondary actions.
*/
class ToolBar extends Disposable {
get onDidChangeDropdownVisibility() { return this._onDidChangeDropdownVisibility.event; }
constructor(container, contextMenuProvider, options = { orientation: 0 /* ActionsOrientation.HORIZONTAL */ }) {
super();
this.submenuActionViewItems = [];
this.hasSecondaryActions = false;
this._onDidChangeDropdownVisibility = this._register(new EventMultiplexer());
this.originalPrimaryActions = [];
this.originalSecondaryActions = [];
this.hiddenActions = [];
this.disposables = this._register(new DisposableStore());
options.hoverDelegate = options.hoverDelegate ?? this._register(createInstantHoverDelegate());
this.options = options;
this.toggleMenuAction = this._register(new ToggleMenuAction(() => this.toggleMenuActionViewItem?.show(), options.toggleMenuTitle));
this.element = document.createElement('div');
this.element.className = 'monaco-toolbar';
container.appendChild(this.element);
this.actionBar = this._register(new ActionBar(this.element, {
orientation: options.orientation,
ariaLabel: options.ariaLabel,
actionRunner: options.actionRunner,
allowContextMenu: options.allowContextMenu,
highlightToggledItems: options.highlightToggledItems,
hoverDelegate: options.hoverDelegate,
actionViewItemProvider: (action, viewItemOptions) => {
if (action.id === ToggleMenuAction.ID) {
this.toggleMenuActionViewItem = new DropdownMenuActionViewItem(action, { getActions: () => this.toggleMenuAction.menuActions }, contextMenuProvider, {
actionViewItemProvider: this.options.actionViewItemProvider,
actionRunner: this.actionRunner,
keybindingProvider: this.options.getKeyBinding,
classNames: ThemeIcon.asClassNameArray(options.moreIcon ?? Codicon.toolBarMore),
anchorAlignmentProvider: this.options.anchorAlignmentProvider,
menuAsChild: !!this.options.renderDropdownAsChildElement,
skipTelemetry: this.options.skipTelemetry,
isMenu: true,
hoverDelegate: this.options.hoverDelegate
});
this.toggleMenuActionViewItem.setActionContext(this.actionBar.context);
this.disposables.add(this._onDidChangeDropdownVisibility.add(this.toggleMenuActionViewItem.onDidChangeVisibility));
return this.toggleMenuActionViewItem;
}
if (options.actionViewItemProvider) {
const result = options.actionViewItemProvider(action, viewItemOptions);
if (result) {
return result;
}
}
if (action instanceof SubmenuAction) {
const result = new DropdownMenuActionViewItem(action, action.actions, contextMenuProvider, {
actionViewItemProvider: this.options.actionViewItemProvider,
actionRunner: this.actionRunner,
keybindingProvider: this.options.getKeyBinding,
classNames: action.class,
anchorAlignmentProvider: this.options.anchorAlignmentProvider,
menuAsChild: !!this.options.renderDropdownAsChildElement,
skipTelemetry: this.options.skipTelemetry,
hoverDelegate: this.options.hoverDelegate
});
result.setActionContext(this.actionBar.context);
this.submenuActionViewItems.push(result);
this.disposables.add(this._onDidChangeDropdownVisibility.add(result.onDidChangeVisibility));
return result;
}
return undefined;
}
}));
// Responsive support
if (this.options.responsive) {
this.element.classList.add('responsive');
const observer = new ResizeObserver(() => {
this.setToolbarMaxWidth(this.element.getBoundingClientRect().width);
});
observer.observe(this.element);
this._store.add(toDisposable(() => observer.disconnect()));
}
}
set actionRunner(actionRunner) {
this.actionBar.actionRunner = actionRunner;
}
get actionRunner() {
return this.actionBar.actionRunner;
}
set context(context) {
this.actionBar.context = context;
this.toggleMenuActionViewItem?.setActionContext(context);
for (const actionViewItem of this.submenuActionViewItems) {
actionViewItem.setActionContext(context);
}
}
getElement() {
return this.element;
}
getItemAction(indexOrElement) {
return this.actionBar.getAction(indexOrElement);
}
getItemWidth(index) {
return this.actionBar.getWidth(index);
}
setActions(primaryActions, secondaryActions) {
this.clear();
// Store primary and secondary actions as rendered initially
this.originalPrimaryActions = primaryActions ? primaryActions.slice(0) : [];
this.originalSecondaryActions = secondaryActions ? secondaryActions.slice(0) : [];
const primaryActionsToSet = primaryActions ? primaryActions.slice(0) : [];
// Inject additional action to open secondary actions if present
this.hasSecondaryActions = !!(secondaryActions && secondaryActions.length > 0);
if (this.hasSecondaryActions && secondaryActions) {
this.toggleMenuAction.menuActions = secondaryActions.slice(0);
primaryActionsToSet.push(this.toggleMenuAction);
}
if (primaryActionsToSet.length > 0 && this.options.trailingSeparator) {
primaryActionsToSet.push(new Separator());
}
primaryActionsToSet.forEach(action => {
this.actionBar.push(action, { icon: this.options.icon ?? true, label: this.options.label ?? false, keybinding: this.getKeybindingLabel(action) });
});
if (this.options.responsive) {
// Reset hidden actions
this.hiddenActions.length = 0;
// Update toolbar to fit with container width
this.setToolbarMaxWidth(this.element.getBoundingClientRect().width);
}
}
getKeybindingLabel(action) {
const key = this.options.getKeyBinding?.(action);
return key?.getLabel() ?? undefined;
}
getItemsWidthResponsive() {
// Each action is assumed to have a minimum width so that actions with a label
// can shrink to the action's minimum width. We do this so that action visibility
// takes precedence over the action label.
return this.actionBar.length() * ACTION_MIN_WIDTH;
}
setToolbarMaxWidth(maxWidth) {
if (this.actionBar.isEmpty() ||
(this.getItemsWidthResponsive() <= maxWidth && this.hiddenActions.length === 0)) {
return;
}
if (this.getItemsWidthResponsive() > maxWidth) {
// Hide actions from the right
while (this.getItemsWidthResponsive() > maxWidth && this.actionBar.length() > 0) {
const index = this.originalPrimaryActions.length - this.hiddenActions.length - 1;
if (index < 0) {
break;
}
// Store the action and its size
const size = Math.min(ACTION_MIN_WIDTH, this.getItemWidth(index));
const action = this.originalPrimaryActions[index];
this.hiddenActions.unshift({ action, size });
// Remove the action
this.actionBar.pull(index);
// There are no secondary actions, but we have actions that we need to hide so we
// create the overflow menu. This will ensure that another primary action will be
// removed making space for the overflow menu.
if (this.originalSecondaryActions.length === 0 && this.hiddenActions.length === 1) {
this.actionBar.push(this.toggleMenuAction, {
icon: this.options.icon ?? true,
label: this.options.label ?? false,
keybinding: this.getKeybindingLabel(this.toggleMenuAction),
});
}
}
}
else {
// Show actions from the top of the toggle menu
while (this.hiddenActions.length > 0) {
const entry = this.hiddenActions.shift();
if (this.getItemsWidthResponsive() + entry.size > maxWidth) {
// Not enough space to show the action
this.hiddenActions.unshift(entry);
break;
}
// Add the action
this.actionBar.push(entry.action, {
icon: this.options.icon ?? true,
label: this.options.label ?? false,
keybinding: this.getKeybindingLabel(entry.action),
index: this.originalPrimaryActions.length - this.hiddenActions.length - 1
});
// There are no secondary actions, and there is only one hidden item left so we
// remove the overflow menu making space for the last hidden action to be shown.
if (this.originalSecondaryActions.length === 0 && this.hiddenActions.length === 1) {
this.toggleMenuAction.menuActions = [];
this.actionBar.pull(this.actionBar.length() - 1);
}
}
}
// Update overflow menu
const hiddenActions = this.hiddenActions.map(entry => entry.action);
if (this.originalSecondaryActions.length > 0 || hiddenActions.length > 0) {
const secondaryActions = this.originalSecondaryActions.slice(0);
this.toggleMenuAction.menuActions = Separator.join(hiddenActions, secondaryActions);
}
}
clear() {
this.submenuActionViewItems = [];
this.disposables.clear();
this.actionBar.clear();
}
dispose() {
this.clear();
this.disposables.dispose();
super.dispose();
}
}
class ToggleMenuAction extends Action {
static { this.ID = 'toolbar.toggle.more'; }
constructor(toggleDropdownMenu, title) {
title = title || localize(17, "More Actions...");
super(ToggleMenuAction.ID, title, undefined, true);
this._menuActions = [];
this.toggleDropdownMenu = toggleDropdownMenu;
}
async run() {
this.toggleDropdownMenu();
}
get menuActions() {
return this._menuActions;
}
set menuActions(actions) {
this._menuActions = actions;
}
}
export { ToggleMenuAction, ToolBar };
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,946 @@
import { ElementsDragAndDropData } from '../list/listView.js';
import { ComposedTreeDelegate, FindFilter, TreeFindMode, FindController } from './abstractTree.js';
import { isFilterResult, getVisibleState } from './indexTreeModel.js';
import { ObjectTree, CompressibleObjectTree } from './objectTree.js';
import { WeakMapper, ObjectTreeElementCollapseState, TreeError } from './tree.js';
import { createCancelablePromise, Promises, timeout } from '../../../common/async.js';
import { Codicon } from '../../../common/codicons.js';
import { ThemeIcon } from '../../../common/themables.js';
import { isCancellationError, onUnexpectedError } from '../../../common/errors.js';
import { Event, Emitter } from '../../../common/event.js';
import { Iterable } from '../../../common/iterator.js';
import { DisposableStore, toDisposable, dispose } from '../../../common/lifecycle.js';
import { isIterable } from '../../../common/types.js';
import { FuzzyScore } from '../../../common/filters.js';
import { splice } from '../../../common/arrays.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
function createAsyncDataTreeNode(props) {
return {
...props,
children: [],
refreshPromise: undefined,
stale: true,
slow: false,
forceExpanded: false
};
}
function isAncestor(ancestor, descendant) {
if (!descendant.parent) {
return false;
}
else if (descendant.parent === ancestor) {
return true;
}
else {
return isAncestor(ancestor, descendant.parent);
}
}
function intersects(node, other) {
return node === other || isAncestor(node, other) || isAncestor(other, node);
}
class AsyncDataTreeNodeWrapper {
get element() { return this.node.element.element; }
get children() { return this.node.children.map(node => new AsyncDataTreeNodeWrapper(node)); }
get depth() { return this.node.depth; }
get visibleChildrenCount() { return this.node.visibleChildrenCount; }
get visibleChildIndex() { return this.node.visibleChildIndex; }
get collapsible() { return this.node.collapsible; }
get collapsed() { return this.node.collapsed; }
get visible() { return this.node.visible; }
get filterData() { return this.node.filterData; }
constructor(node) {
this.node = node;
}
}
class AsyncDataTreeRenderer {
constructor(renderer, nodeMapper, onDidChangeTwistieState) {
this.renderer = renderer;
this.nodeMapper = nodeMapper;
this.onDidChangeTwistieState = onDidChangeTwistieState;
this.renderedNodes = new Map();
this.templateId = renderer.templateId;
}
renderTemplate(container) {
const templateData = this.renderer.renderTemplate(container);
return { templateData };
}
renderElement(node, index, templateData, details) {
this.renderer.renderElement(this.nodeMapper.map(node), index, templateData.templateData, details);
}
renderTwistie(element, twistieElement) {
if (element.slow) {
twistieElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.treeItemLoading));
return true;
}
else {
twistieElement.classList.remove(...ThemeIcon.asClassNameArray(Codicon.treeItemLoading));
return false;
}
}
disposeElement(node, index, templateData, details) {
this.renderer.disposeElement?.(this.nodeMapper.map(node), index, templateData.templateData, details);
}
disposeTemplate(templateData) {
this.renderer.disposeTemplate(templateData.templateData);
}
dispose() {
this.renderedNodes.clear();
}
}
function asTreeEvent(e) {
return {
browserEvent: e.browserEvent,
elements: e.elements.map(e => e.element)
};
}
function asTreeMouseEvent(e) {
return {
browserEvent: e.browserEvent,
element: e.element && e.element.element,
target: e.target
};
}
class AsyncDataTreeElementsDragAndDropData extends ElementsDragAndDropData {
constructor(data) {
super(data.elements.map(node => node.element));
this.data = data;
}
}
function asAsyncDataTreeDragAndDropData(data) {
if (data instanceof ElementsDragAndDropData) {
return new AsyncDataTreeElementsDragAndDropData(data);
}
return data;
}
class AsyncDataTreeNodeListDragAndDrop {
constructor(dnd) {
this.dnd = dnd;
}
getDragURI(node) {
return this.dnd.getDragURI(node.element);
}
getDragLabel(nodes, originalEvent) {
if (this.dnd.getDragLabel) {
return this.dnd.getDragLabel(nodes.map(node => node.element), originalEvent);
}
return undefined;
}
onDragStart(data, originalEvent) {
this.dnd.onDragStart?.(asAsyncDataTreeDragAndDropData(data), originalEvent);
}
onDragOver(data, targetNode, targetIndex, targetSector, originalEvent, raw = true) {
return this.dnd.onDragOver(asAsyncDataTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, targetSector, originalEvent);
}
drop(data, targetNode, targetIndex, targetSector, originalEvent) {
this.dnd.drop(asAsyncDataTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, targetSector, originalEvent);
}
onDragEnd(originalEvent) {
this.dnd.onDragEnd?.(originalEvent);
}
dispose() {
this.dnd.dispose();
}
}
class AsyncFindFilter extends FindFilter {
constructor(findProvider, // remove public
keyboardNavigationLabelProvider, filter) {
super(keyboardNavigationLabelProvider, filter);
this.findProvider = findProvider;
this.isFindSessionActive = false;
}
filter(element, parentVisibility) {
const filterResult = super.filter(element, parentVisibility);
if (!this.isFindSessionActive || this.findMode === TreeFindMode.Highlight || !this.findProvider.isVisible) {
return filterResult;
}
const visibility = isFilterResult(filterResult) ? filterResult.visibility : filterResult;
if (getVisibleState(visibility) === 0 /* TreeVisibility.Hidden */) {
return 0 /* TreeVisibility.Hidden */;
}
return this.findProvider.isVisible(element) ? filterResult : 0 /* TreeVisibility.Hidden */;
}
}
// TODO Fix types
class AsyncFindController extends FindController {
constructor(tree, findProvider, filter, contextViewProvider, options) {
super(tree, filter, contextViewProvider, options);
this.findProvider = findProvider;
this.filter = filter;
this.activeSession = false;
this.asyncWorkInProgress = false;
// Always make sure to end the session before disposing
this.disposables.add(toDisposable(async () => {
if (this.activeSession) {
await this.findProvider.endSession?.();
}
}));
}
render() {
if (this.asyncWorkInProgress || !this.activeFindMetadata) {
return;
}
const showNotFound = this.activeFindMetadata.matchCount === 0 && this.pattern.length > 0;
this.renderMessage(showNotFound);
if (this.pattern.length) {
this.alertResults(this.activeFindMetadata.matchCount);
}
}
shouldAllowFocus(node) {
return this.shouldFocusWhenNavigating(node);
}
shouldFocusWhenNavigating(node) {
if (!this.activeSession || !this.activeFindMetadata) {
return true;
}
const element = node.element?.element;
if (element && this.activeFindMetadata.isMatch(element)) {
return true;
}
return !FuzzyScore.isDefault(node.filterData);
}
}
function asObjectTreeOptions(options) {
return options && {
...options,
collapseByDefault: true,
identityProvider: options.identityProvider && {
getId(el) {
return options.identityProvider.getId(el.element);
}
},
dnd: options.dnd && new AsyncDataTreeNodeListDragAndDrop(options.dnd),
multipleSelectionController: options.multipleSelectionController && {
isSelectionSingleChangeEvent(e) {
// eslint-disable-next-line local/code-no-dangerous-type-assertions
return options.multipleSelectionController.isSelectionSingleChangeEvent({ ...e, element: e.element });
},
isSelectionRangeChangeEvent(e) {
// eslint-disable-next-line local/code-no-dangerous-type-assertions
return options.multipleSelectionController.isSelectionRangeChangeEvent({ ...e, element: e.element });
}
},
accessibilityProvider: options.accessibilityProvider && {
...options.accessibilityProvider,
getPosInSet: undefined,
getSetSize: undefined,
getRole: options.accessibilityProvider.getRole ? (el) => {
return options.accessibilityProvider.getRole(el.element);
} : () => 'treeitem',
isChecked: options.accessibilityProvider.isChecked ? (e) => {
return !!(options.accessibilityProvider?.isChecked(e.element));
} : undefined,
getAriaLabel(e) {
return options.accessibilityProvider.getAriaLabel(e.element);
},
getWidgetAriaLabel() {
return options.accessibilityProvider.getWidgetAriaLabel();
},
getWidgetRole: options.accessibilityProvider.getWidgetRole ? () => options.accessibilityProvider.getWidgetRole() : () => 'tree',
getAriaLevel: options.accessibilityProvider.getAriaLevel && (node => {
return options.accessibilityProvider.getAriaLevel(node.element);
}),
getActiveDescendantId: options.accessibilityProvider.getActiveDescendantId && (node => {
return options.accessibilityProvider.getActiveDescendantId(node.element);
})
},
filter: options.filter && {
filter(e, parentVisibility) {
return options.filter.filter(e.element, parentVisibility);
}
},
keyboardNavigationLabelProvider: options.keyboardNavigationLabelProvider && {
...options.keyboardNavigationLabelProvider,
getKeyboardNavigationLabel(e) {
return options.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(e.element);
}
},
sorter: undefined,
expandOnlyOnTwistieClick: typeof options.expandOnlyOnTwistieClick === 'undefined' ? undefined : (typeof options.expandOnlyOnTwistieClick !== 'function' ? options.expandOnlyOnTwistieClick : ((e) => options.expandOnlyOnTwistieClick(e.element))),
defaultFindVisibility: (e) => {
if (e.hasChildren && e.stale) {
return 1 /* TreeVisibility.Visible */;
}
else if (typeof options.defaultFindVisibility === 'number') {
return options.defaultFindVisibility;
}
else if (typeof options.defaultFindVisibility === 'undefined') {
return 2 /* TreeVisibility.Recurse */;
}
else {
return options.defaultFindVisibility(e.element);
}
},
stickyScrollDelegate: options.stickyScrollDelegate
};
}
function dfs(node, fn) {
fn(node);
node.children.forEach(child => dfs(child, fn));
}
class AsyncDataTree {
get onDidScroll() { return this.tree.onDidScroll; }
get onDidChangeFocus() { return Event.map(this.tree.onDidChangeFocus, asTreeEvent); }
get onDidChangeSelection() { return Event.map(this.tree.onDidChangeSelection, asTreeEvent); }
get onMouseDblClick() { return Event.map(this.tree.onMouseDblClick, asTreeMouseEvent); }
get onPointer() { return Event.map(this.tree.onPointer, asTreeMouseEvent); }
get onDidFocus() { return this.tree.onDidFocus; }
/**
* To be used internally only!
* @deprecated
*/
get onDidChangeModel() { return this.tree.onDidChangeModel; }
get onDidChangeCollapseState() { return this.tree.onDidChangeCollapseState; }
get onDidChangeStickyScrollFocused() { return this.tree.onDidChangeStickyScrollFocused; }
get onDidDispose() { return this.tree.onDidDispose; }
constructor(user, container, delegate, renderers, dataSource, options = {}) {
this.user = user;
this.dataSource = dataSource;
this.nodes = new Map();
this.subTreeRefreshPromises = new Map();
this.refreshPromises = new Map();
this._onDidRender = new Emitter();
this._onDidChangeNodeSlowState = new Emitter();
this.nodeMapper = new WeakMapper(node => new AsyncDataTreeNodeWrapper(node));
this.disposables = new DisposableStore();
this.identityProvider = options.identityProvider;
this.autoExpandSingleChildren = typeof options.autoExpandSingleChildren === 'undefined' ? false : options.autoExpandSingleChildren;
this.sorter = options.sorter;
this.getDefaultCollapseState = e => options.collapseByDefault ? (options.collapseByDefault(e) ? ObjectTreeElementCollapseState.PreserveOrCollapsed : ObjectTreeElementCollapseState.PreserveOrExpanded) : undefined;
let asyncFindEnabled = false;
let findFilter;
if (options.findProvider && (options.findWidgetEnabled ?? true) && options.keyboardNavigationLabelProvider && options.contextViewProvider) {
asyncFindEnabled = true;
findFilter = new AsyncFindFilter(options.findProvider, options.keyboardNavigationLabelProvider, options.filter);
}
this.tree = this.createTree(user, container, delegate, renderers, { ...options, findWidgetEnabled: !asyncFindEnabled, filter: findFilter ?? options.filter });
this.root = createAsyncDataTreeNode({
element: undefined,
parent: null,
hasChildren: true,
defaultCollapseState: undefined
});
if (this.identityProvider) {
this.root = {
...this.root,
id: null
};
}
this.nodes.set(null, this.root);
this.tree.onDidChangeCollapseState(this._onDidChangeCollapseState, this, this.disposables);
if (asyncFindEnabled) {
const findOptions = {
styles: options.findWidgetStyles,
showNotFoundMessage: options.showNotFoundMessage,
defaultFindMatchType: options.defaultFindMatchType,
defaultFindMode: options.defaultFindMode,
};
this.findController = this.disposables.add(new AsyncFindController(this.tree, options.findProvider, findFilter, this.tree.options.contextViewProvider, findOptions));
this.focusNavigationFilter = node => this.findController.shouldFocusWhenNavigating(node);
this.onDidChangeFindOpenState = this.findController.onDidChangeOpenState;
this.onDidChangeFindMode = this.findController.onDidChangeMode;
this.onDidChangeFindMatchType = this.findController.onDidChangeMatchType;
}
else {
this.onDidChangeFindOpenState = this.tree.onDidChangeFindOpenState;
this.onDidChangeFindMode = this.tree.onDidChangeFindMode;
this.onDidChangeFindMatchType = this.tree.onDidChangeFindMatchType;
}
}
createTree(user, container, delegate, renderers, options) {
const objectTreeDelegate = new ComposedTreeDelegate(delegate);
const objectTreeRenderers = renderers.map(r => new AsyncDataTreeRenderer(r, this.nodeMapper, this._onDidChangeNodeSlowState.event));
const objectTreeOptions = asObjectTreeOptions(options) || {};
return new ObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions);
}
updateOptions(optionsUpdate = {}) {
if (this.findController) {
if (optionsUpdate.defaultFindMode !== undefined) {
this.findController.mode = optionsUpdate.defaultFindMode;
}
if (optionsUpdate.defaultFindMatchType !== undefined) {
this.findController.matchType = optionsUpdate.defaultFindMatchType;
}
}
this.tree.updateOptions(optionsUpdate);
}
// Widget
getHTMLElement() {
return this.tree.getHTMLElement();
}
get scrollTop() {
return this.tree.scrollTop;
}
set scrollTop(scrollTop) {
this.tree.scrollTop = scrollTop;
}
get scrollHeight() {
return this.tree.scrollHeight;
}
get renderHeight() {
return this.tree.renderHeight;
}
domFocus() {
this.tree.domFocus();
}
layout(height, width) {
this.tree.layout(height, width);
}
style(styles) {
this.tree.style(styles);
}
// Model
getInput() {
return this.root.element;
}
async setInput(input, viewState) {
this.cancelAllRefreshPromises();
this.root.element = input;
const viewStateContext = viewState && { viewState, focus: [], selection: [] };
await this._updateChildren(input, true, false, viewStateContext);
if (viewStateContext) {
this.tree.setFocus(viewStateContext.focus);
this.tree.setSelection(viewStateContext.selection);
}
if (viewState && typeof viewState.scrollTop === 'number') {
this.scrollTop = viewState.scrollTop;
}
}
cancelAllRefreshPromises(includeSubTrees = false) {
this.refreshPromises.forEach(promise => promise.cancel());
this.refreshPromises.clear();
if (includeSubTrees) {
this.subTreeRefreshPromises.forEach(promise => promise.cancel());
this.subTreeRefreshPromises.clear();
}
}
async _updateChildren(element = this.root.element, recursive = true, rerender = false, viewStateContext, options) {
if (typeof this.root.element === 'undefined') {
throw new TreeError(this.user, 'Tree input not set');
}
if (this.root.refreshPromise) {
await this.root.refreshPromise;
await Event.toPromise(this._onDidRender.event);
}
const node = this.getDataNode(element);
await this.refreshAndRenderNode(node, recursive, viewStateContext, options);
if (rerender) {
try {
this.tree.rerender(node);
}
catch {
// missing nodes are fine, this could've resulted from
// parallel refresh calls, removing `node` altogether
}
}
}
// View
rerender(element) {
if (element === undefined || element === this.root.element) {
this.tree.rerender();
return;
}
const node = this.getDataNode(element);
this.tree.rerender(node);
}
// Tree
getNode(element = this.root.element) {
const dataNode = this.getDataNode(element);
const node = this.tree.getNode(dataNode === this.root ? null : dataNode);
return this.nodeMapper.map(node);
}
collapse(element, recursive = false) {
const node = this.getDataNode(element);
return this.tree.collapse(node === this.root ? null : node, recursive);
}
async expand(element, recursive = false) {
if (typeof this.root.element === 'undefined') {
throw new TreeError(this.user, 'Tree input not set');
}
if (this.root.refreshPromise) {
await this.root.refreshPromise;
await Event.toPromise(this._onDidRender.event);
}
const node = this.getDataNode(element);
if (this.tree.hasElement(node) && !this.tree.isCollapsible(node)) {
return false;
}
if (node.refreshPromise) {
await node.refreshPromise;
await Event.toPromise(this._onDidRender.event);
}
if (node !== this.root && !node.refreshPromise && !this.tree.isCollapsed(node)) {
return false;
}
const result = this.tree.expand(node === this.root ? null : node, recursive);
if (node.refreshPromise) {
await node.refreshPromise;
await Event.toPromise(this._onDidRender.event);
}
return result;
}
setSelection(elements, browserEvent) {
const nodes = elements.map(e => this.getDataNode(e));
this.tree.setSelection(nodes, browserEvent);
}
getSelection() {
const nodes = this.tree.getSelection();
return nodes.map(n => n.element);
}
setFocus(elements, browserEvent) {
const nodes = elements.map(e => this.getDataNode(e));
this.tree.setFocus(nodes, browserEvent);
}
getFocus() {
const nodes = this.tree.getFocus();
return nodes.map(n => n.element);
}
reveal(element, relativeTop) {
this.tree.reveal(this.getDataNode(element), relativeTop);
}
// Tree navigation
getParentElement(element) {
const node = this.tree.getParentElement(this.getDataNode(element));
return (node && node.element);
}
getFirstElementChild(element = this.root.element) {
const dataNode = this.getDataNode(element);
const node = this.tree.getFirstElementChild(dataNode === this.root ? null : dataNode);
return (node && node.element);
}
// Implementation
getDataNode(element) {
const node = this.nodes.get((element === this.root.element ? null : element));
if (!node) {
const nodeIdentity = this.identityProvider?.getId(element).toString();
throw new TreeError(this.user, `Data tree node not found${nodeIdentity ? `: ${nodeIdentity}` : ''}`);
}
return node;
}
async refreshAndRenderNode(node, recursive, viewStateContext, options) {
if (this.disposables.isDisposed) {
return; // tree disposed during refresh, again (#228211)
}
await this.refreshNode(node, recursive, viewStateContext);
if (this.disposables.isDisposed) {
return; // tree disposed during refresh (#199264)
}
this.render(node, viewStateContext, options);
}
async refreshNode(node, recursive, viewStateContext) {
let result;
this.subTreeRefreshPromises.forEach((refreshPromise, refreshNode) => {
if (!result && intersects(refreshNode, node)) {
result = refreshPromise.then(() => this.refreshNode(node, recursive, viewStateContext));
}
});
if (result) {
return result;
}
if (node !== this.root) {
const treeNode = this.tree.getNode(node);
if (treeNode.collapsed) {
node.hasChildren = !!this.dataSource.hasChildren(node.element);
node.stale = true;
this.setChildren(node, [], recursive, viewStateContext);
return;
}
}
return this.doRefreshSubTree(node, recursive, viewStateContext);
}
async doRefreshSubTree(node, recursive, viewStateContext) {
const cancelablePromise = createCancelablePromise(async () => {
const childrenToRefresh = await this.doRefreshNode(node, recursive, viewStateContext);
node.stale = false;
await Promises.settled(childrenToRefresh.map(child => this.doRefreshSubTree(child, recursive, viewStateContext)));
});
node.refreshPromise = cancelablePromise;
this.subTreeRefreshPromises.set(node, cancelablePromise);
cancelablePromise.finally(() => {
node.refreshPromise = undefined;
this.subTreeRefreshPromises.delete(node);
});
return cancelablePromise;
}
async doRefreshNode(node, recursive, viewStateContext) {
node.hasChildren = !!this.dataSource.hasChildren(node.element);
let childrenPromise;
if (!node.hasChildren) {
childrenPromise = Promise.resolve(Iterable.empty());
}
else {
const children = this.doGetChildren(node);
if (isIterable(children)) {
childrenPromise = Promise.resolve(children);
}
else {
const slowTimeout = timeout(800);
slowTimeout.then(() => {
node.slow = true;
this._onDidChangeNodeSlowState.fire(node);
}, _ => null);
childrenPromise = children.finally(() => slowTimeout.cancel());
}
}
try {
const children = await childrenPromise;
return this.setChildren(node, children, recursive, viewStateContext);
}
catch (err) {
if (node !== this.root && this.tree.hasElement(node)) {
this.tree.collapse(node);
}
if (isCancellationError(err)) {
return [];
}
throw err;
}
finally {
if (node.slow) {
node.slow = false;
this._onDidChangeNodeSlowState.fire(node);
}
}
}
doGetChildren(node) {
let result = this.refreshPromises.get(node);
if (result) {
return result;
}
const children = this.dataSource.getChildren(node.element);
if (isIterable(children)) {
return this.processChildren(children);
}
else {
result = createCancelablePromise(async () => this.processChildren(await children));
this.refreshPromises.set(node, result);
return result.finally(() => { this.refreshPromises.delete(node); });
}
}
_onDidChangeCollapseState({ node, deep }) {
if (node.element === null) {
return;
}
if (!node.collapsed && node.element.stale) {
if (deep) {
this.collapse(node.element.element);
}
else {
this.refreshAndRenderNode(node.element, false)
.catch(onUnexpectedError);
}
}
}
setChildren(node, childrenElementsIterable, recursive, viewStateContext) {
const childrenElements = [...childrenElementsIterable];
// perf: if the node was and still is a leaf, avoid all this hassle
if (node.children.length === 0 && childrenElements.length === 0) {
return [];
}
const nodesToForget = new Map();
const childrenTreeNodesById = new Map();
for (const child of node.children) {
nodesToForget.set(child.element, child);
if (this.identityProvider) {
childrenTreeNodesById.set(child.id, { node: child, collapsed: this.tree.hasElement(child) && this.tree.isCollapsed(child) });
}
}
const childrenToRefresh = [];
const children = childrenElements.map(element => {
const hasChildren = !!this.dataSource.hasChildren(element);
if (!this.identityProvider) {
const asyncDataTreeNode = createAsyncDataTreeNode({ element, parent: node, hasChildren, defaultCollapseState: this.getDefaultCollapseState(element) });
if (hasChildren && asyncDataTreeNode.defaultCollapseState === ObjectTreeElementCollapseState.PreserveOrExpanded) {
childrenToRefresh.push(asyncDataTreeNode);
}
return asyncDataTreeNode;
}
const id = this.identityProvider.getId(element).toString();
const result = childrenTreeNodesById.get(id);
if (result) {
const asyncDataTreeNode = result.node;
nodesToForget.delete(asyncDataTreeNode.element);
this.nodes.delete(asyncDataTreeNode.element);
this.nodes.set(element, asyncDataTreeNode);
asyncDataTreeNode.element = element;
asyncDataTreeNode.hasChildren = hasChildren;
if (recursive) {
if (result.collapsed) {
asyncDataTreeNode.children.forEach(node => dfs(node, node => this.nodes.delete(node.element)));
asyncDataTreeNode.children.splice(0, asyncDataTreeNode.children.length);
asyncDataTreeNode.stale = true;
}
else {
childrenToRefresh.push(asyncDataTreeNode);
}
}
else if (hasChildren && !result.collapsed) {
childrenToRefresh.push(asyncDataTreeNode);
}
return asyncDataTreeNode;
}
const childAsyncDataTreeNode = createAsyncDataTreeNode({ element, parent: node, id, hasChildren, defaultCollapseState: this.getDefaultCollapseState(element) });
if (viewStateContext && viewStateContext.viewState.focus && viewStateContext.viewState.focus.indexOf(id) > -1) {
viewStateContext.focus.push(childAsyncDataTreeNode);
}
if (viewStateContext && viewStateContext.viewState.selection && viewStateContext.viewState.selection.indexOf(id) > -1) {
viewStateContext.selection.push(childAsyncDataTreeNode);
}
if (viewStateContext && viewStateContext.viewState.expanded && viewStateContext.viewState.expanded.indexOf(id) > -1) {
childrenToRefresh.push(childAsyncDataTreeNode);
}
else if (hasChildren && childAsyncDataTreeNode.defaultCollapseState === ObjectTreeElementCollapseState.PreserveOrExpanded) {
childrenToRefresh.push(childAsyncDataTreeNode);
}
return childAsyncDataTreeNode;
});
for (const node of nodesToForget.values()) {
dfs(node, node => this.nodes.delete(node.element));
}
for (const child of children) {
this.nodes.set(child.element, child);
}
splice(node.children, 0, node.children.length, children);
// TODO@joao this doesn't take filter into account
if (node !== this.root && this.autoExpandSingleChildren && children.length === 1 && childrenToRefresh.length === 0) {
children[0].forceExpanded = true;
childrenToRefresh.push(children[0]);
}
return childrenToRefresh;
}
render(node, viewStateContext, options) {
const children = node.children.map(node => this.asTreeElement(node, viewStateContext));
const objectTreeOptions = options && {
...options,
diffIdentityProvider: options.diffIdentityProvider && {
getId(node) {
return options.diffIdentityProvider.getId(node.element);
}
}
};
this.tree.setChildren(node === this.root ? null : node, children, objectTreeOptions);
if (node !== this.root) {
this.tree.setCollapsible(node, node.hasChildren);
}
this._onDidRender.fire();
}
asTreeElement(node, viewStateContext) {
if (node.stale) {
return {
element: node,
collapsible: node.hasChildren,
collapsed: true
};
}
let collapsed;
if (viewStateContext && viewStateContext.viewState.expanded && node.id && viewStateContext.viewState.expanded.indexOf(node.id) > -1) {
collapsed = false;
}
else if (node.forceExpanded) {
collapsed = false;
node.forceExpanded = false;
}
else {
collapsed = node.defaultCollapseState;
}
return {
element: node,
children: node.hasChildren ? Iterable.map(node.children, child => this.asTreeElement(child, viewStateContext)) : [],
collapsible: node.hasChildren,
collapsed
};
}
processChildren(children) {
if (this.sorter) {
children = [...children].sort(this.sorter.compare.bind(this.sorter));
}
return children;
}
dispose() {
this.disposables.dispose();
this.tree.dispose();
}
}
class CompressibleAsyncDataTreeNodeWrapper {
get element() {
return {
elements: this.node.element.elements.map(e => e.element),
incompressible: this.node.element.incompressible
};
}
get children() { return this.node.children.map(node => new CompressibleAsyncDataTreeNodeWrapper(node)); }
get depth() { return this.node.depth; }
get visibleChildrenCount() { return this.node.visibleChildrenCount; }
get visibleChildIndex() { return this.node.visibleChildIndex; }
get collapsible() { return this.node.collapsible; }
get collapsed() { return this.node.collapsed; }
get visible() { return this.node.visible; }
get filterData() { return this.node.filterData; }
constructor(node) {
this.node = node;
}
}
class CompressibleAsyncDataTreeRenderer {
constructor(renderer, nodeMapper, compressibleNodeMapperProvider, onDidChangeTwistieState) {
this.renderer = renderer;
this.nodeMapper = nodeMapper;
this.compressibleNodeMapperProvider = compressibleNodeMapperProvider;
this.onDidChangeTwistieState = onDidChangeTwistieState;
this.renderedNodes = new Map();
this.disposables = [];
this.templateId = renderer.templateId;
}
renderTemplate(container) {
const templateData = this.renderer.renderTemplate(container);
return { templateData };
}
renderElement(node, index, templateData, details) {
this.renderer.renderElement(this.nodeMapper.map(node), index, templateData.templateData, details);
}
renderCompressedElements(node, index, templateData, details) {
this.renderer.renderCompressedElements(this.compressibleNodeMapperProvider().map(node), index, templateData.templateData, details);
}
renderTwistie(element, twistieElement) {
if (element.slow) {
twistieElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.treeItemLoading));
return true;
}
else {
twistieElement.classList.remove(...ThemeIcon.asClassNameArray(Codicon.treeItemLoading));
return false;
}
}
disposeElement(node, index, templateData, details) {
this.renderer.disposeElement?.(this.nodeMapper.map(node), index, templateData.templateData, details);
}
disposeCompressedElements(node, index, templateData, details) {
this.renderer.disposeCompressedElements?.(this.compressibleNodeMapperProvider().map(node), index, templateData.templateData, details);
}
disposeTemplate(templateData) {
this.renderer.disposeTemplate(templateData.templateData);
}
dispose() {
this.renderedNodes.clear();
this.disposables = dispose(this.disposables);
}
}
function asCompressibleObjectTreeOptions(options) {
const objectTreeOptions = options && asObjectTreeOptions(options);
return objectTreeOptions && {
...objectTreeOptions,
keyboardNavigationLabelProvider: objectTreeOptions.keyboardNavigationLabelProvider && {
...objectTreeOptions.keyboardNavigationLabelProvider,
getCompressedNodeKeyboardNavigationLabel(els) {
return options.keyboardNavigationLabelProvider.getCompressedNodeKeyboardNavigationLabel(els.map(e => e.element));
}
},
stickyScrollDelegate: objectTreeOptions.stickyScrollDelegate
};
}
class CompressibleAsyncDataTree extends AsyncDataTree {
constructor(user, container, virtualDelegate, compressionDelegate, renderers, dataSource, options = {}) {
super(user, container, virtualDelegate, renderers, dataSource, options);
this.compressionDelegate = compressionDelegate;
this.compressibleNodeMapper = new WeakMapper(node => new CompressibleAsyncDataTreeNodeWrapper(node));
this.filter = options.filter;
}
createTree(user, container, delegate, renderers, options) {
const objectTreeDelegate = new ComposedTreeDelegate(delegate);
const objectTreeRenderers = renderers.map(r => new CompressibleAsyncDataTreeRenderer(r, this.nodeMapper, () => this.compressibleNodeMapper, this._onDidChangeNodeSlowState.event));
const objectTreeOptions = asCompressibleObjectTreeOptions(options) || {};
return new CompressibleObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions);
}
asTreeElement(node, viewStateContext) {
return {
incompressible: this.compressionDelegate.isIncompressible(node.element),
...super.asTreeElement(node, viewStateContext)
};
}
render(node, viewStateContext, options) {
if (!this.identityProvider) {
return super.render(node, viewStateContext);
}
// Preserve traits across compressions. Hacky but does the trick.
// This is hard to fix properly since it requires rewriting the traits
// across trees and lists. Let's just keep it this way for now.
const getId = (element) => this.identityProvider.getId(element).toString();
const getUncompressedIds = (nodes) => {
const result = new Set();
for (const node of nodes) {
const compressedNode = this.tree.getCompressedTreeNode(node === this.root ? null : node);
if (!compressedNode.element) {
continue;
}
for (const node of compressedNode.element.elements) {
result.add(getId(node.element));
}
}
return result;
};
const oldSelection = getUncompressedIds(this.tree.getSelection());
const oldFocus = getUncompressedIds(this.tree.getFocus());
super.render(node, viewStateContext, options);
const selection = this.getSelection();
let didChangeSelection = false;
const focus = this.getFocus();
let didChangeFocus = false;
const visit = (node) => {
const compressedNode = node.element;
if (compressedNode) {
for (let i = 0; i < compressedNode.elements.length; i++) {
const id = getId(compressedNode.elements[i].element);
const element = compressedNode.elements[compressedNode.elements.length - 1].element;
// github.com/microsoft/vscode/issues/85938
if (oldSelection.has(id) && selection.indexOf(element) === -1) {
selection.push(element);
didChangeSelection = true;
}
if (oldFocus.has(id) && focus.indexOf(element) === -1) {
focus.push(element);
didChangeFocus = true;
}
}
}
node.children.forEach(visit);
};
visit(this.tree.getCompressedTreeNode(node === this.root ? null : node));
if (didChangeSelection) {
this.setSelection(selection);
}
if (didChangeFocus) {
this.setFocus(focus);
}
}
// For compressed async data trees, `TreeVisibility.Recurse` doesn't currently work
// and we have to filter everything beforehand
// Related to #85193 and #85835
processChildren(children) {
if (this.filter) {
children = Iterable.filter(children, e => {
const result = this.filter.filter(e, 1 /* TreeVisibility.Visible */);
const visibility = getVisibility(result);
if (visibility === 2 /* TreeVisibility.Recurse */) {
throw new Error('Recursive tree visibility not supported in async data compressed trees');
}
return visibility === 1 /* TreeVisibility.Visible */;
});
}
return super.processChildren(children);
}
}
function getVisibility(filterResult) {
if (typeof filterResult === 'boolean') {
return filterResult ? 1 /* TreeVisibility.Visible */ : 0 /* TreeVisibility.Hidden */;
}
else if (isFilterResult(filterResult)) {
return getVisibleState(filterResult.visibility);
}
else {
return getVisibleState(filterResult);
}
}
export { AsyncDataTree, CompressibleAsyncDataTree };

Some files were not shown because too many files have changed in this diff Show More