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
@@ -0,0 +1,104 @@
import { applyFontInfo } from './domFontInfo.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class CharWidthRequest {
constructor(chr, type) {
this.chr = chr;
this.type = type;
this.width = 0;
}
fulfill(width) {
this.width = width;
}
}
class DomCharWidthReader {
constructor(bareFontInfo, requests) {
this._bareFontInfo = bareFontInfo;
this._requests = requests;
this._container = null;
this._testElements = null;
}
read(targetWindow) {
// Create a test container with all these test elements
this._createDomElements();
// Add the container to the DOM
targetWindow.document.body.appendChild(this._container);
// Read character widths
this._readFromDomElements();
// Remove the container from the DOM
this._container?.remove();
this._container = null;
this._testElements = null;
}
_createDomElements() {
const container = document.createElement('div');
container.style.position = 'absolute';
container.style.top = '-50000px';
container.style.width = '50000px';
const regularDomNode = document.createElement('div');
applyFontInfo(regularDomNode, this._bareFontInfo);
container.appendChild(regularDomNode);
const boldDomNode = document.createElement('div');
applyFontInfo(boldDomNode, this._bareFontInfo);
boldDomNode.style.fontWeight = 'bold';
container.appendChild(boldDomNode);
const italicDomNode = document.createElement('div');
applyFontInfo(italicDomNode, this._bareFontInfo);
italicDomNode.style.fontStyle = 'italic';
container.appendChild(italicDomNode);
const testElements = [];
for (const request of this._requests) {
let parent;
if (request.type === 0 /* CharWidthRequestType.Regular */) {
parent = regularDomNode;
}
if (request.type === 2 /* CharWidthRequestType.Bold */) {
parent = boldDomNode;
}
if (request.type === 1 /* CharWidthRequestType.Italic */) {
parent = italicDomNode;
}
parent.appendChild(document.createElement('br'));
const testElement = document.createElement('span');
DomCharWidthReader._render(testElement, request);
parent.appendChild(testElement);
testElements.push(testElement);
}
this._container = container;
this._testElements = testElements;
}
static _render(testElement, request) {
if (request.chr === ' ') {
let htmlString = '\u00a0';
// Repeat character 256 (2^8) times
for (let i = 0; i < 8; i++) {
htmlString += htmlString;
}
testElement.innerText = htmlString;
}
else {
let testString = request.chr;
// Repeat character 256 (2^8) times
for (let i = 0; i < 8; i++) {
testString += testString;
}
testElement.textContent = testString;
}
}
_readFromDomElements() {
for (let i = 0, len = this._requests.length; i < len; i++) {
const request = this._requests[i];
const testElement = this._testElements[i];
request.fulfill(testElement.offsetWidth / 256);
}
}
}
function readCharWidths(targetWindow, bareFontInfo, requests) {
const reader = new DomCharWidthReader(bareFontInfo, requests);
reader.read(targetWindow);
}
export { CharWidthRequest, readCharWidths };
@@ -0,0 +1,28 @@
import { FastDomNode } from '../../../base/browser/fastDomNode.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
function applyFontInfo(domNode, fontInfo) {
if (domNode instanceof FastDomNode) {
domNode.setFontFamily(fontInfo.getMassagedFontFamily());
domNode.setFontWeight(fontInfo.fontWeight);
domNode.setFontSize(fontInfo.fontSize);
domNode.setFontFeatureSettings(fontInfo.fontFeatureSettings);
domNode.setFontVariationSettings(fontInfo.fontVariationSettings);
domNode.setLineHeight(fontInfo.lineHeight);
domNode.setLetterSpacing(fontInfo.letterSpacing);
}
else {
domNode.style.fontFamily = fontInfo.getMassagedFontFamily();
domNode.style.fontWeight = fontInfo.fontWeight;
domNode.style.fontSize = fontInfo.fontSize + 'px';
domNode.style.fontFeatureSettings = fontInfo.fontFeatureSettings;
domNode.style.fontVariationSettings = fontInfo.fontVariationSettings;
domNode.style.lineHeight = fontInfo.lineHeight + 'px';
domNode.style.letterSpacing = fontInfo.letterSpacing + 'px';
}
}
export { applyFontInfo };
@@ -0,0 +1,292 @@
import { isWebKit, isFirefox, isSafari, isWebkitWebView } from '../../../base/browser/browser.js';
import { equals } from '../../../base/common/arrays.js';
import { Emitter } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { deepClone } from '../../../base/common/objects.js';
import { isMacintosh } from '../../../base/common/platform.js';
import { ElementSizeObserver } from './elementSizeObserver.js';
import { FontMeasurements } from './fontMeasurements.js';
import { migrateOptions } from './migrateOptions.js';
import { TabFocus } from './tabFocus.js';
import { ComputeOptionsMemory, editorOptionsRegistry, ConfigurationChangedEvent } from '../../common/config/editorOptions.js';
import { EditorZoom } from '../../common/config/editorZoom.js';
import { createBareFontInfoFromValidatedSettings } from '../../common/config/fontInfoFromSettings.js';
import { IAccessibilityService } from '../../../platform/accessibility/common/accessibility.js';
import { getWindow, getWindowById } from '../../../base/browser/dom.js';
import { PixelRatio } from '../../../base/browser/pixelRatio.js';
import { InputMode } from '../../common/inputMode.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
let EditorConfiguration = class EditorConfiguration extends Disposable {
constructor(isSimpleWidget, contextMenuId, options, container, _accessibilityService) {
super();
this._accessibilityService = _accessibilityService;
this._onDidChange = this._register(new Emitter());
this.onDidChange = this._onDidChange.event;
this._onDidChangeFast = this._register(new Emitter());
this.onDidChangeFast = this._onDidChangeFast.event;
this._isDominatedByLongLines = false;
this._viewLineCount = 1;
this._lineNumbersDigitCount = 1;
this._reservedHeight = 0;
this._glyphMarginDecorationLaneCount = 1;
this._computeOptionsMemory = new ComputeOptionsMemory();
this.isSimpleWidget = isSimpleWidget;
this.contextMenuId = contextMenuId;
this._containerObserver = this._register(new ElementSizeObserver(container, options.dimension));
this._targetWindowId = getWindow(container).vscodeWindowId;
this._rawOptions = deepCloneAndMigrateOptions(options);
this._validatedOptions = EditorOptionsUtil.validateOptions(this._rawOptions);
this.options = this._computeOptions();
if (this.options.get(19 /* EditorOption.automaticLayout */)) {
this._containerObserver.startObserving();
}
this._register(EditorZoom.onDidChangeZoomLevel(() => this._recomputeOptions()));
this._register(TabFocus.onDidChangeTabFocus(() => this._recomputeOptions()));
this._register(this._containerObserver.onDidChange(() => this._recomputeOptions()));
this._register(FontMeasurements.onDidChange(() => this._recomputeOptions()));
this._register(PixelRatio.getInstance(getWindow(container)).onDidChange(() => this._recomputeOptions()));
this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => this._recomputeOptions()));
this._register(InputMode.onDidChangeInputMode(() => this._recomputeOptions()));
}
_recomputeOptions() {
const newOptions = this._computeOptions();
const changeEvent = EditorOptionsUtil.checkEquals(this.options, newOptions);
if (changeEvent === null) {
// nothing changed!
return;
}
this.options = newOptions;
this._onDidChangeFast.fire(changeEvent);
this._onDidChange.fire(changeEvent);
}
_computeOptions() {
const partialEnv = this._readEnvConfiguration();
const bareFontInfo = createBareFontInfoFromValidatedSettings(this._validatedOptions, partialEnv.pixelRatio, this.isSimpleWidget);
const fontInfo = this._readFontInfo(bareFontInfo);
const env = {
memory: this._computeOptionsMemory,
outerWidth: partialEnv.outerWidth,
outerHeight: partialEnv.outerHeight - this._reservedHeight,
fontInfo: fontInfo,
extraEditorClassName: partialEnv.extraEditorClassName,
isDominatedByLongLines: this._isDominatedByLongLines,
viewLineCount: this._viewLineCount,
lineNumbersDigitCount: this._lineNumbersDigitCount,
emptySelectionClipboard: partialEnv.emptySelectionClipboard,
pixelRatio: partialEnv.pixelRatio,
tabFocusMode: this._validatedOptions.get(164 /* EditorOption.tabFocusMode */) || TabFocus.getTabFocusMode(),
inputMode: InputMode.getInputMode(),
accessibilitySupport: partialEnv.accessibilitySupport,
glyphMarginDecorationLaneCount: this._glyphMarginDecorationLaneCount,
editContextSupported: partialEnv.editContextSupported
};
return EditorOptionsUtil.computeOptions(this._validatedOptions, env);
}
_readEnvConfiguration() {
return {
extraEditorClassName: getExtraEditorClassName(),
outerWidth: this._containerObserver.getWidth(),
outerHeight: this._containerObserver.getHeight(),
emptySelectionClipboard: isWebKit || isFirefox,
pixelRatio: PixelRatio.getInstance(getWindowById(this._targetWindowId, true).window).value,
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
editContextSupported: typeof globalThis.EditContext === 'function',
accessibilitySupport: (this._accessibilityService.isScreenReaderOptimized()
? 2 /* AccessibilitySupport.Enabled */
: this._accessibilityService.getAccessibilitySupport())
};
}
_readFontInfo(bareFontInfo) {
return FontMeasurements.readFontInfo(getWindowById(this._targetWindowId, true).window, bareFontInfo);
}
getRawOptions() {
return this._rawOptions;
}
updateOptions(_newOptions) {
const newOptions = deepCloneAndMigrateOptions(_newOptions);
const didChange = EditorOptionsUtil.applyUpdate(this._rawOptions, newOptions);
if (!didChange) {
return;
}
this._validatedOptions = EditorOptionsUtil.validateOptions(this._rawOptions);
this._recomputeOptions();
}
observeContainer(dimension) {
this._containerObserver.observe(dimension);
}
setIsDominatedByLongLines(isDominatedByLongLines) {
if (this._isDominatedByLongLines === isDominatedByLongLines) {
return;
}
this._isDominatedByLongLines = isDominatedByLongLines;
this._recomputeOptions();
}
setModelLineCount(modelLineCount) {
const lineNumbersDigitCount = digitCount(modelLineCount);
if (this._lineNumbersDigitCount === lineNumbersDigitCount) {
return;
}
this._lineNumbersDigitCount = lineNumbersDigitCount;
this._recomputeOptions();
}
setViewLineCount(viewLineCount) {
if (this._viewLineCount === viewLineCount) {
return;
}
this._viewLineCount = viewLineCount;
this._recomputeOptions();
}
setReservedHeight(reservedHeight) {
if (this._reservedHeight === reservedHeight) {
return;
}
this._reservedHeight = reservedHeight;
this._recomputeOptions();
}
setGlyphMarginDecorationLaneCount(decorationLaneCount) {
if (this._glyphMarginDecorationLaneCount === decorationLaneCount) {
return;
}
this._glyphMarginDecorationLaneCount = decorationLaneCount;
this._recomputeOptions();
}
};
EditorConfiguration = __decorate([
__param(4, IAccessibilityService)
], EditorConfiguration);
function digitCount(n) {
let r = 0;
while (n) {
n = Math.floor(n / 10);
r++;
}
return r ? r : 1;
}
function getExtraEditorClassName() {
let extra = '';
if (isSafari || isWebkitWebView) {
// See https://github.com/microsoft/vscode/issues/108822
extra += 'no-minimap-shadow ';
extra += 'enable-user-select ';
}
else {
// Use user-select: none in all browsers except Safari and native macOS WebView
extra += 'no-user-select ';
}
if (isMacintosh) {
extra += 'mac ';
}
return extra;
}
class ValidatedEditorOptions {
constructor() {
this._values = [];
}
_read(option) {
return this._values[option];
}
get(id) {
return this._values[id];
}
_write(option, value) {
this._values[option] = value;
}
}
class ComputedEditorOptions {
constructor() {
this._values = [];
}
_read(id) {
if (id >= this._values.length) {
throw new Error('Cannot read uninitialized value');
}
return this._values[id];
}
get(id) {
return this._read(id);
}
_write(id, value) {
this._values[id] = value;
}
}
class EditorOptionsUtil {
static validateOptions(options) {
const result = new ValidatedEditorOptions();
for (const editorOption of editorOptionsRegistry) {
const value = (editorOption.name === '_never_' ? undefined : options[editorOption.name]);
result._write(editorOption.id, editorOption.validate(value));
}
return result;
}
static computeOptions(options, env) {
const result = new ComputedEditorOptions();
for (const editorOption of editorOptionsRegistry) {
result._write(editorOption.id, editorOption.compute(env, result, options._read(editorOption.id)));
}
return result;
}
static _deepEquals(a, b) {
if (typeof a !== 'object' || typeof b !== 'object' || !a || !b) {
return a === b;
}
if (Array.isArray(a) || Array.isArray(b)) {
return (Array.isArray(a) && Array.isArray(b) ? equals(a, b) : false);
}
if (Object.keys(a).length !== Object.keys(b).length) {
return false;
}
for (const key in a) {
if (!EditorOptionsUtil._deepEquals(a[key], b[key])) {
return false;
}
}
return true;
}
static checkEquals(a, b) {
const result = [];
let somethingChanged = false;
for (const editorOption of editorOptionsRegistry) {
const changed = !EditorOptionsUtil._deepEquals(a._read(editorOption.id), b._read(editorOption.id));
result[editorOption.id] = changed;
if (changed) {
somethingChanged = true;
}
}
return (somethingChanged ? new ConfigurationChangedEvent(result) : null);
}
/**
* Returns true if something changed.
* Modifies `options`.
*/
static applyUpdate(options, update) {
let changed = false;
for (const editorOption of editorOptionsRegistry) {
if (update.hasOwnProperty(editorOption.name)) {
const result = editorOption.applyUpdate(options[editorOption.name], update[editorOption.name]);
options[editorOption.name] = result.newValue;
changed = changed || result.didChange;
}
}
return changed;
}
}
function deepCloneAndMigrateOptions(_options) {
const options = deepClone(_options);
migrateOptions(options);
return options;
}
export { ComputedEditorOptions, EditorConfiguration };
@@ -0,0 +1,107 @@
import { Disposable } from '../../../base/common/lifecycle.js';
import { Emitter } from '../../../base/common/event.js';
import { scheduleAtNextAnimationFrame, getWindow } from '../../../base/browser/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 ElementSizeObserver extends Disposable {
constructor(referenceDomElement, dimension) {
super();
this._onDidChange = this._register(new Emitter());
this.onDidChange = this._onDidChange.event;
this._referenceDomElement = referenceDomElement;
this._width = -1;
this._height = -1;
this._resizeObserver = null;
this.measureReferenceDomElement(false, dimension);
}
dispose() {
this.stopObserving();
super.dispose();
}
getWidth() {
return this._width;
}
getHeight() {
return this._height;
}
startObserving() {
if (!this._resizeObserver && this._referenceDomElement) {
// We want to react to the resize observer only once per animation frame
// The first time the resize observer fires, we will react to it immediately.
// Otherwise we will postpone to the next animation frame.
// We'll use `observeContentRect` to store the content rect we received.
let observedDimenstion = null;
const observeNow = () => {
if (observedDimenstion) {
this.observe({ width: observedDimenstion.width, height: observedDimenstion.height });
}
else {
this.observe();
}
};
let shouldObserve = false;
let alreadyObservedThisAnimationFrame = false;
const update = () => {
if (shouldObserve && !alreadyObservedThisAnimationFrame) {
try {
shouldObserve = false;
alreadyObservedThisAnimationFrame = true;
observeNow();
}
finally {
scheduleAtNextAnimationFrame(getWindow(this._referenceDomElement), () => {
alreadyObservedThisAnimationFrame = false;
update();
});
}
}
};
this._resizeObserver = new ResizeObserver((entries) => {
if (entries && entries[0] && entries[0].contentRect) {
observedDimenstion = { width: entries[0].contentRect.width, height: entries[0].contentRect.height };
}
else {
observedDimenstion = null;
}
shouldObserve = true;
update();
});
this._resizeObserver.observe(this._referenceDomElement);
}
}
stopObserving() {
if (this._resizeObserver) {
this._resizeObserver.disconnect();
this._resizeObserver = null;
}
}
observe(dimension) {
this.measureReferenceDomElement(true, dimension);
}
measureReferenceDomElement(emitEvent, dimension) {
let observedWidth = 0;
let observedHeight = 0;
if (dimension) {
observedWidth = dimension.width;
observedHeight = dimension.height;
}
else if (this._referenceDomElement) {
observedWidth = this._referenceDomElement.clientWidth;
observedHeight = this._referenceDomElement.clientHeight;
}
observedWidth = Math.max(5, observedWidth);
observedHeight = Math.max(5, observedHeight);
if (this._width !== observedWidth || this._height !== observedHeight) {
this._width = observedWidth;
this._height = observedHeight;
if (emitEvent) {
this._onDidChange.fire();
}
}
}
}
export { ElementSizeObserver };
@@ -0,0 +1,206 @@
import { getWindowId } from '../../../base/browser/dom.js';
import { PixelRatio } from '../../../base/browser/pixelRatio.js';
import { Emitter } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { CharWidthRequest, readCharWidths } from './charWidthReader.js';
import { EditorFontLigatures } from '../../common/config/editorOptions.js';
import { FontInfo } from '../../common/config/fontInfo.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class FontMeasurementsImpl extends Disposable {
constructor() {
super(...arguments);
this._cache = new Map();
this._evictUntrustedReadingsTimeout = -1;
this._onDidChange = this._register(new Emitter());
this.onDidChange = this._onDidChange.event;
}
dispose() {
if (this._evictUntrustedReadingsTimeout !== -1) {
clearTimeout(this._evictUntrustedReadingsTimeout);
this._evictUntrustedReadingsTimeout = -1;
}
super.dispose();
}
/**
* Clear all cached font information and trigger a change event.
*/
clearAllFontInfos() {
this._cache.clear();
this._onDidChange.fire();
}
_ensureCache(targetWindow) {
const windowId = getWindowId(targetWindow);
let cache = this._cache.get(windowId);
if (!cache) {
cache = new FontMeasurementsCache();
this._cache.set(windowId, cache);
}
return cache;
}
_writeToCache(targetWindow, item, value) {
const cache = this._ensureCache(targetWindow);
cache.put(item, value);
if (!value.isTrusted && this._evictUntrustedReadingsTimeout === -1) {
// Try reading again after some time
this._evictUntrustedReadingsTimeout = targetWindow.setTimeout(() => {
this._evictUntrustedReadingsTimeout = -1;
this._evictUntrustedReadings(targetWindow);
}, 5000);
}
}
_evictUntrustedReadings(targetWindow) {
const cache = this._ensureCache(targetWindow);
const values = cache.getValues();
let somethingRemoved = false;
for (const item of values) {
if (!item.isTrusted) {
somethingRemoved = true;
cache.remove(item);
}
}
if (somethingRemoved) {
this._onDidChange.fire();
}
}
/**
* Read font information.
*/
readFontInfo(targetWindow, bareFontInfo) {
const cache = this._ensureCache(targetWindow);
if (!cache.has(bareFontInfo)) {
let readConfig = this._actualReadFontInfo(targetWindow, bareFontInfo);
if (readConfig.typicalHalfwidthCharacterWidth <= 2 || readConfig.typicalFullwidthCharacterWidth <= 2 || readConfig.spaceWidth <= 2 || readConfig.maxDigitWidth <= 2) {
// Hey, it's Bug 14341 ... we couldn't read
readConfig = new FontInfo({
pixelRatio: PixelRatio.getInstance(targetWindow).value,
fontFamily: readConfig.fontFamily,
fontWeight: readConfig.fontWeight,
fontSize: readConfig.fontSize,
fontFeatureSettings: readConfig.fontFeatureSettings,
fontVariationSettings: readConfig.fontVariationSettings,
lineHeight: readConfig.lineHeight,
letterSpacing: readConfig.letterSpacing,
isMonospace: readConfig.isMonospace,
typicalHalfwidthCharacterWidth: Math.max(readConfig.typicalHalfwidthCharacterWidth, 5),
typicalFullwidthCharacterWidth: Math.max(readConfig.typicalFullwidthCharacterWidth, 5),
canUseHalfwidthRightwardsArrow: readConfig.canUseHalfwidthRightwardsArrow,
spaceWidth: Math.max(readConfig.spaceWidth, 5),
middotWidth: Math.max(readConfig.middotWidth, 5),
wsmiddotWidth: Math.max(readConfig.wsmiddotWidth, 5),
maxDigitWidth: Math.max(readConfig.maxDigitWidth, 5),
}, false);
}
this._writeToCache(targetWindow, bareFontInfo, readConfig);
}
return cache.get(bareFontInfo);
}
_createRequest(chr, type, all, monospace) {
const result = new CharWidthRequest(chr, type);
all.push(result);
monospace?.push(result);
return result;
}
_actualReadFontInfo(targetWindow, bareFontInfo) {
const all = [];
const monospace = [];
const typicalHalfwidthCharacter = this._createRequest('n', 0 /* CharWidthRequestType.Regular */, all, monospace);
const typicalFullwidthCharacter = this._createRequest('\uff4d', 0 /* CharWidthRequestType.Regular */, all, null);
const space = this._createRequest(' ', 0 /* CharWidthRequestType.Regular */, all, monospace);
const digit0 = this._createRequest('0', 0 /* CharWidthRequestType.Regular */, all, monospace);
const digit1 = this._createRequest('1', 0 /* CharWidthRequestType.Regular */, all, monospace);
const digit2 = this._createRequest('2', 0 /* CharWidthRequestType.Regular */, all, monospace);
const digit3 = this._createRequest('3', 0 /* CharWidthRequestType.Regular */, all, monospace);
const digit4 = this._createRequest('4', 0 /* CharWidthRequestType.Regular */, all, monospace);
const digit5 = this._createRequest('5', 0 /* CharWidthRequestType.Regular */, all, monospace);
const digit6 = this._createRequest('6', 0 /* CharWidthRequestType.Regular */, all, monospace);
const digit7 = this._createRequest('7', 0 /* CharWidthRequestType.Regular */, all, monospace);
const digit8 = this._createRequest('8', 0 /* CharWidthRequestType.Regular */, all, monospace);
const digit9 = this._createRequest('9', 0 /* CharWidthRequestType.Regular */, all, monospace);
// monospace test: used for whitespace rendering
const rightwardsArrow = this._createRequest('→', 0 /* CharWidthRequestType.Regular */, all, monospace);
const halfwidthRightwardsArrow = this._createRequest('→', 0 /* CharWidthRequestType.Regular */, all, null);
// U+00B7 - MIDDLE DOT
const middot = this._createRequest('·', 0 /* CharWidthRequestType.Regular */, all, monospace);
// U+2E31 - WORD SEPARATOR MIDDLE DOT
const wsmiddotWidth = this._createRequest(String.fromCharCode(0x2E31), 0 /* CharWidthRequestType.Regular */, all, null);
// monospace test: some characters
const monospaceTestChars = '|/-_ilm%';
for (let i = 0, len = monospaceTestChars.length; i < len; i++) {
this._createRequest(monospaceTestChars.charAt(i), 0 /* CharWidthRequestType.Regular */, all, monospace);
this._createRequest(monospaceTestChars.charAt(i), 1 /* CharWidthRequestType.Italic */, all, monospace);
this._createRequest(monospaceTestChars.charAt(i), 2 /* CharWidthRequestType.Bold */, all, monospace);
}
readCharWidths(targetWindow, bareFontInfo, all);
const maxDigitWidth = Math.max(digit0.width, digit1.width, digit2.width, digit3.width, digit4.width, digit5.width, digit6.width, digit7.width, digit8.width, digit9.width);
let isMonospace = (bareFontInfo.fontFeatureSettings === EditorFontLigatures.OFF);
const referenceWidth = monospace[0].width;
for (let i = 1, len = monospace.length; isMonospace && i < len; i++) {
const diff = referenceWidth - monospace[i].width;
if (diff < -1e-3 || diff > 0.001) {
isMonospace = false;
break;
}
}
let canUseHalfwidthRightwardsArrow = true;
if (isMonospace && halfwidthRightwardsArrow.width !== referenceWidth) {
// using a halfwidth rightwards arrow would break monospace...
canUseHalfwidthRightwardsArrow = false;
}
if (halfwidthRightwardsArrow.width > rightwardsArrow.width) {
// using a halfwidth rightwards arrow would paint a larger arrow than a regular rightwards arrow
canUseHalfwidthRightwardsArrow = false;
}
return new FontInfo({
pixelRatio: PixelRatio.getInstance(targetWindow).value,
fontFamily: bareFontInfo.fontFamily,
fontWeight: bareFontInfo.fontWeight,
fontSize: bareFontInfo.fontSize,
fontFeatureSettings: bareFontInfo.fontFeatureSettings,
fontVariationSettings: bareFontInfo.fontVariationSettings,
lineHeight: bareFontInfo.lineHeight,
letterSpacing: bareFontInfo.letterSpacing,
isMonospace: isMonospace,
typicalHalfwidthCharacterWidth: typicalHalfwidthCharacter.width,
typicalFullwidthCharacterWidth: typicalFullwidthCharacter.width,
canUseHalfwidthRightwardsArrow: canUseHalfwidthRightwardsArrow,
spaceWidth: space.width,
middotWidth: middot.width,
wsmiddotWidth: wsmiddotWidth.width,
maxDigitWidth: maxDigitWidth
}, true);
}
}
class FontMeasurementsCache {
constructor() {
this._keys = Object.create(null);
this._values = Object.create(null);
}
has(item) {
const itemId = item.getId();
return !!this._values[itemId];
}
get(item) {
const itemId = item.getId();
return this._values[itemId];
}
put(item, value) {
const itemId = item.getId();
this._keys[itemId] = item;
this._values[itemId] = value;
}
remove(item) {
const itemId = item.getId();
delete this._keys[itemId];
delete this._values[itemId];
}
getValues() {
return Object.keys(this._keys).map(id => this._values[id]);
}
}
const FontMeasurements = new FontMeasurementsImpl();
export { FontMeasurements, FontMeasurementsImpl };
@@ -0,0 +1,220 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class EditorSettingMigration {
static { this.items = []; }
constructor(key, migrate) {
this.key = key;
this.migrate = migrate;
}
apply(options) {
const value = EditorSettingMigration._read(options, this.key);
const read = (key) => EditorSettingMigration._read(options, key);
const write = (key, value) => EditorSettingMigration._write(options, key, value);
this.migrate(value, read, write);
}
static _read(source, key) {
if (typeof source === 'undefined' || source === null) {
return undefined;
}
const firstDotIndex = key.indexOf('.');
if (firstDotIndex >= 0) {
const firstSegment = key.substring(0, firstDotIndex);
return this._read(source[firstSegment], key.substring(firstDotIndex + 1));
}
return source[key];
}
static _write(target, key, value) {
const firstDotIndex = key.indexOf('.');
if (firstDotIndex >= 0) {
const firstSegment = key.substring(0, firstDotIndex);
target[firstSegment] = target[firstSegment] || {};
this._write(target[firstSegment], key.substring(firstDotIndex + 1), value);
return;
}
target[key] = value;
}
}
function registerEditorSettingMigration(key, migrate) {
EditorSettingMigration.items.push(new EditorSettingMigration(key, migrate));
}
function registerSimpleEditorSettingMigration(key, values) {
registerEditorSettingMigration(key, (value, read, write) => {
if (typeof value !== 'undefined') {
for (const [oldValue, newValue] of values) {
if (value === oldValue) {
write(key, newValue);
return;
}
}
}
});
}
/**
* Compatibility with old options
*/
function migrateOptions(options) {
EditorSettingMigration.items.forEach(migration => migration.apply(options));
}
registerSimpleEditorSettingMigration('wordWrap', [[true, 'on'], [false, 'off']]);
registerSimpleEditorSettingMigration('lineNumbers', [[true, 'on'], [false, 'off']]);
registerSimpleEditorSettingMigration('cursorBlinking', [['visible', 'solid']]);
registerSimpleEditorSettingMigration('renderWhitespace', [[true, 'boundary'], [false, 'none']]);
registerSimpleEditorSettingMigration('renderLineHighlight', [[true, 'line'], [false, 'none']]);
registerSimpleEditorSettingMigration('acceptSuggestionOnEnter', [[true, 'on'], [false, 'off']]);
registerSimpleEditorSettingMigration('tabCompletion', [[false, 'off'], [true, 'onlySnippets']]);
registerSimpleEditorSettingMigration('hover', [[true, { enabled: true }], [false, { enabled: false }]]);
registerSimpleEditorSettingMigration('parameterHints', [[true, { enabled: true }], [false, { enabled: false }]]);
registerSimpleEditorSettingMigration('autoIndent', [[false, 'advanced'], [true, 'full']]);
registerSimpleEditorSettingMigration('matchBrackets', [[true, 'always'], [false, 'never']]);
registerSimpleEditorSettingMigration('renderFinalNewline', [[true, 'on'], [false, 'off']]);
registerSimpleEditorSettingMigration('cursorSmoothCaretAnimation', [[true, 'on'], [false, 'off']]);
registerSimpleEditorSettingMigration('occurrencesHighlight', [[true, 'singleFile'], [false, 'off']]);
registerSimpleEditorSettingMigration('wordBasedSuggestions', [[true, 'matchingDocuments'], [false, 'off']]);
registerSimpleEditorSettingMigration('defaultColorDecorators', [[true, 'auto'], [false, 'never']]);
registerSimpleEditorSettingMigration('minimap.autohide', [[true, 'mouseover'], [false, 'none']]);
registerEditorSettingMigration('autoClosingBrackets', (value, read, write) => {
if (value === false) {
write('autoClosingBrackets', 'never');
if (typeof read('autoClosingQuotes') === 'undefined') {
write('autoClosingQuotes', 'never');
}
if (typeof read('autoSurround') === 'undefined') {
write('autoSurround', 'never');
}
}
});
registerEditorSettingMigration('renderIndentGuides', (value, read, write) => {
if (typeof value !== 'undefined') {
write('renderIndentGuides', undefined);
if (typeof read('guides.indentation') === 'undefined') {
write('guides.indentation', !!value);
}
}
});
registerEditorSettingMigration('highlightActiveIndentGuide', (value, read, write) => {
if (typeof value !== 'undefined') {
write('highlightActiveIndentGuide', undefined);
if (typeof read('guides.highlightActiveIndentation') === 'undefined') {
write('guides.highlightActiveIndentation', !!value);
}
}
});
const suggestFilteredTypesMapping = {
method: 'showMethods',
function: 'showFunctions',
constructor: 'showConstructors',
deprecated: 'showDeprecated',
field: 'showFields',
variable: 'showVariables',
class: 'showClasses',
struct: 'showStructs',
interface: 'showInterfaces',
module: 'showModules',
property: 'showProperties',
event: 'showEvents',
operator: 'showOperators',
unit: 'showUnits',
value: 'showValues',
constant: 'showConstants',
enum: 'showEnums',
enumMember: 'showEnumMembers',
keyword: 'showKeywords',
text: 'showWords',
color: 'showColors',
file: 'showFiles',
reference: 'showReferences',
folder: 'showFolders',
typeParameter: 'showTypeParameters',
snippet: 'showSnippets',
};
registerEditorSettingMigration('suggest.filteredTypes', (value, read, write) => {
if (value && typeof value === 'object') {
for (const entry of Object.entries(suggestFilteredTypesMapping)) {
const v = value[entry[0]];
if (v === false) {
if (typeof read(`suggest.${entry[1]}`) === 'undefined') {
write(`suggest.${entry[1]}`, false);
}
}
}
write('suggest.filteredTypes', undefined);
}
});
registerEditorSettingMigration('quickSuggestions', (input, read, write) => {
if (typeof input === 'boolean') {
const value = input ? 'on' : 'off';
const newValue = { comments: value, strings: value, other: value };
write('quickSuggestions', newValue);
}
});
// Sticky Scroll
registerEditorSettingMigration('experimental.stickyScroll.enabled', (value, read, write) => {
if (typeof value === 'boolean') {
write('experimental.stickyScroll.enabled', undefined);
if (typeof read('stickyScroll.enabled') === 'undefined') {
write('stickyScroll.enabled', value);
}
}
});
registerEditorSettingMigration('experimental.stickyScroll.maxLineCount', (value, read, write) => {
if (typeof value === 'number') {
write('experimental.stickyScroll.maxLineCount', undefined);
if (typeof read('stickyScroll.maxLineCount') === 'undefined') {
write('stickyScroll.maxLineCount', value);
}
}
});
// Edit Context
registerEditorSettingMigration('editor.experimentalEditContextEnabled', (value, read, write) => {
if (typeof value === 'boolean') {
write('editor.experimentalEditContextEnabled', undefined);
if (typeof read('editor.editContext') === 'undefined') {
write('editor.editContext', value);
}
}
});
// Code Actions on Save
registerEditorSettingMigration('codeActionsOnSave', (value, read, write) => {
if (value && typeof value === 'object') {
let toBeModified = false;
const newValue = {};
for (const entry of Object.entries(value)) {
if (typeof entry[1] === 'boolean') {
toBeModified = true;
newValue[entry[0]] = entry[1] ? 'explicit' : 'never';
}
else {
newValue[entry[0]] = entry[1];
}
}
if (toBeModified) {
write(`codeActionsOnSave`, newValue);
}
}
});
// Migrate Quick Fix Settings
registerEditorSettingMigration('codeActionWidget.includeNearbyQuickfixes', (value, read, write) => {
if (typeof value === 'boolean') {
write('codeActionWidget.includeNearbyQuickfixes', undefined);
if (typeof read('codeActionWidget.includeNearbyQuickFixes') === 'undefined') {
write('codeActionWidget.includeNearbyQuickFixes', value);
}
}
});
// Migrate the lightbulb settings
registerEditorSettingMigration('lightbulb.enabled', (value, read, write) => {
if (typeof value === 'boolean') {
write('lightbulb.enabled', value ? undefined : 'off');
}
});
// NES Code Shifting
registerEditorSettingMigration('inlineSuggest.edits.codeShifting', (value, read, write) => {
if (typeof value === 'boolean') {
write('inlineSuggest.edits.codeShifting', undefined);
write('inlineSuggest.edits.allowCodeShifting', value ? 'always' : 'never');
}
});
export { EditorSettingMigration, migrateOptions };
@@ -0,0 +1,29 @@
import { Emitter } from '../../../base/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 TabFocusImpl {
constructor() {
this._tabFocus = false;
this._onDidChangeTabFocus = new Emitter();
this.onDidChangeTabFocus = this._onDidChangeTabFocus.event;
}
getTabFocusMode() {
return this._tabFocus;
}
setTabFocusMode(tabFocusMode) {
this._tabFocus = tabFocusMode;
this._onDidChangeTabFocus.fire(this._tabFocus);
}
}
/**
* Control what pressing Tab does.
* If it is false, pressing Tab or Shift-Tab will be handled by the editor.
* If it is true, pressing Tab or Shift-Tab will move the browser focus.
* Defaults to false.
*/
const TabFocus = new TabFocusImpl();
export { TabFocus };
@@ -0,0 +1,174 @@
import { scheduleAtNextAnimationFrame, getWindow } from '../../../base/browser/dom.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { Position } from '../../common/core/position.js';
import { createEditorPagePosition, PageCoordinates, createCoordinatesRelativeToEditor } from '../editorDom.js';
import { MouseTarget } from './mouseTarget.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class DragScrolling extends Disposable {
constructor(_context, _viewHelper, _mouseTargetFactory, _dispatchMouse) {
super();
this._context = _context;
this._viewHelper = _viewHelper;
this._mouseTargetFactory = _mouseTargetFactory;
this._dispatchMouse = _dispatchMouse;
this._operation = null;
}
dispose() {
super.dispose();
this.stop();
}
start(position, mouseEvent) {
if (this._operation) {
this._operation.setPosition(position, mouseEvent);
}
else {
this._operation = this._createDragScrollingOperation(position, mouseEvent);
}
}
stop() {
if (this._operation) {
this._operation.dispose();
this._operation = null;
}
}
}
class DragScrollingOperation extends Disposable {
constructor(_context, _viewHelper, _mouseTargetFactory, _dispatchMouse, position, mouseEvent) {
super();
this._context = _context;
this._viewHelper = _viewHelper;
this._mouseTargetFactory = _mouseTargetFactory;
this._dispatchMouse = _dispatchMouse;
this._position = position;
this._mouseEvent = mouseEvent;
this._lastTime = Date.now();
this._animationFrameDisposable = scheduleAtNextAnimationFrame(getWindow(mouseEvent.browserEvent), () => this._execute());
}
dispose() {
this._animationFrameDisposable.dispose();
super.dispose();
}
setPosition(position, mouseEvent) {
this._position = position;
this._mouseEvent = mouseEvent;
}
/**
* update internal state and return elapsed ms since last time
*/
_tick() {
const now = Date.now();
const elapsed = now - this._lastTime;
this._lastTime = now;
return elapsed;
}
}
class TopBottomDragScrolling extends DragScrolling {
_createDragScrollingOperation(position, mouseEvent) {
return new TopBottomDragScrollingOperation(this._context, this._viewHelper, this._mouseTargetFactory, this._dispatchMouse, position, mouseEvent);
}
}
class TopBottomDragScrollingOperation extends DragScrollingOperation {
/**
* get the number of lines per second to auto-scroll
*/
_getScrollSpeed() {
const lineHeight = this._context.configuration.options.get(75 /* EditorOption.lineHeight */);
const viewportInLines = this._context.configuration.options.get(165 /* EditorOption.layoutInfo */).height / lineHeight;
const outsideDistanceInLines = this._position.outsideDistance / lineHeight;
if (outsideDistanceInLines <= 1.5) {
return Math.max(30, viewportInLines * (1 + outsideDistanceInLines));
}
if (outsideDistanceInLines <= 3) {
return Math.max(60, viewportInLines * (2 + outsideDistanceInLines));
}
return Math.max(200, viewportInLines * (7 + outsideDistanceInLines));
}
_execute() {
const lineHeight = this._context.configuration.options.get(75 /* EditorOption.lineHeight */);
const scrollSpeedInLines = this._getScrollSpeed();
const elapsed = this._tick();
const scrollInPixels = scrollSpeedInLines * (elapsed / 1000) * lineHeight;
const scrollValue = (this._position.outsidePosition === 'above' ? -scrollInPixels : scrollInPixels);
this._context.viewModel.viewLayout.deltaScrollNow(0, scrollValue);
this._viewHelper.renderNow();
const viewportData = this._context.viewLayout.getLinesViewportData();
const edgeLineNumber = (this._position.outsidePosition === 'above' ? viewportData.startLineNumber : viewportData.endLineNumber);
// First, try to find a position that matches the horizontal position of the mouse
let mouseTarget;
{
const editorPos = createEditorPagePosition(this._viewHelper.viewDomNode);
const horizontalScrollbarHeight = this._context.configuration.options.get(165 /* EditorOption.layoutInfo */).horizontalScrollbarHeight;
const pos = new PageCoordinates(this._mouseEvent.pos.x, editorPos.y + editorPos.height - horizontalScrollbarHeight - 0.1);
const relativePos = createCoordinatesRelativeToEditor(this._viewHelper.viewDomNode, editorPos, pos);
mouseTarget = this._mouseTargetFactory.createMouseTarget(this._viewHelper.getLastRenderData(), editorPos, pos, relativePos, null);
}
if (!mouseTarget.position || mouseTarget.position.lineNumber !== edgeLineNumber) {
if (this._position.outsidePosition === 'above') {
mouseTarget = MouseTarget.createOutsideEditor(this._position.mouseColumn, new Position(edgeLineNumber, 1), 'above', this._position.outsideDistance);
}
else {
mouseTarget = MouseTarget.createOutsideEditor(this._position.mouseColumn, new Position(edgeLineNumber, this._context.viewModel.getLineMaxColumn(edgeLineNumber)), 'below', this._position.outsideDistance);
}
}
this._dispatchMouse(mouseTarget, true, 2 /* NavigationCommandRevealType.None */);
this._animationFrameDisposable = scheduleAtNextAnimationFrame(getWindow(mouseTarget.element), () => this._execute());
}
}
class LeftRightDragScrolling extends DragScrolling {
_createDragScrollingOperation(position, mouseEvent) {
return new LeftRightDragScrollingOperation(this._context, this._viewHelper, this._mouseTargetFactory, this._dispatchMouse, position, mouseEvent);
}
}
class LeftRightDragScrollingOperation extends DragScrollingOperation {
/**
* get the number of cols per second to auto-scroll
*/
_getScrollSpeed() {
const charWidth = this._context.configuration.options.get(59 /* EditorOption.fontInfo */).typicalFullwidthCharacterWidth;
const viewportInChars = this._context.configuration.options.get(165 /* EditorOption.layoutInfo */).contentWidth / charWidth;
const outsideDistanceInChars = this._position.outsideDistance / charWidth;
if (outsideDistanceInChars <= 1.5) {
return Math.max(30, viewportInChars * (1 + outsideDistanceInChars));
}
if (outsideDistanceInChars <= 3) {
return Math.max(60, viewportInChars * (2 + outsideDistanceInChars));
}
return Math.max(200, viewportInChars * (7 + outsideDistanceInChars));
}
_execute() {
const charWidth = this._context.configuration.options.get(59 /* EditorOption.fontInfo */).typicalFullwidthCharacterWidth;
const scrollSpeedInChars = this._getScrollSpeed();
const elapsed = this._tick();
const scrollInPixels = scrollSpeedInChars * (elapsed / 1000) * charWidth * 0.5;
const scrollValue = (this._position.outsidePosition === 'left' ? -scrollInPixels : scrollInPixels);
this._context.viewModel.viewLayout.deltaScrollNow(scrollValue, 0);
this._viewHelper.renderNow();
if (!this._position.position) {
return;
}
const edgeLineNumber = this._position.position.lineNumber;
// First, try to find a position that matches the horizontal position of the mouse
let mouseTarget;
{
const editorPos = createEditorPagePosition(this._viewHelper.viewDomNode);
const horizontalScrollbarHeight = this._context.configuration.options.get(165 /* EditorOption.layoutInfo */).horizontalScrollbarHeight;
const pos = new PageCoordinates(this._mouseEvent.pos.x, editorPos.y + editorPos.height - horizontalScrollbarHeight - 0.1);
const relativePos = createCoordinatesRelativeToEditor(this._viewHelper.viewDomNode, editorPos, pos);
mouseTarget = this._mouseTargetFactory.createMouseTarget(this._viewHelper.getLastRenderData(), editorPos, pos, relativePos, null);
}
if (this._position.outsidePosition === 'left') {
mouseTarget = MouseTarget.createOutsideEditor(mouseTarget.mouseColumn, new Position(edgeLineNumber, mouseTarget.mouseColumn), 'left', this._position.outsideDistance);
}
else {
mouseTarget = MouseTarget.createOutsideEditor(mouseTarget.mouseColumn, new Position(edgeLineNumber, mouseTarget.mouseColumn), 'right', this._position.outsideDistance);
}
this._dispatchMouse(mouseTarget, true, 2 /* NavigationCommandRevealType.None */);
this._animationFrameDisposable = scheduleAtNextAnimationFrame(getWindow(mouseTarget.element), () => this._execute());
}
}
export { DragScrolling, DragScrollingOperation, LeftRightDragScrolling, LeftRightDragScrollingOperation, TopBottomDragScrolling, TopBottomDragScrollingOperation };
@@ -0,0 +1,85 @@
import { isWindows } from '../../../../base/common/platform.js';
import { Mimes } from '../../../../base/common/mime.js';
function getDataToCopy(viewModel, modelSelections, emptySelectionClipboard, copyWithSyntaxHighlighting) {
const rawTextToCopy = viewModel.getPlainTextToCopy(modelSelections, emptySelectionClipboard, isWindows);
const newLineCharacter = viewModel.model.getEOL();
const isFromEmptySelection = (emptySelectionClipboard && modelSelections.length === 1 && modelSelections[0].isEmpty());
const multicursorText = (Array.isArray(rawTextToCopy) ? rawTextToCopy : null);
const text = (Array.isArray(rawTextToCopy) ? rawTextToCopy.join(newLineCharacter) : rawTextToCopy);
let html = undefined;
let mode = null;
if (CopyOptions.forceCopyWithSyntaxHighlighting || (copyWithSyntaxHighlighting && text.length < 65536)) {
const richText = viewModel.getRichTextToCopy(modelSelections, emptySelectionClipboard);
if (richText) {
html = richText.html;
mode = richText.mode;
}
}
const dataToCopy = {
isFromEmptySelection,
multicursorText,
text,
html,
mode
};
return dataToCopy;
}
/**
* Every time we write to the clipboard, we record a bit of extra metadata here.
* Every time we read from the cipboard, if the text matches our last written text,
* we can fetch the previous metadata.
*/
class InMemoryClipboardMetadataManager {
static { this.INSTANCE = new InMemoryClipboardMetadataManager(); }
constructor() {
this._lastState = null;
}
set(lastCopiedValue, data) {
this._lastState = { lastCopiedValue, data };
}
get(pastedText) {
if (this._lastState && this._lastState.lastCopiedValue === pastedText) {
// match!
return this._lastState.data;
}
this._lastState = null;
return null;
}
}
const CopyOptions = {
forceCopyWithSyntaxHighlighting: false
};
const ClipboardEventUtils = {
getTextData(clipboardData) {
const text = clipboardData.getData(Mimes.text);
let metadata = null;
const rawmetadata = clipboardData.getData('vscode-editor-data');
if (typeof rawmetadata === 'string') {
try {
metadata = JSON.parse(rawmetadata);
if (metadata.version !== 1) {
metadata = null;
}
}
catch (err) {
// no problem!
}
}
if (text.length === 0 && metadata === null && clipboardData.files.length > 0) {
// no textual data pasted, generate text from file names
const files = Array.prototype.slice.call(clipboardData.files, 0);
return [files.map(file => file.name).join('\n'), null];
}
return [text, metadata];
},
setTextData(clipboardData, text, html, metadata) {
clipboardData.setData(Mimes.text, text);
if (typeof html === 'string') {
clipboardData.setData('text/html', html);
}
clipboardData.setData('vscode-editor-data', JSON.stringify(metadata));
}
};
export { ClipboardEventUtils, CopyOptions, InMemoryClipboardMetadataManager, getDataToCopy };
@@ -0,0 +1,10 @@
import { ViewPart } from '../../view/viewPart.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class AbstractEditContext extends ViewPart {
}
export { AbstractEditContext };
@@ -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.
*--------------------------------------------------------------------------------------------*/
var EditContext;
(function (EditContext) {
/**
* Create an edit context.
*/
function create(window, options) {
return new window.EditContext(options);
}
EditContext.create = create;
})(EditContext || (EditContext = {}));
export { EditContext };
@@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .native-edit-context {
margin: 0;
padding: 0;
position: absolute;
overflow-y: scroll;
scrollbar-width: none;
z-index: -10;
white-space: pre-wrap;
}
.monaco-editor .ime-text-area {
min-width: 0;
min-height: 0;
margin: 0;
padding: 0;
position: absolute;
outline: none !important;
resize: none;
border: none;
overflow: hidden;
color: transparent;
background-color: transparent;
z-index: -10;
}
.monaco-editor .edit-context-composition-none {
background-color: transparent;
border-bottom: none;
}
.monaco-editor :not(.hc-black, .hc-light) .edit-context-composition-secondary {
border-bottom: 1px solid var(--vscode-editor-compositionBorder);
}
.monaco-editor :not(.hc-black, .hc-light) .edit-context-composition-primary {
border-bottom: 2px solid var(--vscode-editor-compositionBorder);
}
.monaco-editor :is(.hc-black, .hc-light) .edit-context-composition-secondary {
border: 1px solid var(--vscode-editor-compositionBorder);
}
.monaco-editor :is(.hc-black, .hc-light) .edit-context-composition-primary {
border: 2px solid var(--vscode-editor-compositionBorder);
}
@@ -0,0 +1,518 @@
import './nativeEditContext.css';
import { isFirefox } from '../../../../../base/browser/browser.js';
import { getWindow, addDisposableListener, getActiveElement, getWindowId } from '../../../../../base/browser/dom.js';
import { FastDomNode } from '../../../../../base/browser/fastDomNode.js';
import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { ClipboardEventUtils, InMemoryClipboardMetadataManager, getDataToCopy } from '../clipboardUtils.js';
import { AbstractEditContext } from '../editContext.js';
import { FocusTracker, editContextAddDisposableListener } from './nativeEditContextUtils.js';
import { ScreenReaderSupport } from './screenReaderSupport.js';
import { Range } from '../../../../common/core/range.js';
import { Selection } from '../../../../common/core/selection.js';
import { Position } from '../../../../common/core/position.js';
import '../../../../common/core/text/positionToOffset.js';
import { EditContext } from './editContextFactory.js';
import { NativeEditContextRegistry } from './nativeEditContextRegistry.js';
import { isHighSurrogate, isLowSurrogate } from '../../../../../base/common/strings.js';
import { IME } from '../../../../../base/common/ime.js';
import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js';
import { ILogService, LogLevel } from '../../../../../platform/log/common/log.js';
import { generateUuid } from '../../../../../base/common/uuid.js';
import { PositionOffsetTransformer } from '../../../../common/core/text/positionToOffsetImpl.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
// Corresponds to classes in nativeEditContext.css
var CompositionClassName;
(function (CompositionClassName) {
CompositionClassName["NONE"] = "edit-context-composition-none";
CompositionClassName["SECONDARY"] = "edit-context-composition-secondary";
CompositionClassName["PRIMARY"] = "edit-context-composition-primary";
})(CompositionClassName || (CompositionClassName = {}));
let NativeEditContext = class NativeEditContext extends AbstractEditContext {
constructor(ownerID, context, overflowGuardContainer, _viewController, _visibleRangeProvider, instantiationService, logService) {
super(context);
this._viewController = _viewController;
this._visibleRangeProvider = _visibleRangeProvider;
this.logService = logService;
this._previousEditContextSelection = new OffsetRange(0, 0);
this._editContextPrimarySelection = new Selection(1, 1, 1, 1);
this._decorations = [];
this._primarySelection = new Selection(1, 1, 1, 1);
this._targetWindowId = -1;
this._scrollTop = 0;
this._scrollLeft = 0;
this._linesVisibleRanges = null;
this.domNode = new FastDomNode(document.createElement('div'));
this.domNode.setClassName(`native-edit-context`);
this._imeTextArea = new FastDomNode(document.createElement('textarea'));
this._imeTextArea.setClassName(`ime-text-area`);
this._imeTextArea.setAttribute('readonly', 'true');
this._imeTextArea.setAttribute('tabindex', '-1');
this._imeTextArea.setAttribute('aria-hidden', 'true');
this.domNode.setAttribute('autocorrect', 'off');
this.domNode.setAttribute('autocapitalize', 'off');
this.domNode.setAttribute('autocomplete', 'off');
this.domNode.setAttribute('spellcheck', 'false');
this._updateDomAttributes();
overflowGuardContainer.appendChild(this.domNode);
overflowGuardContainer.appendChild(this._imeTextArea);
this._parent = overflowGuardContainer.domNode;
this._focusTracker = this._register(new FocusTracker(logService, this.domNode.domNode, (newFocusValue) => {
logService.trace('NativeEditContext#handleFocusChange : ', newFocusValue);
this._screenReaderSupport.handleFocusChange(newFocusValue);
this._context.viewModel.setHasFocus(newFocusValue);
}));
const window = getWindow(this.domNode.domNode);
this._editContext = EditContext.create(window);
this.setEditContextOnDomNode();
this._screenReaderSupport = this._register(instantiationService.createInstance(ScreenReaderSupport, this.domNode, context, this._viewController));
this._register(addDisposableListener(this.domNode.domNode, 'copy', (e) => {
this.logService.trace('NativeEditContext#copy');
this._ensureClipboardGetsEditorSelection(e);
}));
this._register(addDisposableListener(this.domNode.domNode, 'cut', (e) => {
this.logService.trace('NativeEditContext#cut');
// Pretend here we touched the text area, as the `cut` event will most likely
// result in a `selectionchange` event which we want to ignore
this._screenReaderSupport.onWillCut();
this._ensureClipboardGetsEditorSelection(e);
this.logService.trace('NativeEditContext#cut (before viewController.cut)');
this._viewController.cut();
}));
this._register(addDisposableListener(this.domNode.domNode, 'keyup', (e) => this._onKeyUp(e)));
this._register(addDisposableListener(this.domNode.domNode, 'keydown', async (e) => this._onKeyDown(e)));
this._register(addDisposableListener(this._imeTextArea.domNode, 'keyup', (e) => this._onKeyUp(e)));
this._register(addDisposableListener(this._imeTextArea.domNode, 'keydown', async (e) => this._onKeyDown(e)));
this._register(addDisposableListener(this.domNode.domNode, 'beforeinput', async (e) => {
if (e.inputType === 'insertParagraph' || e.inputType === 'insertLineBreak') {
this._onType(this._viewController, { text: '\n', replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 });
}
}));
this._register(addDisposableListener(this.domNode.domNode, 'paste', (e) => {
this.logService.trace('NativeEditContext#paste');
e.preventDefault();
if (!e.clipboardData) {
return;
}
let [text, metadata] = ClipboardEventUtils.getTextData(e.clipboardData);
this.logService.trace('NativeEditContext#paste with id : ', metadata?.id, ' with text.length: ', text.length);
if (!text) {
return;
}
metadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text);
let pasteOnNewLine = false;
let multicursorText = null;
let mode = null;
if (metadata) {
const options = this._context.configuration.options;
const emptySelectionClipboard = options.get(45 /* EditorOption.emptySelectionClipboard */);
pasteOnNewLine = emptySelectionClipboard && !!metadata.isFromEmptySelection;
multicursorText = typeof metadata.multicursorText !== 'undefined' ? metadata.multicursorText : null;
mode = metadata.mode;
}
this.logService.trace('NativeEditContext#paste (before viewController.paste)');
this._viewController.paste(text, pasteOnNewLine, multicursorText, mode);
}));
// Edit context events
this._register(editContextAddDisposableListener(this._editContext, 'textformatupdate', (e) => this._handleTextFormatUpdate(e)));
this._register(editContextAddDisposableListener(this._editContext, 'characterboundsupdate', (e) => this._updateCharacterBounds(e)));
let highSurrogateCharacter;
this._register(editContextAddDisposableListener(this._editContext, 'textupdate', (e) => {
const text = e.text;
if (text.length === 1) {
const charCode = text.charCodeAt(0);
if (isHighSurrogate(charCode)) {
highSurrogateCharacter = text;
return;
}
if (isLowSurrogate(charCode) && highSurrogateCharacter) {
const textUpdateEvent = {
text: highSurrogateCharacter + text,
selectionEnd: e.selectionEnd,
selectionStart: e.selectionStart,
updateRangeStart: e.updateRangeStart - 1,
updateRangeEnd: e.updateRangeEnd - 1
};
highSurrogateCharacter = undefined;
this._emitTypeEvent(this._viewController, textUpdateEvent);
return;
}
}
this._emitTypeEvent(this._viewController, e);
}));
this._register(editContextAddDisposableListener(this._editContext, 'compositionstart', (e) => {
this._updateEditContext();
// Utlimately fires onDidCompositionStart() on the editor to notify for example suggest model of composition state
// Updates the composition state of the cursor controller which determines behavior of typing with interceptors
this._viewController.compositionStart();
// Emits ViewCompositionStartEvent which can be depended on by ViewEventHandlers
this._context.viewModel.onCompositionStart();
}));
this._register(editContextAddDisposableListener(this._editContext, 'compositionend', (e) => {
this._updateEditContext();
// Utlimately fires compositionEnd() on the editor to notify for example suggest model of composition state
// Updates the composition state of the cursor controller which determines behavior of typing with interceptors
this._viewController.compositionEnd();
// Emits ViewCompositionEndEvent which can be depended on by ViewEventHandlers
this._context.viewModel.onCompositionEnd();
}));
let reenableTracking = false;
this._register(IME.onDidChange(() => {
if (IME.enabled && reenableTracking) {
this._focusTracker.resume();
this.domNode.focus();
reenableTracking = false;
}
if (!IME.enabled && this.isFocused()) {
this._focusTracker.pause();
this._imeTextArea.focus();
reenableTracking = true;
}
}));
this._register(NativeEditContextRegistry.register(ownerID, this));
}
// --- Public methods ---
dispose() {
// Force blue the dom node so can write in pane with no native edit context after disposal
this.domNode.domNode.editContext = undefined;
this.domNode.domNode.blur();
this.domNode.domNode.remove();
this._imeTextArea.domNode.remove();
super.dispose();
}
setAriaOptions(options) {
this._screenReaderSupport.setAriaOptions(options);
}
/* Last rendered data needed for correct hit-testing and determining the mouse position.
* Without this, the selection will blink as incorrect mouse position is calculated */
getLastRenderData() {
return this._primarySelection.getPosition();
}
prepareRender(ctx) {
this._screenReaderSupport.prepareRender(ctx);
this._updateSelectionAndControlBoundsData(ctx);
}
onDidRender() {
this._updateSelectionAndControlBoundsAfterRender();
}
render(ctx) {
this._screenReaderSupport.render(ctx);
}
onCursorStateChanged(e) {
this._primarySelection = e.modelSelections[0] ?? new Selection(1, 1, 1, 1);
this._screenReaderSupport.onCursorStateChanged(e);
this._updateEditContext();
return true;
}
onConfigurationChanged(e) {
this._screenReaderSupport.onConfigurationChanged(e);
this._updateDomAttributes();
return true;
}
onDecorationsChanged(e) {
// true for inline decorations that can end up relayouting text
return true;
}
onFlushed(e) {
return true;
}
onLinesChanged(e) {
this._updateEditContextOnLineChange(e.fromLineNumber, e.fromLineNumber + e.count - 1);
return true;
}
onLinesDeleted(e) {
this._updateEditContextOnLineChange(e.fromLineNumber, e.toLineNumber);
return true;
}
onLinesInserted(e) {
this._updateEditContextOnLineChange(e.fromLineNumber, e.toLineNumber);
return true;
}
_updateEditContextOnLineChange(fromLineNumber, toLineNumber) {
if (this._editContextPrimarySelection.endLineNumber < fromLineNumber || this._editContextPrimarySelection.startLineNumber > toLineNumber) {
return;
}
this._updateEditContext();
}
onScrollChanged(e) {
this._scrollLeft = e.scrollLeft;
this._scrollTop = e.scrollTop;
return true;
}
onZonesChanged(e) {
return true;
}
onWillPaste() {
this.logService.trace('NativeEditContext#onWillPaste');
this._onWillPaste();
}
_onWillPaste() {
this._screenReaderSupport.onWillPaste();
}
onWillCopy() {
this.logService.trace('NativeEditContext#onWillCopy');
this.logService.trace('NativeEditContext#isFocused : ', this.domNode.domNode === getActiveElement());
}
writeScreenReaderContent() {
this._screenReaderSupport.writeScreenReaderContent();
}
isFocused() {
return this._focusTracker.isFocused;
}
focus() {
this._focusTracker.focus();
// If the editor is off DOM, focus cannot be really set, so let's double check that we have managed to set the focus
this.refreshFocusState();
}
refreshFocusState() {
this._focusTracker.refreshFocusState();
}
// TODO: added as a workaround fix for https://github.com/microsoft/vscode/issues/229825
// When this issue will be fixed the following should be removed.
setEditContextOnDomNode() {
const targetWindow = getWindow(this.domNode.domNode);
const targetWindowId = getWindowId(targetWindow);
if (this._targetWindowId !== targetWindowId) {
this.domNode.domNode.editContext = this._editContext;
this._targetWindowId = targetWindowId;
}
}
// --- Private methods ---
_onKeyUp(e) {
this._viewController.emitKeyUp(new StandardKeyboardEvent(e));
}
_onKeyDown(e) {
const standardKeyboardEvent = new StandardKeyboardEvent(e);
// When the IME is visible, the keys, like arrow-left and arrow-right, should be used to navigate in the IME, and should not be propagated further
if (standardKeyboardEvent.keyCode === 114 /* KeyCode.KEY_IN_COMPOSITION */) {
standardKeyboardEvent.stopPropagation();
}
this._viewController.emitKeyDown(standardKeyboardEvent);
}
_updateDomAttributes() {
const options = this._context.configuration.options;
this.domNode.domNode.setAttribute('tabindex', String(options.get(140 /* EditorOption.tabIndex */)));
}
_updateEditContext() {
const editContextState = this._getNewEditContextState();
if (!editContextState) {
return;
}
this._editContext.updateText(0, Number.MAX_SAFE_INTEGER, editContextState.text ?? ' ');
this._editContext.updateSelection(editContextState.selectionStartOffset, editContextState.selectionEndOffset);
this._editContextPrimarySelection = editContextState.editContextPrimarySelection;
this._previousEditContextSelection = new OffsetRange(editContextState.selectionStartOffset, editContextState.selectionEndOffset);
}
_emitTypeEvent(viewController, e) {
if (!this._editContext) {
return;
}
const selectionEndOffset = this._previousEditContextSelection.endExclusive;
const selectionStartOffset = this._previousEditContextSelection.start;
this._previousEditContextSelection = new OffsetRange(e.selectionStart, e.selectionEnd);
let replaceNextCharCnt = 0;
let replacePrevCharCnt = 0;
if (e.updateRangeEnd > selectionEndOffset) {
replaceNextCharCnt = e.updateRangeEnd - selectionEndOffset;
}
if (e.updateRangeStart < selectionStartOffset) {
replacePrevCharCnt = selectionStartOffset - e.updateRangeStart;
}
let text = '';
if (selectionStartOffset < e.updateRangeStart) {
text += this._editContext.text.substring(selectionStartOffset, e.updateRangeStart);
}
text += e.text;
if (selectionEndOffset > e.updateRangeEnd) {
text += this._editContext.text.substring(e.updateRangeEnd, selectionEndOffset);
}
let positionDelta = 0;
if (e.selectionStart === e.selectionEnd && selectionStartOffset === selectionEndOffset) {
positionDelta = e.selectionStart - (e.updateRangeStart + e.text.length);
}
const typeInput = {
text,
replacePrevCharCnt,
replaceNextCharCnt,
positionDelta
};
this._onType(viewController, typeInput);
}
_onType(viewController, typeInput) {
if (typeInput.replacePrevCharCnt || typeInput.replaceNextCharCnt || typeInput.positionDelta) {
viewController.compositionType(typeInput.text, typeInput.replacePrevCharCnt, typeInput.replaceNextCharCnt, typeInput.positionDelta);
}
else {
viewController.type(typeInput.text);
}
}
_getNewEditContextState() {
const editContextPrimarySelection = this._primarySelection;
const model = this._context.viewModel.model;
if (!model.isValidRange(editContextPrimarySelection)) {
return;
}
const primarySelectionStartLine = editContextPrimarySelection.startLineNumber;
const primarySelectionEndLine = editContextPrimarySelection.endLineNumber;
const endColumnOfEndLineNumber = model.getLineMaxColumn(primarySelectionEndLine);
const rangeOfText = new Range(primarySelectionStartLine, 1, primarySelectionEndLine, endColumnOfEndLineNumber);
const text = model.getValueInRange(rangeOfText, 0 /* EndOfLinePreference.TextDefined */);
const selectionStartOffset = editContextPrimarySelection.startColumn - 1;
const selectionEndOffset = text.length + editContextPrimarySelection.endColumn - endColumnOfEndLineNumber;
return {
text,
selectionStartOffset,
selectionEndOffset,
editContextPrimarySelection
};
}
_editContextStartPosition() {
return new Position(this._editContextPrimarySelection.startLineNumber, 1);
}
_handleTextFormatUpdate(e) {
if (!this._editContext) {
return;
}
const formats = e.getTextFormats();
const editContextStartPosition = this._editContextStartPosition();
const decorations = [];
formats.forEach(f => {
const textModel = this._context.viewModel.model;
const offsetOfEditContextText = textModel.getOffsetAt(editContextStartPosition);
const startPositionOfDecoration = textModel.getPositionAt(offsetOfEditContextText + f.rangeStart);
const endPositionOfDecoration = textModel.getPositionAt(offsetOfEditContextText + f.rangeEnd);
const decorationRange = Range.fromPositions(startPositionOfDecoration, endPositionOfDecoration);
const thickness = f.underlineThickness.toLowerCase();
let decorationClassName = CompositionClassName.NONE;
switch (thickness) {
case 'thin':
decorationClassName = CompositionClassName.SECONDARY;
break;
case 'thick':
decorationClassName = CompositionClassName.PRIMARY;
break;
}
decorations.push({
range: decorationRange,
options: {
description: 'textFormatDecoration',
inlineClassName: decorationClassName,
}
});
});
this._decorations = this._context.viewModel.model.deltaDecorations(this._decorations, decorations);
}
_updateSelectionAndControlBoundsData(ctx) {
const viewSelection = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(this._primarySelection);
if (this._primarySelection.isEmpty()) {
const linesVisibleRanges = ctx.visibleRangeForPosition(viewSelection.getStartPosition());
this._linesVisibleRanges = linesVisibleRanges;
}
else {
this._linesVisibleRanges = null;
}
}
_updateSelectionAndControlBoundsAfterRender() {
const options = this._context.configuration.options;
const contentLeft = options.get(165 /* EditorOption.layoutInfo */).contentLeft;
const viewSelection = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(this._primarySelection);
const verticalOffsetStart = this._context.viewLayout.getVerticalOffsetForLineNumber(viewSelection.startLineNumber);
const verticalOffsetEnd = this._context.viewLayout.getVerticalOffsetAfterLineNumber(viewSelection.endLineNumber);
// Make sure this doesn't force an extra layout (i.e. don't call it before rendering finished)
const parentBounds = this._parent.getBoundingClientRect();
const top = parentBounds.top + verticalOffsetStart - this._scrollTop;
const height = verticalOffsetEnd - verticalOffsetStart;
let left = parentBounds.left + contentLeft - this._scrollLeft;
let width;
if (this._primarySelection.isEmpty()) {
if (this._linesVisibleRanges) {
left += this._linesVisibleRanges.left;
}
width = 0;
}
else {
width = parentBounds.width - contentLeft;
}
const selectionBounds = new DOMRect(left, top, width, height);
this._editContext.updateSelectionBounds(selectionBounds);
this._editContext.updateControlBounds(selectionBounds);
}
_updateCharacterBounds(e) {
const options = this._context.configuration.options;
const typicalHalfWidthCharacterWidth = options.get(59 /* EditorOption.fontInfo */).typicalHalfwidthCharacterWidth;
const contentLeft = options.get(165 /* EditorOption.layoutInfo */).contentLeft;
const parentBounds = this._parent.getBoundingClientRect();
const characterBounds = [];
const offsetTransformer = new PositionOffsetTransformer(this._editContext.text);
for (let offset = e.rangeStart; offset < e.rangeEnd; offset++) {
const editContextStartPosition = offsetTransformer.getPosition(offset);
const textStartLineOffsetWithinEditor = this._editContextPrimarySelection.startLineNumber - 1;
const characterStartPosition = new Position(textStartLineOffsetWithinEditor + editContextStartPosition.lineNumber, editContextStartPosition.column);
const characterEndPosition = characterStartPosition.delta(0, 1);
const characterModelRange = Range.fromPositions(characterStartPosition, characterEndPosition);
const characterViewRange = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(characterModelRange);
const characterLinesVisibleRanges = this._visibleRangeProvider.linesVisibleRangesForRange(characterViewRange, true) ?? [];
const lineNumber = characterViewRange.startLineNumber;
const characterVerticalOffset = this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber);
const top = parentBounds.top + characterVerticalOffset - this._scrollTop;
let left = 0;
let width = typicalHalfWidthCharacterWidth;
if (characterLinesVisibleRanges.length > 0) {
for (const visibleRange of characterLinesVisibleRanges[0].ranges) {
left = visibleRange.left;
width = visibleRange.width;
break;
}
}
const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(lineNumber);
characterBounds.push(new DOMRect(parentBounds.left + contentLeft + left - this._scrollLeft, top, width, lineHeight));
}
this._editContext.updateCharacterBounds(e.rangeStart, characterBounds);
}
_ensureClipboardGetsEditorSelection(e) {
const options = this._context.configuration.options;
const emptySelectionClipboard = options.get(45 /* EditorOption.emptySelectionClipboard */);
const copyWithSyntaxHighlighting = options.get(31 /* EditorOption.copyWithSyntaxHighlighting */);
const selections = this._context.viewModel.getCursorStates().map(cursorState => cursorState.modelState.selection);
const dataToCopy = getDataToCopy(this._context.viewModel, selections, emptySelectionClipboard, copyWithSyntaxHighlighting);
let id = undefined;
if (this.logService.getLevel() === LogLevel.Trace) {
id = generateUuid();
}
const storedMetadata = {
version: 1,
id,
isFromEmptySelection: dataToCopy.isFromEmptySelection,
multicursorText: dataToCopy.multicursorText,
mode: dataToCopy.mode
};
InMemoryClipboardMetadataManager.INSTANCE.set(
// When writing "LINE\r\n" to the clipboard and then pasting,
// Firefox pastes "LINE\n", so let's work around this quirk
(isFirefox ? dataToCopy.text.replace(/\r\n/g, '\n') : dataToCopy.text), storedMetadata);
e.preventDefault();
if (e.clipboardData) {
ClipboardEventUtils.setTextData(e.clipboardData, dataToCopy.text, dataToCopy.html, storedMetadata);
}
this.logService.trace('NativeEditContext#_ensureClipboardGetsEditorSelectios with id : ', id, ' with text.length: ', dataToCopy.text.length);
}
};
NativeEditContext = __decorate([
__param(5, IInstantiationService),
__param(6, ILogService)
], NativeEditContext);
export { NativeEditContext };
@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class NativeEditContextRegistryImpl {
constructor() {
this._nativeEditContextMapping = new Map();
}
register(ownerID, nativeEditContext) {
this._nativeEditContextMapping.set(ownerID, nativeEditContext);
return {
dispose: () => {
this._nativeEditContextMapping.delete(ownerID);
}
};
}
get(ownerID) {
return this._nativeEditContextMapping.get(ownerID);
}
}
const NativeEditContextRegistry = new NativeEditContextRegistryImpl();
export { NativeEditContextRegistry };
@@ -0,0 +1,85 @@
import { addDisposableListener, getShadowRoot, getActiveElement } from '../../../../../base/browser/dom.js';
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { ILogService } from '../../../../../platform/log/common/log.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
let FocusTracker = class FocusTracker extends Disposable {
constructor(_logService, _domNode, _onFocusChange) {
super();
this._domNode = _domNode;
this._onFocusChange = _onFocusChange;
this._isFocused = false;
this._isPaused = false;
this._register(addDisposableListener(this._domNode, 'focus', () => {
_logService.trace('NativeEditContext.focus');
if (this._isPaused) {
return;
}
// Here we don't trust the browser and instead we check
// that the active element is the one we are tracking
// (this happens when cmd+tab is used to switch apps)
this.refreshFocusState();
}));
this._register(addDisposableListener(this._domNode, 'blur', () => {
_logService.trace('NativeEditContext.blur');
if (this._isPaused) {
return;
}
this._handleFocusedChanged(false);
}));
}
pause() {
this._isPaused = true;
}
resume() {
this._isPaused = false;
this.refreshFocusState();
}
_handleFocusedChanged(focused) {
if (this._isFocused === focused) {
return;
}
this._isFocused = focused;
this._onFocusChange(this._isFocused);
}
focus() {
this._domNode.focus();
this.refreshFocusState();
}
refreshFocusState() {
const shadowRoot = getShadowRoot(this._domNode);
const activeElement = shadowRoot ? shadowRoot.activeElement : getActiveElement();
const focused = this._domNode === activeElement;
this._handleFocusedChanged(focused);
}
get isFocused() {
return this._isFocused;
}
};
FocusTracker = __decorate([
__param(0, ILogService)
], FocusTracker);
function editContextAddDisposableListener(target, type, listener, options) {
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
target.addEventListener(type, listener, options);
return {
dispose() {
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
target.removeEventListener(type, listener);
}
};
}
export { FocusTracker, editContextAddDisposableListener };
@@ -0,0 +1,337 @@
import { getActiveWindow, addDisposableListener, isHTMLElement } from '../../../../../base/browser/dom.js';
import { createTrustedTypesPolicy } from '../../../../../base/browser/trustedTypes.js';
import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';
import { EditorFontLigatures } from '../../../../common/config/editorOptions.js';
import { Range } from '../../../../common/core/range.js';
import { Selection } from '../../../../common/core/selection.js';
import { StringBuilder } from '../../../../common/core/stringBuilder.js';
import { LineDecoration } from '../../../../common/viewLayout/lineDecorations.js';
import { RenderLineInput, renderViewLine } from '../../../../common/viewLayout/viewLineRenderer.js';
import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
import { IME } from '../../../../../base/common/ime.js';
import { getColumnOfNodeOffset } from '../../../viewParts/viewLines/viewLine.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
const ttPolicy = createTrustedTypesPolicy('richScreenReaderContent', { createHTML: value => value });
const LINE_NUMBER_ATTRIBUTE = 'data-line-number';
let RichScreenReaderContent = class RichScreenReaderContent extends Disposable {
constructor(_domNode, _context, _viewController, _accessibilityService) {
super();
this._domNode = _domNode;
this._context = _context;
this._viewController = _viewController;
this._accessibilityService = _accessibilityService;
this._selectionChangeListener = this._register(new MutableDisposable());
this._accessibilityPageSize = 1;
this._ignoreSelectionChangeTime = 0;
this._state = RichScreenReaderState.NULL;
this._strategy = new RichPagedScreenReaderStrategy();
this._renderedLines = new Map();
this._renderedSelection = new Selection(1, 1, 1, 1);
this.onConfigurationChanged(this._context.configuration.options);
}
updateScreenReaderContent(primarySelection) {
const focusedElement = getActiveWindow().document.activeElement;
if (!focusedElement || focusedElement !== this._domNode.domNode) {
return;
}
const isScreenReaderOptimized = this._accessibilityService.isScreenReaderOptimized();
if (isScreenReaderOptimized) {
const state = this._getScreenReaderContentLineIntervals(primarySelection);
if (!this._state.equals(state)) {
this._state = state;
this._renderedLines = this._renderScreenReaderContent(state);
}
if (!this._renderedSelection.equalsSelection(primarySelection)) {
this._renderedSelection = primarySelection;
this._setSelectionOnScreenReaderContent(this._context, this._renderedLines, primarySelection);
}
}
else {
this._state = RichScreenReaderState.NULL;
this._setIgnoreSelectionChangeTime('setValue');
this._domNode.domNode.textContent = '';
}
}
updateScrollTop(primarySelection) {
const intervals = this._state.intervals;
if (!intervals.length) {
return;
}
const viewLayout = this._context.viewModel.viewLayout;
const stateStartLineNumber = intervals[0].startLine;
const verticalOffsetOfStateStartLineNumber = viewLayout.getVerticalOffsetForLineNumber(stateStartLineNumber);
const verticalOffsetOfPositionLineNumber = viewLayout.getVerticalOffsetForLineNumber(primarySelection.positionLineNumber);
this._domNode.domNode.scrollTop = verticalOffsetOfPositionLineNumber - verticalOffsetOfStateStartLineNumber;
}
onFocusChange(newFocusValue) {
if (newFocusValue) {
this._selectionChangeListener.value = this._setSelectionChangeListener();
}
else {
this._selectionChangeListener.value = undefined;
}
}
onConfigurationChanged(options) {
this._accessibilityPageSize = options.get(3 /* EditorOption.accessibilityPageSize */);
}
onWillCut() {
this._setIgnoreSelectionChangeTime('onCut');
}
onWillPaste() {
this._setIgnoreSelectionChangeTime('onWillPaste');
}
// --- private methods
_setIgnoreSelectionChangeTime(reason) {
this._ignoreSelectionChangeTime = Date.now();
}
_setSelectionChangeListener() {
// See https://github.com/microsoft/vscode/issues/27216 and https://github.com/microsoft/vscode/issues/98256
// When using a Braille display or NVDA for example, it is possible for users to reposition the
// system caret. This is reflected in Chrome as a `selectionchange` event and needs to be reflected within the editor.
// `selectionchange` events often come multiple times for a single logical change
// so throttle multiple `selectionchange` events that burst in a short period of time.
let previousSelectionChangeEventTime = 0;
return addDisposableListener(this._domNode.domNode.ownerDocument, 'selectionchange', () => {
const activeElement = getActiveWindow().document.activeElement;
const isFocused = activeElement === this._domNode.domNode;
if (!isFocused) {
return;
}
const isScreenReaderOptimized = this._accessibilityService.isScreenReaderOptimized();
if (!isScreenReaderOptimized || !IME.enabled) {
return;
}
const now = Date.now();
const delta1 = now - previousSelectionChangeEventTime;
previousSelectionChangeEventTime = now;
if (delta1 < 5) {
// received another `selectionchange` event within 5ms of the previous `selectionchange` event
// => ignore it
return;
}
const delta2 = now - this._ignoreSelectionChangeTime;
this._ignoreSelectionChangeTime = 0;
if (delta2 < 100) {
// received a `selectionchange` event within 100ms since we touched the hidden div
// => ignore it, since we caused it
return;
}
const selection = this._getEditorSelectionFromDomRange();
if (!selection) {
return;
}
this._viewController.setSelection(selection);
});
}
_renderScreenReaderContent(state) {
const nodes = [];
const renderedLines = new Map();
for (const interval of state.intervals) {
for (let lineNumber = interval.startLine; lineNumber <= interval.endLine; lineNumber++) {
const renderedLine = this._renderLine(lineNumber);
renderedLines.set(lineNumber, renderedLine);
nodes.push(renderedLine.domNode);
}
}
this._setIgnoreSelectionChangeTime('setValue');
this._domNode.domNode.replaceChildren(...nodes);
return renderedLines;
}
_renderLine(viewLineNumber) {
const viewModel = this._context.viewModel;
const positionLineData = viewModel.getViewLineRenderingData(viewLineNumber);
const options = this._context.configuration.options;
const fontInfo = options.get(59 /* EditorOption.fontInfo */);
const stopRenderingLineAfter = options.get(133 /* EditorOption.stopRenderingLineAfter */);
const renderControlCharacters = options.get(108 /* EditorOption.renderControlCharacters */);
const fontLigatures = options.get(60 /* EditorOption.fontLigatures */);
const disableMonospaceOptimizations = options.get(40 /* EditorOption.disableMonospaceOptimizations */);
const lineDecorations = LineDecoration.filter(positionLineData.inlineDecorations, viewLineNumber, positionLineData.minColumn, positionLineData.maxColumn);
const useMonospaceOptimizations = fontInfo.isMonospace && !disableMonospaceOptimizations;
const useFontLigatures = fontLigatures !== EditorFontLigatures.OFF;
let renderWhitespace;
const experimentalWhitespaceRendering = options.get(47 /* EditorOption.experimentalWhitespaceRendering */);
if (experimentalWhitespaceRendering === 'off') {
renderWhitespace = options.get(113 /* EditorOption.renderWhitespace */);
}
else {
renderWhitespace = 'none';
}
const renderLineInput = new RenderLineInput(useMonospaceOptimizations, fontInfo.canUseHalfwidthRightwardsArrow, positionLineData.content, positionLineData.continuesWithWrappedLine, positionLineData.isBasicASCII, positionLineData.containsRTL, positionLineData.minColumn - 1, positionLineData.tokens, lineDecorations, positionLineData.tabSize, positionLineData.startVisibleColumn, fontInfo.spaceWidth, fontInfo.middotWidth, fontInfo.wsmiddotWidth, stopRenderingLineAfter, renderWhitespace, renderControlCharacters, useFontLigatures, null, null, 0, true);
const htmlBuilder = new StringBuilder(10000);
const renderOutput = renderViewLine(renderLineInput, htmlBuilder);
const html = htmlBuilder.build();
const trustedhtml = ttPolicy?.createHTML(html) ?? html;
const lineHeight = viewModel.viewLayout.getLineHeightForLineNumber(viewLineNumber) + 'px';
const domNode = document.createElement('div');
domNode.innerHTML = trustedhtml;
domNode.style.lineHeight = lineHeight;
domNode.style.height = lineHeight;
domNode.setAttribute(LINE_NUMBER_ATTRIBUTE, viewLineNumber.toString());
return new RichRenderedScreenReaderLine(domNode, renderOutput.characterMapping);
}
_setSelectionOnScreenReaderContent(context, renderedLines, viewSelection) {
const activeDocument = getActiveWindow().document;
const activeDocumentSelection = activeDocument.getSelection();
if (!activeDocumentSelection) {
return;
}
const startLineNumber = viewSelection.startLineNumber;
const endLineNumber = viewSelection.endLineNumber;
const startRenderedLine = renderedLines.get(startLineNumber);
const endRenderedLine = renderedLines.get(endLineNumber);
if (!startRenderedLine || !endRenderedLine) {
return;
}
const viewModel = context.viewModel;
const model = viewModel.model;
const coordinatesConverter = viewModel.coordinatesConverter;
const startRange = new Range(startLineNumber, 1, startLineNumber, viewSelection.selectionStartColumn);
const modelStartRange = coordinatesConverter.convertViewRangeToModelRange(startRange);
const characterCountForStart = model.getCharacterCountInRange(modelStartRange);
const endRange = new Range(endLineNumber, 1, endLineNumber, viewSelection.positionColumn);
const modelEndRange = coordinatesConverter.convertViewRangeToModelRange(endRange);
const characterCountForEnd = model.getCharacterCountInRange(modelEndRange);
const startDomPosition = startRenderedLine.characterMapping.getDomPosition(characterCountForStart);
const endDomPosition = endRenderedLine.characterMapping.getDomPosition(characterCountForEnd);
const startDomNode = startRenderedLine.domNode.firstChild;
const endDomNode = endRenderedLine.domNode.firstChild;
const startChildren = startDomNode.childNodes;
const endChildren = endDomNode.childNodes;
const startNode = startChildren.item(startDomPosition.partIndex);
const endNode = endChildren.item(endDomPosition.partIndex);
if (!startNode.firstChild || !endNode.firstChild) {
return;
}
this._setIgnoreSelectionChangeTime('setRange');
activeDocumentSelection.setBaseAndExtent(startNode.firstChild, viewSelection.startColumn === 1 ? 0 : startDomPosition.charIndex + 1, endNode.firstChild, viewSelection.endColumn === 1 ? 0 : endDomPosition.charIndex + 1);
}
_getScreenReaderContentLineIntervals(primarySelection) {
return this._strategy.fromEditorSelection(this._context.viewModel, primarySelection, this._accessibilityPageSize);
}
_getEditorSelectionFromDomRange() {
if (!this._renderedLines) {
return;
}
const selection = getActiveWindow().document.getSelection();
if (!selection) {
return;
}
const rangeCount = selection.rangeCount;
if (rangeCount === 0) {
return;
}
const range = selection.getRangeAt(0);
const startContainer = range.startContainer;
const endContainer = range.endContainer;
const startSpanElement = startContainer.parentElement;
const endSpanElement = endContainer.parentElement;
if (!startSpanElement || !isHTMLElement(startSpanElement) || !endSpanElement || !isHTMLElement(endSpanElement)) {
return;
}
const startLineDomNode = startSpanElement.parentElement?.parentElement;
const endLineDomNode = endSpanElement.parentElement?.parentElement;
if (!startLineDomNode || !endLineDomNode) {
return;
}
const startLineNumberAttribute = startLineDomNode.getAttribute(LINE_NUMBER_ATTRIBUTE);
const endLineNumberAttribute = endLineDomNode.getAttribute(LINE_NUMBER_ATTRIBUTE);
if (!startLineNumberAttribute || !endLineNumberAttribute) {
return;
}
const startLineNumber = parseInt(startLineNumberAttribute);
const endLineNumber = parseInt(endLineNumberAttribute);
const startMapping = this._renderedLines.get(startLineNumber)?.characterMapping;
const endMapping = this._renderedLines.get(endLineNumber)?.characterMapping;
if (!startMapping || !endMapping) {
return;
}
const startColumn = getColumnOfNodeOffset(startMapping, startSpanElement, range.startOffset);
const endColumn = getColumnOfNodeOffset(endMapping, endSpanElement, range.endOffset);
if (selection.direction === 'forward') {
return new Selection(startLineNumber, startColumn, endLineNumber, endColumn);
}
else {
return new Selection(endLineNumber, endColumn, startLineNumber, startColumn);
}
}
};
RichScreenReaderContent = __decorate([
__param(3, IAccessibilityService)
], RichScreenReaderContent);
class RichRenderedScreenReaderLine {
constructor(domNode, characterMapping) {
this.domNode = domNode;
this.characterMapping = characterMapping;
}
}
class LineInterval {
constructor(startLine, endLine) {
this.startLine = startLine;
this.endLine = endLine;
}
}
class RichScreenReaderState {
constructor(model, intervals) {
this.intervals = intervals;
let value = '';
for (const interval of intervals) {
for (let lineNumber = interval.startLine; lineNumber <= interval.endLine; lineNumber++) {
value += model.getLineContent(lineNumber) + '\n';
}
}
this.value = value;
}
equals(other) {
return this.value === other.value;
}
static get NULL() {
const nullModel = {
getLineContent: () => '',
getLineCount: () => 1,
getLineMaxColumn: () => 1,
getValueInRange: () => '',
getValueLengthInRange: () => 0,
modifyPosition: (position, offset) => position
};
return new RichScreenReaderState(nullModel, []);
}
}
class RichPagedScreenReaderStrategy {
constructor() { }
_getPageOfLine(lineNumber, linesPerPage) {
return Math.floor((lineNumber - 1) / linesPerPage);
}
_getRangeForPage(context, page, linesPerPage) {
const offset = page * linesPerPage;
const startLineNumber = offset + 1;
const endLineNumber = Math.min(offset + linesPerPage, context.getLineCount());
return new LineInterval(startLineNumber, endLineNumber);
}
fromEditorSelection(context, viewSelection, linesPerPage) {
const selectionStartPage = this._getPageOfLine(viewSelection.startLineNumber, linesPerPage);
const selectionStartPageRange = this._getRangeForPage(context, selectionStartPage, linesPerPage);
const selectionEndPage = this._getPageOfLine(viewSelection.endLineNumber, linesPerPage);
const selectionEndPageRange = this._getRangeForPage(context, selectionEndPage, linesPerPage);
const lineIntervals = [{ startLine: selectionStartPageRange.startLine, endLine: selectionStartPageRange.endLine }];
if (selectionStartPage + 1 < selectionEndPage) {
lineIntervals.push({ startLine: selectionEndPageRange.startLine, endLine: selectionEndPageRange.endLine });
}
return new RichScreenReaderState(context, lineIntervals);
}
}
export { RichScreenReaderContent };
@@ -0,0 +1,194 @@
import { getActiveWindow, addDisposableListener } from '../../../../../base/browser/dom.js';
import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';
import { Selection } from '../../../../common/core/selection.js';
import { SimplePagedScreenReaderStrategy } from '../screenReaderUtils.js';
import '../../../../common/core/text/positionToOffset.js';
import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
import { IME } from '../../../../../base/common/ime.js';
import { PositionOffsetTransformer } from '../../../../common/core/text/positionToOffsetImpl.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
let SimpleScreenReaderContent = class SimpleScreenReaderContent extends Disposable {
constructor(_domNode, _context, _viewController, _accessibilityService) {
super();
this._domNode = _domNode;
this._context = _context;
this._viewController = _viewController;
this._accessibilityService = _accessibilityService;
this._selectionChangeListener = this._register(new MutableDisposable());
this._accessibilityPageSize = 1;
this._ignoreSelectionChangeTime = 0;
this._strategy = new SimplePagedScreenReaderStrategy();
this.onConfigurationChanged(this._context.configuration.options);
}
updateScreenReaderContent(primarySelection) {
const domNode = this._domNode.domNode;
const focusedElement = getActiveWindow().document.activeElement;
if (!focusedElement || focusedElement !== domNode) {
return;
}
const isScreenReaderOptimized = this._accessibilityService.isScreenReaderOptimized();
if (isScreenReaderOptimized) {
this._state = this._getScreenReaderContentState(primarySelection);
if (domNode.textContent !== this._state.value) {
this._setIgnoreSelectionChangeTime('setValue');
domNode.textContent = this._state.value;
}
const selection = getActiveWindow().document.getSelection();
if (!selection) {
return;
}
const data = this._getScreenReaderRange(this._state.selectionStart, this._state.selectionEnd);
if (!data) {
return;
}
this._setIgnoreSelectionChangeTime('setRange');
selection.setBaseAndExtent(data.anchorNode, data.anchorOffset, data.focusNode, data.focusOffset);
}
else {
this._state = undefined;
this._setIgnoreSelectionChangeTime('setValue');
this._domNode.domNode.textContent = '';
}
}
updateScrollTop(primarySelection) {
if (!this._state) {
return;
}
const viewLayout = this._context.viewModel.viewLayout;
const stateStartLineNumber = this._state.startPositionWithinEditor.lineNumber;
const verticalOffsetOfStateStartLineNumber = viewLayout.getVerticalOffsetForLineNumber(stateStartLineNumber);
const verticalOffsetOfPositionLineNumber = viewLayout.getVerticalOffsetForLineNumber(primarySelection.positionLineNumber);
this._domNode.domNode.scrollTop = verticalOffsetOfPositionLineNumber - verticalOffsetOfStateStartLineNumber;
}
onFocusChange(newFocusValue) {
if (newFocusValue) {
this._selectionChangeListener.value = this._setSelectionChangeListener();
}
else {
this._selectionChangeListener.value = undefined;
}
}
onConfigurationChanged(options) {
this._accessibilityPageSize = options.get(3 /* EditorOption.accessibilityPageSize */);
}
onWillCut() {
this._setIgnoreSelectionChangeTime('onCut');
}
onWillPaste() {
this._setIgnoreSelectionChangeTime('onWillPaste');
}
// --- private methods
_setIgnoreSelectionChangeTime(reason) {
this._ignoreSelectionChangeTime = Date.now();
}
_setSelectionChangeListener() {
// See https://github.com/microsoft/vscode/issues/27216 and https://github.com/microsoft/vscode/issues/98256
// When using a Braille display or NVDA for example, it is possible for users to reposition the
// system caret. This is reflected in Chrome as a `selectionchange` event and needs to be reflected within the editor.
// `selectionchange` events often come multiple times for a single logical change
// so throttle multiple `selectionchange` events that burst in a short period of time.
let previousSelectionChangeEventTime = 0;
return addDisposableListener(this._domNode.domNode.ownerDocument, 'selectionchange', () => {
const isScreenReaderOptimized = this._accessibilityService.isScreenReaderOptimized();
if (!this._state || !isScreenReaderOptimized || !IME.enabled) {
return;
}
const activeElement = getActiveWindow().document.activeElement;
const isFocused = activeElement === this._domNode.domNode;
if (!isFocused) {
return;
}
const selection = getActiveWindow().document.getSelection();
if (!selection) {
return;
}
const rangeCount = selection.rangeCount;
if (rangeCount === 0) {
return;
}
const range = selection.getRangeAt(0);
const now = Date.now();
const delta1 = now - previousSelectionChangeEventTime;
previousSelectionChangeEventTime = now;
if (delta1 < 5) {
// received another `selectionchange` event within 5ms of the previous `selectionchange` event
// => ignore it
return;
}
const delta2 = now - this._ignoreSelectionChangeTime;
this._ignoreSelectionChangeTime = 0;
if (delta2 < 100) {
// received a `selectionchange` event within 100ms since we touched the hidden div
// => ignore it, since we caused it
return;
}
this._viewController.setSelection(this._getEditorSelectionFromDomRange(this._context, this._state, selection.direction, range));
});
}
_getScreenReaderContentState(primarySelection) {
const state = this._strategy.fromEditorSelection(this._context.viewModel, primarySelection, this._accessibilityPageSize, this._accessibilityService.getAccessibilitySupport() === 0 /* AccessibilitySupport.Unknown */);
const endPosition = this._context.viewModel.model.getPositionAt(Infinity);
let value = state.value;
if (endPosition.column === 1 && primarySelection.getEndPosition().equals(endPosition)) {
value += '\n';
}
state.value = value;
return state;
}
_getScreenReaderRange(selectionOffsetStart, selectionOffsetEnd) {
const textContent = this._domNode.domNode.firstChild;
if (!textContent) {
return;
}
const range = new globalThis.Range();
range.setStart(textContent, selectionOffsetStart);
range.setEnd(textContent, selectionOffsetEnd);
return {
anchorNode: textContent,
anchorOffset: selectionOffsetStart,
focusNode: textContent,
focusOffset: selectionOffsetEnd
};
}
_getEditorSelectionFromDomRange(context, state, direction, range) {
const viewModel = context.viewModel;
const model = viewModel.model;
const coordinatesConverter = viewModel.coordinatesConverter;
const modelScreenReaderContentStartPositionWithinEditor = coordinatesConverter.convertViewPositionToModelPosition(state.startPositionWithinEditor);
const offsetOfStartOfScreenReaderContent = model.getOffsetAt(modelScreenReaderContentStartPositionWithinEditor);
let offsetOfSelectionStart = range.startOffset + offsetOfStartOfScreenReaderContent;
let offsetOfSelectionEnd = range.endOffset + offsetOfStartOfScreenReaderContent;
const modelUsesCRLF = model.getEndOfLineSequence() === 1 /* EndOfLineSequence.CRLF */;
if (modelUsesCRLF) {
const screenReaderContentText = state.value;
const offsetTransformer = new PositionOffsetTransformer(screenReaderContentText);
const positionOfStartWithinText = offsetTransformer.getPosition(range.startOffset);
const positionOfEndWithinText = offsetTransformer.getPosition(range.endOffset);
offsetOfSelectionStart += positionOfStartWithinText.lineNumber - 1;
offsetOfSelectionEnd += positionOfEndWithinText.lineNumber - 1;
}
const positionOfSelectionStart = model.getPositionAt(offsetOfSelectionStart);
const positionOfSelectionEnd = model.getPositionAt(offsetOfSelectionEnd);
const selectionStart = direction === 'forward' ? positionOfSelectionStart : positionOfSelectionEnd;
const selectionEnd = direction === 'forward' ? positionOfSelectionEnd : positionOfSelectionStart;
return Selection.fromPositions(selectionStart, selectionEnd);
}
};
SimpleScreenReaderContent = __decorate([
__param(3, IAccessibilityService)
], SimpleScreenReaderContent);
export { SimpleScreenReaderContent };
@@ -0,0 +1,175 @@
import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
import { localize } from '../../../../../nls.js';
import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
import { Selection } from '../../../../common/core/selection.js';
import { applyFontInfo } from '../../../config/domFontInfo.js';
import { ariaLabelForScreenReaderContent } from '../screenReaderUtils.js';
import { RichScreenReaderContent } from './screenReaderContentRich.js';
import { SimpleScreenReaderContent } from './screenReaderContentSimple.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
let ScreenReaderSupport = class ScreenReaderSupport extends Disposable {
constructor(_domNode, _context, _viewController, _keybindingService, _accessibilityService) {
super();
this._domNode = _domNode;
this._context = _context;
this._viewController = _viewController;
this._keybindingService = _keybindingService;
this._accessibilityService = _accessibilityService;
// Configuration values
this._contentLeft = 1;
this._contentWidth = 1;
this._contentHeight = 1;
this._divWidth = 1;
this._primarySelection = new Selection(1, 1, 1, 1);
this._primaryCursorVisibleRange = null;
this._state = this._register(new MutableDisposable());
this._instantiateScreenReaderContent();
this._updateConfigurationSettings();
this._updateDomAttributes();
}
onWillPaste() {
this._state.value?.onWillPaste();
}
onWillCut() {
this._state.value?.onWillCut();
}
handleFocusChange(newFocusValue) {
this._state.value?.onFocusChange(newFocusValue);
this.writeScreenReaderContent();
}
onConfigurationChanged(e) {
this._instantiateScreenReaderContent();
this._updateConfigurationSettings();
this._updateDomAttributes();
if (e.hasChanged(2 /* EditorOption.accessibilitySupport */)) {
this.writeScreenReaderContent();
}
}
_instantiateScreenReaderContent() {
const renderRichContent = this._context.configuration.options.get(107 /* EditorOption.renderRichScreenReaderContent */);
if (this._renderRichContent !== renderRichContent) {
this._renderRichContent = renderRichContent;
this._state.value = this._createScreenReaderContent(renderRichContent);
}
}
_createScreenReaderContent(renderRichContent) {
if (renderRichContent) {
return new RichScreenReaderContent(this._domNode, this._context, this._viewController, this._accessibilityService);
}
else {
return new SimpleScreenReaderContent(this._domNode, this._context, this._viewController, this._accessibilityService);
}
}
_updateConfigurationSettings() {
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
const wrappingColumn = layoutInfo.wrappingColumn;
this._contentLeft = layoutInfo.contentLeft;
this._contentWidth = layoutInfo.contentWidth;
this._contentHeight = layoutInfo.height;
this._fontInfo = options.get(59 /* EditorOption.fontInfo */);
this._divWidth = Math.round(wrappingColumn * this._fontInfo.typicalHalfwidthCharacterWidth);
this._state.value?.onConfigurationChanged(options);
}
_updateDomAttributes() {
const options = this._context.configuration.options;
this._domNode.domNode.setAttribute('role', 'textbox');
this._domNode.domNode.setAttribute('aria-required', options.get(9 /* EditorOption.ariaRequired */) ? 'true' : 'false');
this._domNode.domNode.setAttribute('aria-multiline', 'true');
this._domNode.domNode.setAttribute('aria-autocomplete', options.get(104 /* EditorOption.readOnly */) ? 'none' : 'both');
this._domNode.domNode.setAttribute('aria-roledescription', localize(60, "editor"));
this._domNode.domNode.setAttribute('aria-label', ariaLabelForScreenReaderContent(options, this._keybindingService));
const tabSize = this._context.viewModel.model.getOptions().tabSize;
const spaceWidth = options.get(59 /* EditorOption.fontInfo */).spaceWidth;
this._domNode.domNode.style.tabSize = `${tabSize * spaceWidth}px`;
const wordWrapOverride2 = options.get(154 /* EditorOption.wordWrapOverride2 */);
const wordWrapOverride1 = (wordWrapOverride2 === 'inherit' ? options.get(153 /* EditorOption.wordWrapOverride1 */) : wordWrapOverride2);
const wordWrap = (wordWrapOverride1 === 'inherit' ? options.get(149 /* EditorOption.wordWrap */) : wordWrapOverride1);
this._domNode.domNode.style.textWrap = wordWrap === 'off' ? 'nowrap' : 'wrap';
}
onCursorStateChanged(e) {
this._primarySelection = e.selections[0] ?? new Selection(1, 1, 1, 1);
}
prepareRender(ctx) {
this.writeScreenReaderContent();
this._primaryCursorVisibleRange = ctx.visibleRangeForPosition(this._primarySelection.getPosition());
}
render(ctx) {
if (!this._primaryCursorVisibleRange) {
// The primary cursor is outside the viewport => place textarea to the top left
this._renderAtTopLeft();
return;
}
const editorScrollLeft = this._context.viewLayout.getCurrentScrollLeft();
const left = this._contentLeft + this._primaryCursorVisibleRange.left - editorScrollLeft;
if (left < this._contentLeft || left > this._contentLeft + this._contentWidth) {
// cursor is outside the viewport
this._renderAtTopLeft();
return;
}
const editorScrollTop = this._context.viewLayout.getCurrentScrollTop();
const positionLineNumber = this._primarySelection.positionLineNumber;
const top = this._context.viewLayout.getVerticalOffsetForLineNumber(positionLineNumber) - editorScrollTop;
if (top < 0 || top > this._contentHeight) {
// cursor is outside the viewport
this._renderAtTopLeft();
return;
}
// The <div> where we render the screen reader content does not support variable line heights,
// all the lines must have the same height. We use the line height of the cursor position as the
// line height for all lines.
const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(positionLineNumber);
this._doRender(top, this._contentLeft, this._divWidth, lineHeight);
this._state.value?.updateScrollTop(this._primarySelection);
}
_renderAtTopLeft() {
this._doRender(0, 0, this._contentWidth, 1);
}
_doRender(top, left, width, height) {
// For correct alignment of the screen reader content, we need to apply the correct font
applyFontInfo(this._domNode, this._fontInfo);
this._domNode.setTop(top);
this._domNode.setLeft(left);
this._domNode.setWidth(width);
this._domNode.setHeight(height);
this._domNode.setLineHeight(height);
}
setAriaOptions(options) {
if (options.activeDescendant) {
this._domNode.setAttribute('aria-haspopup', 'true');
this._domNode.setAttribute('aria-autocomplete', 'list');
this._domNode.setAttribute('aria-activedescendant', options.activeDescendant);
}
else {
this._domNode.setAttribute('aria-haspopup', 'false');
this._domNode.setAttribute('aria-autocomplete', 'both');
this._domNode.removeAttribute('aria-activedescendant');
}
if (options.role) {
this._domNode.setAttribute('role', options.role);
}
}
writeScreenReaderContent() {
this._state.value?.updateScreenReaderContent(this._primarySelection);
}
};
ScreenReaderSupport = __decorate([
__param(3, IKeybindingService),
__param(4, IAccessibilityService)
], ScreenReaderSupport);
export { ScreenReaderSupport };
@@ -0,0 +1,111 @@
import { Range } from '../../../common/core/range.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.
*--------------------------------------------------------------------------------------------*/
class SimplePagedScreenReaderStrategy {
_getPageOfLine(lineNumber, linesPerPage) {
return Math.floor((lineNumber - 1) / linesPerPage);
}
_getRangeForPage(page, linesPerPage) {
const offset = page * linesPerPage;
const startLineNumber = offset + 1;
const endLineNumber = offset + linesPerPage;
return new Range(startLineNumber, 1, endLineNumber + 1, 1);
}
fromEditorSelection(model, selection, linesPerPage, trimLongText) {
// Chromium handles very poorly text even of a few thousand chars
// Cut text to avoid stalling the entire UI
const LIMIT_CHARS = 500;
const selectionStartPage = this._getPageOfLine(selection.startLineNumber, linesPerPage);
const selectionStartPageRange = this._getRangeForPage(selectionStartPage, linesPerPage);
const selectionEndPage = this._getPageOfLine(selection.endLineNumber, linesPerPage);
const selectionEndPageRange = this._getRangeForPage(selectionEndPage, linesPerPage);
let pretextRange = selectionStartPageRange.intersectRanges(new Range(1, 1, selection.startLineNumber, selection.startColumn));
if (trimLongText && model.getValueLengthInRange(pretextRange, 1 /* EndOfLinePreference.LF */) > LIMIT_CHARS) {
const pretextStart = model.modifyPosition(pretextRange.getEndPosition(), -LIMIT_CHARS);
pretextRange = Range.fromPositions(pretextStart, pretextRange.getEndPosition());
}
const pretext = model.getValueInRange(pretextRange, 1 /* EndOfLinePreference.LF */);
const lastLine = model.getLineCount();
const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
let posttextRange = selectionEndPageRange.intersectRanges(new Range(selection.endLineNumber, selection.endColumn, lastLine, lastLineMaxColumn));
if (trimLongText && model.getValueLengthInRange(posttextRange, 1 /* EndOfLinePreference.LF */) > LIMIT_CHARS) {
const posttextEnd = model.modifyPosition(posttextRange.getStartPosition(), LIMIT_CHARS);
posttextRange = Range.fromPositions(posttextRange.getStartPosition(), posttextEnd);
}
const posttext = model.getValueInRange(posttextRange, 1 /* EndOfLinePreference.LF */);
let text;
if (selectionStartPage === selectionEndPage || selectionStartPage + 1 === selectionEndPage) {
// take full selection
text = model.getValueInRange(selection, 1 /* EndOfLinePreference.LF */);
}
else {
const selectionRange1 = selectionStartPageRange.intersectRanges(selection);
const selectionRange2 = selectionEndPageRange.intersectRanges(selection);
text = (model.getValueInRange(selectionRange1, 1 /* EndOfLinePreference.LF */)
+ String.fromCharCode(8230)
+ model.getValueInRange(selectionRange2, 1 /* EndOfLinePreference.LF */));
}
if (trimLongText && text.length > 2 * LIMIT_CHARS) {
text = text.substring(0, LIMIT_CHARS) + String.fromCharCode(8230) + text.substring(text.length - LIMIT_CHARS, text.length);
}
let selectionStart;
let selectionEnd;
if (selection.getDirection() === 0 /* SelectionDirection.LTR */) {
selectionStart = pretext.length;
selectionEnd = pretext.length + text.length;
}
else {
selectionEnd = pretext.length;
selectionStart = pretext.length + text.length;
}
return {
value: pretext + text + posttext,
selection: selection,
selectionStart,
selectionEnd,
startPositionWithinEditor: pretextRange.getStartPosition(),
newlineCountBeforeSelection: pretextRange.endLineNumber - pretextRange.startLineNumber,
};
}
}
function ariaLabelForScreenReaderContent(options, keybindingService) {
const accessibilitySupport = options.get(2 /* EditorOption.accessibilitySupport */);
if (accessibilitySupport === 1 /* AccessibilitySupport.Disabled */) {
const toggleKeybindingLabel = keybindingService.lookupKeybinding('editor.action.toggleScreenReaderAccessibilityMode')?.getAriaLabel();
const runCommandKeybindingLabel = keybindingService.lookupKeybinding('workbench.action.showCommands')?.getAriaLabel();
const keybindingEditorKeybindingLabel = keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings')?.getAriaLabel();
const editorNotAccessibleMessage = localize(61, "The editor is not accessible at this time.");
if (toggleKeybindingLabel) {
return localize(62, "{0} To enable screen reader optimized mode, use {1}", editorNotAccessibleMessage, toggleKeybindingLabel);
}
else if (runCommandKeybindingLabel) {
return localize(63, "{0} To enable screen reader optimized mode, open the quick pick with {1} and run the command Toggle Screen Reader Accessibility Mode, which is currently not triggerable via keyboard.", editorNotAccessibleMessage, runCommandKeybindingLabel);
}
else if (keybindingEditorKeybindingLabel) {
return localize(64, "{0} Please assign a keybinding for the command Toggle Screen Reader Accessibility Mode by accessing the keybindings editor with {1} and run it.", editorNotAccessibleMessage, keybindingEditorKeybindingLabel);
}
else {
// SOS
return editorNotAccessibleMessage;
}
}
return options.get(8 /* EditorOption.ariaLabel */);
}
function newlinecount(text) {
let result = 0;
let startIndex = -1;
do {
startIndex = text.indexOf('\n', startIndex + 1);
if (startIndex === -1) {
break;
}
result++;
} while (true);
return result;
}
export { SimplePagedScreenReaderStrategy, ariaLabelForScreenReaderContent, newlinecount };
@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .inputarea {
min-width: 0;
min-height: 0;
margin: 0;
padding: 0;
position: absolute;
outline: none !important;
resize: none;
border: none;
overflow: hidden;
color: transparent;
background-color: transparent;
z-index: -10;
}
/*.monaco-editor .inputarea {
position: fixed !important;
width: 800px !important;
height: 500px !important;
top: initial !important;
left: initial !important;
bottom: 0 !important;
right: 0 !important;
color: black !important;
background: white !important;
line-height: 15px !important;
font-size: 14px !important;
z-index: 10 !important;
}*/
.monaco-editor .inputarea.ime-input {
z-index: 10;
caret-color: var(--vscode-editorCursor-foreground);
color: var(--vscode-editor-foreground);
}
@@ -0,0 +1,720 @@
import './textAreaEditContext.css';
import { localize } from '../../../../../nls.js';
import { isSafari, isFirefox, isChrome, isAndroid } from '../../../../../base/browser/browser.js';
import { createFastDomNode } from '../../../../../base/browser/fastDomNode.js';
import { OS, isMacintosh } from '../../../../../base/common/platform.js';
import { isHighSurrogate } from '../../../../../base/common/strings.js';
import { applyFontInfo } from '../../../config/domFontInfo.js';
import { PartFingerprints } from '../../../view/viewPart.js';
import { LineNumbersOverlay } from '../../../viewParts/lineNumbers/lineNumbers.js';
import { Margin } from '../../../viewParts/margin/margin.js';
import { EditorOptions } from '../../../../common/config/editorOptions.js';
import { Position } from '../../../../common/core/position.js';
import { Range } from '../../../../common/core/range.js';
import { Selection } from '../../../../common/core/selection.js';
import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../../../base/browser/ui/mouseCursor/mouseCursor.js';
import { TokenizationRegistry } from '../../../../common/languages.js';
import { Color } from '../../../../../base/common/color.js';
import { IME } from '../../../../../base/common/ime.js';
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { AbstractEditContext } from '../editContext.js';
import { TextAreaWrapper, TextAreaInput } from './textAreaEditContextInput.js';
import { ariaLabelForScreenReaderContent, newlinecount, SimplePagedScreenReaderStrategy } from '../screenReaderUtils.js';
import { getDataToCopy } from '../clipboardUtils.js';
import { TextAreaState } from './textAreaEditContextState.js';
import { getMapForWordSeparators } from '../../../../common/core/wordCharacterClassifier.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
class VisibleTextAreaData {
constructor(_context, modelLineNumber, distanceToModelLineStart, widthOfHiddenLineTextBefore, distanceToModelLineEnd) {
this._context = _context;
this.modelLineNumber = modelLineNumber;
this.distanceToModelLineStart = distanceToModelLineStart;
this.widthOfHiddenLineTextBefore = widthOfHiddenLineTextBefore;
this.distanceToModelLineEnd = distanceToModelLineEnd;
this._visibleTextAreaBrand = undefined;
this.startPosition = null;
this.endPosition = null;
this.visibleTextareaStart = null;
this.visibleTextareaEnd = null;
/**
* When doing composition, the currently composed text might be split up into
* multiple tokens, then merged again into a single token, etc. Here we attempt
* to keep the presentation of the <textarea> stable by using the previous used
* style if multiple tokens come into play. This avoids flickering.
*/
this._previousPresentation = null;
}
prepareRender(visibleRangeProvider) {
const startModelPosition = new Position(this.modelLineNumber, this.distanceToModelLineStart + 1);
const endModelPosition = new Position(this.modelLineNumber, this._context.viewModel.model.getLineMaxColumn(this.modelLineNumber) - this.distanceToModelLineEnd);
this.startPosition = this._context.viewModel.coordinatesConverter.convertModelPositionToViewPosition(startModelPosition);
this.endPosition = this._context.viewModel.coordinatesConverter.convertModelPositionToViewPosition(endModelPosition);
if (this.startPosition.lineNumber === this.endPosition.lineNumber) {
this.visibleTextareaStart = visibleRangeProvider.visibleRangeForPosition(this.startPosition);
this.visibleTextareaEnd = visibleRangeProvider.visibleRangeForPosition(this.endPosition);
}
else {
// TODO: what if the view positions are not on the same line?
this.visibleTextareaStart = null;
this.visibleTextareaEnd = null;
}
}
definePresentation(tokenPresentation) {
if (!this._previousPresentation) {
// To avoid flickering, once set, always reuse a presentation throughout the entire IME session
if (tokenPresentation) {
this._previousPresentation = tokenPresentation;
}
else {
this._previousPresentation = {
foreground: 1 /* ColorId.DefaultForeground */,
italic: false,
bold: false,
underline: false,
strikethrough: false,
};
}
}
return this._previousPresentation;
}
}
const canUseZeroSizeTextarea = (isFirefox);
let TextAreaEditContext = class TextAreaEditContext extends AbstractEditContext {
constructor(context, overflowGuardContainer, viewController, visibleRangeProvider, _keybindingService, _instantiationService) {
super(context);
this._keybindingService = _keybindingService;
this._instantiationService = _instantiationService;
this._primaryCursorPosition = new Position(1, 1);
this._primaryCursorVisibleRange = null;
this._viewController = viewController;
this._visibleRangeProvider = visibleRangeProvider;
this._scrollLeft = 0;
this._scrollTop = 0;
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this._setAccessibilityOptions(options);
this._contentLeft = layoutInfo.contentLeft;
this._contentWidth = layoutInfo.contentWidth;
this._contentHeight = layoutInfo.height;
this._fontInfo = options.get(59 /* EditorOption.fontInfo */);
this._emptySelectionClipboard = options.get(45 /* EditorOption.emptySelectionClipboard */);
this._copyWithSyntaxHighlighting = options.get(31 /* EditorOption.copyWithSyntaxHighlighting */);
this._visibleTextArea = null;
this._selections = [new Selection(1, 1, 1, 1)];
this._modelSelections = [new Selection(1, 1, 1, 1)];
this._lastRenderPosition = null;
// Text Area (The focus will always be in the textarea when the cursor is blinking)
this.textArea = createFastDomNode(document.createElement('textarea'));
PartFingerprints.write(this.textArea, 7 /* PartFingerprint.TextArea */);
this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`);
this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');
const { tabSize } = this._context.viewModel.model.getOptions();
this.textArea.domNode.style.tabSize = `${tabSize * this._fontInfo.spaceWidth}px`;
this.textArea.setAttribute('autocorrect', 'off');
this.textArea.setAttribute('autocapitalize', 'off');
this.textArea.setAttribute('autocomplete', 'off');
this.textArea.setAttribute('spellcheck', 'false');
this.textArea.setAttribute('aria-label', ariaLabelForScreenReaderContent(options, this._keybindingService));
this.textArea.setAttribute('aria-required', options.get(9 /* EditorOption.ariaRequired */) ? 'true' : 'false');
this.textArea.setAttribute('tabindex', String(options.get(140 /* EditorOption.tabIndex */)));
this.textArea.setAttribute('role', 'textbox');
this.textArea.setAttribute('aria-roledescription', localize(65, "editor"));
this.textArea.setAttribute('aria-multiline', 'true');
this.textArea.setAttribute('aria-autocomplete', options.get(104 /* EditorOption.readOnly */) ? 'none' : 'both');
this._ensureReadOnlyAttribute();
this.textAreaCover = createFastDomNode(document.createElement('div'));
this.textAreaCover.setPosition('absolute');
overflowGuardContainer.appendChild(this.textArea);
overflowGuardContainer.appendChild(this.textAreaCover);
const simplePagedScreenReaderStrategy = new SimplePagedScreenReaderStrategy();
const textAreaInputHost = {
getDataToCopy: () => {
return getDataToCopy(this._context.viewModel, this._modelSelections, this._emptySelectionClipboard, this._copyWithSyntaxHighlighting);
},
getScreenReaderContent: () => {
if (this._accessibilitySupport === 1 /* AccessibilitySupport.Disabled */) {
// We know for a fact that a screen reader is not attached
// On OSX, we write the character before the cursor to allow for "long-press" composition
// Also on OSX, we write the word before the cursor to allow for the Accessibility Keyboard to give good hints
const selection = this._selections[0];
if (isMacintosh && selection.isEmpty()) {
const position = selection.getStartPosition();
let textBefore = this._getWordBeforePosition(position);
if (textBefore.length === 0) {
textBefore = this._getCharacterBeforePosition(position);
}
if (textBefore.length > 0) {
return new TextAreaState(textBefore, textBefore.length, textBefore.length, Range.fromPositions(position), 0);
}
}
// on macOS, write current selection into textarea will allow system text services pick selected text,
// but we still want to limit the amount of text given Chromium handles very poorly text even of a few
// thousand chars
// (https://github.com/microsoft/vscode/issues/27799)
const LIMIT_CHARS = 500;
if (isMacintosh && !selection.isEmpty() && this._context.viewModel.getValueLengthInRange(selection, 0 /* EndOfLinePreference.TextDefined */) < LIMIT_CHARS) {
const text = this._context.viewModel.getValueInRange(selection, 0 /* EndOfLinePreference.TextDefined */);
return new TextAreaState(text, 0, text.length, selection, 0);
}
// on Safari, document.execCommand('cut') and document.execCommand('copy') will just not work
// if the textarea has no content selected. So if there is an editor selection, ensure something
// is selected in the textarea.
if (isSafari && !selection.isEmpty()) {
const placeholderText = 'vscode-placeholder';
return new TextAreaState(placeholderText, 0, placeholderText.length, null, undefined);
}
return TextAreaState.EMPTY;
}
if (isAndroid) {
// when tapping in the editor on a word, Android enters composition mode.
// in the `compositionstart` event we cannot clear the textarea, because
// it then forgets to ever send a `compositionend`.
// we therefore only write the current word in the textarea
const selection = this._selections[0];
if (selection.isEmpty()) {
const position = selection.getStartPosition();
const [wordAtPosition, positionOffsetInWord] = this._getAndroidWordAtPosition(position);
if (wordAtPosition.length > 0) {
return new TextAreaState(wordAtPosition, positionOffsetInWord, positionOffsetInWord, Range.fromPositions(position), 0);
}
}
return TextAreaState.EMPTY;
}
const screenReaderContentState = simplePagedScreenReaderStrategy.fromEditorSelection(this._context.viewModel, this._selections[0], this._accessibilityPageSize, this._accessibilitySupport === 0 /* AccessibilitySupport.Unknown */);
return TextAreaState.fromScreenReaderContentState(screenReaderContentState);
},
deduceModelPosition: (viewAnchorPosition, deltaOffset, lineFeedCnt) => {
return this._context.viewModel.deduceModelPositionRelativeToViewPosition(viewAnchorPosition, deltaOffset, lineFeedCnt);
}
};
const textAreaWrapper = this._register(new TextAreaWrapper(this.textArea.domNode));
this._textAreaInput = this._register(this._instantiationService.createInstance(TextAreaInput, textAreaInputHost, textAreaWrapper, OS, {
isAndroid: isAndroid,
isChrome: isChrome,
isFirefox: isFirefox,
isSafari: isSafari,
}));
this._register(this._textAreaInput.onKeyDown((e) => {
this._viewController.emitKeyDown(e);
}));
this._register(this._textAreaInput.onKeyUp((e) => {
this._viewController.emitKeyUp(e);
}));
this._register(this._textAreaInput.onPaste((e) => {
let pasteOnNewLine = false;
let multicursorText = null;
let mode = null;
if (e.metadata) {
pasteOnNewLine = (this._emptySelectionClipboard && !!e.metadata.isFromEmptySelection);
multicursorText = (typeof e.metadata.multicursorText !== 'undefined' ? e.metadata.multicursorText : null);
mode = e.metadata.mode;
}
this._viewController.paste(e.text, pasteOnNewLine, multicursorText, mode);
}));
this._register(this._textAreaInput.onCut(() => {
this._viewController.cut();
}));
this._register(this._textAreaInput.onType((e) => {
if (e.replacePrevCharCnt || e.replaceNextCharCnt || e.positionDelta) {
this._viewController.compositionType(e.text, e.replacePrevCharCnt, e.replaceNextCharCnt, e.positionDelta);
}
else {
this._viewController.type(e.text);
}
}));
this._register(this._textAreaInput.onSelectionChangeRequest((modelSelection) => {
this._viewController.setSelection(modelSelection);
}));
this._register(this._textAreaInput.onCompositionStart((e) => {
// The textarea might contain some content when composition starts.
//
// When we make the textarea visible, it always has a height of 1 line,
// so we don't need to worry too much about content on lines above or below
// the selection.
//
// However, the text on the current line needs to be made visible because
// some IME methods allow to move to other glyphs on the current line
// (by pressing arrow keys).
//
// (1) The textarea might contain only some parts of the current line,
// like the word before the selection. Also, the content inside the textarea
// can grow or shrink as composition occurs. We therefore anchor the textarea
// in terms of distance to a certain line start and line end.
//
// (2) Also, we should not make \t characters visible, because their rendering
// inside the <textarea> will not align nicely with our rendering. We therefore
// will hide (if necessary) some of the leading text on the current line.
const ta = this.textArea.domNode;
const modelSelection = this._modelSelections[0];
const { distanceToModelLineStart, widthOfHiddenTextBefore } = (() => {
// Find the text that is on the current line before the selection
const textBeforeSelection = ta.value.substring(0, Math.min(ta.selectionStart, ta.selectionEnd));
const lineFeedOffset1 = textBeforeSelection.lastIndexOf('\n');
const lineTextBeforeSelection = textBeforeSelection.substring(lineFeedOffset1 + 1);
// We now search to see if we should hide some part of it (if it contains \t)
const tabOffset1 = lineTextBeforeSelection.lastIndexOf('\t');
const desiredVisibleBeforeCharCount = lineTextBeforeSelection.length - tabOffset1 - 1;
const startModelPosition = modelSelection.getStartPosition();
const visibleBeforeCharCount = Math.min(startModelPosition.column - 1, desiredVisibleBeforeCharCount);
const distanceToModelLineStart = startModelPosition.column - 1 - visibleBeforeCharCount;
const hiddenLineTextBefore = lineTextBeforeSelection.substring(0, lineTextBeforeSelection.length - visibleBeforeCharCount);
const { tabSize } = this._context.viewModel.model.getOptions();
const widthOfHiddenTextBefore = measureText(this.textArea.domNode.ownerDocument, hiddenLineTextBefore, this._fontInfo, tabSize);
return { distanceToModelLineStart, widthOfHiddenTextBefore };
})();
const { distanceToModelLineEnd } = (() => {
// Find the text that is on the current line after the selection
const textAfterSelection = ta.value.substring(Math.max(ta.selectionStart, ta.selectionEnd));
const lineFeedOffset2 = textAfterSelection.indexOf('\n');
const lineTextAfterSelection = lineFeedOffset2 === -1 ? textAfterSelection : textAfterSelection.substring(0, lineFeedOffset2);
const tabOffset2 = lineTextAfterSelection.indexOf('\t');
const desiredVisibleAfterCharCount = (tabOffset2 === -1 ? lineTextAfterSelection.length : lineTextAfterSelection.length - tabOffset2 - 1);
const endModelPosition = modelSelection.getEndPosition();
const visibleAfterCharCount = Math.min(this._context.viewModel.model.getLineMaxColumn(endModelPosition.lineNumber) - endModelPosition.column, desiredVisibleAfterCharCount);
const distanceToModelLineEnd = this._context.viewModel.model.getLineMaxColumn(endModelPosition.lineNumber) - endModelPosition.column - visibleAfterCharCount;
return { distanceToModelLineEnd };
})();
// Scroll to reveal the location in the editor where composition occurs
this._context.viewModel.revealRange('keyboard', true, Range.fromPositions(this._selections[0].getStartPosition()), 0 /* viewEvents.VerticalRevealType.Simple */, 1 /* ScrollType.Immediate */);
this._visibleTextArea = new VisibleTextAreaData(this._context, modelSelection.startLineNumber, distanceToModelLineStart, widthOfHiddenTextBefore, distanceToModelLineEnd);
// We turn off wrapping if the <textarea> becomes visible for composition
this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');
this._visibleTextArea.prepareRender(this._visibleRangeProvider);
this._render();
// Show the textarea
this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ime-input`);
this._viewController.compositionStart();
this._context.viewModel.onCompositionStart();
}));
this._register(this._textAreaInput.onCompositionUpdate((e) => {
if (!this._visibleTextArea) {
return;
}
this._visibleTextArea.prepareRender(this._visibleRangeProvider);
this._render();
}));
this._register(this._textAreaInput.onCompositionEnd(() => {
this._visibleTextArea = null;
// We turn on wrapping as necessary if the <textarea> hides after composition
this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');
this._render();
this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`);
this._viewController.compositionEnd();
this._context.viewModel.onCompositionEnd();
}));
this._register(this._textAreaInput.onFocus(() => {
this._context.viewModel.setHasFocus(true);
}));
this._register(this._textAreaInput.onBlur(() => {
this._context.viewModel.setHasFocus(false);
}));
this._register(IME.onDidChange(() => {
this._ensureReadOnlyAttribute();
}));
}
get domNode() {
return this.textArea;
}
writeScreenReaderContent(reason) {
this._textAreaInput.writeNativeTextAreaContent(reason);
}
dispose() {
super.dispose();
this.textArea.domNode.remove();
this.textAreaCover.domNode.remove();
}
_getAndroidWordAtPosition(position) {
const ANDROID_WORD_SEPARATORS = '`~!@#$%^&*()-=+[{]}\\|;:",.<>/?';
const lineContent = this._context.viewModel.getLineContent(position.lineNumber);
const wordSeparators = getMapForWordSeparators(ANDROID_WORD_SEPARATORS, []);
let goingLeft = true;
let startColumn = position.column;
let goingRight = true;
let endColumn = position.column;
let distance = 0;
while (distance < 50 && (goingLeft || goingRight)) {
if (goingLeft && startColumn <= 1) {
goingLeft = false;
}
if (goingLeft) {
const charCode = lineContent.charCodeAt(startColumn - 2);
const charClass = wordSeparators.get(charCode);
if (charClass !== 0 /* WordCharacterClass.Regular */) {
goingLeft = false;
}
else {
startColumn--;
}
}
if (goingRight && endColumn > lineContent.length) {
goingRight = false;
}
if (goingRight) {
const charCode = lineContent.charCodeAt(endColumn - 1);
const charClass = wordSeparators.get(charCode);
if (charClass !== 0 /* WordCharacterClass.Regular */) {
goingRight = false;
}
else {
endColumn++;
}
}
distance++;
}
return [lineContent.substring(startColumn - 1, endColumn - 1), position.column - startColumn];
}
_getWordBeforePosition(position) {
const lineContent = this._context.viewModel.getLineContent(position.lineNumber);
const wordSeparators = getMapForWordSeparators(this._context.configuration.options.get(148 /* EditorOption.wordSeparators */), []);
let column = position.column;
let distance = 0;
while (column > 1) {
const charCode = lineContent.charCodeAt(column - 2);
const charClass = wordSeparators.get(charCode);
if (charClass !== 0 /* WordCharacterClass.Regular */ || distance > 50) {
return lineContent.substring(column - 1, position.column - 1);
}
distance++;
column--;
}
return lineContent.substring(0, position.column - 1);
}
_getCharacterBeforePosition(position) {
if (position.column > 1) {
const lineContent = this._context.viewModel.getLineContent(position.lineNumber);
const charBefore = lineContent.charAt(position.column - 2);
if (!isHighSurrogate(charBefore.charCodeAt(0))) {
return charBefore;
}
}
return '';
}
_setAccessibilityOptions(options) {
this._accessibilitySupport = options.get(2 /* EditorOption.accessibilitySupport */);
const accessibilityPageSize = options.get(3 /* EditorOption.accessibilityPageSize */);
if (this._accessibilitySupport === 2 /* AccessibilitySupport.Enabled */ && accessibilityPageSize === EditorOptions.accessibilityPageSize.defaultValue) {
// If a screen reader is attached and the default value is not set we should automatically increase the page size to 500 for a better experience
this._accessibilityPageSize = 500;
}
else {
this._accessibilityPageSize = accessibilityPageSize;
}
// When wrapping is enabled and a screen reader might be attached,
// we will size the textarea to match the width used for wrapping points computation (see `domLineBreaksComputer.ts`).
// This is because screen readers will read the text in the textarea and we'd like that the
// wrapping points in the textarea match the wrapping points in the editor.
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
const wrappingColumn = layoutInfo.wrappingColumn;
if (wrappingColumn !== -1 && this._accessibilitySupport !== 1 /* AccessibilitySupport.Disabled */) {
const fontInfo = options.get(59 /* EditorOption.fontInfo */);
this._textAreaWrapping = true;
this._textAreaWidth = Math.round(wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth);
}
else {
this._textAreaWrapping = false;
this._textAreaWidth = (canUseZeroSizeTextarea ? 0 : 1);
}
}
// --- begin event handlers
onConfigurationChanged(e) {
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this._setAccessibilityOptions(options);
this._contentLeft = layoutInfo.contentLeft;
this._contentWidth = layoutInfo.contentWidth;
this._contentHeight = layoutInfo.height;
this._fontInfo = options.get(59 /* EditorOption.fontInfo */);
this._emptySelectionClipboard = options.get(45 /* EditorOption.emptySelectionClipboard */);
this._copyWithSyntaxHighlighting = options.get(31 /* EditorOption.copyWithSyntaxHighlighting */);
this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off');
const { tabSize } = this._context.viewModel.model.getOptions();
this.textArea.domNode.style.tabSize = `${tabSize * this._fontInfo.spaceWidth}px`;
this.textArea.setAttribute('aria-label', ariaLabelForScreenReaderContent(options, this._keybindingService));
this.textArea.setAttribute('aria-required', options.get(9 /* EditorOption.ariaRequired */) ? 'true' : 'false');
this.textArea.setAttribute('tabindex', String(options.get(140 /* EditorOption.tabIndex */)));
if (e.hasChanged(41 /* EditorOption.domReadOnly */) || e.hasChanged(104 /* EditorOption.readOnly */)) {
this._ensureReadOnlyAttribute();
}
if (e.hasChanged(2 /* EditorOption.accessibilitySupport */)) {
this._textAreaInput.writeNativeTextAreaContent('strategy changed');
}
return true;
}
onCursorStateChanged(e) {
this._selections = e.selections.slice(0);
this._modelSelections = e.modelSelections.slice(0);
// We must update the <textarea> synchronously, otherwise long press IME on macos breaks.
// See https://github.com/microsoft/vscode/issues/165821
this._textAreaInput.writeNativeTextAreaContent('selection changed');
return true;
}
onDecorationsChanged(e) {
// true for inline decorations that can end up relayouting text
return true;
}
onFlushed(e) {
return true;
}
onLinesChanged(e) {
return true;
}
onLinesDeleted(e) {
return true;
}
onLinesInserted(e) {
return true;
}
onScrollChanged(e) {
this._scrollLeft = e.scrollLeft;
this._scrollTop = e.scrollTop;
return true;
}
onZonesChanged(e) {
return true;
}
// --- end event handlers
// --- begin view API
isFocused() {
return this._textAreaInput.isFocused();
}
focus() {
this._textAreaInput.focusTextArea();
}
refreshFocusState() {
this._textAreaInput.refreshFocusState();
}
getLastRenderData() {
return this._lastRenderPosition;
}
setAriaOptions(options) {
if (options.activeDescendant) {
this.textArea.setAttribute('aria-haspopup', 'true');
this.textArea.setAttribute('aria-autocomplete', 'list');
this.textArea.setAttribute('aria-activedescendant', options.activeDescendant);
}
else {
this.textArea.setAttribute('aria-haspopup', 'false');
this.textArea.setAttribute('aria-autocomplete', 'both');
this.textArea.removeAttribute('aria-activedescendant');
}
if (options.role) {
this.textArea.setAttribute('role', options.role);
}
}
// --- end view API
_ensureReadOnlyAttribute() {
const options = this._context.configuration.options;
// When someone requests to disable IME, we set the "readonly" attribute on the <textarea>.
// This will prevent composition.
const useReadOnly = !IME.enabled || (options.get(41 /* EditorOption.domReadOnly */) && options.get(104 /* EditorOption.readOnly */));
if (useReadOnly) {
this.textArea.setAttribute('readonly', 'true');
}
else {
this.textArea.removeAttribute('readonly');
}
}
prepareRender(ctx) {
this._primaryCursorPosition = new Position(this._selections[0].positionLineNumber, this._selections[0].positionColumn);
this._primaryCursorVisibleRange = ctx.visibleRangeForPosition(this._primaryCursorPosition);
this._visibleTextArea?.prepareRender(ctx);
}
render(ctx) {
this._textAreaInput.writeNativeTextAreaContent('render');
this._render();
}
_render() {
if (this._visibleTextArea) {
// The text area is visible for composition reasons
const visibleStart = this._visibleTextArea.visibleTextareaStart;
const visibleEnd = this._visibleTextArea.visibleTextareaEnd;
const startPosition = this._visibleTextArea.startPosition;
const endPosition = this._visibleTextArea.endPosition;
if (startPosition && endPosition && visibleStart && visibleEnd && visibleEnd.left >= this._scrollLeft && visibleStart.left <= this._scrollLeft + this._contentWidth) {
const top = (this._context.viewLayout.getVerticalOffsetForLineNumber(this._primaryCursorPosition.lineNumber) - this._scrollTop);
const lineCount = newlinecount(this.textArea.domNode.value.substr(0, this.textArea.domNode.selectionStart));
let scrollLeft = this._visibleTextArea.widthOfHiddenLineTextBefore;
let left = (this._contentLeft + visibleStart.left - this._scrollLeft);
// See https://github.com/microsoft/vscode/issues/141725#issuecomment-1050670841
// Here we are adding +1 to avoid flickering that might be caused by having a width that is too small.
// This could be caused by rounding errors that might only show up with certain font families.
// In other words, a pixel might be lost when doing something like
// `Math.round(end) - Math.round(start)`
// vs
// `Math.round(end - start)`
let width = visibleEnd.left - visibleStart.left + 1;
if (left < this._contentLeft) {
// the textarea would be rendered on top of the margin,
// so reduce its width. We use the same technique as
// for hiding text before
const delta = (this._contentLeft - left);
left += delta;
scrollLeft += delta;
width -= delta;
}
if (width > this._contentWidth) {
// the textarea would be wider than the content width,
// so reduce its width.
width = this._contentWidth;
}
// Try to render the textarea with the color/font style to match the text under it
const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(startPosition.lineNumber);
const fontSize = this._context.viewModel.getFontSizeAtPosition(this._primaryCursorPosition);
const viewLineData = this._context.viewModel.getViewLineData(startPosition.lineNumber);
const startTokenIndex = viewLineData.tokens.findTokenIndexAtOffset(startPosition.column - 1);
const endTokenIndex = viewLineData.tokens.findTokenIndexAtOffset(endPosition.column - 1);
const textareaSpansSingleToken = (startTokenIndex === endTokenIndex);
const presentation = this._visibleTextArea.definePresentation((textareaSpansSingleToken ? viewLineData.tokens.getPresentation(startTokenIndex) : null));
this.textArea.domNode.scrollTop = lineCount * lineHeight;
this.textArea.domNode.scrollLeft = scrollLeft;
this._doRender({
lastRenderPosition: null,
top: top,
left: left,
width: width,
height: lineHeight,
useCover: false,
color: (TokenizationRegistry.getColorMap() || [])[presentation.foreground],
italic: presentation.italic,
bold: presentation.bold,
underline: presentation.underline,
strikethrough: presentation.strikethrough,
fontSize
});
}
return;
}
if (!this._primaryCursorVisibleRange) {
// The primary cursor is outside the viewport => place textarea to the top left
this._renderAtTopLeft();
return;
}
const left = this._contentLeft + this._primaryCursorVisibleRange.left - this._scrollLeft;
if (left < this._contentLeft || left > this._contentLeft + this._contentWidth) {
// cursor is outside the viewport
this._renderAtTopLeft();
return;
}
const top = this._context.viewLayout.getVerticalOffsetForLineNumber(this._selections[0].positionLineNumber) - this._scrollTop;
if (top < 0 || top > this._contentHeight) {
// cursor is outside the viewport
this._renderAtTopLeft();
return;
}
// The primary cursor is in the viewport (at least vertically) => place textarea on the cursor
if (isMacintosh || this._accessibilitySupport === 2 /* AccessibilitySupport.Enabled */) {
// For the popup emoji input, we will make the text area as high as the line height
// We will also make the fontSize and lineHeight the correct dimensions to help with the placement of these pickers
const lineNumber = this._primaryCursorPosition.lineNumber;
const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(lineNumber);
this._doRender({
lastRenderPosition: this._primaryCursorPosition,
top,
left: this._textAreaWrapping ? this._contentLeft : left,
width: this._textAreaWidth,
height: lineHeight,
useCover: false
});
// In case the textarea contains a word, we're going to try to align the textarea's cursor
// with our cursor by scrolling the textarea as much as possible
this.textArea.domNode.scrollLeft = this._primaryCursorVisibleRange.left;
const lineCount = this._textAreaInput.textAreaState.newlineCountBeforeSelection ?? newlinecount(this.textArea.domNode.value.substring(0, this.textArea.domNode.selectionStart));
this.textArea.domNode.scrollTop = lineCount * lineHeight;
return;
}
this._doRender({
lastRenderPosition: this._primaryCursorPosition,
top: top,
left: this._textAreaWrapping ? this._contentLeft : left,
width: this._textAreaWidth,
height: (canUseZeroSizeTextarea ? 0 : 1),
useCover: false
});
}
_renderAtTopLeft() {
// (in WebKit the textarea is 1px by 1px because it cannot handle input to a 0x0 textarea)
// specifically, when doing Korean IME, setting the textarea to 0x0 breaks IME badly.
this._doRender({
lastRenderPosition: null,
top: 0,
left: 0,
width: this._textAreaWidth,
height: (canUseZeroSizeTextarea ? 0 : 1),
useCover: true
});
}
_doRender(renderData) {
this._lastRenderPosition = renderData.lastRenderPosition;
const ta = this.textArea;
const tac = this.textAreaCover;
applyFontInfo(ta, this._fontInfo);
ta.setTop(renderData.top);
ta.setLeft(renderData.left);
ta.setWidth(renderData.width);
ta.setHeight(renderData.height);
ta.setLineHeight(renderData.height);
ta.setFontSize(renderData.fontSize ?? this._fontInfo.fontSize);
ta.setColor(renderData.color ? Color.Format.CSS.formatHex(renderData.color) : '');
ta.setFontStyle(renderData.italic ? 'italic' : '');
if (renderData.bold) {
// fontWeight is also set by `applyFontInfo`, so only overwrite it if necessary
ta.setFontWeight('bold');
}
ta.setTextDecoration(`${renderData.underline ? ' underline' : ''}${renderData.strikethrough ? ' line-through' : ''}`);
tac.setTop(renderData.useCover ? renderData.top : 0);
tac.setLeft(renderData.useCover ? renderData.left : 0);
tac.setWidth(renderData.useCover ? renderData.width : 0);
tac.setHeight(renderData.useCover ? renderData.height : 0);
const options = this._context.configuration.options;
if (options.get(66 /* EditorOption.glyphMargin */)) {
tac.setClassName('monaco-editor-background textAreaCover ' + Margin.OUTER_CLASS_NAME);
}
else {
if (options.get(76 /* EditorOption.lineNumbers */).renderType !== 0 /* RenderLineNumbersType.Off */) {
tac.setClassName('monaco-editor-background textAreaCover ' + LineNumbersOverlay.CLASS_NAME);
}
else {
tac.setClassName('monaco-editor-background textAreaCover');
}
}
}
};
TextAreaEditContext = __decorate([
__param(4, IKeybindingService),
__param(5, IInstantiationService)
], TextAreaEditContext);
function measureText(targetDocument, text, fontInfo, tabSize) {
if (text.length === 0) {
return 0;
}
const container = targetDocument.createElement('div');
container.style.position = 'absolute';
container.style.top = '-50000px';
container.style.width = '50000px';
const regularDomNode = targetDocument.createElement('span');
applyFontInfo(regularDomNode, fontInfo);
regularDomNode.style.whiteSpace = 'pre'; // just like the textarea
regularDomNode.style.tabSize = `${tabSize * fontInfo.spaceWidth}px`; // just like the textarea
regularDomNode.append(text);
container.appendChild(regularDomNode);
targetDocument.body.appendChild(container);
const res = regularDomNode.offsetWidth;
container.remove();
return res;
}
export { TextAreaEditContext };
@@ -0,0 +1,588 @@
import { isFirefox } from '../../../../../base/browser/browser.js';
import { addDisposableListener, getShadowRoot, getActiveElement, getWindow, saveParentsScrollTop, restoreParentsScrollTop } from '../../../../../base/browser/dom.js';
import { DomEmitter } from '../../../../../base/browser/event.js';
import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js';
import { inputLatency } from '../../../../../base/browser/performance.js';
import { RunOnceScheduler } from '../../../../../base/common/async.js';
import { Emitter, Event } from '../../../../../base/common/event.js';
import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
import { isHighSurrogate } from '../../../../../base/common/strings.js';
import { Selection } from '../../../../common/core/selection.js';
import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';
import { ILogService, LogLevel } from '../../../../../platform/log/common/log.js';
import { InMemoryClipboardMetadataManager, ClipboardEventUtils } from '../clipboardUtils.js';
import { TextAreaState } from './textAreaEditContextState.js';
import { generateUuid } from '../../../../../base/common/uuid.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var TextAreaSyntethicEvents;
(function (TextAreaSyntethicEvents) {
TextAreaSyntethicEvents.Tap = '-monaco-textarea-synthetic-tap';
})(TextAreaSyntethicEvents || (TextAreaSyntethicEvents = {}));
class CompositionContext {
constructor() {
this._lastTypeTextLength = 0;
}
handleCompositionUpdate(text) {
text = text || '';
const typeInput = {
text: text,
replacePrevCharCnt: this._lastTypeTextLength,
replaceNextCharCnt: 0,
positionDelta: 0
};
this._lastTypeTextLength = text.length;
return typeInput;
}
}
/**
* Writes screen reader content to the textarea and is able to analyze its input events to generate:
* - onCut
* - onPaste
* - onType
*
* Composition events are generated for presentation purposes (composition input is reflected in onType).
*/
let TextAreaInput = class TextAreaInput extends Disposable {
get textAreaState() {
return this._textAreaState;
}
constructor(_host, _textArea, _OS, _browser, _accessibilityService, _logService) {
super();
this._host = _host;
this._textArea = _textArea;
this._OS = _OS;
this._browser = _browser;
this._accessibilityService = _accessibilityService;
this._logService = _logService;
this._onFocus = this._register(new Emitter());
this.onFocus = this._onFocus.event;
this._onBlur = this._register(new Emitter());
this.onBlur = this._onBlur.event;
this._onKeyDown = this._register(new Emitter());
this.onKeyDown = this._onKeyDown.event;
this._onKeyUp = this._register(new Emitter());
this.onKeyUp = this._onKeyUp.event;
this._onCut = this._register(new Emitter());
this.onCut = this._onCut.event;
this._onPaste = this._register(new Emitter());
this.onPaste = this._onPaste.event;
this._onType = this._register(new Emitter());
this.onType = this._onType.event;
this._onCompositionStart = this._register(new Emitter());
this.onCompositionStart = this._onCompositionStart.event;
this._onCompositionUpdate = this._register(new Emitter());
this.onCompositionUpdate = this._onCompositionUpdate.event;
this._onCompositionEnd = this._register(new Emitter());
this.onCompositionEnd = this._onCompositionEnd.event;
this._onSelectionChangeRequest = this._register(new Emitter());
this.onSelectionChangeRequest = this._onSelectionChangeRequest.event;
this._asyncFocusGainWriteScreenReaderContent = this._register(new MutableDisposable());
this._asyncTriggerCut = this._register(new RunOnceScheduler(() => this._onCut.fire(), 0));
this._textAreaState = TextAreaState.EMPTY;
this._selectionChangeListener = null;
if (this._accessibilityService.isScreenReaderOptimized()) {
this.writeNativeTextAreaContent('ctor');
}
this._register(Event.runAndSubscribe(this._accessibilityService.onDidChangeScreenReaderOptimized, () => {
if (this._accessibilityService.isScreenReaderOptimized() && !this._asyncFocusGainWriteScreenReaderContent.value) {
this._asyncFocusGainWriteScreenReaderContent.value = this._register(new RunOnceScheduler(() => this.writeNativeTextAreaContent('asyncFocusGain'), 0));
}
else {
this._asyncFocusGainWriteScreenReaderContent.clear();
}
}));
this._hasFocus = false;
this._currentComposition = null;
let lastKeyDown = null;
this._register(this._textArea.onKeyDown((_e) => {
const e = new StandardKeyboardEvent(_e);
if (e.keyCode === 114 /* KeyCode.KEY_IN_COMPOSITION */
|| (this._currentComposition && e.keyCode === 1 /* KeyCode.Backspace */)) {
// Stop propagation for keyDown events if the IME is processing key input
e.stopPropagation();
}
if (e.equals(9 /* KeyCode.Escape */)) {
// Prevent default always for `Esc`, otherwise it will generate a keypress
// See https://msdn.microsoft.com/en-us/library/ie/ms536939(v=vs.85).aspx
e.preventDefault();
}
lastKeyDown = e;
this._onKeyDown.fire(e);
}));
this._register(this._textArea.onKeyUp((_e) => {
const e = new StandardKeyboardEvent(_e);
this._onKeyUp.fire(e);
}));
this._register(this._textArea.onCompositionStart((e) => {
const currentComposition = new CompositionContext();
if (this._currentComposition) {
// simply reset the composition context
this._currentComposition = currentComposition;
return;
}
this._currentComposition = currentComposition;
if (this._OS === 2 /* OperatingSystem.Macintosh */
&& lastKeyDown
&& lastKeyDown.equals(114 /* KeyCode.KEY_IN_COMPOSITION */)
&& this._textAreaState.selectionStart === this._textAreaState.selectionEnd
&& this._textAreaState.selectionStart > 0
&& this._textAreaState.value.substr(this._textAreaState.selectionStart - 1, 1) === e.data
&& (lastKeyDown.code === 'ArrowRight' || lastKeyDown.code === 'ArrowLeft')) {
// Pretend the previous character was composed (in order to get it removed by subsequent compositionupdate events)
currentComposition.handleCompositionUpdate('x');
this._onCompositionStart.fire({ data: e.data });
return;
}
if (this._browser.isAndroid) {
// when tapping on the editor, Android enters composition mode to edit the current word
// so we cannot clear the textarea on Android and we must pretend the current word was selected
this._onCompositionStart.fire({ data: e.data });
return;
}
this._onCompositionStart.fire({ data: e.data });
}));
this._register(this._textArea.onCompositionUpdate((e) => {
const currentComposition = this._currentComposition;
if (!currentComposition) {
// should not be possible to receive a 'compositionupdate' without a 'compositionstart'
return;
}
if (this._browser.isAndroid) {
// On Android, the data sent with the composition update event is unusable.
// For example, if the cursor is in the middle of a word like Mic|osoft
// and Microsoft is chosen from the keyboard's suggestions, the e.data will contain "Microsoft".
// This is not really usable because it doesn't tell us where the edit began and where it ended.
const newState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);
const typeInput = TextAreaState.deduceAndroidCompositionInput(this._textAreaState, newState);
this._textAreaState = newState;
this._onType.fire(typeInput);
this._onCompositionUpdate.fire(e);
return;
}
const typeInput = currentComposition.handleCompositionUpdate(e.data);
this._textAreaState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);
this._onType.fire(typeInput);
this._onCompositionUpdate.fire(e);
}));
this._register(this._textArea.onCompositionEnd((e) => {
const currentComposition = this._currentComposition;
if (!currentComposition) {
// https://github.com/microsoft/monaco-editor/issues/1663
// On iOS 13.2, Chinese system IME randomly trigger an additional compositionend event with empty data
return;
}
this._currentComposition = null;
if (this._browser.isAndroid) {
// On Android, the data sent with the composition update event is unusable.
// For example, if the cursor is in the middle of a word like Mic|osoft
// and Microsoft is chosen from the keyboard's suggestions, the e.data will contain "Microsoft".
// This is not really usable because it doesn't tell us where the edit began and where it ended.
const newState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);
const typeInput = TextAreaState.deduceAndroidCompositionInput(this._textAreaState, newState);
this._textAreaState = newState;
this._onType.fire(typeInput);
this._onCompositionEnd.fire();
return;
}
const typeInput = currentComposition.handleCompositionUpdate(e.data);
this._textAreaState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);
this._onType.fire(typeInput);
this._onCompositionEnd.fire();
}));
this._register(this._textArea.onInput((e) => {
// Pretend here we touched the text area, as the `input` event will most likely
// result in a `selectionchange` event which we want to ignore
this._textArea.setIgnoreSelectionChangeTime('received input event');
if (this._currentComposition) {
return;
}
const newState = TextAreaState.readFromTextArea(this._textArea, this._textAreaState);
const typeInput = TextAreaState.deduceInput(this._textAreaState, newState, /*couldBeEmojiInput*/ this._OS === 2 /* OperatingSystem.Macintosh */);
if (typeInput.replacePrevCharCnt === 0 && typeInput.text.length === 1) {
// one character was typed
if (isHighSurrogate(typeInput.text.charCodeAt(0))
|| typeInput.text.charCodeAt(0) === 0x7f /* Delete */) {
// Ignore invalid input but keep it around for next time
return;
}
}
this._textAreaState = newState;
if (typeInput.text !== ''
|| typeInput.replacePrevCharCnt !== 0
|| typeInput.replaceNextCharCnt !== 0
|| typeInput.positionDelta !== 0) {
// https://w3c.github.io/input-events/#interface-InputEvent-Attributes
if (e.inputType === 'insertFromPaste') {
this._onPaste.fire({
text: typeInput.text,
metadata: InMemoryClipboardMetadataManager.INSTANCE.get(typeInput.text)
});
}
else {
this._onType.fire(typeInput);
}
}
}));
// --- Clipboard operations
this._register(this._textArea.onCut((e) => {
this._logService.trace(`TextAreaInput#onCut`, e);
// Pretend here we touched the text area, as the `cut` event will most likely
// result in a `selectionchange` event which we want to ignore
this._textArea.setIgnoreSelectionChangeTime('received cut event');
this._ensureClipboardGetsEditorSelection(e);
this._asyncTriggerCut.schedule();
}));
this._register(this._textArea.onCopy((e) => {
this._logService.trace(`TextAreaInput#onCopy`, e);
this._ensureClipboardGetsEditorSelection(e);
}));
this._register(this._textArea.onPaste((e) => {
this._logService.trace(`TextAreaInput#onPaste`, e);
// Pretend here we touched the text area, as the `paste` event will most likely
// result in a `selectionchange` event which we want to ignore
this._textArea.setIgnoreSelectionChangeTime('received paste event');
e.preventDefault();
if (!e.clipboardData) {
return;
}
let [text, metadata] = ClipboardEventUtils.getTextData(e.clipboardData);
this._logService.trace(`TextAreaInput#onPaste with id : `, metadata?.id, ' with text.length: ', text.length);
if (!text) {
return;
}
// try the in-memory store
metadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text);
this._logService.trace(`TextAreaInput#onPaste (before onPaste)`);
this._onPaste.fire({
text: text,
metadata: metadata
});
}));
this._register(this._textArea.onFocus(() => {
const hadFocus = this._hasFocus;
this._setHasFocus(true);
if (this._accessibilityService.isScreenReaderOptimized() && this._browser.isSafari && !hadFocus && this._hasFocus) {
// When "tabbing into" the textarea, immediately after dispatching the 'focus' event,
// Safari will always move the selection at offset 0 in the textarea
if (!this._asyncFocusGainWriteScreenReaderContent.value) {
this._asyncFocusGainWriteScreenReaderContent.value = new RunOnceScheduler(() => this.writeNativeTextAreaContent('asyncFocusGain'), 0);
}
this._asyncFocusGainWriteScreenReaderContent.value.schedule();
}
}));
this._register(this._textArea.onBlur(() => {
if (this._currentComposition) {
// See https://github.com/microsoft/vscode/issues/112621
// where compositionend is not triggered when the editor
// is taken off-dom during a composition
// Clear the flag to be able to write to the textarea
this._currentComposition = null;
// Clear the textarea to avoid an unwanted cursor type
this.writeNativeTextAreaContent('blurWithoutCompositionEnd');
// Fire artificial composition end
this._onCompositionEnd.fire();
}
this._setHasFocus(false);
}));
this._register(this._textArea.onSyntheticTap(() => {
if (this._browser.isAndroid && this._currentComposition) {
// on Android, tapping does not cancel the current composition, so the
// textarea is stuck showing the old composition
// Clear the flag to be able to write to the textarea
this._currentComposition = null;
// Clear the textarea to avoid an unwanted cursor type
this.writeNativeTextAreaContent('tapWithoutCompositionEnd');
// Fire artificial composition end
this._onCompositionEnd.fire();
}
}));
}
_installSelectionChangeListener() {
// See https://github.com/microsoft/vscode/issues/27216 and https://github.com/microsoft/vscode/issues/98256
// When using a Braille display, it is possible for users to reposition the
// system caret. This is reflected in Chrome as a `selectionchange` event.
//
// The `selectionchange` event appears to be emitted under numerous other circumstances,
// so it is quite a challenge to distinguish a `selectionchange` coming in from a user
// using a Braille display from all the other cases.
//
// The problems with the `selectionchange` event are:
// * the event is emitted when the textarea is focused programmatically -- textarea.focus()
// * the event is emitted when the selection is changed in the textarea programmatically -- textarea.setSelectionRange(...)
// * the event is emitted when the value of the textarea is changed programmatically -- textarea.value = '...'
// * the event is emitted when tabbing into the textarea
// * the event is emitted asynchronously (sometimes with a delay as high as a few tens of ms)
// * the event sometimes comes in bursts for a single logical textarea operation
// `selectionchange` events often come multiple times for a single logical change
// so throttle multiple `selectionchange` events that burst in a short period of time.
let previousSelectionChangeEventTime = 0;
return addDisposableListener(this._textArea.ownerDocument, 'selectionchange', (e) => {
inputLatency.onSelectionChange();
if (!this._hasFocus) {
return;
}
if (this._currentComposition) {
return;
}
if (!this._browser.isChrome) {
// Support only for Chrome until testing happens on other browsers
return;
}
const now = Date.now();
const delta1 = now - previousSelectionChangeEventTime;
previousSelectionChangeEventTime = now;
if (delta1 < 5) {
// received another `selectionchange` event within 5ms of the previous `selectionchange` event
// => ignore it
return;
}
const delta2 = now - this._textArea.getIgnoreSelectionChangeTime();
this._textArea.resetSelectionChangeTime();
if (delta2 < 100) {
// received a `selectionchange` event within 100ms since we touched the textarea
// => ignore it, since we caused it
return;
}
if (!this._textAreaState.selection) {
// Cannot correlate a position in the textarea with a position in the editor...
return;
}
const newValue = this._textArea.getValue();
if (this._textAreaState.value !== newValue) {
// Cannot correlate a position in the textarea with a position in the editor...
return;
}
const newSelectionStart = this._textArea.getSelectionStart();
const newSelectionEnd = this._textArea.getSelectionEnd();
if (this._textAreaState.selectionStart === newSelectionStart && this._textAreaState.selectionEnd === newSelectionEnd) {
// Nothing to do...
return;
}
const _newSelectionStartPosition = this._textAreaState.deduceEditorPosition(newSelectionStart);
const newSelectionStartPosition = this._host.deduceModelPosition(_newSelectionStartPosition[0], _newSelectionStartPosition[1], _newSelectionStartPosition[2]);
const _newSelectionEndPosition = this._textAreaState.deduceEditorPosition(newSelectionEnd);
const newSelectionEndPosition = this._host.deduceModelPosition(_newSelectionEndPosition[0], _newSelectionEndPosition[1], _newSelectionEndPosition[2]);
const newSelection = new Selection(newSelectionStartPosition.lineNumber, newSelectionStartPosition.column, newSelectionEndPosition.lineNumber, newSelectionEndPosition.column);
this._onSelectionChangeRequest.fire(newSelection);
});
}
dispose() {
super.dispose();
if (this._selectionChangeListener) {
this._selectionChangeListener.dispose();
this._selectionChangeListener = null;
}
}
focusTextArea() {
// Setting this._hasFocus and writing the screen reader content
// will result in a focus() and setSelectionRange() in the textarea
this._setHasFocus(true);
// If the editor is off DOM, focus cannot be really set, so let's double check that we have managed to set the focus
this.refreshFocusState();
}
isFocused() {
return this._hasFocus;
}
refreshFocusState() {
this._setHasFocus(this._textArea.hasFocus());
}
_setHasFocus(newHasFocus) {
if (this._hasFocus === newHasFocus) {
// no change
return;
}
this._hasFocus = newHasFocus;
if (this._selectionChangeListener) {
this._selectionChangeListener.dispose();
this._selectionChangeListener = null;
}
if (this._hasFocus) {
this._selectionChangeListener = this._installSelectionChangeListener();
}
if (this._hasFocus) {
this.writeNativeTextAreaContent('focusgain');
}
if (this._hasFocus) {
this._onFocus.fire();
}
else {
this._onBlur.fire();
}
}
_setAndWriteTextAreaState(reason, textAreaState) {
if (!this._hasFocus) {
textAreaState = textAreaState.collapseSelection();
}
if (!textAreaState.isWrittenToTextArea(this._textArea, this._hasFocus)) {
this._logService.trace(`writeTextAreaState(reason: ${reason})`);
}
textAreaState.writeToTextArea(reason, this._textArea, this._hasFocus);
this._textAreaState = textAreaState;
}
writeNativeTextAreaContent(reason) {
if ((!this._accessibilityService.isScreenReaderOptimized() && reason === 'render') || this._currentComposition) {
// Do not write to the text on render unless a screen reader is being used #192278
// Do not write to the text area when doing composition
return;
}
this._setAndWriteTextAreaState(reason, this._host.getScreenReaderContent());
}
_ensureClipboardGetsEditorSelection(e) {
const dataToCopy = this._host.getDataToCopy();
let id = undefined;
if (this._logService.getLevel() === LogLevel.Trace) {
id = generateUuid();
}
const storedMetadata = {
version: 1,
id,
isFromEmptySelection: dataToCopy.isFromEmptySelection,
multicursorText: dataToCopy.multicursorText,
mode: dataToCopy.mode
};
InMemoryClipboardMetadataManager.INSTANCE.set(
// When writing "LINE\r\n" to the clipboard and then pasting,
// Firefox pastes "LINE\n", so let's work around this quirk
(this._browser.isFirefox ? dataToCopy.text.replace(/\r\n/g, '\n') : dataToCopy.text), storedMetadata);
e.preventDefault();
if (e.clipboardData) {
ClipboardEventUtils.setTextData(e.clipboardData, dataToCopy.text, dataToCopy.html, storedMetadata);
}
this._logService.trace('TextAreaEditContextInput#_ensureClipboardGetsEditorSelection with id : ', id, ' with text.length: ', dataToCopy.text.length);
}
};
TextAreaInput = __decorate([
__param(4, IAccessibilityService),
__param(5, ILogService)
], TextAreaInput);
class TextAreaWrapper extends Disposable {
get ownerDocument() {
return this._actual.ownerDocument;
}
constructor(_actual) {
super();
this._actual = _actual;
this._onSyntheticTap = this._register(new Emitter());
this.onSyntheticTap = this._onSyntheticTap.event;
this._ignoreSelectionChangeTime = 0;
this.onKeyDown = this._register(new DomEmitter(this._actual, 'keydown')).event;
this.onKeyPress = this._register(new DomEmitter(this._actual, 'keypress')).event;
this.onKeyUp = this._register(new DomEmitter(this._actual, 'keyup')).event;
this.onCompositionStart = this._register(new DomEmitter(this._actual, 'compositionstart')).event;
this.onCompositionUpdate = this._register(new DomEmitter(this._actual, 'compositionupdate')).event;
this.onCompositionEnd = this._register(new DomEmitter(this._actual, 'compositionend')).event;
this.onBeforeInput = this._register(new DomEmitter(this._actual, 'beforeinput')).event;
this.onInput = this._register(new DomEmitter(this._actual, 'input')).event;
this.onCut = this._register(new DomEmitter(this._actual, 'cut')).event;
this.onCopy = this._register(new DomEmitter(this._actual, 'copy')).event;
this.onPaste = this._register(new DomEmitter(this._actual, 'paste')).event;
this.onFocus = this._register(new DomEmitter(this._actual, 'focus')).event;
this.onBlur = this._register(new DomEmitter(this._actual, 'blur')).event;
this._register(this.onKeyDown(() => inputLatency.onKeyDown()));
this._register(this.onBeforeInput(() => inputLatency.onBeforeInput()));
this._register(this.onInput(() => inputLatency.onInput()));
this._register(this.onKeyUp(() => inputLatency.onKeyUp()));
this._register(addDisposableListener(this._actual, TextAreaSyntethicEvents.Tap, () => this._onSyntheticTap.fire()));
}
hasFocus() {
const shadowRoot = getShadowRoot(this._actual);
if (shadowRoot) {
return shadowRoot.activeElement === this._actual;
}
else if (this._actual.isConnected) {
return getActiveElement() === this._actual;
}
else {
return false;
}
}
setIgnoreSelectionChangeTime(reason) {
this._ignoreSelectionChangeTime = Date.now();
}
getIgnoreSelectionChangeTime() {
return this._ignoreSelectionChangeTime;
}
resetSelectionChangeTime() {
this._ignoreSelectionChangeTime = 0;
}
getValue() {
// console.log('current value: ' + this._textArea.value);
return this._actual.value;
}
setValue(reason, value) {
const textArea = this._actual;
if (textArea.value === value) {
// No change
return;
}
// console.log('reason: ' + reason + ', current value: ' + textArea.value + ' => new value: ' + value);
this.setIgnoreSelectionChangeTime('setValue');
textArea.value = value;
}
getSelectionStart() {
return this._actual.selectionDirection === 'backward' ? this._actual.selectionEnd : this._actual.selectionStart;
}
getSelectionEnd() {
return this._actual.selectionDirection === 'backward' ? this._actual.selectionStart : this._actual.selectionEnd;
}
setSelectionRange(reason, selectionStart, selectionEnd) {
const textArea = this._actual;
let activeElement = null;
const shadowRoot = getShadowRoot(textArea);
if (shadowRoot) {
activeElement = shadowRoot.activeElement;
}
else {
activeElement = getActiveElement();
}
const activeWindow = getWindow(activeElement);
const currentIsFocused = (activeElement === textArea);
const currentSelectionStart = textArea.selectionStart;
const currentSelectionEnd = textArea.selectionEnd;
if (currentIsFocused && currentSelectionStart === selectionStart && currentSelectionEnd === selectionEnd) {
// No change
// Firefox iframe bug https://github.com/microsoft/monaco-editor/issues/643#issuecomment-367871377
if (isFirefox && activeWindow.parent !== activeWindow) {
textArea.focus();
}
return;
}
// console.log('reason: ' + reason + ', setSelectionRange: ' + selectionStart + ' -> ' + selectionEnd);
if (currentIsFocused) {
// No need to focus, only need to change the selection range
this.setIgnoreSelectionChangeTime('setSelectionRange');
textArea.setSelectionRange(selectionStart, selectionEnd);
if (isFirefox && activeWindow.parent !== activeWindow) {
textArea.focus();
}
return;
}
// If the focus is outside the textarea, browsers will try really hard to reveal the textarea.
// Here, we try to undo the browser's desperate reveal.
try {
const scrollState = saveParentsScrollTop(textArea);
this.setIgnoreSelectionChangeTime('setSelectionRange');
textArea.focus();
textArea.setSelectionRange(selectionStart, selectionEnd);
restoreParentsScrollTop(textArea, scrollState);
}
catch (e) {
// Sometimes IE throws when setting selection (e.g. textarea is off-DOM)
}
}
}
export { TextAreaInput, TextAreaSyntethicEvents, TextAreaWrapper };
@@ -0,0 +1,160 @@
import { commonPrefixLength, commonSuffixLength } from '../../../../../base/common/strings.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class TextAreaState {
static { this.EMPTY = new TextAreaState('', 0, 0, null, undefined); }
constructor(value,
/** the offset where selection starts inside `value` */
selectionStart,
/** the offset where selection ends inside `value` */
selectionEnd,
/** the editor range in the view coordinate system that matches the selection inside `value` */
selection,
/** the visible line count (wrapped, not necessarily matching \n characters) for the text in `value` before `selectionStart` */
newlineCountBeforeSelection) {
this.value = value;
this.selectionStart = selectionStart;
this.selectionEnd = selectionEnd;
this.selection = selection;
this.newlineCountBeforeSelection = newlineCountBeforeSelection;
}
toString() {
return `[ <${this.value}>, selectionStart: ${this.selectionStart}, selectionEnd: ${this.selectionEnd}]`;
}
static readFromTextArea(textArea, previousState) {
const value = textArea.getValue();
const selectionStart = textArea.getSelectionStart();
const selectionEnd = textArea.getSelectionEnd();
let newlineCountBeforeSelection = undefined;
if (previousState) {
const valueBeforeSelectionStart = value.substring(0, selectionStart);
const previousValueBeforeSelectionStart = previousState.value.substring(0, previousState.selectionStart);
if (valueBeforeSelectionStart === previousValueBeforeSelectionStart) {
newlineCountBeforeSelection = previousState.newlineCountBeforeSelection;
}
}
return new TextAreaState(value, selectionStart, selectionEnd, null, newlineCountBeforeSelection);
}
collapseSelection() {
if (this.selectionStart === this.value.length) {
return this;
}
return new TextAreaState(this.value, this.value.length, this.value.length, null, undefined);
}
isWrittenToTextArea(textArea, select) {
const valuesEqual = this.value === textArea.getValue();
if (!select) {
return valuesEqual;
}
const selectionsEqual = this.selectionStart === textArea.getSelectionStart() && this.selectionEnd === textArea.getSelectionEnd();
return selectionsEqual && valuesEqual;
}
writeToTextArea(reason, textArea, select) {
textArea.setValue(reason, this.value);
if (select) {
textArea.setSelectionRange(reason, this.selectionStart, this.selectionEnd);
}
}
deduceEditorPosition(offset) {
if (offset <= this.selectionStart) {
const str = this.value.substring(offset, this.selectionStart);
return this._finishDeduceEditorPosition(this.selection?.getStartPosition() ?? null, str, -1);
}
if (offset >= this.selectionEnd) {
const str = this.value.substring(this.selectionEnd, offset);
return this._finishDeduceEditorPosition(this.selection?.getEndPosition() ?? null, str, 1);
}
const str1 = this.value.substring(this.selectionStart, offset);
if (str1.indexOf(String.fromCharCode(8230)) === -1) {
return this._finishDeduceEditorPosition(this.selection?.getStartPosition() ?? null, str1, 1);
}
const str2 = this.value.substring(offset, this.selectionEnd);
return this._finishDeduceEditorPosition(this.selection?.getEndPosition() ?? null, str2, -1);
}
_finishDeduceEditorPosition(anchor, deltaText, signum) {
let lineFeedCnt = 0;
let lastLineFeedIndex = -1;
while ((lastLineFeedIndex = deltaText.indexOf('\n', lastLineFeedIndex + 1)) !== -1) {
lineFeedCnt++;
}
return [anchor, signum * deltaText.length, lineFeedCnt];
}
static deduceInput(previousState, currentState, couldBeEmojiInput) {
if (!previousState) {
// This is the EMPTY state
return {
text: '',
replacePrevCharCnt: 0,
replaceNextCharCnt: 0,
positionDelta: 0
};
}
const prefixLength = Math.min(commonPrefixLength(previousState.value, currentState.value), previousState.selectionStart, currentState.selectionStart);
const suffixLength = Math.min(commonSuffixLength(previousState.value, currentState.value), previousState.value.length - previousState.selectionEnd, currentState.value.length - currentState.selectionEnd);
previousState.value.substring(prefixLength, previousState.value.length - suffixLength);
const currentValue = currentState.value.substring(prefixLength, currentState.value.length - suffixLength);
const previousSelectionStart = previousState.selectionStart - prefixLength;
const previousSelectionEnd = previousState.selectionEnd - prefixLength;
const currentSelectionStart = currentState.selectionStart - prefixLength;
const currentSelectionEnd = currentState.selectionEnd - prefixLength;
if (currentSelectionStart === currentSelectionEnd) {
// no current selection
const replacePreviousCharacters = (previousState.selectionStart - prefixLength);
return {
text: currentValue,
replacePrevCharCnt: replacePreviousCharacters,
replaceNextCharCnt: 0,
positionDelta: 0
};
}
// there is a current selection => composition case
const replacePreviousCharacters = previousSelectionEnd - previousSelectionStart;
return {
text: currentValue,
replacePrevCharCnt: replacePreviousCharacters,
replaceNextCharCnt: 0,
positionDelta: 0
};
}
static deduceAndroidCompositionInput(previousState, currentState) {
if (!previousState) {
// This is the EMPTY state
return {
text: '',
replacePrevCharCnt: 0,
replaceNextCharCnt: 0,
positionDelta: 0
};
}
if (previousState.value === currentState.value) {
return {
text: '',
replacePrevCharCnt: 0,
replaceNextCharCnt: 0,
positionDelta: currentState.selectionEnd - previousState.selectionEnd
};
}
const prefixLength = Math.min(commonPrefixLength(previousState.value, currentState.value), previousState.selectionEnd);
const suffixLength = Math.min(commonSuffixLength(previousState.value, currentState.value), previousState.value.length - previousState.selectionEnd);
const previousValue = previousState.value.substring(prefixLength, previousState.value.length - suffixLength);
const currentValue = currentState.value.substring(prefixLength, currentState.value.length - suffixLength);
previousState.selectionStart - prefixLength;
const previousSelectionEnd = previousState.selectionEnd - prefixLength;
currentState.selectionStart - prefixLength;
const currentSelectionEnd = currentState.selectionEnd - prefixLength;
return {
text: currentValue,
replacePrevCharCnt: previousSelectionEnd,
replaceNextCharCnt: previousValue.length - previousSelectionEnd,
positionDelta: currentSelectionEnd - currentValue.length
};
}
static fromScreenReaderContentState(screenReaderContentState) {
return new TextAreaState(screenReaderContentState.value, screenReaderContentState.selectionStart, screenReaderContentState.selectionEnd, screenReaderContentState.selection, screenReaderContentState.newlineCountBeforeSelection);
}
}
export { TextAreaState };
@@ -0,0 +1,539 @@
import { addDisposableListener, EventType, getWindow, getShadowRoot, isKeyboardEvent } from '../../../base/browser/dom.js';
import { StandardWheelEvent } from '../../../base/browser/mouseEvent.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { isMacintosh } from '../../../base/common/platform.js';
import { MouseTargetFactory, HitTestContext, MouseTarget } from './mouseTarget.js';
import { EditorMouseEventFactory, EditorMouseEvent, ClientCoordinates, createEditorPagePosition, createCoordinatesRelativeToEditor, GlobalEditorPointerMoveMonitor } from '../editorDom.js';
import { EditorZoom } from '../../common/config/editorZoom.js';
import { Position } from '../../common/core/position.js';
import { Selection } from '../../common/core/selection.js';
import { ViewEventHandler } from '../../common/viewEventHandler.js';
import { MouseWheelClassifier } from '../../../base/browser/ui/scrollbar/scrollableElement.js';
import { TopBottomDragScrolling, LeftRightDragScrolling } from './dragScrolling.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class MouseHandler extends ViewEventHandler {
constructor(context, viewController, viewHelper) {
super();
this._mouseLeaveMonitor = null;
this._context = context;
this.viewController = viewController;
this.viewHelper = viewHelper;
this.mouseTargetFactory = new MouseTargetFactory(this._context, viewHelper);
this._mouseDownOperation = this._register(new MouseDownOperation(this._context, this.viewController, this.viewHelper, this.mouseTargetFactory, (e, testEventTarget) => this._createMouseTarget(e, testEventTarget), (e) => this._getMouseColumn(e)));
this.lastMouseLeaveTime = -1;
this._height = this._context.configuration.options.get(165 /* EditorOption.layoutInfo */).height;
const mouseEvents = new EditorMouseEventFactory(this.viewHelper.viewDomNode);
this._register(mouseEvents.onContextMenu(this.viewHelper.viewDomNode, (e) => this._onContextMenu(e, true)));
this._register(mouseEvents.onMouseMove(this.viewHelper.viewDomNode, (e) => {
this._onMouseMove(e);
// See https://github.com/microsoft/vscode/issues/138789
// When moving the mouse really quickly, the browser sometimes forgets to
// send us a `mouseleave` or `mouseout` event. We therefore install here
// a global `mousemove` listener to manually recover if the mouse goes outside
// the editor. As soon as the mouse leaves outside of the editor, we
// remove this listener
if (!this._mouseLeaveMonitor) {
this._mouseLeaveMonitor = addDisposableListener(this.viewHelper.viewDomNode.ownerDocument, 'mousemove', (e) => {
if (!this.viewHelper.viewDomNode.contains(e.target)) {
// went outside the editor!
this._onMouseLeave(new EditorMouseEvent(e, false, this.viewHelper.viewDomNode));
}
});
}
}));
this._register(mouseEvents.onMouseUp(this.viewHelper.viewDomNode, (e) => this._onMouseUp(e)));
this._register(mouseEvents.onMouseLeave(this.viewHelper.viewDomNode, (e) => this._onMouseLeave(e)));
// `pointerdown` events can't be used to determine if there's a double click, or triple click
// because their `e.detail` is always 0.
// We will therefore save the pointer id for the mouse and then reuse it in the `mousedown` event
// for `element.setPointerCapture`.
let capturePointerId = 0;
this._register(mouseEvents.onPointerDown(this.viewHelper.viewDomNode, (e, pointerId) => {
capturePointerId = pointerId;
}));
// The `pointerup` listener registered by `GlobalEditorPointerMoveMonitor` does not get invoked 100% of the times.
// I speculate that this is because the `pointerup` listener is only registered during the `mousedown` event, and perhaps
// the `pointerup` event is already queued for dispatching, which makes it that the new listener doesn't get fired.
// See https://github.com/microsoft/vscode/issues/146486 for repro steps.
// To compensate for that, we simply register here a `pointerup` listener and just communicate it.
this._register(addDisposableListener(this.viewHelper.viewDomNode, EventType.POINTER_UP, (e) => {
this._mouseDownOperation.onPointerUp();
}));
this._register(mouseEvents.onMouseDown(this.viewHelper.viewDomNode, (e) => this._onMouseDown(e, capturePointerId)));
this._setupMouseWheelZoomListener();
this._context.addEventHandler(this);
}
_setupMouseWheelZoomListener() {
const classifier = MouseWheelClassifier.INSTANCE;
let prevMouseWheelTime = 0;
let gestureStartZoomLevel = EditorZoom.getZoomLevel();
let gestureHasZoomModifiers = false;
let gestureAccumulatedDelta = 0;
const onMouseWheel = (browserEvent) => {
this.viewController.emitMouseWheel(browserEvent);
if (!this._context.configuration.options.get(84 /* EditorOption.mouseWheelZoom */)) {
return;
}
const e = new StandardWheelEvent(browserEvent);
classifier.acceptStandardWheelEvent(e);
if (classifier.isPhysicalMouseWheel()) {
if (hasMouseWheelZoomModifiers(browserEvent)) {
const zoomLevel = EditorZoom.getZoomLevel();
const delta = e.deltaY > 0 ? 1 : -1;
EditorZoom.setZoomLevel(zoomLevel + delta);
e.preventDefault();
e.stopPropagation();
}
}
else {
// we consider mousewheel events that occur within 50ms of each other to be part of the same gesture
// we don't want to consider mouse wheel events where ctrl/cmd is pressed during the inertia phase
// we also want to accumulate deltaY values from the same gesture and use that to set the zoom level
if (Date.now() - prevMouseWheelTime > 50) {
// reset if more than 50ms have passed
gestureStartZoomLevel = EditorZoom.getZoomLevel();
gestureHasZoomModifiers = hasMouseWheelZoomModifiers(browserEvent);
gestureAccumulatedDelta = 0;
}
prevMouseWheelTime = Date.now();
gestureAccumulatedDelta += e.deltaY;
if (gestureHasZoomModifiers) {
EditorZoom.setZoomLevel(gestureStartZoomLevel + gestureAccumulatedDelta / 5);
e.preventDefault();
e.stopPropagation();
}
}
};
this._register(addDisposableListener(this.viewHelper.viewDomNode, EventType.MOUSE_WHEEL, onMouseWheel, { capture: true, passive: false }));
function hasMouseWheelZoomModifiers(browserEvent) {
return (isMacintosh
// on macOS we support cmd + two fingers scroll (`metaKey` set)
// and also the two fingers pinch gesture (`ctrKey` set)
? ((browserEvent.metaKey || browserEvent.ctrlKey) && !browserEvent.shiftKey && !browserEvent.altKey)
: (browserEvent.ctrlKey && !browserEvent.metaKey && !browserEvent.shiftKey && !browserEvent.altKey));
}
}
dispose() {
this._context.removeEventHandler(this);
if (this._mouseLeaveMonitor) {
this._mouseLeaveMonitor.dispose();
this._mouseLeaveMonitor = null;
}
super.dispose();
}
// --- begin event handlers
onConfigurationChanged(e) {
if (e.hasChanged(165 /* EditorOption.layoutInfo */)) {
// layout change
const height = this._context.configuration.options.get(165 /* EditorOption.layoutInfo */).height;
if (this._height !== height) {
this._height = height;
this._mouseDownOperation.onHeightChanged();
}
}
return false;
}
onCursorStateChanged(e) {
this._mouseDownOperation.onCursorStateChanged(e);
return false;
}
onFocusChanged(e) {
return false;
}
// --- end event handlers
getTargetAtClientPoint(clientX, clientY) {
const clientPos = new ClientCoordinates(clientX, clientY);
const pos = clientPos.toPageCoordinates(getWindow(this.viewHelper.viewDomNode));
const editorPos = createEditorPagePosition(this.viewHelper.viewDomNode);
if (pos.y < editorPos.y || pos.y > editorPos.y + editorPos.height || pos.x < editorPos.x || pos.x > editorPos.x + editorPos.width) {
return null;
}
const relativePos = createCoordinatesRelativeToEditor(this.viewHelper.viewDomNode, editorPos, pos);
return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), editorPos, pos, relativePos, null);
}
_createMouseTarget(e, testEventTarget) {
let target = e.target;
if (!this.viewHelper.viewDomNode.contains(target)) {
const shadowRoot = getShadowRoot(this.viewHelper.viewDomNode);
if (shadowRoot) {
const potentialTarget = shadowRoot.elementsFromPoint(e.posx, e.posy).find((el) => this.viewHelper.viewDomNode.contains(el)) ?? null;
target = potentialTarget;
}
}
return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(), e.editorPos, e.pos, e.relativePos, testEventTarget ? target : null);
}
_getMouseColumn(e) {
return this.mouseTargetFactory.getMouseColumn(e.relativePos);
}
_onContextMenu(e, testEventTarget) {
this.viewController.emitContextMenu({
event: e,
target: this._createMouseTarget(e, testEventTarget)
});
}
_onMouseMove(e) {
const targetIsWidget = this.mouseTargetFactory.mouseTargetIsWidget(e);
if (!targetIsWidget) {
e.preventDefault();
}
if (this._mouseDownOperation.isActive()) {
// In selection/drag operation
return;
}
const actualMouseMoveTime = e.timestamp;
if (actualMouseMoveTime < this.lastMouseLeaveTime) {
// Due to throttling, this event occurred before the mouse left the editor, therefore ignore it.
return;
}
this.viewController.emitMouseMove({
event: e,
target: this._createMouseTarget(e, true)
});
}
_onMouseLeave(e) {
if (this._mouseLeaveMonitor) {
this._mouseLeaveMonitor.dispose();
this._mouseLeaveMonitor = null;
}
this.lastMouseLeaveTime = (new Date()).getTime();
this.viewController.emitMouseLeave({
event: e,
target: null
});
}
_onMouseUp(e) {
this.viewController.emitMouseUp({
event: e,
target: this._createMouseTarget(e, true)
});
}
_onMouseDown(e, pointerId) {
const t = this._createMouseTarget(e, true);
const targetIsContent = (t.type === 6 /* MouseTargetType.CONTENT_TEXT */ || t.type === 7 /* MouseTargetType.CONTENT_EMPTY */);
const targetIsGutter = (t.type === 2 /* MouseTargetType.GUTTER_GLYPH_MARGIN */ || t.type === 3 /* MouseTargetType.GUTTER_LINE_NUMBERS */ || t.type === 4 /* MouseTargetType.GUTTER_LINE_DECORATIONS */);
const targetIsLineNumbers = (t.type === 3 /* MouseTargetType.GUTTER_LINE_NUMBERS */);
const selectOnLineNumbers = this._context.configuration.options.get(125 /* EditorOption.selectOnLineNumbers */);
const targetIsViewZone = (t.type === 8 /* MouseTargetType.CONTENT_VIEW_ZONE */ || t.type === 5 /* MouseTargetType.GUTTER_VIEW_ZONE */);
const targetIsWidget = (t.type === 9 /* MouseTargetType.CONTENT_WIDGET */);
let shouldHandle = e.leftButton || e.middleButton;
if (isMacintosh && e.leftButton && e.ctrlKey) {
shouldHandle = false;
}
const focus = () => {
e.preventDefault();
this.viewHelper.focusTextArea();
};
if (shouldHandle && (targetIsContent || (targetIsLineNumbers && selectOnLineNumbers))) {
focus();
this._mouseDownOperation.start(t.type, e, pointerId);
}
else if (targetIsGutter) {
// Do not steal focus
e.preventDefault();
}
else if (targetIsViewZone) {
const viewZoneData = t.detail;
if (shouldHandle && this.viewHelper.shouldSuppressMouseDownOnViewZone(viewZoneData.viewZoneId)) {
focus();
this._mouseDownOperation.start(t.type, e, pointerId);
e.preventDefault();
}
}
else if (targetIsWidget && this.viewHelper.shouldSuppressMouseDownOnWidget(t.detail)) {
focus();
e.preventDefault();
}
this.viewController.emitMouseDown({
event: e,
target: t
});
}
}
class MouseDownOperation extends Disposable {
constructor(_context, _viewController, _viewHelper, _mouseTargetFactory, createMouseTarget, getMouseColumn) {
super();
this._context = _context;
this._viewController = _viewController;
this._viewHelper = _viewHelper;
this._mouseTargetFactory = _mouseTargetFactory;
this._createMouseTarget = createMouseTarget;
this._getMouseColumn = getMouseColumn;
this._mouseMoveMonitor = this._register(new GlobalEditorPointerMoveMonitor(this._viewHelper.viewDomNode));
this._topBottomDragScrolling = this._register(new TopBottomDragScrolling(this._context, this._viewHelper, this._mouseTargetFactory, (position, inSelectionMode, revealType) => this._dispatchMouse(position, inSelectionMode, revealType)));
this._leftRightDragScrolling = this._register(new LeftRightDragScrolling(this._context, this._viewHelper, this._mouseTargetFactory, (position, inSelectionMode, revealType) => this._dispatchMouse(position, inSelectionMode, revealType)));
this._mouseState = new MouseDownState();
this._currentSelection = new Selection(1, 1, 1, 1);
this._isActive = false;
this._lastMouseEvent = null;
}
dispose() {
super.dispose();
}
isActive() {
return this._isActive;
}
_onMouseDownThenMove(e) {
this._lastMouseEvent = e;
this._mouseState.setModifiers(e);
const position = this._findMousePosition(e, false);
if (!position) {
// Ignoring because position is unknown
return;
}
if (this._mouseState.isDragAndDrop) {
this._viewController.emitMouseDrag({
event: e,
target: position
});
}
else {
if (position.type === 13 /* MouseTargetType.OUTSIDE_EDITOR */) {
if (position.outsidePosition === 'above' || position.outsidePosition === 'below') {
this._topBottomDragScrolling.start(position, e);
this._leftRightDragScrolling.stop();
}
else {
this._leftRightDragScrolling.start(position, e);
this._topBottomDragScrolling.stop();
}
}
else {
this._topBottomDragScrolling.stop();
this._leftRightDragScrolling.stop();
this._dispatchMouse(position, true, 1 /* NavigationCommandRevealType.Minimal */);
}
}
}
start(targetType, e, pointerId) {
this._lastMouseEvent = e;
this._mouseState.setStartedOnLineNumbers(targetType === 3 /* MouseTargetType.GUTTER_LINE_NUMBERS */);
this._mouseState.setStartButtons(e);
this._mouseState.setModifiers(e);
const position = this._findMousePosition(e, true);
if (!position || !position.position) {
// Ignoring because position is unknown
return;
}
this._mouseState.trySetCount(e.detail, position.position);
// Overwrite the detail of the MouseEvent, as it will be sent out in an event and contributions might rely on it.
e.detail = this._mouseState.count;
const options = this._context.configuration.options;
if (!options.get(104 /* EditorOption.readOnly */)
&& options.get(42 /* EditorOption.dragAndDrop */)
&& !options.get(28 /* EditorOption.columnSelection */)
&& !this._mouseState.altKey // we don't support multiple mouse
&& e.detail < 2 // only single click on a selection can work
&& !this._isActive // the mouse is not down yet
&& !this._currentSelection.isEmpty() // we don't drag single cursor
&& (position.type === 6 /* MouseTargetType.CONTENT_TEXT */) // single click on text
&& position.position && this._currentSelection.containsPosition(position.position) // single click on a selection
) {
this._mouseState.isDragAndDrop = true;
this._isActive = true;
this._mouseMoveMonitor.startMonitoring(this._viewHelper.viewLinesDomNode, pointerId, e.buttons, (e) => this._onMouseDownThenMove(e), (browserEvent) => {
const position = this._findMousePosition(this._lastMouseEvent, false);
if (isKeyboardEvent(browserEvent)) {
// cancel
this._viewController.emitMouseDropCanceled();
}
else {
this._viewController.emitMouseDrop({
event: this._lastMouseEvent,
target: (position ? this._createMouseTarget(this._lastMouseEvent, true) : null) // Ignoring because position is unknown, e.g., Content View Zone
});
}
this._stop();
});
return;
}
this._mouseState.isDragAndDrop = false;
this._dispatchMouse(position, e.shiftKey, 1 /* NavigationCommandRevealType.Minimal */);
if (!this._isActive) {
this._isActive = true;
this._mouseMoveMonitor.startMonitoring(this._viewHelper.viewLinesDomNode, pointerId, e.buttons, (e) => this._onMouseDownThenMove(e), () => this._stop());
}
}
_stop() {
this._isActive = false;
this._topBottomDragScrolling.stop();
this._leftRightDragScrolling.stop();
}
onHeightChanged() {
this._mouseMoveMonitor.stopMonitoring();
}
onPointerUp() {
this._mouseMoveMonitor.stopMonitoring();
}
onCursorStateChanged(e) {
this._currentSelection = e.selections[0];
}
_getPositionOutsideEditor(e) {
const editorContent = e.editorPos;
const model = this._context.viewModel;
const viewLayout = this._context.viewLayout;
const mouseColumn = this._getMouseColumn(e);
if (e.posy < editorContent.y) {
const outsideDistance = editorContent.y - e.posy;
const verticalOffset = Math.max(viewLayout.getCurrentScrollTop() - outsideDistance, 0);
const viewZoneData = HitTestContext.getZoneAtCoord(this._context, verticalOffset);
if (viewZoneData) {
const newPosition = this._helpPositionJumpOverViewZone(viewZoneData);
if (newPosition) {
return MouseTarget.createOutsideEditor(mouseColumn, newPosition, 'above', outsideDistance);
}
}
const aboveLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset);
return MouseTarget.createOutsideEditor(mouseColumn, new Position(aboveLineNumber, 1), 'above', outsideDistance);
}
if (e.posy > editorContent.y + editorContent.height) {
const outsideDistance = e.posy - editorContent.y - editorContent.height;
const verticalOffset = viewLayout.getCurrentScrollTop() + e.relativePos.y;
const viewZoneData = HitTestContext.getZoneAtCoord(this._context, verticalOffset);
if (viewZoneData) {
const newPosition = this._helpPositionJumpOverViewZone(viewZoneData);
if (newPosition) {
return MouseTarget.createOutsideEditor(mouseColumn, newPosition, 'below', outsideDistance);
}
}
const belowLineNumber = viewLayout.getLineNumberAtVerticalOffset(verticalOffset);
return MouseTarget.createOutsideEditor(mouseColumn, new Position(belowLineNumber, model.getLineMaxColumn(belowLineNumber)), 'below', outsideDistance);
}
const possibleLineNumber = viewLayout.getLineNumberAtVerticalOffset(viewLayout.getCurrentScrollTop() + e.relativePos.y);
const layoutInfo = this._context.configuration.options.get(165 /* EditorOption.layoutInfo */);
const xLeftBoundary = layoutInfo.contentLeft;
if (e.relativePos.x <= xLeftBoundary) {
const outsideDistance = xLeftBoundary - e.relativePos.x;
return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, 1), 'left', outsideDistance);
}
const contentRight = (layoutInfo.minimap.minimapLeft === 0
? layoutInfo.width - layoutInfo.verticalScrollbarWidth // Happens when minimap is hidden
: layoutInfo.minimap.minimapLeft);
const xRightBoundary = contentRight;
if (e.relativePos.x >= xRightBoundary) {
const outsideDistance = e.relativePos.x - xRightBoundary;
return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, model.getLineMaxColumn(possibleLineNumber)), 'right', outsideDistance);
}
return null;
}
_findMousePosition(e, testEventTarget) {
const positionOutsideEditor = this._getPositionOutsideEditor(e);
if (positionOutsideEditor) {
return positionOutsideEditor;
}
const t = this._createMouseTarget(e, testEventTarget);
const hintedPosition = t.position;
if (!hintedPosition) {
return null;
}
if (t.type === 8 /* MouseTargetType.CONTENT_VIEW_ZONE */ || t.type === 5 /* MouseTargetType.GUTTER_VIEW_ZONE */) {
const newPosition = this._helpPositionJumpOverViewZone(t.detail);
if (newPosition) {
return MouseTarget.createViewZone(t.type, t.element, t.mouseColumn, newPosition, t.detail);
}
}
return t;
}
_helpPositionJumpOverViewZone(viewZoneData) {
// Force position on view zones to go above or below depending on where selection started from
const selectionStart = new Position(this._currentSelection.selectionStartLineNumber, this._currentSelection.selectionStartColumn);
const positionBefore = viewZoneData.positionBefore;
const positionAfter = viewZoneData.positionAfter;
if (positionBefore && positionAfter) {
if (positionBefore.isBefore(selectionStart)) {
return positionBefore;
}
else {
return positionAfter;
}
}
return null;
}
_dispatchMouse(position, inSelectionMode, revealType) {
if (!position.position) {
return;
}
this._viewController.dispatchMouse({
position: position.position,
mouseColumn: position.mouseColumn,
startedOnLineNumbers: this._mouseState.startedOnLineNumbers,
revealType,
inSelectionMode: inSelectionMode,
mouseDownCount: this._mouseState.count,
altKey: this._mouseState.altKey,
ctrlKey: this._mouseState.ctrlKey,
metaKey: this._mouseState.metaKey,
shiftKey: this._mouseState.shiftKey,
leftButton: this._mouseState.leftButton,
middleButton: this._mouseState.middleButton,
onInjectedText: position.type === 6 /* MouseTargetType.CONTENT_TEXT */ && position.detail.injectedText !== null
});
}
}
class MouseDownState {
static { this.CLEAR_MOUSE_DOWN_COUNT_TIME = 400; } // ms
get altKey() { return this._altKey; }
get ctrlKey() { return this._ctrlKey; }
get metaKey() { return this._metaKey; }
get shiftKey() { return this._shiftKey; }
get leftButton() { return this._leftButton; }
get middleButton() { return this._middleButton; }
get startedOnLineNumbers() { return this._startedOnLineNumbers; }
constructor() {
this._altKey = false;
this._ctrlKey = false;
this._metaKey = false;
this._shiftKey = false;
this._leftButton = false;
this._middleButton = false;
this._startedOnLineNumbers = false;
this._lastMouseDownPosition = null;
this._lastMouseDownPositionEqualCount = 0;
this._lastMouseDownCount = 0;
this._lastSetMouseDownCountTime = 0;
this.isDragAndDrop = false;
}
get count() {
return this._lastMouseDownCount;
}
setModifiers(source) {
this._altKey = source.altKey;
this._ctrlKey = source.ctrlKey;
this._metaKey = source.metaKey;
this._shiftKey = source.shiftKey;
}
setStartButtons(source) {
this._leftButton = source.leftButton;
this._middleButton = source.middleButton;
}
setStartedOnLineNumbers(startedOnLineNumbers) {
this._startedOnLineNumbers = startedOnLineNumbers;
}
trySetCount(setMouseDownCount, newMouseDownPosition) {
// a. Invalidate multiple clicking if too much time has passed (will be hit by IE because the detail field of mouse events contains garbage in IE10)
const currentTime = (new Date()).getTime();
if (currentTime - this._lastSetMouseDownCountTime > MouseDownState.CLEAR_MOUSE_DOWN_COUNT_TIME) {
setMouseDownCount = 1;
}
this._lastSetMouseDownCountTime = currentTime;
// b. Ensure that we don't jump from single click to triple click in one go (will be hit by IE because the detail field of mouse events contains garbage in IE10)
if (setMouseDownCount > this._lastMouseDownCount + 1) {
setMouseDownCount = this._lastMouseDownCount + 1;
}
// c. Invalidate multiple clicking if the logical position is different
if (this._lastMouseDownPosition && this._lastMouseDownPosition.equals(newMouseDownPosition)) {
this._lastMouseDownPositionEqualCount++;
}
else {
this._lastMouseDownPositionEqualCount = 1;
}
this._lastMouseDownPosition = newMouseDownPosition;
// Finally set the lastMouseDownCount
this._lastMouseDownCount = Math.min(setMouseDownCount, this._lastMouseDownPositionEqualCount);
}
}
export { MouseHandler };
@@ -0,0 +1,978 @@
import { PageCoordinates } from '../editorDom.js';
import { PartFingerprints } from '../view/viewPart.js';
import { ViewLine } from '../viewParts/viewLines/viewLine.js';
import { Position } from '../../common/core/position.js';
import { Range } from '../../common/core/range.js';
import { CursorColumns } from '../../common/core/cursorColumns.js';
import { getWindow, getShadowRoot } from '../../../base/browser/dom.js';
import { AtomicTabMoveOperations } from '../../common/cursor/cursorAtomicMoveOperations.js';
import { TextDirection } from '../../common/model.js';
import { Lazy } from '../../../base/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.
*--------------------------------------------------------------------------------------------*/
class UnknownHitTestResult {
constructor(hitTarget = null) {
this.hitTarget = hitTarget;
this.type = 0 /* HitTestResultType.Unknown */;
}
}
class ContentHitTestResult {
get hitTarget() { return this.spanNode; }
constructor(position, spanNode, injectedText) {
this.position = position;
this.spanNode = spanNode;
this.injectedText = injectedText;
this.type = 1 /* HitTestResultType.Content */;
}
}
var HitTestResult;
(function (HitTestResult) {
function createFromDOMInfo(ctx, spanNode, offset) {
const position = ctx.getPositionFromDOMInfo(spanNode, offset);
if (position) {
return new ContentHitTestResult(position, spanNode, null);
}
return new UnknownHitTestResult(spanNode);
}
HitTestResult.createFromDOMInfo = createFromDOMInfo;
})(HitTestResult || (HitTestResult = {}));
class PointerHandlerLastRenderData {
constructor(lastViewCursorsRenderData, lastTextareaPosition) {
this.lastViewCursorsRenderData = lastViewCursorsRenderData;
this.lastTextareaPosition = lastTextareaPosition;
}
}
class MouseTarget {
static _deduceRage(position, range = null) {
if (!range && position) {
return new Range(position.lineNumber, position.column, position.lineNumber, position.column);
}
return range ?? null;
}
static createUnknown(element, mouseColumn, position) {
return { type: 0 /* MouseTargetType.UNKNOWN */, element, mouseColumn, position, range: this._deduceRage(position) };
}
static createTextarea(element, mouseColumn) {
return { type: 1 /* MouseTargetType.TEXTAREA */, element, mouseColumn, position: null, range: null };
}
static createMargin(type, element, mouseColumn, position, range, detail) {
return { type, element, mouseColumn, position, range, detail };
}
static createViewZone(type, element, mouseColumn, position, detail) {
return { type, element, mouseColumn, position, range: this._deduceRage(position), detail };
}
static createContentText(element, mouseColumn, position, range, detail) {
return { type: 6 /* MouseTargetType.CONTENT_TEXT */, element, mouseColumn, position, range: this._deduceRage(position, range), detail };
}
static createContentEmpty(element, mouseColumn, position, detail) {
return { type: 7 /* MouseTargetType.CONTENT_EMPTY */, element, mouseColumn, position, range: this._deduceRage(position), detail };
}
static createContentWidget(element, mouseColumn, detail) {
return { type: 9 /* MouseTargetType.CONTENT_WIDGET */, element, mouseColumn, position: null, range: null, detail };
}
static createScrollbar(element, mouseColumn, position) {
return { type: 11 /* MouseTargetType.SCROLLBAR */, element, mouseColumn, position, range: this._deduceRage(position) };
}
static createOverlayWidget(element, mouseColumn, detail) {
return { type: 12 /* MouseTargetType.OVERLAY_WIDGET */, element, mouseColumn, position: null, range: null, detail };
}
static createOutsideEditor(mouseColumn, position, outsidePosition, outsideDistance) {
return { type: 13 /* MouseTargetType.OUTSIDE_EDITOR */, element: null, mouseColumn, position, range: this._deduceRage(position), outsidePosition, outsideDistance };
}
static _typeToString(type) {
if (type === 1 /* MouseTargetType.TEXTAREA */) {
return 'TEXTAREA';
}
if (type === 2 /* MouseTargetType.GUTTER_GLYPH_MARGIN */) {
return 'GUTTER_GLYPH_MARGIN';
}
if (type === 3 /* MouseTargetType.GUTTER_LINE_NUMBERS */) {
return 'GUTTER_LINE_NUMBERS';
}
if (type === 4 /* MouseTargetType.GUTTER_LINE_DECORATIONS */) {
return 'GUTTER_LINE_DECORATIONS';
}
if (type === 5 /* MouseTargetType.GUTTER_VIEW_ZONE */) {
return 'GUTTER_VIEW_ZONE';
}
if (type === 6 /* MouseTargetType.CONTENT_TEXT */) {
return 'CONTENT_TEXT';
}
if (type === 7 /* MouseTargetType.CONTENT_EMPTY */) {
return 'CONTENT_EMPTY';
}
if (type === 8 /* MouseTargetType.CONTENT_VIEW_ZONE */) {
return 'CONTENT_VIEW_ZONE';
}
if (type === 9 /* MouseTargetType.CONTENT_WIDGET */) {
return 'CONTENT_WIDGET';
}
if (type === 10 /* MouseTargetType.OVERVIEW_RULER */) {
return 'OVERVIEW_RULER';
}
if (type === 11 /* MouseTargetType.SCROLLBAR */) {
return 'SCROLLBAR';
}
if (type === 12 /* MouseTargetType.OVERLAY_WIDGET */) {
return 'OVERLAY_WIDGET';
}
return 'UNKNOWN';
}
static toString(target) {
return this._typeToString(target.type) + ': ' + target.position + ' - ' + target.range + ' - ' + JSON.stringify(target.detail);
}
}
class ElementPath {
static isTextArea(path) {
return (path.length === 2
&& path[0] === 3 /* PartFingerprint.OverflowGuard */
&& path[1] === 7 /* PartFingerprint.TextArea */);
}
static isChildOfViewLines(path) {
return (path.length >= 4
&& path[0] === 3 /* PartFingerprint.OverflowGuard */
&& path[3] === 8 /* PartFingerprint.ViewLines */);
}
static isStrictChildOfViewLines(path) {
return (path.length > 4
&& path[0] === 3 /* PartFingerprint.OverflowGuard */
&& path[3] === 8 /* PartFingerprint.ViewLines */);
}
static isChildOfScrollableElement(path) {
return (path.length >= 2
&& path[0] === 3 /* PartFingerprint.OverflowGuard */
&& path[1] === 6 /* PartFingerprint.ScrollableElement */);
}
static isChildOfMinimap(path) {
return (path.length >= 2
&& path[0] === 3 /* PartFingerprint.OverflowGuard */
&& path[1] === 9 /* PartFingerprint.Minimap */);
}
static isChildOfContentWidgets(path) {
return (path.length >= 4
&& path[0] === 3 /* PartFingerprint.OverflowGuard */
&& path[3] === 1 /* PartFingerprint.ContentWidgets */);
}
static isChildOfOverflowGuard(path) {
return (path.length >= 1
&& path[0] === 3 /* PartFingerprint.OverflowGuard */);
}
static isChildOfOverflowingContentWidgets(path) {
return (path.length >= 1
&& path[0] === 2 /* PartFingerprint.OverflowingContentWidgets */);
}
static isChildOfOverlayWidgets(path) {
return (path.length >= 2
&& path[0] === 3 /* PartFingerprint.OverflowGuard */
&& path[1] === 4 /* PartFingerprint.OverlayWidgets */);
}
static isChildOfOverflowingOverlayWidgets(path) {
return (path.length >= 1
&& path[0] === 5 /* PartFingerprint.OverflowingOverlayWidgets */);
}
}
class HitTestContext {
constructor(context, viewHelper, lastRenderData) {
this.viewModel = context.viewModel;
const options = context.configuration.options;
this.layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this.viewDomNode = viewHelper.viewDomNode;
this.viewLinesGpu = viewHelper.viewLinesGpu;
this.lineHeight = options.get(75 /* EditorOption.lineHeight */);
this.stickyTabStops = options.get(132 /* EditorOption.stickyTabStops */);
this.typicalHalfwidthCharacterWidth = options.get(59 /* EditorOption.fontInfo */).typicalHalfwidthCharacterWidth;
this.lastRenderData = lastRenderData;
this._context = context;
this._viewHelper = viewHelper;
}
getZoneAtCoord(mouseVerticalOffset) {
return HitTestContext.getZoneAtCoord(this._context, mouseVerticalOffset);
}
static getZoneAtCoord(context, mouseVerticalOffset) {
// The target is either a view zone or the empty space after the last view-line
const viewZoneWhitespace = context.viewLayout.getWhitespaceAtVerticalOffset(mouseVerticalOffset);
if (viewZoneWhitespace) {
const viewZoneMiddle = viewZoneWhitespace.verticalOffset + viewZoneWhitespace.height / 2;
const lineCount = context.viewModel.getLineCount();
let positionBefore = null;
let position;
let positionAfter = null;
if (viewZoneWhitespace.afterLineNumber !== lineCount) {
// There are more lines after this view zone
positionAfter = new Position(viewZoneWhitespace.afterLineNumber + 1, 1);
}
if (viewZoneWhitespace.afterLineNumber > 0) {
// There are more lines above this view zone
positionBefore = new Position(viewZoneWhitespace.afterLineNumber, context.viewModel.getLineMaxColumn(viewZoneWhitespace.afterLineNumber));
}
if (positionAfter === null) {
position = positionBefore;
}
else if (positionBefore === null) {
position = positionAfter;
}
else if (mouseVerticalOffset < viewZoneMiddle) {
position = positionBefore;
}
else {
position = positionAfter;
}
return {
viewZoneId: viewZoneWhitespace.id,
afterLineNumber: viewZoneWhitespace.afterLineNumber,
positionBefore: positionBefore,
positionAfter: positionAfter,
position: position
};
}
return null;
}
getFullLineRangeAtCoord(mouseVerticalOffset) {
if (this._context.viewLayout.isAfterLines(mouseVerticalOffset)) {
// Below the last line
const lineNumber = this._context.viewModel.getLineCount();
const maxLineColumn = this._context.viewModel.getLineMaxColumn(lineNumber);
return {
range: new Range(lineNumber, maxLineColumn, lineNumber, maxLineColumn),
isAfterLines: true
};
}
const lineNumber = this._context.viewLayout.getLineNumberAtVerticalOffset(mouseVerticalOffset);
const maxLineColumn = this._context.viewModel.getLineMaxColumn(lineNumber);
return {
range: new Range(lineNumber, 1, lineNumber, maxLineColumn),
isAfterLines: false
};
}
getLineNumberAtVerticalOffset(mouseVerticalOffset) {
return this._context.viewLayout.getLineNumberAtVerticalOffset(mouseVerticalOffset);
}
isAfterLines(mouseVerticalOffset) {
return this._context.viewLayout.isAfterLines(mouseVerticalOffset);
}
isInTopPadding(mouseVerticalOffset) {
return this._context.viewLayout.isInTopPadding(mouseVerticalOffset);
}
isInBottomPadding(mouseVerticalOffset) {
return this._context.viewLayout.isInBottomPadding(mouseVerticalOffset);
}
getVerticalOffsetForLineNumber(lineNumber) {
return this._context.viewLayout.getVerticalOffsetForLineNumber(lineNumber);
}
findAttribute(element, attr) {
return HitTestContext._findAttribute(element, attr, this._viewHelper.viewDomNode);
}
static _findAttribute(element, attr, stopAt) {
while (element && element !== element.ownerDocument.body) {
if (element.hasAttribute && element.hasAttribute(attr)) {
return element.getAttribute(attr);
}
if (element === stopAt) {
return null;
}
element = element.parentNode;
}
return null;
}
getLineWidth(lineNumber) {
return this._viewHelper.getLineWidth(lineNumber);
}
isRtl(lineNumber) {
return this.viewModel.getTextDirection(lineNumber) === TextDirection.RTL;
}
visibleRangeForPosition(lineNumber, column) {
return this._viewHelper.visibleRangeForPosition(lineNumber, column);
}
getPositionFromDOMInfo(spanNode, offset) {
return this._viewHelper.getPositionFromDOMInfo(spanNode, offset);
}
getCurrentScrollTop() {
return this._context.viewLayout.getCurrentScrollTop();
}
getCurrentScrollLeft() {
return this._context.viewLayout.getCurrentScrollLeft();
}
}
class BareHitTestRequest {
constructor(ctx, editorPos, pos, relativePos) {
this.editorPos = editorPos;
this.pos = pos;
this.relativePos = relativePos;
this.mouseVerticalOffset = Math.max(0, ctx.getCurrentScrollTop() + this.relativePos.y);
this.mouseContentHorizontalOffset = ctx.getCurrentScrollLeft() + this.relativePos.x - ctx.layoutInfo.contentLeft;
this.isInMarginArea = (this.relativePos.x < ctx.layoutInfo.contentLeft && this.relativePos.x >= ctx.layoutInfo.glyphMarginLeft);
this.isInContentArea = !this.isInMarginArea;
this.mouseColumn = Math.max(0, MouseTargetFactory._getMouseColumn(this.mouseContentHorizontalOffset, ctx.typicalHalfwidthCharacterWidth));
}
}
class HitTestRequest extends BareHitTestRequest {
get target() {
if (this._useHitTestTarget) {
return this.hitTestResult.value.hitTarget;
}
return this._eventTarget;
}
get targetPath() {
if (this._targetPathCacheElement !== this.target) {
this._targetPathCacheElement = this.target;
this._targetPathCacheValue = PartFingerprints.collect(this.target, this._ctx.viewDomNode);
}
return this._targetPathCacheValue;
}
constructor(ctx, editorPos, pos, relativePos, eventTarget) {
super(ctx, editorPos, pos, relativePos);
this.hitTestResult = new Lazy(() => MouseTargetFactory.doHitTest(this._ctx, this));
this._targetPathCacheElement = null;
this._targetPathCacheValue = new Uint8Array(0);
this._ctx = ctx;
this._eventTarget = eventTarget;
// If no event target is passed in, we will use the hit test target
const hasEventTarget = Boolean(this._eventTarget);
this._useHitTestTarget = !hasEventTarget;
}
toString() {
return `pos(${this.pos.x},${this.pos.y}), editorPos(${this.editorPos.x},${this.editorPos.y}), relativePos(${this.relativePos.x},${this.relativePos.y}), mouseVerticalOffset: ${this.mouseVerticalOffset}, mouseContentHorizontalOffset: ${this.mouseContentHorizontalOffset}\n\ttarget: ${this.target ? this.target.outerHTML : null}`;
}
get wouldBenefitFromHitTestTargetSwitch() {
return (!this._useHitTestTarget
&& this.hitTestResult.value.hitTarget !== null
&& this.target !== this.hitTestResult.value.hitTarget);
}
switchToHitTestTarget() {
this._useHitTestTarget = true;
}
_getMouseColumn(position = null) {
if (position && position.column < this._ctx.viewModel.getLineMaxColumn(position.lineNumber)) {
// Most likely, the line contains foreign decorations...
return CursorColumns.visibleColumnFromColumn(this._ctx.viewModel.getLineContent(position.lineNumber), position.column, this._ctx.viewModel.model.getOptions().tabSize) + 1;
}
return this.mouseColumn;
}
fulfillUnknown(position = null) {
return MouseTarget.createUnknown(this.target, this._getMouseColumn(position), position);
}
fulfillTextarea() {
return MouseTarget.createTextarea(this.target, this._getMouseColumn());
}
fulfillMargin(type, position, range, detail) {
return MouseTarget.createMargin(type, this.target, this._getMouseColumn(position), position, range, detail);
}
fulfillViewZone(type, position, detail) {
// Always return the usual mouse column for a view zone.
return MouseTarget.createViewZone(type, this.target, this._getMouseColumn(), position, detail);
}
fulfillContentText(position, range, detail) {
return MouseTarget.createContentText(this.target, this._getMouseColumn(position), position, range, detail);
}
fulfillContentEmpty(position, detail) {
return MouseTarget.createContentEmpty(this.target, this._getMouseColumn(position), position, detail);
}
fulfillContentWidget(detail) {
return MouseTarget.createContentWidget(this.target, this._getMouseColumn(), detail);
}
fulfillScrollbar(position) {
return MouseTarget.createScrollbar(this.target, this._getMouseColumn(position), position);
}
fulfillOverlayWidget(detail) {
return MouseTarget.createOverlayWidget(this.target, this._getMouseColumn(), detail);
}
}
const EMPTY_CONTENT_AFTER_LINES = { isAfterLines: true };
function createEmptyContentDataInLines(horizontalDistanceToText) {
return {
isAfterLines: false,
horizontalDistanceToText: horizontalDistanceToText
};
}
class MouseTargetFactory {
constructor(context, viewHelper) {
this._context = context;
this._viewHelper = viewHelper;
}
mouseTargetIsWidget(e) {
const t = e.target;
const path = PartFingerprints.collect(t, this._viewHelper.viewDomNode);
// Is it a content widget?
if (ElementPath.isChildOfContentWidgets(path) || ElementPath.isChildOfOverflowingContentWidgets(path)) {
return true;
}
// Is it an overlay widget?
if (ElementPath.isChildOfOverlayWidgets(path) || ElementPath.isChildOfOverflowingOverlayWidgets(path)) {
return true;
}
return false;
}
createMouseTarget(lastRenderData, editorPos, pos, relativePos, target) {
const ctx = new HitTestContext(this._context, this._viewHelper, lastRenderData);
const request = new HitTestRequest(ctx, editorPos, pos, relativePos, target);
try {
const r = MouseTargetFactory._createMouseTarget(ctx, request);
if (r.type === 6 /* MouseTargetType.CONTENT_TEXT */) {
// Snap to the nearest soft tab boundary if atomic soft tabs are enabled.
if (ctx.stickyTabStops && r.position !== null) {
const position = MouseTargetFactory._snapToSoftTabBoundary(r.position, ctx.viewModel);
const range = Range.fromPositions(position, position).plusRange(r.range);
return request.fulfillContentText(position, range, r.detail);
}
}
// console.log(MouseTarget.toString(r));
return r;
}
catch (err) {
// console.log(err);
return request.fulfillUnknown();
}
}
static _createMouseTarget(ctx, request) {
// console.log(`${domHitTestExecuted ? '=>' : ''}CAME IN REQUEST: ${request}`);
if (request.target === null) {
// No target
return request.fulfillUnknown();
}
// we know for a fact that request.target is not null
const resolvedRequest = request;
let result = null;
if (!ElementPath.isChildOfOverflowGuard(request.targetPath) && !ElementPath.isChildOfOverflowingContentWidgets(request.targetPath) && !ElementPath.isChildOfOverflowingOverlayWidgets(request.targetPath)) {
// We only render dom nodes inside the overflow guard or in the overflowing content widgets
result = result || request.fulfillUnknown();
}
result = result || MouseTargetFactory._hitTestContentWidget(ctx, resolvedRequest);
result = result || MouseTargetFactory._hitTestOverlayWidget(ctx, resolvedRequest);
result = result || MouseTargetFactory._hitTestMinimap(ctx, resolvedRequest);
result = result || MouseTargetFactory._hitTestScrollbarSlider(ctx, resolvedRequest);
result = result || MouseTargetFactory._hitTestViewZone(ctx, resolvedRequest);
result = result || MouseTargetFactory._hitTestMargin(ctx, resolvedRequest);
result = result || MouseTargetFactory._hitTestViewCursor(ctx, resolvedRequest);
result = result || MouseTargetFactory._hitTestTextArea(ctx, resolvedRequest);
result = result || MouseTargetFactory._hitTestViewLines(ctx, resolvedRequest);
result = result || MouseTargetFactory._hitTestScrollbar(ctx, resolvedRequest);
return (result || request.fulfillUnknown());
}
static _hitTestContentWidget(ctx, request) {
// Is it a content widget?
if (ElementPath.isChildOfContentWidgets(request.targetPath) || ElementPath.isChildOfOverflowingContentWidgets(request.targetPath)) {
const widgetId = ctx.findAttribute(request.target, 'widgetId');
if (widgetId) {
return request.fulfillContentWidget(widgetId);
}
else {
return request.fulfillUnknown();
}
}
return null;
}
static _hitTestOverlayWidget(ctx, request) {
// Is it an overlay widget?
if (ElementPath.isChildOfOverlayWidgets(request.targetPath) || ElementPath.isChildOfOverflowingOverlayWidgets(request.targetPath)) {
const widgetId = ctx.findAttribute(request.target, 'widgetId');
if (widgetId) {
return request.fulfillOverlayWidget(widgetId);
}
else {
return request.fulfillUnknown();
}
}
return null;
}
static _hitTestViewCursor(ctx, request) {
if (request.target) {
// Check if we've hit a painted cursor
const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData;
for (const d of lastViewCursorsRenderData) {
if (request.target === d.domNode) {
return request.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null });
}
}
}
if (request.isInContentArea) {
// Edge has a bug when hit-testing the exact position of a cursor,
// instead of returning the correct dom node, it returns the
// first or last rendered view line dom node, therefore help it out
// and first check if we are on top of a cursor
const lastViewCursorsRenderData = ctx.lastRenderData.lastViewCursorsRenderData;
const mouseContentHorizontalOffset = request.mouseContentHorizontalOffset;
const mouseVerticalOffset = request.mouseVerticalOffset;
for (const d of lastViewCursorsRenderData) {
if (mouseContentHorizontalOffset < d.contentLeft) {
// mouse position is to the left of the cursor
continue;
}
if (mouseContentHorizontalOffset > d.contentLeft + d.width) {
// mouse position is to the right of the cursor
continue;
}
const cursorVerticalOffset = ctx.getVerticalOffsetForLineNumber(d.position.lineNumber);
if (cursorVerticalOffset <= mouseVerticalOffset
&& mouseVerticalOffset <= cursorVerticalOffset + d.height) {
return request.fulfillContentText(d.position, null, { mightBeForeignElement: false, injectedText: null });
}
}
}
return null;
}
static _hitTestViewZone(ctx, request) {
const viewZoneData = ctx.getZoneAtCoord(request.mouseVerticalOffset);
if (viewZoneData) {
const mouseTargetType = (request.isInContentArea ? 8 /* MouseTargetType.CONTENT_VIEW_ZONE */ : 5 /* MouseTargetType.GUTTER_VIEW_ZONE */);
return request.fulfillViewZone(mouseTargetType, viewZoneData.position, viewZoneData);
}
return null;
}
static _hitTestTextArea(ctx, request) {
// Is it the textarea?
if (ElementPath.isTextArea(request.targetPath)) {
if (ctx.lastRenderData.lastTextareaPosition) {
return request.fulfillContentText(ctx.lastRenderData.lastTextareaPosition, null, { mightBeForeignElement: false, injectedText: null });
}
return request.fulfillTextarea();
}
return null;
}
static _hitTestMargin(ctx, request) {
if (request.isInMarginArea) {
const res = ctx.getFullLineRangeAtCoord(request.mouseVerticalOffset);
const pos = res.range.getStartPosition();
let offset = Math.abs(request.relativePos.x);
const detail = {
isAfterLines: res.isAfterLines,
glyphMarginLeft: ctx.layoutInfo.glyphMarginLeft,
glyphMarginWidth: ctx.layoutInfo.glyphMarginWidth,
lineNumbersWidth: ctx.layoutInfo.lineNumbersWidth,
offsetX: offset
};
offset -= ctx.layoutInfo.glyphMarginLeft;
if (offset <= ctx.layoutInfo.glyphMarginWidth) {
// On the glyph margin
const modelCoordinate = ctx.viewModel.coordinatesConverter.convertViewPositionToModelPosition(res.range.getStartPosition());
const lanes = ctx.viewModel.glyphLanes.getLanesAtLine(modelCoordinate.lineNumber);
detail.glyphMarginLane = lanes[Math.floor(offset / ctx.lineHeight)];
return request.fulfillMargin(2 /* MouseTargetType.GUTTER_GLYPH_MARGIN */, pos, res.range, detail);
}
offset -= ctx.layoutInfo.glyphMarginWidth;
if (offset <= ctx.layoutInfo.lineNumbersWidth) {
// On the line numbers
return request.fulfillMargin(3 /* MouseTargetType.GUTTER_LINE_NUMBERS */, pos, res.range, detail);
}
offset -= ctx.layoutInfo.lineNumbersWidth;
// On the line decorations
return request.fulfillMargin(4 /* MouseTargetType.GUTTER_LINE_DECORATIONS */, pos, res.range, detail);
}
return null;
}
static _hitTestViewLines(ctx, request) {
if (!ElementPath.isChildOfViewLines(request.targetPath)) {
return null;
}
if (ctx.isInTopPadding(request.mouseVerticalOffset)) {
return request.fulfillContentEmpty(new Position(1, 1), EMPTY_CONTENT_AFTER_LINES);
}
// Check if it is below any lines and any view zones
if (ctx.isAfterLines(request.mouseVerticalOffset) || ctx.isInBottomPadding(request.mouseVerticalOffset)) {
// This most likely indicates it happened after the last view-line
const lineCount = ctx.viewModel.getLineCount();
const maxLineColumn = ctx.viewModel.getLineMaxColumn(lineCount);
return request.fulfillContentEmpty(new Position(lineCount, maxLineColumn), EMPTY_CONTENT_AFTER_LINES);
}
// Check if we are hitting a view-line (can happen in the case of inline decorations on empty lines)
// See https://github.com/microsoft/vscode/issues/46942
if (ElementPath.isStrictChildOfViewLines(request.targetPath)) {
const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
const lineLength = ctx.viewModel.getLineLength(lineNumber);
const lineWidth = ctx.getLineWidth(lineNumber);
if (lineLength === 0) {
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
return request.fulfillContentEmpty(new Position(lineNumber, 1), detail);
}
const isRtl = ctx.isRtl(lineNumber);
if (isRtl) {
if (request.mouseContentHorizontalOffset + lineWidth <= ctx.layoutInfo.contentWidth - ctx.layoutInfo.verticalScrollbarWidth) {
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
return request.fulfillContentEmpty(pos, detail);
}
}
else if (request.mouseContentHorizontalOffset >= lineWidth) {
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
return request.fulfillContentEmpty(pos, detail);
}
}
else {
if (ctx.viewLinesGpu) {
const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
if (ctx.viewModel.getLineLength(lineNumber) === 0) {
const lineWidth = ctx.getLineWidth(lineNumber);
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
return request.fulfillContentEmpty(new Position(lineNumber, 1), detail);
}
const lineWidth = ctx.getLineWidth(lineNumber);
const isRtl = ctx.isRtl(lineNumber);
if (isRtl) {
if (request.mouseContentHorizontalOffset + lineWidth <= ctx.layoutInfo.contentWidth - ctx.layoutInfo.verticalScrollbarWidth) {
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
return request.fulfillContentEmpty(pos, detail);
}
}
else if (request.mouseContentHorizontalOffset >= lineWidth) {
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
const pos = new Position(lineNumber, ctx.viewModel.getLineMaxColumn(lineNumber));
return request.fulfillContentEmpty(pos, detail);
}
const position = ctx.viewLinesGpu.getPositionAtCoordinate(lineNumber, request.mouseContentHorizontalOffset);
if (position) {
const detail = {
injectedText: null,
mightBeForeignElement: false
};
return request.fulfillContentText(position, Range.fromPositions(position, position), detail);
}
}
}
// Do the hit test (if not already done)
const hitTestResult = request.hitTestResult.value;
if (hitTestResult.type === 1 /* HitTestResultType.Content */) {
return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.spanNode, hitTestResult.position, hitTestResult.injectedText);
}
// We didn't hit content...
if (request.wouldBenefitFromHitTestTargetSwitch) {
// We actually hit something different... Give it one last change by trying again with this new target
request.switchToHitTestTarget();
return this._createMouseTarget(ctx, request);
}
// We have tried everything...
return request.fulfillUnknown();
}
static _hitTestMinimap(ctx, request) {
if (ElementPath.isChildOfMinimap(request.targetPath)) {
const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);
return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));
}
return null;
}
static _hitTestScrollbarSlider(ctx, request) {
if (ElementPath.isChildOfScrollableElement(request.targetPath)) {
if (request.target && request.target.nodeType === 1) {
const className = request.target.className;
if (className && /\b(slider|scrollbar)\b/.test(className)) {
const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);
return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));
}
}
}
return null;
}
static _hitTestScrollbar(ctx, request) {
// Is it the overview ruler?
// Is it a child of the scrollable element?
if (ElementPath.isChildOfScrollableElement(request.targetPath)) {
const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
const maxColumn = ctx.viewModel.getLineMaxColumn(possibleLineNumber);
return request.fulfillScrollbar(new Position(possibleLineNumber, maxColumn));
}
return null;
}
getMouseColumn(relativePos) {
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
const mouseContentHorizontalOffset = this._context.viewLayout.getCurrentScrollLeft() + relativePos.x - layoutInfo.contentLeft;
return MouseTargetFactory._getMouseColumn(mouseContentHorizontalOffset, options.get(59 /* EditorOption.fontInfo */).typicalHalfwidthCharacterWidth);
}
static _getMouseColumn(mouseContentHorizontalOffset, typicalHalfwidthCharacterWidth) {
if (mouseContentHorizontalOffset < 0) {
return 1;
}
const chars = Math.round(mouseContentHorizontalOffset / typicalHalfwidthCharacterWidth);
return (chars + 1);
}
static createMouseTargetFromHitTestPosition(ctx, request, spanNode, pos, injectedText) {
const lineNumber = pos.lineNumber;
const column = pos.column;
const lineWidth = ctx.getLineWidth(lineNumber);
if (request.mouseContentHorizontalOffset > lineWidth) {
const detail = createEmptyContentDataInLines(request.mouseContentHorizontalOffset - lineWidth);
return request.fulfillContentEmpty(pos, detail);
}
const visibleRange = ctx.visibleRangeForPosition(lineNumber, column);
if (!visibleRange) {
return request.fulfillUnknown(pos);
}
const columnHorizontalOffset = visibleRange.left;
if (Math.abs(request.mouseContentHorizontalOffset - columnHorizontalOffset) < 1) {
return request.fulfillContentText(pos, null, { mightBeForeignElement: !!injectedText, injectedText });
}
const points = [];
points.push({ offset: visibleRange.left, column: column });
if (column > 1) {
const visibleRange = ctx.visibleRangeForPosition(lineNumber, column - 1);
if (visibleRange) {
points.push({ offset: visibleRange.left, column: column - 1 });
}
}
const lineMaxColumn = ctx.viewModel.getLineMaxColumn(lineNumber);
if (column < lineMaxColumn) {
const visibleRange = ctx.visibleRangeForPosition(lineNumber, column + 1);
if (visibleRange) {
points.push({ offset: visibleRange.left, column: column + 1 });
}
}
points.sort((a, b) => a.offset - b.offset);
const mouseCoordinates = request.pos.toClientCoordinates(getWindow(ctx.viewDomNode));
const spanNodeClientRect = spanNode.getBoundingClientRect();
const mouseIsOverSpanNode = (spanNodeClientRect.left <= mouseCoordinates.clientX && mouseCoordinates.clientX <= spanNodeClientRect.right);
let rng = null;
for (let i = 1; i < points.length; i++) {
const prev = points[i - 1];
const curr = points[i];
if (prev.offset <= request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset <= curr.offset) {
rng = new Range(lineNumber, prev.column, lineNumber, curr.column);
// See https://github.com/microsoft/vscode/issues/152819
// Due to the use of zwj, the browser's hit test result is skewed towards the left
// Here we try to correct that if the mouse horizontal offset is closer to the right than the left
const prevDelta = Math.abs(prev.offset - request.mouseContentHorizontalOffset);
const nextDelta = Math.abs(curr.offset - request.mouseContentHorizontalOffset);
pos = (prevDelta < nextDelta
? new Position(lineNumber, prev.column)
: new Position(lineNumber, curr.column));
break;
}
}
return request.fulfillContentText(pos, rng, { mightBeForeignElement: !mouseIsOverSpanNode || !!injectedText, injectedText });
}
/**
* Most probably WebKit browsers and Edge
*/
static _doHitTestWithCaretRangeFromPoint(ctx, request) {
// In Chrome, especially on Linux it is possible to click between lines,
// so try to adjust the `hity` below so that it lands in the center of a line
const lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset);
const lineStartVerticalOffset = ctx.getVerticalOffsetForLineNumber(lineNumber);
const lineEndVerticalOffset = lineStartVerticalOffset + ctx.lineHeight;
const isBelowLastLine = (lineNumber === ctx.viewModel.getLineCount()
&& request.mouseVerticalOffset > lineEndVerticalOffset);
if (!isBelowLastLine) {
const lineCenteredVerticalOffset = Math.floor((lineStartVerticalOffset + lineEndVerticalOffset) / 2);
let adjustedPageY = request.pos.y + (lineCenteredVerticalOffset - request.mouseVerticalOffset);
if (adjustedPageY <= request.editorPos.y) {
adjustedPageY = request.editorPos.y + 1;
}
if (adjustedPageY >= request.editorPos.y + request.editorPos.height) {
adjustedPageY = request.editorPos.y + request.editorPos.height - 1;
}
const adjustedPage = new PageCoordinates(request.pos.x, adjustedPageY);
const r = this._actualDoHitTestWithCaretRangeFromPoint(ctx, adjustedPage.toClientCoordinates(getWindow(ctx.viewDomNode)));
if (r.type === 1 /* HitTestResultType.Content */) {
return r;
}
}
// Also try to hit test without the adjustment (for the edge cases that we are near the top or bottom)
return this._actualDoHitTestWithCaretRangeFromPoint(ctx, request.pos.toClientCoordinates(getWindow(ctx.viewDomNode)));
}
static _actualDoHitTestWithCaretRangeFromPoint(ctx, coords) {
const shadowRoot = getShadowRoot(ctx.viewDomNode);
let range;
if (shadowRoot) {
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
if (typeof shadowRoot.caretRangeFromPoint === 'undefined') {
range = shadowCaretRangeFromPoint(shadowRoot, coords.clientX, coords.clientY);
}
else {
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
range = shadowRoot.caretRangeFromPoint(coords.clientX, coords.clientY);
}
}
else {
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
range = ctx.viewDomNode.ownerDocument.caretRangeFromPoint(coords.clientX, coords.clientY);
}
if (!range || !range.startContainer) {
return new UnknownHitTestResult();
}
// Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span
const startContainer = range.startContainer;
if (startContainer.nodeType === startContainer.TEXT_NODE) {
// startContainer is expected to be the token text
const parent1 = startContainer.parentNode; // expected to be the token span
const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span
const parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div
const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? parent3.className : null;
if (parent3ClassName === ViewLine.CLASS_NAME) {
return HitTestResult.createFromDOMInfo(ctx, parent1, range.startOffset);
}
else {
return new UnknownHitTestResult(startContainer.parentNode);
}
}
else if (startContainer.nodeType === startContainer.ELEMENT_NODE) {
// startContainer is expected to be the token span
const parent1 = startContainer.parentNode; // expected to be the view line container span
const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line div
const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? parent2.className : null;
if (parent2ClassName === ViewLine.CLASS_NAME) {
return HitTestResult.createFromDOMInfo(ctx, startContainer, startContainer.textContent.length);
}
else {
return new UnknownHitTestResult(startContainer);
}
}
return new UnknownHitTestResult();
}
/**
* Most probably Gecko
*/
static _doHitTestWithCaretPositionFromPoint(ctx, coords) {
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
const hitResult = ctx.viewDomNode.ownerDocument.caretPositionFromPoint(coords.clientX, coords.clientY);
if (hitResult.offsetNode.nodeType === hitResult.offsetNode.TEXT_NODE) {
// offsetNode is expected to be the token text
const parent1 = hitResult.offsetNode.parentNode; // expected to be the token span
const parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span
const parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div
const parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? parent3.className : null;
if (parent3ClassName === ViewLine.CLASS_NAME) {
return HitTestResult.createFromDOMInfo(ctx, hitResult.offsetNode.parentNode, hitResult.offset);
}
else {
return new UnknownHitTestResult(hitResult.offsetNode.parentNode);
}
}
// For inline decorations, Gecko sometimes returns the `<span>` of the line and the offset is the `<span>` with the inline decoration
// Some other times, it returns the `<span>` with the inline decoration
if (hitResult.offsetNode.nodeType === hitResult.offsetNode.ELEMENT_NODE) {
const parent1 = hitResult.offsetNode.parentNode;
const parent1ClassName = parent1 && parent1.nodeType === parent1.ELEMENT_NODE ? parent1.className : null;
const parent2 = parent1 ? parent1.parentNode : null;
const parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? parent2.className : null;
if (parent1ClassName === ViewLine.CLASS_NAME) {
// it returned the `<span>` of the line and the offset is the `<span>` with the inline decoration
const tokenSpan = hitResult.offsetNode.childNodes[Math.min(hitResult.offset, hitResult.offsetNode.childNodes.length - 1)];
if (tokenSpan) {
return HitTestResult.createFromDOMInfo(ctx, tokenSpan, 0);
}
}
else if (parent2ClassName === ViewLine.CLASS_NAME) {
// it returned the `<span>` with the inline decoration
return HitTestResult.createFromDOMInfo(ctx, hitResult.offsetNode, 0);
}
}
return new UnknownHitTestResult(hitResult.offsetNode);
}
static _snapToSoftTabBoundary(position, viewModel) {
const lineContent = viewModel.getLineContent(position.lineNumber);
const { tabSize } = viewModel.model.getOptions();
const newPosition = AtomicTabMoveOperations.atomicPosition(lineContent, position.column - 1, tabSize, 2 /* Direction.Nearest */);
if (newPosition !== -1) {
return new Position(position.lineNumber, newPosition + 1);
}
return position;
}
static doHitTest(ctx, request) {
let result = new UnknownHitTestResult();
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
if (typeof ctx.viewDomNode.ownerDocument.caretRangeFromPoint === 'function') {
result = this._doHitTestWithCaretRangeFromPoint(ctx, request);
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
}
else if (ctx.viewDomNode.ownerDocument.caretPositionFromPoint) {
result = this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates(getWindow(ctx.viewDomNode)));
}
if (result.type === 1 /* HitTestResultType.Content */) {
const injectedText = ctx.viewModel.getInjectedTextAt(result.position);
const normalizedPosition = ctx.viewModel.normalizePosition(result.position, 2 /* PositionAffinity.None */);
if (injectedText || !normalizedPosition.equals(result.position)) {
result = new ContentHitTestResult(normalizedPosition, result.spanNode, injectedText);
}
}
return result;
}
}
function shadowCaretRangeFromPoint(shadowRoot, x, y) {
const range = document.createRange();
// Get the element under the point
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
let el = shadowRoot.elementFromPoint(x, y);
// When el is not null, it may be div.monaco-mouse-cursor-text Element, which has not childNodes, we don't need to handle it.
if (el?.hasChildNodes()) {
// Get the last child of the element until its firstChild is a text node
// This assumes that the pointer is on the right of the line, out of the tokens
// and that we want to get the offset of the last token of the line
while (el && el.firstChild && el.firstChild.nodeType !== el.firstChild.TEXT_NODE && el.lastChild && el.lastChild.firstChild) {
el = el.lastChild;
}
// Grab its rect
const rect = el.getBoundingClientRect();
// And its font (the computed shorthand font property might be empty, see #3217)
const elWindow = getWindow(el);
const fontStyle = elWindow.getComputedStyle(el, null).getPropertyValue('font-style');
const fontVariant = elWindow.getComputedStyle(el, null).getPropertyValue('font-variant');
const fontWeight = elWindow.getComputedStyle(el, null).getPropertyValue('font-weight');
const fontSize = elWindow.getComputedStyle(el, null).getPropertyValue('font-size');
const lineHeight = elWindow.getComputedStyle(el, null).getPropertyValue('line-height');
const fontFamily = elWindow.getComputedStyle(el, null).getPropertyValue('font-family');
const font = `${fontStyle} ${fontVariant} ${fontWeight} ${fontSize}/${lineHeight} ${fontFamily}`;
// And also its txt content
const text = el.innerText;
// Position the pixel cursor at the left of the element
let pixelCursor = rect.left;
let offset = 0;
let step;
// If the point is on the right of the box put the cursor after the last character
if (x > rect.left + rect.width) {
offset = text.length;
}
else {
const charWidthReader = CharWidthReader.getInstance();
// Goes through all the characters of the innerText, and checks if the x of the point
// belongs to the character.
for (let i = 0; i < text.length + 1; i++) {
// The step is half the width of the character
step = charWidthReader.getCharWidth(text.charAt(i), font) / 2;
// Move to the center of the character
pixelCursor += step;
// If the x of the point is smaller that the position of the cursor, the point is over that character
if (x < pixelCursor) {
offset = i;
break;
}
// Move between the current character and the next
pixelCursor += step;
}
}
// Creates a range with the text node of the element and set the offset found
range.setStart(el.firstChild, offset);
range.setEnd(el.firstChild, offset);
}
return range;
}
class CharWidthReader {
static { this._INSTANCE = null; }
static getInstance() {
if (!CharWidthReader._INSTANCE) {
CharWidthReader._INSTANCE = new CharWidthReader();
}
return CharWidthReader._INSTANCE;
}
constructor() {
this._cache = {};
this._canvas = document.createElement('canvas');
}
getCharWidth(char, font) {
const cacheKey = char + font;
if (this._cache[cacheKey]) {
return this._cache[cacheKey];
}
const context = this._canvas.getContext('2d');
context.font = font;
const metrics = context.measureText(char);
const width = metrics.width;
this._cache[cacheKey] = width;
return width;
}
}
export { HitTestContext, MouseTarget, MouseTargetFactory, PointerHandlerLastRenderData };
@@ -0,0 +1,132 @@
import { BrowserFeatures } from '../../../base/browser/canIUse.js';
import { addDisposableListener } from '../../../base/browser/dom.js';
import { Gesture, EventType } from '../../../base/browser/touch.js';
import { mainWindow } from '../../../base/browser/window.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { isIOS, isAndroid, isMobile } from '../../../base/common/platform.js';
import { MouseHandler } from './mouseHandler.js';
import { EditorMouseEvent, EditorPointerEventFactory } from '../editorDom.js';
import { TextAreaSyntethicEvents } from './editContext/textArea/textAreaEditContextInput.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Currently only tested on iOS 13/ iPadOS.
*/
class PointerEventHandler extends MouseHandler {
constructor(context, viewController, viewHelper) {
super(context, viewController, viewHelper);
this._register(Gesture.addTarget(this.viewHelper.linesContentDomNode));
this._register(addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Tap, (e) => this.onTap(e)));
this._register(addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Change, (e) => this.onChange(e)));
this._register(addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Contextmenu, (e) => this._onContextMenu(new EditorMouseEvent(e, false, this.viewHelper.viewDomNode), false)));
this._lastPointerType = 'mouse';
this._register(addDisposableListener(this.viewHelper.linesContentDomNode, 'pointerdown', (e) => {
const pointerType = e.pointerType;
if (pointerType === 'mouse') {
this._lastPointerType = 'mouse';
return;
}
else if (pointerType === 'touch') {
this._lastPointerType = 'touch';
}
else {
this._lastPointerType = 'pen';
}
}));
// PonterEvents
const pointerEvents = new EditorPointerEventFactory(this.viewHelper.viewDomNode);
this._register(pointerEvents.onPointerMove(this.viewHelper.viewDomNode, (e) => this._onMouseMove(e)));
this._register(pointerEvents.onPointerUp(this.viewHelper.viewDomNode, (e) => this._onMouseUp(e)));
this._register(pointerEvents.onPointerLeave(this.viewHelper.viewDomNode, (e) => this._onMouseLeave(e)));
this._register(pointerEvents.onPointerDown(this.viewHelper.viewDomNode, (e, pointerId) => this._onMouseDown(e, pointerId)));
}
onTap(event) {
if (!event.initialTarget || !this.viewHelper.linesContentDomNode.contains(event.initialTarget)) {
return;
}
event.preventDefault();
this.viewHelper.focusTextArea();
this._dispatchGesture(event, /*inSelectionMode*/ false);
}
onChange(event) {
if (this._lastPointerType === 'touch') {
this._context.viewModel.viewLayout.deltaScrollNow(-event.translationX, -event.translationY);
}
if (this._lastPointerType === 'pen') {
this._dispatchGesture(event, /*inSelectionMode*/ true);
}
}
_dispatchGesture(event, inSelectionMode) {
const target = this._createMouseTarget(new EditorMouseEvent(event, false, this.viewHelper.viewDomNode), false);
if (target.position) {
this.viewController.dispatchMouse({
position: target.position,
mouseColumn: target.position.column,
startedOnLineNumbers: false,
revealType: 1 /* NavigationCommandRevealType.Minimal */,
mouseDownCount: event.tapCount,
inSelectionMode,
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
leftButton: false,
middleButton: false,
onInjectedText: target.type === 6 /* MouseTargetType.CONTENT_TEXT */ && target.detail.injectedText !== null
});
}
}
_onMouseDown(e, pointerId) {
if (e.browserEvent.pointerType === 'touch') {
return;
}
super._onMouseDown(e, pointerId);
}
}
class TouchHandler extends MouseHandler {
constructor(context, viewController, viewHelper) {
super(context, viewController, viewHelper);
this._register(Gesture.addTarget(this.viewHelper.linesContentDomNode));
this._register(addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Tap, (e) => this.onTap(e)));
this._register(addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Change, (e) => this.onChange(e)));
this._register(addDisposableListener(this.viewHelper.linesContentDomNode, EventType.Contextmenu, (e) => this._onContextMenu(new EditorMouseEvent(e, false, this.viewHelper.viewDomNode), false)));
}
onTap(event) {
event.preventDefault();
this.viewHelper.focusTextArea();
const target = this._createMouseTarget(new EditorMouseEvent(event, false, this.viewHelper.viewDomNode), false);
if (target.position) {
// Send the tap event also to the <textarea> (for input purposes)
const event = document.createEvent('CustomEvent');
event.initEvent(TextAreaSyntethicEvents.Tap, false, true);
this.viewHelper.dispatchTextAreaEvent(event);
this.viewController.moveTo(target.position, 1 /* NavigationCommandRevealType.Minimal */);
}
}
onChange(e) {
this._context.viewModel.viewLayout.deltaScrollNow(-e.translationX, -e.translationY);
}
}
class PointerHandler extends Disposable {
constructor(context, viewController, viewHelper) {
super();
const isPhone = isIOS || (isAndroid && isMobile);
if (isPhone && BrowserFeatures.pointerEvents) {
this.handler = this._register(new PointerEventHandler(context, viewController, viewHelper));
}
else if (mainWindow.TouchEvent) {
this.handler = this._register(new TouchHandler(context, viewController, viewHelper));
}
else {
this.handler = this._register(new MouseHandler(context, viewController, viewHelper));
}
}
getTargetAtClientPoint(clientX, clientY) {
return this.handler.getTargetAtClientPoint(clientX, clientY);
}
}
export { PointerEventHandler, PointerHandler };
@@ -0,0 +1 @@
export {}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,80 @@
import { DataTransfers } from '../../base/browser/dnd.js';
import { VSDataTransfer, createStringDataTransferItem, UriList, createFileDataTransferItem } from '../../base/common/dataTransfer.js';
import { Mimes } from '../../base/common/mime.js';
import { URI } from '../../base/common/uri.js';
import { getPathForFile, CodeDataTransfers } from '../../platform/dnd/browser/dnd.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
function toVSDataTransfer(dataTransfer) {
const vsDataTransfer = new VSDataTransfer();
for (const item of dataTransfer.items) {
const type = item.type;
if (item.kind === 'string') {
const asStringValue = new Promise(resolve => item.getAsString(resolve));
vsDataTransfer.append(type, createStringDataTransferItem(asStringValue));
}
else if (item.kind === 'file') {
const file = item.getAsFile();
if (file) {
vsDataTransfer.append(type, createFileDataTransferItemFromFile(file));
}
}
}
return vsDataTransfer;
}
function createFileDataTransferItemFromFile(file) {
const path = getPathForFile(file);
const uri = path ? URI.parse(path) : undefined;
return createFileDataTransferItem(file.name, uri, async () => {
return new Uint8Array(await file.arrayBuffer());
});
}
const INTERNAL_DND_MIME_TYPES = Object.freeze([
CodeDataTransfers.EDITORS,
CodeDataTransfers.FILES,
DataTransfers.RESOURCES,
DataTransfers.INTERNAL_URI_LIST,
]);
function toExternalVSDataTransfer(sourceDataTransfer, overwriteUriList = false) {
const vsDataTransfer = toVSDataTransfer(sourceDataTransfer);
// Try to expose the internal uri-list type as the standard type
const uriList = vsDataTransfer.get(DataTransfers.INTERNAL_URI_LIST);
if (uriList) {
vsDataTransfer.replace(Mimes.uriList, uriList);
}
else {
if (overwriteUriList || !vsDataTransfer.has(Mimes.uriList)) {
// Otherwise, fallback to adding dragged resources to the uri list
const editorData = [];
for (const item of sourceDataTransfer.items) {
const file = item.getAsFile();
if (file) {
const path = getPathForFile(file);
try {
if (path) {
editorData.push(URI.file(path).toString());
}
else {
editorData.push(URI.parse(file.name, true).toString());
}
}
catch {
// Parsing failed. Leave out from list
}
}
}
if (editorData.length) {
vsDataTransfer.replace(Mimes.uriList, createStringDataTransferItem(UriList.create(editorData)));
}
}
}
for (const internal of INTERNAL_DND_MIME_TYPES) {
vsDataTransfer.delete(internal);
}
return vsDataTransfer;
}
export { toExternalVSDataTransfer, toVSDataTransfer };
@@ -0,0 +1,53 @@
import { EditorType } from '../common/editorCommon.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
*@internal
*/
function isCodeEditor(thing) {
if (thing && typeof thing.getEditorType === 'function') {
return thing.getEditorType() === EditorType.ICodeEditor;
}
else {
return false;
}
}
/**
*@internal
*/
function isDiffEditor(thing) {
if (thing && typeof thing.getEditorType === 'function') {
return thing.getEditorType() === EditorType.IDiffEditor;
}
else {
return false;
}
}
/**
*@internal
*/
function isCompositeEditor(thing) {
return !!thing
&& typeof thing === 'object'
&& typeof thing.onDidChangeActiveEditor === 'function';
}
/**
*@internal
*/
function getCodeEditor(thing) {
if (isCodeEditor(thing)) {
return thing;
}
if (isDiffEditor(thing)) {
return thing.getModifiedEditor();
}
if (isCompositeEditor(thing) && isCodeEditor(thing.activeCodeEditor)) {
return thing.activeCodeEditor;
}
return null;
}
export { getCodeEditor, isCodeEditor, isCompositeEditor, isDiffEditor };
+299
View File
@@ -0,0 +1,299 @@
import { getWindow, addDisposableListener, EventType, getDomNodePagePosition, addStandardDisposableListener, isInShadowDOM } from '../../base/browser/dom.js';
import { createStyleSheet } from '../../base/browser/domStylesheets.js';
import { GlobalPointerMoveMonitor } from '../../base/browser/globalPointerMoveMonitor.js';
import { StandardMouseEvent } from '../../base/browser/mouseEvent.js';
import { RunOnceScheduler } from '../../base/common/async.js';
import { Disposable, DisposableMap, DisposableStore } from '../../base/common/lifecycle.js';
import { asCssVariable } from '../../platform/theme/common/colorUtils.js';
import '../../platform/theme/common/colors/baseColors.js';
import '../../platform/theme/common/colors/chartsColors.js';
import '../../platform/theme/common/colors/editorColors.js';
import '../../platform/theme/common/colors/inputColors.js';
import '../../platform/theme/common/colors/listColors.js';
import '../../platform/theme/common/colors/menuColors.js';
import '../../platform/theme/common/colors/minimapColors.js';
import '../../platform/theme/common/colors/miscColors.js';
import '../../platform/theme/common/colors/quickpickColors.js';
import '../../platform/theme/common/colors/searchColors.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Coordinates relative to the whole document (e.g. mouse event's pageX and pageY)
*/
class PageCoordinates {
constructor(x, y) {
this.x = x;
this.y = y;
this._pageCoordinatesBrand = undefined;
}
toClientCoordinates(targetWindow) {
return new ClientCoordinates(this.x - targetWindow.scrollX, this.y - targetWindow.scrollY);
}
}
/**
* Coordinates within the application's client area (i.e. origin is document's scroll position).
*
* For example, clicking in the top-left corner of the client area will
* always result in a mouse event with a client.x value of 0, regardless
* of whether the page is scrolled horizontally.
*/
class ClientCoordinates {
constructor(clientX, clientY) {
this.clientX = clientX;
this.clientY = clientY;
this._clientCoordinatesBrand = undefined;
}
toPageCoordinates(targetWindow) {
return new PageCoordinates(this.clientX + targetWindow.scrollX, this.clientY + targetWindow.scrollY);
}
}
/**
* The position of the editor in the page.
*/
class EditorPagePosition {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this._editorPagePositionBrand = undefined;
}
}
/**
* Coordinates relative to the (top;left) of the editor that can be used safely with other internal editor metrics.
* **NOTE**: This position is obtained by taking page coordinates and transforming them relative to the
* editor's (top;left) position in a way in which scale transformations are taken into account.
* **NOTE**: These coordinates could be negative if the mouse position is outside the editor.
*/
class CoordinatesRelativeToEditor {
constructor(x, y) {
this.x = x;
this.y = y;
this._positionRelativeToEditorBrand = undefined;
}
}
function createEditorPagePosition(editorViewDomNode) {
const editorPos = getDomNodePagePosition(editorViewDomNode);
return new EditorPagePosition(editorPos.left, editorPos.top, editorPos.width, editorPos.height);
}
function createCoordinatesRelativeToEditor(editorViewDomNode, editorPagePosition, pos) {
// The editor's page position is read from the DOM using getBoundingClientRect().
//
// getBoundingClientRect() returns the actual dimensions, while offsetWidth and offsetHeight
// reflect the unscaled size. We can use this difference to detect a transform:scale()
// and we will apply the transformation in inverse to get mouse coordinates that make sense inside the editor.
//
// This could be expanded to cover rotation as well maybe by walking the DOM up from `editorViewDomNode`
// and computing the effective transformation matrix using getComputedStyle(element).transform.
//
const scaleX = editorPagePosition.width / editorViewDomNode.offsetWidth;
const scaleY = editorPagePosition.height / editorViewDomNode.offsetHeight;
// Adjust mouse offsets if editor appears to be scaled via transforms
const relativeX = (pos.x - editorPagePosition.x) / scaleX;
const relativeY = (pos.y - editorPagePosition.y) / scaleY;
return new CoordinatesRelativeToEditor(relativeX, relativeY);
}
class EditorMouseEvent extends StandardMouseEvent {
constructor(e, isFromPointerCapture, editorViewDomNode) {
super(getWindow(editorViewDomNode), e);
this._editorMouseEventBrand = undefined;
this.isFromPointerCapture = isFromPointerCapture;
this.pos = new PageCoordinates(this.posx, this.posy);
this.editorPos = createEditorPagePosition(editorViewDomNode);
this.relativePos = createCoordinatesRelativeToEditor(editorViewDomNode, this.editorPos, this.pos);
}
}
class EditorMouseEventFactory {
constructor(editorViewDomNode) {
this._editorViewDomNode = editorViewDomNode;
}
_create(e) {
return new EditorMouseEvent(e, false, this._editorViewDomNode);
}
onContextMenu(target, callback) {
return addDisposableListener(target, EventType.CONTEXT_MENU, (e) => {
callback(this._create(e));
});
}
onMouseUp(target, callback) {
return addDisposableListener(target, EventType.MOUSE_UP, (e) => {
callback(this._create(e));
});
}
onMouseDown(target, callback) {
return addDisposableListener(target, EventType.MOUSE_DOWN, (e) => {
callback(this._create(e));
});
}
onPointerDown(target, callback) {
return addDisposableListener(target, EventType.POINTER_DOWN, (e) => {
callback(this._create(e), e.pointerId);
});
}
onMouseLeave(target, callback) {
return addDisposableListener(target, EventType.MOUSE_LEAVE, (e) => {
callback(this._create(e));
});
}
onMouseMove(target, callback) {
return addDisposableListener(target, EventType.MOUSE_MOVE, (e) => callback(this._create(e)));
}
}
class EditorPointerEventFactory {
constructor(editorViewDomNode) {
this._editorViewDomNode = editorViewDomNode;
}
_create(e) {
return new EditorMouseEvent(e, false, this._editorViewDomNode);
}
onPointerUp(target, callback) {
return addDisposableListener(target, 'pointerup', (e) => {
callback(this._create(e));
});
}
onPointerDown(target, callback) {
return addDisposableListener(target, EventType.POINTER_DOWN, (e) => {
callback(this._create(e), e.pointerId);
});
}
onPointerLeave(target, callback) {
return addDisposableListener(target, EventType.POINTER_LEAVE, (e) => {
callback(this._create(e));
});
}
onPointerMove(target, callback) {
return addDisposableListener(target, 'pointermove', (e) => callback(this._create(e)));
}
}
class GlobalEditorPointerMoveMonitor extends Disposable {
constructor(editorViewDomNode) {
super();
this._editorViewDomNode = editorViewDomNode;
this._globalPointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
this._keydownListener = null;
}
startMonitoring(initialElement, pointerId, initialButtons, pointerMoveCallback, onStopCallback) {
// Add a <<capture>> keydown event listener that will cancel the monitoring
// if something other than a modifier key is pressed
this._keydownListener = addStandardDisposableListener(initialElement.ownerDocument, 'keydown', (e) => {
const chord = e.toKeyCodeChord();
if (chord.isModifierKey()) {
// Allow modifier keys
return;
}
this._globalPointerMoveMonitor.stopMonitoring(true, e.browserEvent);
}, true);
this._globalPointerMoveMonitor.startMonitoring(initialElement, pointerId, initialButtons, (e) => {
pointerMoveCallback(new EditorMouseEvent(e, true, this._editorViewDomNode));
}, (e) => {
this._keydownListener.dispose();
onStopCallback(e);
});
}
stopMonitoring() {
this._globalPointerMoveMonitor.stopMonitoring(true);
}
}
/**
* A helper to create dynamic css rules, bound to a class name.
* Rules are reused.
* Reference counting and delayed garbage collection ensure that no rules leak.
*/
class DynamicCssRules {
static { this._idPool = 0; }
constructor(_editor) {
this._editor = _editor;
this._instanceId = ++DynamicCssRules._idPool;
this._counter = 0;
this._rules = new DisposableMap();
// We delay garbage collection so that hanging rules can be reused.
this._garbageCollectionScheduler = new RunOnceScheduler(() => this.garbageCollect(), 1000);
}
dispose() {
this._rules.dispose();
this._garbageCollectionScheduler.dispose();
}
createClassNameRef(options) {
const rule = this.getOrCreateRule(options);
rule.increaseRefCount();
return {
className: rule.className,
dispose: () => {
rule.decreaseRefCount();
this._garbageCollectionScheduler.schedule();
}
};
}
getOrCreateRule(properties) {
const key = this.computeUniqueKey(properties);
let existingRule = this._rules.get(key);
if (!existingRule) {
const counter = this._counter++;
existingRule = new RefCountedCssRule(key, `dyn-rule-${this._instanceId}-${counter}`, isInShadowDOM(this._editor.getContainerDomNode())
? this._editor.getContainerDomNode()
: undefined, properties);
this._rules.set(key, existingRule);
}
return existingRule;
}
computeUniqueKey(properties) {
return JSON.stringify(properties);
}
garbageCollect() {
for (const rule of this._rules.values()) {
if (!rule.hasReferences()) {
this._rules.deleteAndDispose(rule.key);
}
}
}
}
class RefCountedCssRule {
constructor(key, className, _containerElement, properties) {
this.key = key;
this.className = className;
this.properties = properties;
this._referenceCount = 0;
this._styleElementDisposables = new DisposableStore();
this._styleElement = createStyleSheet(_containerElement, undefined, this._styleElementDisposables);
this._styleElement.textContent = this.getCssText(this.className, this.properties);
}
getCssText(className, properties) {
let str = `.${className} {`;
for (const prop in properties) {
const value = properties[prop];
let cssValue;
if (typeof value === 'object') {
cssValue = asCssVariable(value.id);
}
else {
cssValue = value;
}
const cssPropName = camelToDashes(prop);
str += `\n\t${cssPropName}: ${cssValue};`;
}
str += `\n}`;
return str;
}
dispose() {
this._styleElementDisposables.dispose();
this._styleElement = undefined;
}
increaseRefCount() {
this._referenceCount++;
}
decreaseRefCount() {
this._referenceCount--;
}
hasReferences() {
return this._referenceCount > 0;
}
}
function camelToDashes(str) {
return str.replace(/(^[A-Z])/, ([first]) => first.toLowerCase())
.replace(/([A-Z])/g, ([letter]) => `-${letter.toLowerCase()}`);
}
export { ClientCoordinates, CoordinatesRelativeToEditor, DynamicCssRules, EditorMouseEvent, EditorMouseEventFactory, EditorPagePosition, EditorPointerEventFactory, GlobalEditorPointerMoveMonitor, PageCoordinates, createCoordinatesRelativeToEditor, createEditorPagePosition };
@@ -0,0 +1,489 @@
import { localize } from '../../nls.js';
import { URI } from '../../base/common/uri.js';
import { ICodeEditorService } from './services/codeEditorService.js';
import { Position } from '../common/core/position.js';
import { IModelService } from '../common/services/model.js';
import { ITextModelService } from '../common/services/resolverService.js';
import { MenuId, MenuRegistry, Action2 } from '../../platform/actions/common/actions.js';
import { CommandsRegistry } from '../../platform/commands/common/commands.js';
import { IContextKeyService, ContextKeyExpr } from '../../platform/contextkey/common/contextkey.js';
import { IInstantiationService } from '../../platform/instantiation/common/instantiation.js';
import { KeybindingsRegistry } from '../../platform/keybinding/common/keybindingsRegistry.js';
import { Registry } from '../../platform/registry/common/platform.js';
import { ITelemetryService } from '../../platform/telemetry/common/telemetry.js';
import { assertType } from '../../base/common/types.js';
import { ILogService } from '../../platform/log/common/log.js';
import { getActiveElement } from '../../base/browser/dom.js';
import { TriggerInlineEditCommandsRegistry } from './triggerInlineEditCommandsRegistry.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class Command {
constructor(opts) {
this.id = opts.id;
this.precondition = opts.precondition;
this._kbOpts = opts.kbOpts;
this._menuOpts = opts.menuOpts;
this.metadata = opts.metadata;
this.canTriggerInlineEdits = opts.canTriggerInlineEdits;
}
register() {
if (Array.isArray(this._menuOpts)) {
this._menuOpts.forEach(this._registerMenuItem, this);
}
else if (this._menuOpts) {
this._registerMenuItem(this._menuOpts);
}
if (this._kbOpts) {
const kbOptsArr = Array.isArray(this._kbOpts) ? this._kbOpts : [this._kbOpts];
for (const kbOpts of kbOptsArr) {
let kbWhen = kbOpts.kbExpr;
if (this.precondition) {
if (kbWhen) {
kbWhen = ContextKeyExpr.and(kbWhen, this.precondition);
}
else {
kbWhen = this.precondition;
}
}
const desc = {
id: this.id,
weight: kbOpts.weight,
args: kbOpts.args,
when: kbWhen,
primary: kbOpts.primary,
secondary: kbOpts.secondary,
win: kbOpts.win,
linux: kbOpts.linux,
mac: kbOpts.mac,
};
KeybindingsRegistry.registerKeybindingRule(desc);
}
}
CommandsRegistry.registerCommand({
id: this.id,
handler: (accessor, args) => this.runCommand(accessor, args),
metadata: this.metadata
});
if (this.canTriggerInlineEdits) {
TriggerInlineEditCommandsRegistry.registerCommand(this.id);
}
}
_registerMenuItem(item) {
MenuRegistry.appendMenuItem(item.menuId, {
group: item.group,
command: {
id: this.id,
title: item.title,
icon: item.icon,
precondition: this.precondition
},
when: item.when,
order: item.order
});
}
}
class MultiCommand extends Command {
constructor() {
super(...arguments);
this._implementations = [];
}
/**
* A higher priority gets to be looked at first
*/
addImplementation(priority, name, implementation, when) {
this._implementations.push({ priority, name, implementation, when });
this._implementations.sort((a, b) => b.priority - a.priority);
return {
dispose: () => {
for (let i = 0; i < this._implementations.length; i++) {
if (this._implementations[i].implementation === implementation) {
this._implementations.splice(i, 1);
return;
}
}
}
};
}
runCommand(accessor, args) {
const logService = accessor.get(ILogService);
const contextKeyService = accessor.get(IContextKeyService);
logService.trace(`Executing Command '${this.id}' which has ${this._implementations.length} bound.`);
for (const impl of this._implementations) {
if (impl.when) {
const context = contextKeyService.getContext(getActiveElement());
const value = impl.when.evaluate(context);
if (!value) {
continue;
}
}
const result = impl.implementation(accessor, args);
if (result) {
logService.trace(`Command '${this.id}' was handled by '${impl.name}'.`);
if (typeof result === 'boolean') {
return;
}
return result;
}
}
logService.trace(`The Command '${this.id}' was not handled by any implementation.`);
}
}
//#endregion
/**
* A command that delegates to another command's implementation.
*
* This lets different commands be registered but share the same implementation
*/
class ProxyCommand extends Command {
constructor(command, opts) {
super(opts);
this.command = command;
}
runCommand(accessor, args) {
return this.command.runCommand(accessor, args);
}
}
class EditorCommand extends Command {
/**
* Create a command class that is bound to a certain editor contribution.
*/
static bindToContribution(controllerGetter) {
return class EditorControllerCommandImpl extends EditorCommand {
constructor(opts) {
super(opts);
this._callback = opts.handler;
}
runEditorCommand(accessor, editor, args) {
const controller = controllerGetter(editor);
if (controller) {
this._callback(controller, args);
}
}
};
}
static runEditorCommand(accessor, args, precondition, runner) {
const codeEditorService = accessor.get(ICodeEditorService);
// Find the editor with text focus or active
const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor();
if (!editor) {
// well, at least we tried...
return;
}
return editor.invokeWithinContext((editorAccessor) => {
const kbService = editorAccessor.get(IContextKeyService);
if (!kbService.contextMatchesRules(precondition ?? undefined)) {
// precondition does not hold
return;
}
return runner(editorAccessor, editor, args);
});
}
runCommand(accessor, args) {
return EditorCommand.runEditorCommand(accessor, args, this.precondition, (accessor, editor, args) => this.runEditorCommand(accessor, editor, args));
}
}
class EditorAction extends EditorCommand {
static convertOptions(opts) {
let menuOpts;
if (Array.isArray(opts.menuOpts)) {
menuOpts = opts.menuOpts;
}
else if (opts.menuOpts) {
menuOpts = [opts.menuOpts];
}
else {
menuOpts = [];
}
function withDefaults(item) {
if (!item.menuId) {
item.menuId = MenuId.EditorContext;
}
if (!item.title) {
item.title = typeof opts.label === 'string' ? opts.label : opts.label.value;
}
item.when = ContextKeyExpr.and(opts.precondition, item.when);
return item;
}
if (Array.isArray(opts.contextMenuOpts)) {
menuOpts.push(...opts.contextMenuOpts.map(withDefaults));
}
else if (opts.contextMenuOpts) {
menuOpts.push(withDefaults(opts.contextMenuOpts));
}
opts.menuOpts = menuOpts;
return opts;
}
constructor(opts) {
super(EditorAction.convertOptions(opts));
if (typeof opts.label === 'string') {
this.label = opts.label;
this.alias = opts.alias ?? opts.label;
}
else {
this.label = opts.label.value;
this.alias = opts.alias ?? opts.label.original;
}
}
runEditorCommand(accessor, editor, args) {
this.reportTelemetry(accessor, editor);
return this.run(accessor, editor, args || {});
}
reportTelemetry(accessor, editor) {
accessor.get(ITelemetryService).publicLog2('editorActionInvoked', { name: this.label, id: this.id });
}
}
class MultiEditorAction extends EditorAction {
constructor() {
super(...arguments);
this._implementations = [];
}
/**
* A higher priority gets to be looked at first
*/
addImplementation(priority, implementation) {
this._implementations.push([priority, implementation]);
this._implementations.sort((a, b) => b[0] - a[0]);
return {
dispose: () => {
for (let i = 0; i < this._implementations.length; i++) {
if (this._implementations[i][1] === implementation) {
this._implementations.splice(i, 1);
return;
}
}
}
};
}
run(accessor, editor, args) {
for (const impl of this._implementations) {
const result = impl[1](accessor, editor, args);
if (result) {
if (typeof result === 'boolean') {
return;
}
return result;
}
}
}
}
//#endregion EditorAction
//#region EditorAction2
class EditorAction2 extends Action2 {
run(accessor, ...args) {
// Find the editor with text focus or active
const codeEditorService = accessor.get(ICodeEditorService);
const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor();
if (!editor) {
// well, at least we tried...
return;
}
// precondition does hold
return editor.invokeWithinContext((editorAccessor) => {
const kbService = editorAccessor.get(IContextKeyService);
const logService = editorAccessor.get(ILogService);
const enabled = kbService.contextMatchesRules(this.desc.precondition ?? undefined);
if (!enabled) {
logService.debug(`[EditorAction2] NOT running command because its precondition is FALSE`, this.desc.id, this.desc.precondition?.serialize());
return;
}
return this.runEditorCommand(editorAccessor, editor, ...args);
});
}
}
//#endregion
// --- Registration of commands and actions
function registerModelAndPositionCommand(id, handler) {
CommandsRegistry.registerCommand(id, function (accessor, ...args) {
const instaService = accessor.get(IInstantiationService);
const [resource, position] = args;
assertType(URI.isUri(resource));
assertType(Position.isIPosition(position));
const model = accessor.get(IModelService).getModel(resource);
if (model) {
const editorPosition = Position.lift(position);
return instaService.invokeFunction(handler, model, editorPosition, ...args.slice(2));
}
return accessor.get(ITextModelService).createModelReference(resource).then(reference => {
return new Promise((resolve, reject) => {
try {
const result = instaService.invokeFunction(handler, reference.object.textEditorModel, Position.lift(position), args.slice(2));
resolve(result);
}
catch (err) {
reject(err);
}
}).finally(() => {
reference.dispose();
});
});
});
}
function registerEditorCommand(editorCommand) {
EditorContributionRegistry.INSTANCE.registerEditorCommand(editorCommand);
return editorCommand;
}
function registerEditorAction(ctor) {
const action = new ctor();
EditorContributionRegistry.INSTANCE.registerEditorAction(action);
return action;
}
function registerMultiEditorAction(action) {
EditorContributionRegistry.INSTANCE.registerEditorAction(action);
return action;
}
function registerInstantiatedEditorAction(editorAction) {
EditorContributionRegistry.INSTANCE.registerEditorAction(editorAction);
}
/**
* Registers an editor contribution. Editor contributions have a lifecycle which is bound
* to a specific code editor instance.
*/
function registerEditorContribution(id, ctor, instantiation) {
EditorContributionRegistry.INSTANCE.registerEditorContribution(id, ctor, instantiation);
}
var EditorExtensionsRegistry;
(function (EditorExtensionsRegistry) {
function getEditorCommand(commandId) {
return EditorContributionRegistry.INSTANCE.getEditorCommand(commandId);
}
EditorExtensionsRegistry.getEditorCommand = getEditorCommand;
function getEditorActions() {
return EditorContributionRegistry.INSTANCE.getEditorActions();
}
EditorExtensionsRegistry.getEditorActions = getEditorActions;
function getEditorContributions() {
return EditorContributionRegistry.INSTANCE.getEditorContributions();
}
EditorExtensionsRegistry.getEditorContributions = getEditorContributions;
function getSomeEditorContributions(ids) {
return EditorContributionRegistry.INSTANCE.getEditorContributions().filter(c => ids.indexOf(c.id) >= 0);
}
EditorExtensionsRegistry.getSomeEditorContributions = getSomeEditorContributions;
function getDiffEditorContributions() {
return EditorContributionRegistry.INSTANCE.getDiffEditorContributions();
}
EditorExtensionsRegistry.getDiffEditorContributions = getDiffEditorContributions;
})(EditorExtensionsRegistry || (EditorExtensionsRegistry = {}));
// Editor extension points
const Extensions = {
EditorCommonContributions: 'editor.contributions'
};
class EditorContributionRegistry {
static { this.INSTANCE = new EditorContributionRegistry(); }
constructor() {
this.editorContributions = [];
this.diffEditorContributions = [];
this.editorActions = [];
this.editorCommands = Object.create(null);
}
registerEditorContribution(id, ctor, instantiation) {
this.editorContributions.push({ id, ctor: ctor, instantiation });
}
getEditorContributions() {
return this.editorContributions.slice(0);
}
getDiffEditorContributions() {
return this.diffEditorContributions.slice(0);
}
registerEditorAction(action) {
action.register();
this.editorActions.push(action);
}
getEditorActions() {
return this.editorActions;
}
registerEditorCommand(editorCommand) {
editorCommand.register();
this.editorCommands[editorCommand.id] = editorCommand;
}
getEditorCommand(commandId) {
return (this.editorCommands[commandId] || null);
}
}
Registry.add(Extensions.EditorCommonContributions, EditorContributionRegistry.INSTANCE);
function registerCommand(command) {
command.register();
return command;
}
const UndoCommand = registerCommand(new MultiCommand({
id: 'undo',
precondition: undefined,
kbOpts: {
weight: 0 /* KeybindingWeight.EditorCore */,
primary: 2048 /* KeyMod.CtrlCmd */ | 56 /* KeyCode.KeyZ */
},
menuOpts: [{
menuId: MenuId.MenubarEditMenu,
group: '1_do',
title: localize(69, "&&Undo"),
order: 1
}, {
menuId: MenuId.CommandPalette,
group: '',
title: localize(70, "Undo"),
order: 1
}, {
menuId: MenuId.SimpleEditorContext,
group: '1_do',
title: localize(71, "Undo"),
order: 1
}]
}));
registerCommand(new ProxyCommand(UndoCommand, { id: 'default:undo', precondition: undefined }));
const RedoCommand = registerCommand(new MultiCommand({
id: 'redo',
precondition: undefined,
kbOpts: {
weight: 0 /* KeybindingWeight.EditorCore */,
primary: 2048 /* KeyMod.CtrlCmd */ | 55 /* KeyCode.KeyY */,
secondary: [2048 /* KeyMod.CtrlCmd */ | 1024 /* KeyMod.Shift */ | 56 /* KeyCode.KeyZ */],
mac: { primary: 2048 /* KeyMod.CtrlCmd */ | 1024 /* KeyMod.Shift */ | 56 /* KeyCode.KeyZ */ }
},
menuOpts: [{
menuId: MenuId.MenubarEditMenu,
group: '1_do',
title: localize(72, "&&Redo"),
order: 2
}, {
menuId: MenuId.CommandPalette,
group: '',
title: localize(73, "Redo"),
order: 1
}, {
menuId: MenuId.SimpleEditorContext,
group: '1_do',
title: localize(74, "Redo"),
order: 2
}]
}));
registerCommand(new ProxyCommand(RedoCommand, { id: 'default:redo', precondition: undefined }));
const SelectAllCommand = registerCommand(new MultiCommand({
id: 'editor.action.selectAll',
precondition: undefined,
kbOpts: {
weight: 0 /* KeybindingWeight.EditorCore */,
kbExpr: null,
primary: 2048 /* KeyMod.CtrlCmd */ | 31 /* KeyCode.KeyA */
},
menuOpts: [{
menuId: MenuId.MenubarSelectionMenu,
group: '1_basic',
title: localize(75, "&&Select All"),
order: 1
}, {
menuId: MenuId.CommandPalette,
group: '',
title: localize(76, "Select All"),
order: 1
}, {
menuId: MenuId.SimpleEditorContext,
group: '9_select',
title: localize(77, "Select All"),
order: 1
}]
}));
export { Command, EditorAction, EditorAction2, EditorCommand, EditorExtensionsRegistry, MultiCommand, MultiEditorAction, ProxyCommand, RedoCommand, SelectAllCommand, UndoCommand, registerEditorAction, registerEditorCommand, registerEditorContribution, registerInstantiatedEditorAction, registerModelAndPositionCommand, registerMultiEditorAction };
@@ -0,0 +1,179 @@
import { getActiveWindow } from '../../../../base/browser/dom.js';
import { BugIndicatingError } from '../../../../base/common/errors.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable, MutableDisposable, toDisposable, dispose } from '../../../../base/common/lifecycle.js';
import { NKeyMap } from '../../../../base/common/map.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { GlyphRasterizer } from '../raster/glyphRasterizer.js';
import { IdleTaskQueue } from '../taskQueue.js';
import { TextureAtlasPage } from './textureAtlasPage.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var TextureAtlas_1;
let TextureAtlas = class TextureAtlas extends Disposable {
static { TextureAtlas_1 = this; }
/**
* The maximum number of texture atlas pages. This is currently a hard static cap that must not
* be reached.
*/
static { this.maximumPageCount = 16; }
get pages() { return this._pages; }
constructor(
/** The maximum texture size supported by the GPU. */
_maxTextureSize, options, _decorationStyleCache, _themeService, _instantiationService) {
super();
this._maxTextureSize = _maxTextureSize;
this._decorationStyleCache = _decorationStyleCache;
this._themeService = _themeService;
this._instantiationService = _instantiationService;
this._warmUpTask = this._register(new MutableDisposable());
this._warmedUpRasterizers = new Set();
/**
* The main texture atlas pages which are both larger textures and more efficiently packed
* relative to the scratch page. The idea is the main pages are drawn to and uploaded to the GPU
* much less frequently so as to not drop frames.
*/
this._pages = [];
/**
* A maps of glyph keys to the page to start searching for the glyph. This is set before
* searching to have as little runtime overhead (branching, intermediate variables) as possible,
* so it is not guaranteed to be the actual page the glyph is on. But it is guaranteed that all
* pages with a lower index do not contain the glyph.
*/
this._glyphPageIndex = new NKeyMap();
this._onDidDeleteGlyphs = this._register(new Emitter());
this.onDidDeleteGlyphs = this._onDidDeleteGlyphs.event;
this._allocatorType = options?.allocatorType ?? 'slab';
this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => {
if (this._colorMap) {
this.clear();
}
this._colorMap = this._themeService.getColorTheme().tokenColorMap;
}));
const dprFactor = Math.max(1, Math.floor(getActiveWindow().devicePixelRatio));
this.pageSize = Math.min(1024 * dprFactor, this._maxTextureSize);
this._initFirstPage();
this._register(toDisposable(() => dispose(this._pages)));
}
_initFirstPage() {
const firstPage = this._instantiationService.createInstance(TextureAtlasPage, 0, this.pageSize, this._allocatorType);
this._pages.push(firstPage);
// IMPORTANT: The first glyph on the first page must be an empty glyph such that zeroed out
// cells end up rendering nothing
// TODO: This currently means the first slab is for 0x0 glyphs and is wasted
const nullRasterizer = new GlyphRasterizer(1, '', 1, this._decorationStyleCache);
firstPage.getGlyph(nullRasterizer, '', 0, 0);
nullRasterizer.dispose();
}
clear() {
// Clear all pages
for (const page of this._pages) {
page.dispose();
}
this._pages.length = 0;
this._glyphPageIndex.clear();
this._warmedUpRasterizers.clear();
this._warmUpTask.clear();
// Recreate first
this._initFirstPage();
// Tell listeners
this._onDidDeleteGlyphs.fire();
}
getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId, x) {
// TODO: Encode font size and family into key
// Ignore metadata that doesn't affect the glyph
tokenMetadata &= -2048;
// Add x offset for sub-pixel rendering to the unused portion or tokenMetadata. This
// converts the decimal part of the x to a range from 0 to 9, where 0 = 0.0px x offset,
// 9 = 0.9px x offset
tokenMetadata |= Math.floor((x % 1) * 10);
// Warm up common glyphs
if (!this._warmedUpRasterizers.has(rasterizer.id)) {
this._warmUpAtlas(rasterizer);
this._warmedUpRasterizers.add(rasterizer.id);
}
// Try get the glyph, overflowing to a new page if necessary
return this._tryGetGlyph(this._glyphPageIndex.get(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey) ?? 0, rasterizer, chars, tokenMetadata, decorationStyleSetId);
}
_tryGetGlyph(pageIndex, rasterizer, chars, tokenMetadata, decorationStyleSetId) {
this._glyphPageIndex.set(pageIndex, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey);
return (this._pages[pageIndex].getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId)
?? (pageIndex + 1 < this._pages.length
? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, tokenMetadata, decorationStyleSetId)
: undefined)
?? this._getGlyphFromNewPage(rasterizer, chars, tokenMetadata, decorationStyleSetId));
}
_getGlyphFromNewPage(rasterizer, chars, tokenMetadata, decorationStyleSetId) {
if (this._pages.length >= TextureAtlas_1.maximumPageCount) {
throw new Error(`Attempt to create a texture atlas page past the limit ${TextureAtlas_1.maximumPageCount}`);
}
this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, this._pages.length, this.pageSize, this._allocatorType));
this._glyphPageIndex.set(this._pages.length - 1, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey);
return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId);
}
getStats() {
return this._pages.map(e => e.getStats());
}
/**
* Warms up the atlas by rasterizing all printable ASCII characters for each token color. This
* is distrubuted over multiple idle callbacks to avoid blocking the main thread.
*/
_warmUpAtlas(rasterizer) {
const colorMap = this._colorMap;
if (!colorMap) {
throw new BugIndicatingError('Cannot warm atlas without color map');
}
this._warmUpTask.value?.clear();
const taskQueue = this._warmUpTask.value = this._instantiationService.createInstance(IdleTaskQueue);
// Warm up using roughly the larger glyphs first to help optimize atlas allocation
// A-Z
for (let code = 65 /* CharCode.A */; code <= 90 /* CharCode.Z */; code++) {
for (const fgColor of colorMap.keys()) {
taskQueue.enqueue(() => {
for (let x = 0; x < 1; x += 0.1) {
this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << 15 /* MetadataConsts.FOREGROUND_OFFSET */) & 16744448 /* MetadataConsts.FOREGROUND_MASK */, 0, x);
}
});
}
}
// a-z
for (let code = 97 /* CharCode.a */; code <= 122 /* CharCode.z */; code++) {
for (const fgColor of colorMap.keys()) {
taskQueue.enqueue(() => {
for (let x = 0; x < 1; x += 0.1) {
this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << 15 /* MetadataConsts.FOREGROUND_OFFSET */) & 16744448 /* MetadataConsts.FOREGROUND_MASK */, 0, x);
}
});
}
}
// Remaining ascii
for (let code = 33 /* CharCode.ExclamationMark */; code <= 126 /* CharCode.Tilde */; code++) {
for (const fgColor of colorMap.keys()) {
taskQueue.enqueue(() => {
for (let x = 0; x < 1; x += 0.1) {
this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << 15 /* MetadataConsts.FOREGROUND_OFFSET */) & 16744448 /* MetadataConsts.FOREGROUND_MASK */, 0, x);
}
});
}
}
}
};
TextureAtlas = TextureAtlas_1 = __decorate([
__param(3, IThemeService),
__param(4, IInstantiationService)
], TextureAtlas);
export { TextureAtlas };
@@ -0,0 +1,110 @@
import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { NKeyMap } from '../../../../base/common/map.js';
import { LogLevel, ILogService } from '../../../../platform/log/common/log.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { TextureAtlasShelfAllocator } from './textureAtlasShelfAllocator.js';
import { TextureAtlasSlabAllocator } from './textureAtlasSlabAllocator.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var TextureAtlasPage_1;
let TextureAtlasPage = class TextureAtlasPage extends Disposable {
static { TextureAtlasPage_1 = this; }
get version() { return this._version; }
/**
* The maximum number of glyphs that can be drawn to the page. This is currently a hard static
* cap that must not be reached as it will cause the GPU buffer to overflow.
*/
static { this.maximumGlyphCount = 5_000; }
get usedArea() { return this._usedArea; }
get source() { return this._canvas; }
get glyphs() {
return this._glyphInOrderSet.values();
}
constructor(textureIndex, pageSize, allocatorType, _logService, themeService) {
super();
this._logService = _logService;
this._version = 0;
this._usedArea = { left: 0, top: 0, right: 0, bottom: 0 };
this._glyphMap = new NKeyMap();
this._glyphInOrderSet = new Set();
this._canvas = new OffscreenCanvas(pageSize, pageSize);
this._colorMap = themeService.getColorTheme().tokenColorMap;
switch (allocatorType) {
case 'shelf':
this._allocator = new TextureAtlasShelfAllocator(this._canvas, textureIndex);
break;
case 'slab':
this._allocator = new TextureAtlasSlabAllocator(this._canvas, textureIndex);
break;
default:
this._allocator = allocatorType(this._canvas, textureIndex);
break;
}
// Reduce impact of a memory leak if this object is not released
this._register(toDisposable(() => {
this._canvas.width = 1;
this._canvas.height = 1;
}));
}
getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId) {
// IMPORTANT: There are intentionally no intermediate variables here to aid in runtime
// optimization as it's a very hot function
return this._glyphMap.get(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey) ?? this._createGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId);
}
_createGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId) {
// Ensure the glyph can fit on the page
if (this._glyphInOrderSet.size >= TextureAtlasPage_1.maximumGlyphCount) {
return undefined;
}
// Rasterize and allocate the glyph
const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, tokenMetadata, decorationStyleSetId, this._colorMap);
const glyph = this._allocator.allocate(rasterizedGlyph);
// Ensure the glyph was allocated
if (glyph === undefined) {
// TODO: undefined here can mean the glyph was too large for a slab on the page, this
// can lead to big problems if we don't handle it properly https://github.com/microsoft/vscode/issues/232984
return undefined;
}
// Save the glyph
this._glyphMap.set(glyph, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey);
this._glyphInOrderSet.add(glyph);
// Update page version and it's tracked used area
this._version++;
this._usedArea.right = Math.max(this._usedArea.right, glyph.x + glyph.w - 1);
this._usedArea.bottom = Math.max(this._usedArea.bottom, glyph.y + glyph.h - 1);
if (this._logService.getLevel() === LogLevel.Trace) {
this._logService.trace('New glyph', {
chars,
tokenMetadata,
decorationStyleSetId,
rasterizedGlyph,
glyph
});
}
return glyph;
}
getUsagePreview() {
return this._allocator.getUsagePreview();
}
getStats() {
return this._allocator.getStats();
}
};
TextureAtlasPage = TextureAtlasPage_1 = __decorate([
__param(3, ILogService),
__param(4, IThemeService)
], TextureAtlasPage);
export { TextureAtlasPage };
@@ -0,0 +1,129 @@
import { BugIndicatingError } from '../../../../base/common/errors.js';
import { ensureNonNullable } from '../gpuUtils.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* The shelf allocator is a simple allocator that places glyphs in rows, starting a new row when the
* current row is full. Due to its simplicity, it can waste space but it is very fast.
*/
class TextureAtlasShelfAllocator {
constructor(_canvas, _textureIndex) {
this._canvas = _canvas;
this._textureIndex = _textureIndex;
this._currentRow = {
x: 0,
y: 0,
h: 0
};
/** A set of all glyphs allocated, this is only tracked to enable debug related functionality */
this._allocatedGlyphs = new Set();
this._nextIndex = 0;
this._ctx = ensureNonNullable(this._canvas.getContext('2d', {
willReadFrequently: true
}));
}
allocate(rasterizedGlyph) {
// The glyph does not fit into the atlas page
const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1;
const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1;
if (glyphWidth > this._canvas.width || glyphHeight > this._canvas.height) {
throw new BugIndicatingError('Glyph is too large for the atlas page');
}
// Finalize and increment row if it doesn't fix horizontally
if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1 > this._canvas.width - this._currentRow.x) {
this._currentRow.x = 0;
this._currentRow.y += this._currentRow.h;
this._currentRow.h = 1;
}
// Return undefined if there isn't any room left
if (this._currentRow.y + rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1 > this._canvas.height) {
return undefined;
}
// Draw glyph
this._ctx.drawImage(rasterizedGlyph.source,
// source
rasterizedGlyph.boundingBox.left, rasterizedGlyph.boundingBox.top, glyphWidth, glyphHeight,
// destination
this._currentRow.x, this._currentRow.y, glyphWidth, glyphHeight);
// Create glyph object
const glyph = {
pageIndex: this._textureIndex,
glyphIndex: this._nextIndex++,
x: this._currentRow.x,
y: this._currentRow.y,
w: glyphWidth,
h: glyphHeight,
originOffsetX: rasterizedGlyph.originOffset.x,
originOffsetY: rasterizedGlyph.originOffset.y,
fontBoundingBoxAscent: rasterizedGlyph.fontBoundingBoxAscent,
fontBoundingBoxDescent: rasterizedGlyph.fontBoundingBoxDescent,
};
// Shift current row
this._currentRow.x += glyphWidth;
this._currentRow.h = Math.max(this._currentRow.h, glyphHeight);
// Set the glyph
this._allocatedGlyphs.add(glyph);
return glyph;
}
getUsagePreview() {
const w = this._canvas.width;
const h = this._canvas.height;
const canvas = new OffscreenCanvas(w, h);
const ctx = ensureNonNullable(canvas.getContext('2d'));
ctx.fillStyle = "#808080" /* UsagePreviewColors.Unused */;
ctx.fillRect(0, 0, w, h);
const rowHeight = new Map(); // y -> h
const rowWidth = new Map(); // y -> w
for (const g of this._allocatedGlyphs) {
rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h));
rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w));
}
for (const g of this._allocatedGlyphs) {
ctx.fillStyle = "#4040FF" /* UsagePreviewColors.Used */;
ctx.fillRect(g.x, g.y, g.w, g.h);
ctx.fillStyle = "#FF0000" /* UsagePreviewColors.Wasted */;
ctx.fillRect(g.x, g.y + g.h, g.w, rowHeight.get(g.y) - g.h);
}
for (const [rowY, rowW] of rowWidth.entries()) {
if (rowY !== this._currentRow.y) {
ctx.fillStyle = "#FF0000" /* UsagePreviewColors.Wasted */;
ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY));
}
}
return canvas.convertToBlob();
}
getStats() {
const w = this._canvas.width;
const h = this._canvas.height;
let usedPixels = 0;
let wastedPixels = 0;
const totalPixels = w * h;
const rowHeight = new Map(); // y -> h
const rowWidth = new Map(); // y -> w
for (const g of this._allocatedGlyphs) {
rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h));
rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w));
}
for (const g of this._allocatedGlyphs) {
usedPixels += g.w * g.h;
wastedPixels += g.w * (rowHeight.get(g.y) - g.h);
}
for (const [rowY, rowW] of rowWidth.entries()) {
if (rowY !== this._currentRow.y) {
wastedPixels += (w - rowW) * rowHeight.get(rowY);
}
}
return [
`page${this._textureIndex}:`,
` Total: ${totalPixels} (${w}x${h})`,
` Used: ${usedPixels} (${((usedPixels / totalPixels) * 100).toPrecision(2)}%)`,
` Wasted: ${wastedPixels} (${((wastedPixels / totalPixels) * 100).toPrecision(2)}%)`,
`Efficiency: ${((usedPixels / (usedPixels + wastedPixels)) * 100).toPrecision(2)}%`,
].join('\n');
}
}
export { TextureAtlasShelfAllocator };
@@ -0,0 +1,346 @@
import { getActiveWindow } from '../../../../base/browser/dom.js';
import { BugIndicatingError } from '../../../../base/common/errors.js';
import { NKeyMap } from '../../../../base/common/map.js';
import { ensureNonNullable } from '../gpuUtils.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* The slab allocator is a more complex allocator that places glyphs in square slabs of a fixed
* size. Slabs are defined by a small range of glyphs sizes they can house, this places like-sized
* glyphs in the same slab which reduces wasted space.
*
* Slabs also may contain "unused" regions on the left and bottom depending on the size of the
* glyphs they include. This space is used to place very thin or short glyphs, which would otherwise
* waste a lot of space in their own slab.
*/
class TextureAtlasSlabAllocator {
constructor(_canvas, _textureIndex, options) {
this._canvas = _canvas;
this._textureIndex = _textureIndex;
this._slabs = [];
this._activeSlabsByDims = new NKeyMap();
this._unusedRects = [];
this._openRegionsByHeight = new Map();
this._openRegionsByWidth = new Map();
/** A set of all glyphs allocated, this is only tracked to enable debug related functionality */
this._allocatedGlyphs = new Set();
this._nextIndex = 0;
this._ctx = ensureNonNullable(this._canvas.getContext('2d', {
willReadFrequently: true
}));
this._slabW = Math.min(options?.slabW ?? (64 << Math.max(Math.floor(getActiveWindow().devicePixelRatio) - 1, 0)), this._canvas.width);
this._slabH = Math.min(options?.slabH ?? this._slabW, this._canvas.height);
this._slabsPerRow = Math.floor(this._canvas.width / this._slabW);
this._slabsPerColumn = Math.floor(this._canvas.height / this._slabH);
}
allocate(rasterizedGlyph) {
// Find ideal slab, creating it if there is none suitable
const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1;
const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1;
// The glyph does not fit into the atlas page, glyphs should never be this large in practice
if (glyphWidth > this._canvas.width || glyphHeight > this._canvas.height) {
throw new BugIndicatingError('Glyph is too large for the atlas page');
}
// The glyph does not fit into a slab
if (glyphWidth > this._slabW || glyphHeight > this._slabH) {
// Only if this is the allocator's first glyph, resize the slab size to fit the glyph.
if (this._allocatedGlyphs.size > 0) {
return undefined;
}
// Find the largest power of 2 devisor that the glyph fits into, this ensure there is no
// wasted space outside the allocated slabs.
let sizeCandidate = this._canvas.width;
while (glyphWidth < sizeCandidate / 2 && glyphHeight < sizeCandidate / 2) {
sizeCandidate /= 2;
}
this._slabW = sizeCandidate;
this._slabH = sizeCandidate;
this._slabsPerRow = Math.floor(this._canvas.width / this._slabW);
this._slabsPerColumn = Math.floor(this._canvas.height / this._slabH);
}
// const dpr = getActiveWindow().devicePixelRatio;
// TODO: Include font size as well as DPR in nearestXPixels calculation
// Round slab glyph dimensions to the nearest x pixels, where x scaled with device pixel ratio
// const nearestXPixels = Math.max(1, Math.floor(dpr / 0.5));
// const nearestXPixels = Math.max(1, Math.floor(dpr));
const desiredSlabSize = {
// Nearest square number
// TODO: This can probably be optimized
// w: 1 << Math.ceil(Math.sqrt(glyphWidth)),
// h: 1 << Math.ceil(Math.sqrt(glyphHeight)),
// Nearest x px
// w: Math.ceil(glyphWidth / nearestXPixels) * nearestXPixels,
// h: Math.ceil(glyphHeight / nearestXPixels) * nearestXPixels,
// Round odd numbers up
// w: glyphWidth % 0 === 1 ? glyphWidth + 1 : glyphWidth,
// h: glyphHeight % 0 === 1 ? glyphHeight + 1 : glyphHeight,
// Exact number only
w: glyphWidth,
h: glyphHeight,
};
// Get any existing slab
let slab = this._activeSlabsByDims.get(desiredSlabSize.w, desiredSlabSize.h);
// Check if the slab is full
if (slab) {
const glyphsPerSlab = Math.floor(this._slabW / slab.entryW) * Math.floor(this._slabH / slab.entryH);
if (slab.count >= glyphsPerSlab) {
slab = undefined;
}
}
let dx;
let dy;
// Search for suitable space in unused rectangles
if (!slab) {
// Only check availability for the smallest side
if (glyphWidth < glyphHeight) {
const openRegions = this._openRegionsByWidth.get(glyphWidth);
if (openRegions?.length) {
// TODO: Don't search everything?
// Search from the end so we can typically pop it off the stack
for (let i = openRegions.length - 1; i >= 0; i--) {
const r = openRegions[i];
if (r.w >= glyphWidth && r.h >= glyphHeight) {
dx = r.x;
dy = r.y;
if (glyphWidth < r.w) {
this._unusedRects.push({
x: r.x + glyphWidth,
y: r.y,
w: r.w - glyphWidth,
h: glyphHeight
});
}
r.y += glyphHeight;
r.h -= glyphHeight;
if (r.h === 0) {
if (i === openRegions.length - 1) {
openRegions.pop();
}
else {
this._unusedRects.splice(i, 1);
}
}
break;
}
}
}
}
else {
const openRegions = this._openRegionsByHeight.get(glyphHeight);
if (openRegions?.length) {
// TODO: Don't search everything?
// Search from the end so we can typically pop it off the stack
for (let i = openRegions.length - 1; i >= 0; i--) {
const r = openRegions[i];
if (r.w >= glyphWidth && r.h >= glyphHeight) {
dx = r.x;
dy = r.y;
if (glyphHeight < r.h) {
this._unusedRects.push({
x: r.x,
y: r.y + glyphHeight,
w: glyphWidth,
h: r.h - glyphHeight
});
}
r.x += glyphWidth;
r.w -= glyphWidth;
if (r.h === 0) {
if (i === openRegions.length - 1) {
openRegions.pop();
}
else {
this._unusedRects.splice(i, 1);
}
}
break;
}
}
}
}
}
// Create a new slab
if (dx === undefined || dy === undefined) {
if (!slab) {
if (this._slabs.length >= this._slabsPerRow * this._slabsPerColumn) {
return undefined;
}
slab = {
x: Math.floor(this._slabs.length % this._slabsPerRow) * this._slabW,
y: Math.floor(this._slabs.length / this._slabsPerRow) * this._slabH,
entryW: desiredSlabSize.w,
entryH: desiredSlabSize.h,
count: 0
};
// Track unused regions to use for small glyphs
// +-------------+----+
// | | |
// | | | <- Unused W region
// | | |
// |-------------+----+
// | | <- Unused H region
// +------------------+
const unusedW = this._slabW % slab.entryW;
const unusedH = this._slabH % slab.entryH;
if (unusedW) {
addEntryToMapArray(this._openRegionsByWidth, unusedW, {
x: slab.x + this._slabW - unusedW,
w: unusedW,
y: slab.y,
h: this._slabH - (unusedH ?? 0)
});
}
if (unusedH) {
addEntryToMapArray(this._openRegionsByHeight, unusedH, {
x: slab.x,
w: this._slabW,
y: slab.y + this._slabH - unusedH,
h: unusedH
});
}
this._slabs.push(slab);
this._activeSlabsByDims.set(slab, desiredSlabSize.w, desiredSlabSize.h);
}
const glyphsPerRow = Math.floor(this._slabW / slab.entryW);
dx = slab.x + Math.floor(slab.count % glyphsPerRow) * slab.entryW;
dy = slab.y + Math.floor(slab.count / glyphsPerRow) * slab.entryH;
// Shift current row
slab.count++;
}
// Draw glyph
this._ctx.drawImage(rasterizedGlyph.source,
// source
rasterizedGlyph.boundingBox.left, rasterizedGlyph.boundingBox.top, glyphWidth, glyphHeight,
// destination
dx, dy, glyphWidth, glyphHeight);
// Create glyph object
const glyph = {
pageIndex: this._textureIndex,
glyphIndex: this._nextIndex++,
x: dx,
y: dy,
w: glyphWidth,
h: glyphHeight,
originOffsetX: rasterizedGlyph.originOffset.x,
originOffsetY: rasterizedGlyph.originOffset.y,
fontBoundingBoxAscent: rasterizedGlyph.fontBoundingBoxAscent,
fontBoundingBoxDescent: rasterizedGlyph.fontBoundingBoxDescent,
};
// Set the glyph
this._allocatedGlyphs.add(glyph);
return glyph;
}
getUsagePreview() {
const w = this._canvas.width;
const h = this._canvas.height;
const canvas = new OffscreenCanvas(w, h);
const ctx = ensureNonNullable(canvas.getContext('2d'));
ctx.fillStyle = "#808080" /* UsagePreviewColors.Unused */;
ctx.fillRect(0, 0, w, h);
let slabEntryPixels = 0;
let usedPixels = 0;
let restrictedPixels = 0;
const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1);
const slabH = slabW;
// Draw wasted underneath glyphs first
for (const slab of this._slabs) {
let x = 0;
let y = 0;
for (let i = 0; i < slab.count; i++) {
if (x + slab.entryW > slabW) {
x = 0;
y += slab.entryH;
}
ctx.fillStyle = "#FF0000" /* UsagePreviewColors.Wasted */;
ctx.fillRect(slab.x + x, slab.y + y, slab.entryW, slab.entryH);
slabEntryPixels += slab.entryW * slab.entryH;
x += slab.entryW;
}
const entriesPerRow = Math.floor(slabW / slab.entryW);
const entriesPerCol = Math.floor(slabH / slab.entryH);
slab.entryW * entriesPerRow * slab.entryH * entriesPerCol;
}
// Draw glyphs
for (const g of this._allocatedGlyphs) {
usedPixels += g.w * g.h;
ctx.fillStyle = "#4040FF" /* UsagePreviewColors.Used */;
ctx.fillRect(g.x, g.y, g.w, g.h);
}
// Draw unused space on side
const unusedRegions = Array.from(this._openRegionsByWidth.values()).flat().concat(Array.from(this._openRegionsByHeight.values()).flat());
for (const r of unusedRegions) {
ctx.fillStyle = "#FF000088" /* UsagePreviewColors.Restricted */;
ctx.fillRect(r.x, r.y, r.w, r.h);
restrictedPixels += r.w * r.h;
}
// Overlay actual glyphs on top
ctx.globalAlpha = 0.5;
ctx.drawImage(this._canvas, 0, 0);
ctx.globalAlpha = 1;
return canvas.convertToBlob();
}
getStats() {
const w = this._canvas.width;
const h = this._canvas.height;
let slabEntryPixels = 0;
let usedPixels = 0;
let slabEdgePixels = 0;
let wastedPixels = 0;
let restrictedPixels = 0;
const totalPixels = w * h;
const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1);
const slabH = slabW;
// Draw wasted underneath glyphs first
for (const slab of this._slabs) {
let x = 0;
let y = 0;
for (let i = 0; i < slab.count; i++) {
if (x + slab.entryW > slabW) {
x = 0;
y += slab.entryH;
}
slabEntryPixels += slab.entryW * slab.entryH;
x += slab.entryW;
}
const entriesPerRow = Math.floor(slabW / slab.entryW);
const entriesPerCol = Math.floor(slabH / slab.entryH);
const thisSlabPixels = slab.entryW * entriesPerRow * slab.entryH * entriesPerCol;
slabEdgePixels += (slabW * slabH) - thisSlabPixels;
}
// Draw glyphs
for (const g of this._allocatedGlyphs) {
usedPixels += g.w * g.h;
}
// Draw unused space on side
const unusedRegions = Array.from(this._openRegionsByWidth.values()).flat().concat(Array.from(this._openRegionsByHeight.values()).flat());
for (const r of unusedRegions) {
restrictedPixels += r.w * r.h;
}
const edgeUsedPixels = slabEdgePixels - restrictedPixels;
wastedPixels = slabEntryPixels - (usedPixels - edgeUsedPixels);
// usedPixels += slabEdgePixels - restrictedPixels;
const efficiency = usedPixels / (usedPixels + wastedPixels + restrictedPixels);
return [
`page[${this._textureIndex}]:`,
` Total: ${totalPixels}px (${w}x${h})`,
` Used: ${usedPixels}px (${((usedPixels / totalPixels) * 100).toFixed(2)}%)`,
` Wasted: ${wastedPixels}px (${((wastedPixels / totalPixels) * 100).toFixed(2)}%)`,
`Restricted: ${restrictedPixels}px (${((restrictedPixels / totalPixels) * 100).toFixed(2)}%) (hard to allocate)`,
`Efficiency: ${efficiency === 1 ? '100' : (efficiency * 100).toFixed(2)}%`,
` Slabs: ${this._slabs.length} of ${Math.floor(this._canvas.width / slabW) * Math.floor(this._canvas.height / slabH)}`
].join('\n');
}
}
function addEntryToMapArray(map, key, entry) {
let list = map.get(key);
if (!list) {
list = [];
map.set(key, list);
}
list.push(entry);
}
export { TextureAtlasSlabAllocator };
@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* A simple tracker for dirty regions in a buffer.
*/
class BufferDirtyTracker {
get dataOffset() {
return this._startIndex;
}
get dirtySize() {
if (this._startIndex === undefined || this._endIndex === undefined) {
return undefined;
}
return this._endIndex - this._startIndex + 1;
}
get isDirty() { return this._startIndex !== undefined; }
/**
* Flag the index(es) as modified. Returns the index flagged.
* @param index An index to flag.
* @param length An optional length to flag. Defaults to 1.
*/
flag(index, length = 1) {
this._flag(index);
if (length > 1) {
this._flag(index + length - 1);
}
return index;
}
_flag(index) {
if (this._startIndex === undefined || index < this._startIndex) {
this._startIndex = index;
}
if (this._endIndex === undefined || index > this._endIndex) {
this._endIndex = index;
}
}
clear() {
this._startIndex = undefined;
this._endIndex = undefined;
}
}
export { BufferDirtyTracker };
@@ -0,0 +1,52 @@
import { safeIntl } from '../../../base/common/date.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
function createContentSegmenter(lineData, options) {
if (lineData.isBasicASCII && options.useMonospaceOptimizations) {
return new AsciiContentSegmenter(lineData);
}
return new GraphemeContentSegmenter(lineData);
}
class AsciiContentSegmenter {
constructor(lineData) {
this._content = lineData.content;
}
getSegmentAtIndex(index) {
return this._content[index];
}
}
/**
* This is a more modern version of {@link GraphemeIterator}, relying on browser APIs instead of a
* manual table approach.
*/
class GraphemeContentSegmenter {
constructor(lineData) {
this._segments = [];
const content = lineData.content;
const segmenter = safeIntl.Segmenter(undefined, { granularity: 'grapheme' }).value;
const segmentedContent = Array.from(segmenter.segment(content));
let segmenterIndex = 0;
for (let x = 0; x < content.length; x++) {
const segment = segmentedContent[segmenterIndex];
// No more segments in the string (eg. an emoji is the last segment)
if (!segment) {
break;
}
// The segment isn't renderable (eg. the tail end of an emoji)
if (segment.index !== x) {
this._segments.push(undefined);
continue;
}
segmenterIndex++;
this._segments.push(segment);
}
}
getSegmentAtIndex(index) {
return this._segments[index]?.segment;
}
}
export { createContentSegmenter };
@@ -0,0 +1,73 @@
import { $, getActiveDocument } from '../../../../base/browser/dom.js';
import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js';
import './media/decorationCssRuleExtractor.css';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Extracts CSS rules that would be applied to certain decoration classes.
*/
class DecorationCssRuleExtractor extends Disposable {
constructor() {
super();
this._ruleCache = new Map();
this._container = $('div.monaco-decoration-css-rule-extractor');
this._dummyElement = $('span');
this._container.appendChild(this._dummyElement);
this._register(toDisposable(() => this._container.remove()));
}
getStyleRules(canvas, decorationClassName) {
// Check cache
const existing = this._ruleCache.get(decorationClassName);
if (existing) {
return existing;
}
// Set up DOM
this._dummyElement.className = decorationClassName;
canvas.appendChild(this._container);
// Get rules
const rules = this._getStyleRules(decorationClassName);
this._ruleCache.set(decorationClassName, rules);
// Tear down DOM
canvas.removeChild(this._container);
return rules;
}
_getStyleRules(className) {
// Iterate through all stylesheets and imported stylesheets to find matching rules
const rules = [];
const doc = getActiveDocument();
const stylesheets = [...doc.styleSheets];
for (let i = 0; i < stylesheets.length; i++) {
const stylesheet = stylesheets[i];
for (const rule of stylesheet.cssRules) {
if (rule instanceof CSSImportRule) {
if (rule.styleSheet) {
stylesheets.push(rule.styleSheet);
}
}
else if (rule instanceof CSSStyleRule) {
// Note that originally `.matches(rule.selectorText)` was used but this would
// not pick up pseudo-classes which are important to determine support of the
// returned styles.
//
// Since a selector could contain a class name lookup that is simple a prefix of
// the class name we are looking for, we need to also check the character after
// it.
const searchTerm = `.${className}`;
const index = rule.selectorText.indexOf(searchTerm);
if (index !== -1) {
const endOfResult = index + searchTerm.length;
if (rule.selectorText.length === endOfResult || rule.selectorText.substring(endOfResult, endOfResult + 1).match(/[ :]/)) {
rules.push(rule);
}
}
}
}
}
return rules;
}
}
export { DecorationCssRuleExtractor };
@@ -0,0 +1,40 @@
import { NKeyMap } from '../../../../base/common/map.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class DecorationStyleCache {
constructor() {
this._nextId = 1;
this._cacheById = new Map();
this._cacheByStyle = new NKeyMap();
}
getOrCreateEntry(color, bold, opacity) {
if (color === undefined && bold === undefined && opacity === undefined) {
return 0;
}
const result = this._cacheByStyle.get(color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2));
if (result) {
return result.id;
}
const id = this._nextId++;
const entry = {
id,
color,
bold,
opacity,
};
this._cacheById.set(id, entry);
this._cacheByStyle.set(entry, color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2));
return id;
}
getStyleSet(id) {
if (id === 0) {
return undefined;
}
return this._cacheById.get(id);
}
}
export { DecorationStyleCache };
@@ -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-editor .monaco-decoration-css-rule-extractor {
visibility: hidden;
pointer-events: none;
}
@@ -0,0 +1,48 @@
import { isFunction } from '../../../base/common/types.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var GPULifecycle;
(function (GPULifecycle) {
async function requestDevice(fallback) {
try {
if (!navigator.gpu) {
throw new Error('This browser does not support WebGPU');
}
const adapter = (await navigator.gpu.requestAdapter());
if (!adapter) {
throw new Error('This browser supports WebGPU but it appears to be disabled');
}
return wrapDestroyableInDisposable(await adapter.requestDevice());
}
catch (e) {
if (fallback) {
fallback(e.message);
}
throw e;
}
}
GPULifecycle.requestDevice = requestDevice;
function createBuffer(device, descriptor, initialValues) {
const buffer = device.createBuffer(descriptor);
if (initialValues) {
device.queue.writeBuffer(buffer, 0, (isFunction(initialValues) ? initialValues() : initialValues));
}
return wrapDestroyableInDisposable(buffer);
}
GPULifecycle.createBuffer = createBuffer;
function createTexture(device, descriptor) {
return wrapDestroyableInDisposable(device.createTexture(descriptor));
}
GPULifecycle.createTexture = createTexture;
})(GPULifecycle || (GPULifecycle = {}));
function wrapDestroyableInDisposable(value) {
return {
object: value,
dispose: () => value.destroy()
};
}
export { GPULifecycle };
@@ -0,0 +1,58 @@
import { BugIndicatingError } from '../../../base/common/errors.js';
import { toDisposable } from '../../../base/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 quadVertices = new Float32Array([
1, 0,
1, 1,
0, 1,
0, 0,
0, 1,
1, 0,
]);
function ensureNonNullable(value) {
if (!value) {
throw new Error(`Value "${value}" cannot be null`);
}
return value;
}
// TODO: Move capabilities into ElementSizeObserver?
function observeDevicePixelDimensions(element, parentWindow, callback) {
// Observe any resizes to the element and extract the actual pixel size of the element if the
// devicePixelContentBoxSize API is supported. This allows correcting rounding errors when
// converting between CSS pixels and device pixels which causes blurry rendering when device
// pixel ratio is not a round number.
let observer = new parentWindow.ResizeObserver((entries) => {
const entry = entries.find((entry) => entry.target === element);
if (!entry) {
return;
}
// Disconnect if devicePixelContentBoxSize isn't supported by the browser
if (!('devicePixelContentBoxSize' in entry)) {
observer?.disconnect();
observer = undefined;
return;
}
// Fire the callback, ignore events where the dimensions are 0x0 as the canvas is likely hidden
const width = entry.devicePixelContentBoxSize[0].inlineSize;
const height = entry.devicePixelContentBoxSize[0].blockSize;
if (width > 0 && height > 0) {
callback(width, height);
}
});
try {
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
observer.observe(element, { box: ['device-pixel-content-box'] });
}
catch {
observer.disconnect();
observer = undefined;
throw new BugIndicatingError('Could not observe device pixel dimensions');
}
return toDisposable(() => observer?.disconnect());
}
export { ensureNonNullable, observeDevicePixelDimensions, quadVertices };
@@ -0,0 +1,102 @@
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable, toDisposable, dispose } from '../../../base/common/lifecycle.js';
import { LinkedList } from '../../../base/common/linkedList.js';
import { BufferDirtyTracker } from './bufferDirtyTracker.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
function createObjectCollectionBuffer(propertySpecs, capacity) {
return new ObjectCollectionBuffer(propertySpecs, capacity);
}
class ObjectCollectionBuffer extends Disposable {
get entryCount() {
return this._entries.size;
}
get dirtyTracker() { return this._dirtyTracker; }
constructor(propertySpecs, capacity) {
super();
this.propertySpecs = propertySpecs;
this.capacity = capacity;
this._dirtyTracker = new BufferDirtyTracker();
this._propertySpecsMap = new Map();
this._entries = new LinkedList();
this._onDidChange = this._register(new Emitter());
this._onDidChangeBuffer = this._register(new Emitter());
this.onDidChangeBuffer = this._onDidChangeBuffer.event;
this.view = new Float32Array(capacity * propertySpecs.length);
this.buffer = this.view.buffer;
this._entrySize = propertySpecs.length;
for (let i = 0; i < propertySpecs.length; i++) {
const spec = {
offset: i,
...propertySpecs[i]
};
this._propertySpecsMap.set(spec.name, spec);
}
this._register(toDisposable(() => dispose(this._entries)));
}
createEntry(data) {
if (this._entries.size === this.capacity) {
this._expandBuffer();
this._onDidChangeBuffer.fire();
}
const value = new ObjectCollectionBufferEntry(this.view, this._propertySpecsMap, this._dirtyTracker, this._entries.size, data);
const removeFromEntries = this._entries.push(value);
const listeners = [];
listeners.push(Event.forward(value.onDidChange, this._onDidChange));
listeners.push(value.onWillDispose(() => {
const deletedEntryIndex = value.i;
removeFromEntries();
// Shift all entries after the deleted entry to the left
this.view.set(this.view.subarray(deletedEntryIndex * this._entrySize + 2, this._entries.size * this._entrySize + 2), deletedEntryIndex * this._entrySize);
// Update entries to reflect the new i
for (const entry of this._entries) {
if (entry.i > deletedEntryIndex) {
entry.i--;
}
}
this._dirtyTracker.flag(deletedEntryIndex, (this._entries.size - deletedEntryIndex) * this._entrySize);
dispose(listeners);
}));
return value;
}
_expandBuffer() {
this.capacity *= 2;
const newView = new Float32Array(this.capacity * this._entrySize);
newView.set(this.view);
this.view = newView;
this.buffer = this.view.buffer;
}
}
class ObjectCollectionBufferEntry extends Disposable {
constructor(_view, _propertySpecsMap, _dirtyTracker, i, data) {
super();
this._view = _view;
this._propertySpecsMap = _propertySpecsMap;
this._dirtyTracker = _dirtyTracker;
this.i = i;
this._onDidChange = this._register(new Emitter());
this.onDidChange = this._onDidChange.event;
this._onWillDispose = this._register(new Emitter());
this.onWillDispose = this._onWillDispose.event;
for (const propertySpec of this._propertySpecsMap.values()) {
this._view[this.i * this._propertySpecsMap.size + propertySpec.offset] = data[propertySpec.name];
}
this._dirtyTracker.flag(this.i * this._propertySpecsMap.size, this._propertySpecsMap.size);
}
dispose() {
this._onWillDispose.fire();
super.dispose();
}
setRaw(data) {
if (data.length !== this._propertySpecsMap.size) {
throw new Error(`Data length ${data.length} does not match the number of properties in the collection (${this._propertySpecsMap.size})`);
}
this._view.set(data, this.i * this._propertySpecsMap.size);
this._dirtyTracker.flag(this.i * this._propertySpecsMap.size, this._propertySpecsMap.size);
}
}
export { createObjectCollectionBuffer };
@@ -0,0 +1,276 @@
import { memoize } from '../../../../base/common/decorators.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { isMacintosh } from '../../../../base/common/platform.js';
import { StringBuilder } from '../../../common/core/stringBuilder.js';
import { TokenMetadata } from '../../../common/encodedTokenAttributes.js';
import { ensureNonNullable } from '../gpuUtils.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;
};
let nextId = 0;
class GlyphRasterizer extends Disposable {
get cacheKey() {
return `${this.fontFamily}_${this.fontSize}px`;
}
constructor(fontSize, fontFamily, devicePixelRatio, _decorationStyleCache) {
super();
this.fontSize = fontSize;
this.fontFamily = fontFamily;
this.devicePixelRatio = devicePixelRatio;
this._decorationStyleCache = _decorationStyleCache;
this.id = nextId++;
this._workGlyph = {
source: null,
boundingBox: {
left: 0,
bottom: 0,
right: 0,
top: 0,
},
originOffset: {
x: 0,
y: 0,
},
fontBoundingBoxAscent: 0,
fontBoundingBoxDescent: 0,
};
this._workGlyphConfig = { chars: undefined, tokenMetadata: 0, decorationStyleSetId: 0 };
// TODO: Support workbench.fontAliasing correctly
this._antiAliasing = isMacintosh ? 'greyscale' : 'subpixel';
const devicePixelFontSize = Math.ceil(this.fontSize * devicePixelRatio);
this._canvas = new OffscreenCanvas(devicePixelFontSize * 3, devicePixelFontSize * 3);
this._ctx = ensureNonNullable(this._canvas.getContext('2d', {
willReadFrequently: true,
alpha: this._antiAliasing === 'greyscale',
}));
this._ctx.textBaseline = 'top';
this._ctx.fillStyle = '#FFFFFF';
this._ctx.font = `${devicePixelFontSize}px ${this.fontFamily}`;
this._textMetrics = this._ctx.measureText('A');
}
/**
* Rasterizes a glyph. Note that the returned object is reused across different glyphs and
* therefore is only safe for synchronous access.
*/
rasterizeGlyph(chars, tokenMetadata, decorationStyleSetId, colorMap) {
if (chars === '') {
return {
source: this._canvas,
boundingBox: { top: 0, left: 0, bottom: -1, right: -1 },
originOffset: { x: 0, y: 0 },
fontBoundingBoxAscent: 0,
fontBoundingBoxDescent: 0,
};
}
// Check if the last glyph matches the config, reuse if so. This helps avoid unnecessary
// work when the rasterizer is called multiple times like when the glyph doesn't fit into a
// page.
if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.tokenMetadata === tokenMetadata && this._workGlyphConfig.decorationStyleSetId === decorationStyleSetId) {
return this._workGlyph;
}
this._workGlyphConfig.chars = chars;
this._workGlyphConfig.tokenMetadata = tokenMetadata;
this._workGlyphConfig.decorationStyleSetId = decorationStyleSetId;
return this._rasterizeGlyph(chars, tokenMetadata, decorationStyleSetId, colorMap);
}
_rasterizeGlyph(chars, tokenMetadata, decorationStyleSetId, colorMap) {
const devicePixelFontSize = Math.ceil(this.fontSize * this.devicePixelRatio);
const canvasDim = devicePixelFontSize * 3;
if (this._canvas.width !== canvasDim) {
this._canvas.width = canvasDim;
this._canvas.height = canvasDim;
}
this._ctx.save();
// The sub-pixel x offset is the fractional part of the x pixel coordinate of the cell, this
// is used to improve the spacing between rendered characters.
const xSubPixelXOffset = (tokenMetadata & 0b1111) / 10;
const bgId = TokenMetadata.getBackground(tokenMetadata);
const bg = colorMap[bgId];
const decorationStyleSet = this._decorationStyleCache.getStyleSet(decorationStyleSetId);
// When SPAA is used, the background color must be present to get the right glyph
if (this._antiAliasing === 'subpixel') {
this._ctx.fillStyle = bg;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
}
else {
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
}
const fontSb = new StringBuilder(200);
const fontStyle = TokenMetadata.getFontStyle(tokenMetadata);
if (fontStyle & 1 /* FontStyle.Italic */) {
fontSb.appendString('italic ');
}
if (decorationStyleSet?.bold !== undefined) {
if (decorationStyleSet.bold) {
fontSb.appendString('bold ');
}
}
else if (fontStyle & 2 /* FontStyle.Bold */) {
fontSb.appendString('bold ');
}
fontSb.appendString(`${devicePixelFontSize}px ${this.fontFamily}`);
this._ctx.font = fontSb.build();
// TODO: Support FontStyle.Strikethrough and FontStyle.Underline text decorations, these
// need to be drawn manually to the canvas. See xterm.js for "dodging" the text for
// underlines.
const originX = devicePixelFontSize;
const originY = devicePixelFontSize;
if (decorationStyleSet?.color !== undefined) {
this._ctx.fillStyle = `#${decorationStyleSet.color.toString(16).padStart(8, '0')}`;
}
else {
this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(tokenMetadata)];
}
this._ctx.textBaseline = 'top';
if (decorationStyleSet?.opacity !== undefined) {
this._ctx.globalAlpha = decorationStyleSet.opacity;
}
this._ctx.fillText(chars, originX + xSubPixelXOffset, originY);
this._ctx.restore();
const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
if (this._antiAliasing === 'subpixel') {
const bgR = parseInt(bg.substring(1, 3), 16);
const bgG = parseInt(bg.substring(3, 5), 16);
const bgB = parseInt(bg.substring(5, 7), 16);
this._clearColor(imageData, bgR, bgG, bgB);
this._ctx.putImageData(imageData, 0, 0);
}
this._findGlyphBoundingBox(imageData, this._workGlyph.boundingBox);
// const offset = {
// x: textMetrics.actualBoundingBoxLeft,
// y: textMetrics.actualBoundingBoxAscent
// };
// const size = {
// w: textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft,
// y: textMetrics.actualBoundingBoxDescent + textMetrics.actualBoundingBoxAscent,
// wInt: Math.ceil(textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft),
// yInt: Math.ceil(textMetrics.actualBoundingBoxDescent + textMetrics.actualBoundingBoxAscent),
// };
// console.log(`${chars}_${fg}`, textMetrics, boundingBox, originX, originY, { width: boundingBox.right - boundingBox.left, height: boundingBox.bottom - boundingBox.top });
this._workGlyph.source = this._canvas;
this._workGlyph.originOffset.x = this._workGlyph.boundingBox.left - originX;
this._workGlyph.originOffset.y = this._workGlyph.boundingBox.top - originY;
this._workGlyph.fontBoundingBoxAscent = this._textMetrics.fontBoundingBoxAscent;
this._workGlyph.fontBoundingBoxDescent = this._textMetrics.fontBoundingBoxDescent;
// const result2: IRasterizedGlyph = {
// source: this._canvas,
// boundingBox: {
// left: Math.floor(originX - textMetrics.actualBoundingBoxLeft),
// right: Math.ceil(originX + textMetrics.actualBoundingBoxRight),
// top: Math.floor(originY - textMetrics.actualBoundingBoxAscent),
// bottom: Math.ceil(originY + textMetrics.actualBoundingBoxDescent),
// },
// originOffset: {
// x: Math.floor(boundingBox.left - originX),
// y: Math.floor(boundingBox.top - originY)
// }
// };
// TODO: Verify result 1 and 2 are the same
// if (result2.boundingBox.left > result.boundingBox.left) {
// debugger;
// }
// if (result2.boundingBox.top > result.boundingBox.top) {
// debugger;
// }
// if (result2.boundingBox.right < result.boundingBox.right) {
// debugger;
// }
// if (result2.boundingBox.bottom < result.boundingBox.bottom) {
// debugger;
// }
// if (JSON.stringify(result2.originOffset) !== JSON.stringify(result.originOffset)) {
// debugger;
// }
return this._workGlyph;
}
_clearColor(imageData, r, g, b) {
for (let offset = 0; offset < imageData.data.length; offset += 4) {
// Check exact match
if (imageData.data[offset] === r &&
imageData.data[offset + 1] === g &&
imageData.data[offset + 2] === b) {
imageData.data[offset + 3] = 0;
}
}
}
// TODO: Does this even need to happen when measure text is used?
_findGlyphBoundingBox(imageData, outBoundingBox) {
const height = this._canvas.height;
const width = this._canvas.width;
let found = false;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const alphaOffset = y * width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
outBoundingBox.top = y;
found = true;
break;
}
}
if (found) {
break;
}
}
outBoundingBox.left = 0;
found = false;
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
const alphaOffset = y * width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
outBoundingBox.left = x;
found = true;
break;
}
}
if (found) {
break;
}
}
outBoundingBox.right = width;
found = false;
for (let x = width - 1; x >= outBoundingBox.left; x--) {
for (let y = 0; y < height; y++) {
const alphaOffset = y * width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
outBoundingBox.right = x;
found = true;
break;
}
}
if (found) {
break;
}
}
outBoundingBox.bottom = outBoundingBox.top;
found = false;
for (let y = height - 1; y >= 0; y--) {
for (let x = 0; x < width; x++) {
const alphaOffset = y * width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
outBoundingBox.bottom = y;
found = true;
break;
}
}
if (found) {
break;
}
}
}
getTextMetrics(text) {
return this._ctx.measureText(text);
}
}
__decorate([
memoize
], GlyphRasterizer.prototype, "cacheKey", null);
export { GlyphRasterizer };
@@ -0,0 +1,214 @@
import { getActiveWindow } from '../../../base/browser/dom.js';
import { Event } from '../../../base/common/event.js';
import { MutableDisposable } from '../../../base/common/lifecycle.js';
import { ViewEventHandler } from '../../common/viewEventHandler.js';
import { GPULifecycle } from './gpuDisposable.js';
import { observeDevicePixelDimensions, quadVertices } from './gpuUtils.js';
import { createObjectCollectionBuffer } from './objectCollectionBuffer.js';
import { rectangleRendererWgsl } from './rectangleRenderer.wgsl.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class RectangleRenderer extends ViewEventHandler {
constructor(_context, _contentLeft, _devicePixelRatio, _canvas, _ctx, device) {
super();
this._context = _context;
this._contentLeft = _contentLeft;
this._devicePixelRatio = _devicePixelRatio;
this._canvas = _canvas;
this._ctx = _ctx;
this._shapeBindBuffer = this._register(new MutableDisposable());
this._initialized = false;
this._shapeCollection = this._register(createObjectCollectionBuffer([
{ name: 'x' },
{ name: 'y' },
{ name: 'width' },
{ name: 'height' },
{ name: 'red' },
{ name: 'green' },
{ name: 'blue' },
{ name: 'alpha' },
], 32));
this._context.addEventHandler(this);
this._initWebgpu(device);
}
async _initWebgpu(device) {
// #region General
this._device = await device;
if (this._store.isDisposed) {
return;
}
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
this._ctx.configure({
device: this._device,
format: presentationFormat,
alphaMode: 'premultiplied',
});
this._renderPassColorAttachment = {
view: null, // Will be filled at render time
loadOp: 'load',
storeOp: 'store',
};
this._renderPassDescriptor = {
label: 'Monaco rectangle renderer render pass',
colorAttachments: [this._renderPassColorAttachment],
};
// #endregion General
// #region Uniforms
let layoutInfoUniformBuffer;
{
const bufferValues = new Float32Array(6 /* Info.FloatsPerEntry */);
const updateBufferValues = (canvasDevicePixelWidth = this._canvas.width, canvasDevicePixelHeight = this._canvas.height) => {
bufferValues[0 /* Info.Offset_CanvasWidth____ */] = canvasDevicePixelWidth;
bufferValues[1 /* Info.Offset_CanvasHeight___ */] = canvasDevicePixelHeight;
bufferValues[2 /* Info.Offset_ViewportOffsetX */] = Math.ceil(this._context.configuration.options.get(165 /* EditorOption.layoutInfo */).contentLeft * getActiveWindow().devicePixelRatio);
bufferValues[3 /* Info.Offset_ViewportOffsetY */] = 0;
bufferValues[4 /* Info.Offset_ViewportWidth__ */] = bufferValues[0 /* Info.Offset_CanvasWidth____ */] - bufferValues[2 /* Info.Offset_ViewportOffsetX */];
bufferValues[5 /* Info.Offset_ViewportHeight_ */] = bufferValues[1 /* Info.Offset_CanvasHeight___ */] - bufferValues[3 /* Info.Offset_ViewportOffsetY */];
return bufferValues;
};
layoutInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, {
label: 'Monaco rectangle renderer uniform buffer',
size: 24 /* Info.BytesPerEntry */,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
}, () => updateBufferValues())).object;
this._register(observeDevicePixelDimensions(this._canvas, getActiveWindow(), (w, h) => {
this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, updateBufferValues(w, h));
}));
}
const scrollOffsetBufferSize = 2;
this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {
label: 'Monaco rectangle renderer scroll offset buffer',
size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
})).object;
this._scrollOffsetValueBuffer = new Float32Array(scrollOffsetBufferSize);
// #endregion Uniforms
// #region Storage buffers
const createShapeBindBuffer = () => {
return GPULifecycle.createBuffer(this._device, {
label: 'Monaco rectangle renderer shape buffer',
size: this._shapeCollection.buffer.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
};
this._shapeBindBuffer.value = createShapeBindBuffer();
this._register(Event.runAndSubscribe(this._shapeCollection.onDidChangeBuffer, () => {
this._shapeBindBuffer.value = createShapeBindBuffer();
if (this._pipeline) {
this._updateBindGroup(this._pipeline, layoutInfoUniformBuffer);
}
}));
// #endregion Storage buffers
// #region Vertex buffer
this._vertexBuffer = this._register(GPULifecycle.createBuffer(this._device, {
label: 'Monaco rectangle renderer vertex buffer',
size: quadVertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
}, quadVertices)).object;
// #endregion Vertex buffer
// #region Shader module
const module = this._device.createShaderModule({
label: 'Monaco rectangle renderer shader module',
code: rectangleRendererWgsl,
});
// #endregion Shader module
// #region Pipeline
this._pipeline = this._device.createRenderPipeline({
label: 'Monaco rectangle renderer render pipeline',
layout: 'auto',
vertex: {
module,
buffers: [
{
arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, // 2 floats, 4 bytes each
attributes: [
{ shaderLocation: 0, offset: 0, format: 'float32x2' }, // position
],
}
]
},
fragment: {
module,
targets: [
{
format: presentationFormat,
blend: {
color: {
srcFactor: 'src-alpha',
dstFactor: 'one-minus-src-alpha'
},
alpha: {
srcFactor: 'src-alpha',
dstFactor: 'one-minus-src-alpha'
},
},
}
],
},
});
// #endregion Pipeline
// #region Bind group
this._updateBindGroup(this._pipeline, layoutInfoUniformBuffer);
// endregion Bind group
this._initialized = true;
}
_updateBindGroup(pipeline, layoutInfoUniformBuffer) {
this._bindGroup = this._device.createBindGroup({
label: 'Monaco rectangle renderer bind group',
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0 /* RectangleRendererBindingId.Shapes */, resource: { buffer: this._shapeBindBuffer.value.object } },
{ binding: 1 /* RectangleRendererBindingId.LayoutInfoUniform */, resource: { buffer: layoutInfoUniformBuffer } },
{ binding: 2 /* RectangleRendererBindingId.ScrollOffset */, resource: { buffer: this._scrollOffsetBindBuffer } },
],
});
}
register(x, y, width, height, red, green, blue, alpha) {
return this._shapeCollection.createEntry({ x, y, width, height, red, green, blue, alpha });
}
// #region Event handlers
onScrollChanged(e) {
if (this._device) {
const dpr = getActiveWindow().devicePixelRatio;
this._scrollOffsetValueBuffer[0] = this._context.viewLayout.getCurrentScrollLeft() * dpr;
this._scrollOffsetValueBuffer[1] = this._context.viewLayout.getCurrentScrollTop() * dpr;
this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer);
}
return true;
}
// #endregion
_update() {
if (!this._device) {
return;
}
const shapes = this._shapeCollection;
if (shapes.dirtyTracker.isDirty) {
this._device.queue.writeBuffer(this._shapeBindBuffer.value.object, 0, shapes.buffer, shapes.dirtyTracker.dataOffset, shapes.dirtyTracker.dirtySize * shapes.view.BYTES_PER_ELEMENT);
shapes.dirtyTracker.clear();
}
}
draw(viewportData) {
if (!this._initialized) {
return;
}
this._update();
const encoder = this._device.createCommandEncoder({ label: 'Monaco rectangle renderer command encoder' });
this._renderPassColorAttachment.view = this._ctx.getCurrentTexture().createView();
const pass = encoder.beginRenderPass(this._renderPassDescriptor);
pass.setPipeline(this._pipeline);
pass.setVertexBuffer(0, this._vertexBuffer);
pass.setBindGroup(0, this._bindGroup);
// Only draw the content area
const contentLeft = Math.ceil(this._contentLeft.get() * this._devicePixelRatio.get());
pass.setScissorRect(contentLeft, 0, this._canvas.width - contentLeft, this._canvas.height);
pass.draw(quadVertices.length / 2, this._shapeCollection.entryCount);
pass.end();
const commandBuffer = encoder.finish();
this._device.queue.submit([commandBuffer]);
}
}
export { RectangleRenderer };
@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const rectangleRendererWgsl = /*wgsl*/ `
struct Vertex {
@location(0) position: vec2f,
};
struct LayoutInfo {
canvasDims: vec2f,
viewportOffset: vec2f,
viewportDims: vec2f,
}
struct ScrollOffset {
offset: vec2f,
}
struct Shape {
position: vec2f,
size: vec2f,
color: vec4f,
};
struct VSOutput {
@builtin(position) position: vec4f,
@location(1) color: vec4f,
};
// Uniforms
@group(0) @binding(${1 /* RectangleRendererBindingId.LayoutInfoUniform */}) var<uniform> layoutInfo: LayoutInfo;
// Storage buffers
@group(0) @binding(${0 /* RectangleRendererBindingId.Shapes */}) var<storage, read> shapes: array<Shape>;
@group(0) @binding(${2 /* RectangleRendererBindingId.ScrollOffset */}) var<uniform> scrollOffset: ScrollOffset;
@vertex fn vs(
vert: Vertex,
@builtin(instance_index) instanceIndex: u32,
@builtin(vertex_index) vertexIndex : u32
) -> VSOutput {
let shape = shapes[instanceIndex];
var vsOut: VSOutput;
vsOut.position = vec4f(
(
// Top left corner
vec2f(-1, 1) +
// Convert pixel position to clipspace
vec2f( 2, -2) / layoutInfo.canvasDims *
// Shape position and size
(layoutInfo.viewportOffset - scrollOffset.offset + shape.position + vert.position * shape.size)
),
0.0,
1.0
);
vsOut.color = shape.color;
return vsOut;
}
@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
return vsOut.color;
}
`;
export { rectangleRendererWgsl };
@@ -0,0 +1,19 @@
import { ViewEventHandler } from '../../../common/viewEventHandler.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class BaseRenderStrategy extends ViewEventHandler {
get glyphRasterizer() { return this._glyphRasterizer.value; }
constructor(_context, _viewGpuContext, _device, _glyphRasterizer) {
super();
this._context = _context;
this._viewGpuContext = _viewGpuContext;
this._device = _device;
this._glyphRasterizer = _glyphRasterizer;
this._context.addEventHandler(this);
}
}
export { BaseRenderStrategy };
@@ -0,0 +1,414 @@
import { getActiveWindow } from '../../../../base/browser/dom.js';
import { Color } from '../../../../base/common/color.js';
import { BugIndicatingError } from '../../../../base/common/errors.js';
import { CursorColumns } from '../../../common/core/cursorColumns.js';
import { createContentSegmenter } from '../contentSegmenter.js';
import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js';
import { GPULifecycle } from '../gpuDisposable.js';
import { quadVertices } from '../gpuUtils.js';
import { ViewGpuContext } from '../viewGpuContext.js';
import { BaseRenderStrategy } from './baseRenderStrategy.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* A render strategy that tracks a large buffer, uploading only dirty lines as they change and
* leveraging heavy caching. This is the most performant strategy but has limitations around long
* lines and too many lines.
*/
class FullFileRenderStrategy extends BaseRenderStrategy {
/**
* The hard cap for line count that can be rendered by the GPU renderer.
*/
static { this.maxSupportedLines = 3000; }
/**
* The hard cap for line columns that can be rendered by the GPU renderer.
*/
static { this.maxSupportedColumns = 200; }
get bindGroupEntries() {
return [
{ binding: 1 /* BindingId.Cells */, resource: { buffer: this._cellBindBuffer } },
{ binding: 6 /* BindingId.ScrollOffset */, resource: { buffer: this._scrollOffsetBindBuffer } }
];
}
constructor(context, viewGpuContext, device, glyphRasterizer) {
super(context, viewGpuContext, device, glyphRasterizer);
this.type = 'fullfile';
this.wgsl = fullFileRenderStrategyWgsl;
this._activeDoubleBufferIndex = 0;
this._upToDateLines = [new Set(), new Set()];
this._visibleObjectCount = 0;
this._finalRenderedLine = 0;
this._scrollInitialized = false;
this._queuedBufferUpdates = [[], []];
const bufferSize = FullFileRenderStrategy.maxSupportedLines * FullFileRenderStrategy.maxSupportedColumns * 6 /* Constants.IndicesPerCell */ * Float32Array.BYTES_PER_ELEMENT;
this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {
label: 'Monaco full file cell buffer',
size: bufferSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
})).object;
this._cellValueBuffers = [
new ArrayBuffer(bufferSize),
new ArrayBuffer(bufferSize),
];
const scrollOffsetBufferSize = 2;
this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {
label: 'Monaco scroll offset buffer',
size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
})).object;
this._scrollOffsetValueBuffer = new Float32Array(scrollOffsetBufferSize);
}
// #region Event handlers
// The primary job of these handlers is to:
// 1. Invalidate the up to date line cache, which will cause the line to be re-rendered when
// it's _within the viewport_.
// 2. Pass relevant events on to the render function so it can force certain line ranges to be
// re-rendered even if they're not in the viewport. For example when a view zone is added,
// there are lines that used to be visible but are no longer, so those ranges must be
// cleared and uploaded to the GPU.
onConfigurationChanged(e) {
this._invalidateAllLines();
this._queueBufferUpdate(e);
return true;
}
onDecorationsChanged(e) {
this._invalidateAllLines();
return true;
}
onTokensChanged(e) {
// TODO: This currently fires for the entire viewport whenever scrolling stops
// https://github.com/microsoft/vscode/issues/233942
for (const range of e.ranges) {
this._invalidateLineRange(range.fromLineNumber, range.toLineNumber);
}
return true;
}
onLinesDeleted(e) {
// TODO: This currently invalidates everything after the deleted line, it could shift the
// line data up to retain some up to date lines
// TODO: This does not invalidate lines that are no longer in the file
this._invalidateLinesFrom(e.fromLineNumber);
this._queueBufferUpdate(e);
return true;
}
onLinesInserted(e) {
// TODO: This currently invalidates everything after the deleted line, it could shift the
// line data up to retain some up to date lines
this._invalidateLinesFrom(e.fromLineNumber);
return true;
}
onLinesChanged(e) {
this._invalidateLineRange(e.fromLineNumber, e.fromLineNumber + e.count);
return true;
}
onScrollChanged(e) {
const dpr = getActiveWindow().devicePixelRatio;
this._scrollOffsetValueBuffer[0] = (e?.scrollLeft ?? this._context.viewLayout.getCurrentScrollLeft()) * dpr;
this._scrollOffsetValueBuffer[1] = (e?.scrollTop ?? this._context.viewLayout.getCurrentScrollTop()) * dpr;
this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer);
return true;
}
onThemeChanged(e) {
this._invalidateAllLines();
return true;
}
onLineMappingChanged(e) {
this._invalidateAllLines();
this._queueBufferUpdate(e);
return true;
}
onZonesChanged(e) {
this._invalidateAllLines();
this._queueBufferUpdate(e);
return true;
}
// #endregion
_invalidateAllLines() {
this._upToDateLines[0].clear();
this._upToDateLines[1].clear();
}
_invalidateLinesFrom(lineNumber) {
for (const i of [0, 1]) {
const upToDateLines = this._upToDateLines[i];
for (const upToDateLine of upToDateLines) {
if (upToDateLine >= lineNumber) {
upToDateLines.delete(upToDateLine);
}
}
}
}
_invalidateLineRange(fromLineNumber, toLineNumber) {
for (let i = fromLineNumber; i <= toLineNumber; i++) {
this._upToDateLines[0].delete(i);
this._upToDateLines[1].delete(i);
}
}
reset() {
this._invalidateAllLines();
for (const bufferIndex of [0, 1]) {
// Zero out buffer and upload to GPU to prevent stale rows from rendering
const buffer = new Float32Array(this._cellValueBuffers[bufferIndex]);
buffer.fill(0, 0, buffer.length);
this._device.queue.writeBuffer(this._cellBindBuffer, 0, buffer.buffer, 0, buffer.byteLength);
}
this._finalRenderedLine = 0;
}
update(viewportData, viewLineOptions) {
// IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the
// loop. This is done so we don't need to trust the JIT compiler to do this optimization to
// avoid potential additional blocking time in garbage collector which is a common cause of
// dropped frames.
let chars = '';
let segment;
let charWidth = 0;
let y = 0;
let x = 0;
let absoluteOffsetX = 0;
let absoluteOffsetY = 0;
let tabXOffset = 0;
let glyph;
let cellIndex = 0;
let tokenStartIndex = 0;
let tokenEndIndex = 0;
let tokenMetadata = 0;
let decorationStyleSetBold;
let decorationStyleSetColor;
let decorationStyleSetOpacity;
let lineData;
let decoration;
let fillStartIndex = 0;
let fillEndIndex = 0;
let tokens;
const dpr = getActiveWindow().devicePixelRatio;
let contentSegmenter;
if (!this._scrollInitialized) {
this.onScrollChanged();
this._scrollInitialized = true;
}
// Update cell data
const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]);
const lineIndexCount = FullFileRenderStrategy.maxSupportedColumns * 6 /* Constants.IndicesPerCell */;
const upToDateLines = this._upToDateLines[this._activeDoubleBufferIndex];
let dirtyLineStart = 3000;
let dirtyLineEnd = 0;
// Handle any queued buffer updates
const queuedBufferUpdates = this._queuedBufferUpdates[this._activeDoubleBufferIndex];
while (queuedBufferUpdates.length) {
const e = queuedBufferUpdates.shift();
switch (e.type) {
// TODO: Refine these cases so we're not throwing away everything
case 2 /* ViewEventType.ViewConfigurationChanged */:
case 8 /* ViewEventType.ViewLineMappingChanged */:
case 17 /* ViewEventType.ViewZonesChanged */: {
cellBuffer.fill(0);
dirtyLineStart = 1;
dirtyLineEnd = Math.max(dirtyLineEnd, this._finalRenderedLine);
this._finalRenderedLine = 0;
break;
}
case 10 /* ViewEventType.ViewLinesDeleted */: {
// Shift content below deleted line up
const deletedLineContentStartIndex = (e.fromLineNumber - 1) * FullFileRenderStrategy.maxSupportedColumns * 6 /* Constants.IndicesPerCell */;
const deletedLineContentEndIndex = (e.toLineNumber) * FullFileRenderStrategy.maxSupportedColumns * 6 /* Constants.IndicesPerCell */;
const nullContentStartIndex = (this._finalRenderedLine - (e.toLineNumber - e.fromLineNumber + 1)) * FullFileRenderStrategy.maxSupportedColumns * 6 /* Constants.IndicesPerCell */;
cellBuffer.set(cellBuffer.subarray(deletedLineContentEndIndex), deletedLineContentStartIndex);
// Zero out content on lines that are no longer valid
cellBuffer.fill(0, nullContentStartIndex);
// Update dirty lines and final rendered line
dirtyLineStart = Math.min(dirtyLineStart, e.fromLineNumber);
dirtyLineEnd = Math.max(dirtyLineEnd, this._finalRenderedLine);
this._finalRenderedLine -= e.toLineNumber - e.fromLineNumber + 1;
break;
}
}
}
for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) {
// Only attempt to render lines that the GPU renderer can handle
if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, y)) {
fillStartIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns) * 6 /* Constants.IndicesPerCell */;
fillEndIndex = (y * FullFileRenderStrategy.maxSupportedColumns) * 6 /* Constants.IndicesPerCell */;
cellBuffer.fill(0, fillStartIndex, fillEndIndex);
dirtyLineStart = Math.min(dirtyLineStart, y);
dirtyLineEnd = Math.max(dirtyLineEnd, y);
continue;
}
// Skip updating the line if it's already up to date
if (upToDateLines.has(y)) {
continue;
}
dirtyLineStart = Math.min(dirtyLineStart, y);
dirtyLineEnd = Math.max(dirtyLineEnd, y);
lineData = viewportData.getViewLineRenderingData(y);
tabXOffset = 0;
contentSegmenter = createContentSegmenter(lineData, viewLineOptions);
charWidth = viewLineOptions.spaceWidth * dpr;
absoluteOffsetX = 0;
tokens = lineData.tokens;
tokenStartIndex = lineData.minColumn - 1;
tokenEndIndex = 0;
for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) {
tokenEndIndex = tokens.getEndOffset(tokenIndex);
if (tokenEndIndex <= tokenStartIndex) {
// The faux indent part of the line should have no token type
continue;
}
tokenMetadata = tokens.getMetadata(tokenIndex);
for (x = tokenStartIndex; x < tokenEndIndex; x++) {
// Only render lines that do not exceed maximum columns
if (x > FullFileRenderStrategy.maxSupportedColumns) {
break;
}
segment = contentSegmenter.getSegmentAtIndex(x);
if (segment === undefined) {
continue;
}
chars = segment;
if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) {
charWidth = this.glyphRasterizer.getTextMetrics(chars).width;
}
decorationStyleSetColor = undefined;
decorationStyleSetBold = undefined;
decorationStyleSetOpacity = undefined;
// Apply supported inline decoration styles to the cell metadata
for (decoration of lineData.inlineDecorations) {
// This is Range.strictContainsPosition except it works at the cell level,
// it's also inlined to avoid overhead.
if ((y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) ||
(y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) ||
(y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1)) {
continue;
}
const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.inlineClassName);
for (const rule of rules) {
for (const r of rule.style) {
const value = rule.styleMap.get(r)?.toString() ?? '';
switch (r) {
case 'color': {
// TODO: This parsing and error handling should move into canRender so fallback
// to DOM works
const parsedColor = Color.Format.CSS.parse(value);
if (!parsedColor) {
throw new BugIndicatingError('Invalid color format ' + value);
}
decorationStyleSetColor = parsedColor.toNumber32Bit();
break;
}
case 'font-weight': {
const parsedValue = parseCssFontWeight(value);
if (parsedValue >= 400) {
decorationStyleSetBold = true;
// TODO: Set bold (https://github.com/microsoft/vscode/issues/237584)
}
else {
decorationStyleSetBold = false;
// TODO: Set normal (https://github.com/microsoft/vscode/issues/237584)
}
break;
}
case 'opacity': {
const parsedValue = parseCssOpacity(value);
decorationStyleSetOpacity = parsedValue;
break;
}
default: throw new BugIndicatingError('Unexpected inline decoration style');
}
}
}
}
if (chars === ' ' || chars === '\t') {
// Zero out glyph to ensure it doesn't get rendered
cellIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + x) * 6 /* Constants.IndicesPerCell */;
cellBuffer.fill(0, cellIndex, cellIndex + 6 /* CellBufferInfo.FloatsPerEntry */);
// Adjust xOffset for tab stops
if (chars === '\t') {
// Find the pixel offset between the current position and the next tab stop
const offsetBefore = x + tabXOffset;
tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize);
absoluteOffsetX += charWidth * (tabXOffset - offsetBefore);
// Convert back to offset excluding x and the current character
tabXOffset -= x + 1;
}
else {
absoluteOffsetX += charWidth;
}
continue;
}
const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity);
glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX);
absoluteOffsetY = Math.round(
// Top of layout box (includes line height)
viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] * dpr +
// Delta from top of layout box (includes line height) to top of the inline box (no line height)
Math.floor((viewportData.lineHeight * dpr - (glyph.fontBoundingBoxAscent + glyph.fontBoundingBoxDescent)) / 2) +
// Delta from top of inline box (no line height) to top of glyph origin. If the glyph was drawn
// with a top baseline for example, this ends up drawing the glyph correctly using the alphabetical
// baseline.
glyph.fontBoundingBoxAscent);
cellIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + x) * 6 /* Constants.IndicesPerCell */;
cellBuffer[cellIndex + 0 /* CellBufferInfo.Offset_X */] = Math.floor(absoluteOffsetX);
cellBuffer[cellIndex + 1 /* CellBufferInfo.Offset_Y */] = absoluteOffsetY;
cellBuffer[cellIndex + 4 /* CellBufferInfo.GlyphIndex */] = glyph.glyphIndex;
cellBuffer[cellIndex + 5 /* CellBufferInfo.TextureIndex */] = glyph.pageIndex;
// Adjust the x pixel offset for the next character
absoluteOffsetX += charWidth;
}
tokenStartIndex = tokenEndIndex;
}
// Clear to end of line
fillStartIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + tokenEndIndex) * 6 /* Constants.IndicesPerCell */;
fillEndIndex = (y * FullFileRenderStrategy.maxSupportedColumns) * 6 /* Constants.IndicesPerCell */;
cellBuffer.fill(0, fillStartIndex, fillEndIndex);
upToDateLines.add(y);
}
const visibleObjectCount = (viewportData.endLineNumber - viewportData.startLineNumber + 1) * lineIndexCount;
// Only write when there is changed data
dirtyLineStart = Math.min(dirtyLineStart, FullFileRenderStrategy.maxSupportedLines);
dirtyLineEnd = Math.min(dirtyLineEnd, FullFileRenderStrategy.maxSupportedLines);
if (dirtyLineStart <= dirtyLineEnd) {
// Write buffer and swap it out to unblock writes
this._device.queue.writeBuffer(this._cellBindBuffer, (dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, cellBuffer.buffer, (dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, (dirtyLineEnd - dirtyLineStart + 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT);
}
this._finalRenderedLine = Math.max(this._finalRenderedLine, dirtyLineEnd);
this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1;
this._visibleObjectCount = visibleObjectCount;
return visibleObjectCount;
}
draw(pass, viewportData) {
if (this._visibleObjectCount <= 0) {
throw new BugIndicatingError('Attempt to draw 0 objects');
}
pass.draw(quadVertices.length / 2, this._visibleObjectCount, undefined, (viewportData.startLineNumber - 1) * FullFileRenderStrategy.maxSupportedColumns);
}
/**
* Queue updates that need to happen on the active buffer, not just the cache. This will be
* deferred to when the actual cell buffer is changed since the active buffer could be locked by
* the GPU which would block the main thread.
*/
_queueBufferUpdate(e) {
this._queuedBufferUpdates[0].push(e);
this._queuedBufferUpdates[1].push(e);
}
}
function parseCssFontWeight(value) {
switch (value) {
case 'lighter':
case 'normal': return 400;
case 'bolder':
case 'bold': return 700;
}
return parseInt(value);
}
function parseCssOpacity(value) {
if (value.endsWith('%')) {
return parseFloat(value.substring(0, value.length - 1)) / 100;
}
if (value.match(/^\d+(?:\.\d*)/)) {
return parseFloat(value);
}
return 1;
}
export { FullFileRenderStrategy };
@@ -0,0 +1,93 @@
import { TextureAtlas } from '../atlas/textureAtlas.js';
import { TextureAtlasPage } from '../atlas/textureAtlasPage.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const fullFileRenderStrategyWgsl = /*wgsl*/ `
struct GlyphInfo {
position: vec2f,
size: vec2f,
origin: vec2f,
};
struct Vertex {
@location(0) position: vec2f,
};
struct Cell {
position: vec2f,
unused1: vec2f,
glyphIndex: f32,
textureIndex: f32
};
struct LayoutInfo {
canvasDims: vec2f,
viewportOffset: vec2f,
viewportDims: vec2f,
}
struct ScrollOffset {
offset: vec2f
}
struct VSOutput {
@builtin(position) position: vec4f,
@location(1) layerIndex: f32,
@location(0) texcoord: vec2f,
};
// Uniforms
@group(0) @binding(${4 /* BindingId.LayoutInfoUniform */}) var<uniform> layoutInfo: LayoutInfo;
@group(0) @binding(${5 /* BindingId.AtlasDimensionsUniform */}) var<uniform> atlasDims: vec2f;
@group(0) @binding(${6 /* BindingId.ScrollOffset */}) var<uniform> scrollOffset: ScrollOffset;
// Storage buffers
@group(0) @binding(${0 /* BindingId.GlyphInfo */}) var<storage, read> glyphInfo: array<array<GlyphInfo, ${TextureAtlasPage.maximumGlyphCount}>, ${TextureAtlas.maximumPageCount}>;
@group(0) @binding(${1 /* BindingId.Cells */}) var<storage, read> cells: array<Cell>;
@vertex fn vs(
vert: Vertex,
@builtin(instance_index) instanceIndex: u32,
@builtin(vertex_index) vertexIndex : u32
) -> VSOutput {
let cell = cells[instanceIndex];
var glyph = glyphInfo[u32(cell.textureIndex)][u32(cell.glyphIndex)];
var vsOut: VSOutput;
// Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1
vsOut.position = vec4f(
// Make everything relative to top left instead of center
vec2f(-1, 1) +
((vert.position * vec2f(2, -2)) / layoutInfo.canvasDims) * glyph.size +
((cell.position * vec2f(2, -2)) / layoutInfo.canvasDims) +
((glyph.origin * vec2f(2, -2)) / layoutInfo.canvasDims) +
(((layoutInfo.viewportOffset - scrollOffset.offset * vec2(1, -1)) * 2) / layoutInfo.canvasDims),
0.0,
1.0
);
vsOut.layerIndex = cell.textureIndex;
// Textures are flipped from natural direction on the y-axis, so flip it back
vsOut.texcoord = vert.position;
vsOut.texcoord = (
// Glyph offset (0-1)
(glyph.position / atlasDims) +
// Glyph coordinate (0-1)
(vsOut.texcoord * (glyph.size / atlasDims))
);
return vsOut;
}
@group(0) @binding(${2 /* BindingId.TextureSampler */}) var ourSampler: sampler;
@group(0) @binding(${3 /* BindingId.Texture */}) var ourTexture: texture_2d_array<f32>;
@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
return textureSample(ourTexture, ourSampler, vsOut.texcoord, u32(vsOut.layerIndex));
}
`;
export { fullFileRenderStrategyWgsl };
@@ -0,0 +1,316 @@
import { getActiveWindow } from '../../../../base/browser/dom.js';
import { Color } from '../../../../base/common/color.js';
import { BugIndicatingError } from '../../../../base/common/errors.js';
import { Emitter } from '../../../../base/common/event.js';
import { CursorColumns } from '../../../common/core/cursorColumns.js';
import { createContentSegmenter } from '../contentSegmenter.js';
import { GPULifecycle } from '../gpuDisposable.js';
import { quadVertices } from '../gpuUtils.js';
import { ViewGpuContext } from '../viewGpuContext.js';
import { BaseRenderStrategy } from './baseRenderStrategy.js';
import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* A render strategy that uploads the content of the entire viewport every frame.
*/
class ViewportRenderStrategy extends BaseRenderStrategy {
/**
* The hard cap for line columns that can be rendered by the GPU renderer.
*/
static { this.maxSupportedColumns = 2000; }
get bindGroupEntries() {
return [
{ binding: 1 /* BindingId.Cells */, resource: { buffer: this._cellBindBuffer } },
{ binding: 6 /* BindingId.ScrollOffset */, resource: { buffer: this._scrollOffsetBindBuffer } }
];
}
constructor(context, viewGpuContext, device, glyphRasterizer) {
super(context, viewGpuContext, device, glyphRasterizer);
this.type = 'viewport';
this.wgsl = fullFileRenderStrategyWgsl;
this._cellBindBufferLineCapacity = 63 /* Constants.CellBindBufferInitialCapacity */;
this._activeDoubleBufferIndex = 0;
this._visibleObjectCount = 0;
this._scrollInitialized = false;
this._onDidChangeBindGroupEntries = this._register(new Emitter());
this.onDidChangeBindGroupEntries = this._onDidChangeBindGroupEntries.event;
this._rebuildCellBuffer(this._cellBindBufferLineCapacity);
const scrollOffsetBufferSize = 2;
this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {
label: 'Monaco scroll offset buffer',
size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
})).object;
this._scrollOffsetValueBuffer = new Float32Array(scrollOffsetBufferSize);
}
_rebuildCellBuffer(lineCount) {
this._cellBindBuffer?.destroy();
// Increase in chunks so resizing a window by hand doesn't keep allocating and throwing away
const lineCountWithIncrement = (Math.floor(lineCount / 32 /* Constants.CellBindBufferCapacityIncrement */) + 1) * 32 /* Constants.CellBindBufferCapacityIncrement */;
const bufferSize = lineCountWithIncrement * ViewportRenderStrategy.maxSupportedColumns * 6 /* Constants.IndicesPerCell */ * Float32Array.BYTES_PER_ELEMENT;
this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {
label: 'Monaco full file cell buffer',
size: bufferSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
})).object;
this._cellValueBuffers = [
new ArrayBuffer(bufferSize),
new ArrayBuffer(bufferSize),
];
this._cellBindBufferLineCapacity = lineCountWithIncrement;
this._onDidChangeBindGroupEntries.fire();
}
// #region Event handlers
// The primary job of these handlers is to:
// 1. Invalidate the up to date line cache, which will cause the line to be re-rendered when
// it's _within the viewport_.
// 2. Pass relevant events on to the render function so it can force certain line ranges to be
// re-rendered even if they're not in the viewport. For example when a view zone is added,
// there are lines that used to be visible but are no longer, so those ranges must be
// cleared and uploaded to the GPU.
onConfigurationChanged(e) {
return true;
}
onDecorationsChanged(e) {
return true;
}
onTokensChanged(e) {
return true;
}
onLinesDeleted(e) {
return true;
}
onLinesInserted(e) {
return true;
}
onLinesChanged(e) {
return true;
}
onScrollChanged(e) {
const dpr = getActiveWindow().devicePixelRatio;
this._scrollOffsetValueBuffer[0] = (e?.scrollLeft ?? this._context.viewLayout.getCurrentScrollLeft()) * dpr;
this._scrollOffsetValueBuffer[1] = (e?.scrollTop ?? this._context.viewLayout.getCurrentScrollTop()) * dpr;
this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer);
return true;
}
onThemeChanged(e) {
return true;
}
onLineMappingChanged(e) {
return true;
}
onZonesChanged(e) {
return true;
}
// #endregion
reset() {
for (const bufferIndex of [0, 1]) {
// Zero out buffer and upload to GPU to prevent stale rows from rendering
const buffer = new Float32Array(this._cellValueBuffers[bufferIndex]);
buffer.fill(0, 0, buffer.length);
this._device.queue.writeBuffer(this._cellBindBuffer, 0, buffer.buffer, 0, buffer.byteLength);
}
}
update(viewportData, viewLineOptions) {
// IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the
// loop. This is done so we don't need to trust the JIT compiler to do this optimization to
// avoid potential additional blocking time in garbage collector which is a common cause of
// dropped frames.
let chars = '';
let segment;
let charWidth = 0;
let y = 0;
let x = 0;
let absoluteOffsetX = 0;
let absoluteOffsetY = 0;
let tabXOffset = 0;
let glyph;
let cellIndex = 0;
let tokenStartIndex = 0;
let tokenEndIndex = 0;
let tokenMetadata = 0;
let decorationStyleSetBold;
let decorationStyleSetColor;
let decorationStyleSetOpacity;
let lineData;
let decoration;
let fillStartIndex = 0;
let fillEndIndex = 0;
let tokens;
const dpr = getActiveWindow().devicePixelRatio;
let contentSegmenter;
if (!this._scrollInitialized) {
this.onScrollChanged();
this._scrollInitialized = true;
}
// Zero out cell buffer or rebuild if needed
if (this._cellBindBufferLineCapacity < viewportData.endLineNumber - viewportData.startLineNumber + 1) {
this._rebuildCellBuffer(viewportData.endLineNumber - viewportData.startLineNumber + 1);
}
const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]);
cellBuffer.fill(0);
const lineIndexCount = ViewportRenderStrategy.maxSupportedColumns * 6 /* Constants.IndicesPerCell */;
for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) {
// Only attempt to render lines that the GPU renderer can handle
if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, y)) {
continue;
}
lineData = viewportData.getViewLineRenderingData(y);
tabXOffset = 0;
contentSegmenter = createContentSegmenter(lineData, viewLineOptions);
charWidth = viewLineOptions.spaceWidth * dpr;
absoluteOffsetX = 0;
tokens = lineData.tokens;
tokenStartIndex = lineData.minColumn - 1;
tokenEndIndex = 0;
for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) {
tokenEndIndex = tokens.getEndOffset(tokenIndex);
if (tokenEndIndex <= tokenStartIndex) {
// The faux indent part of the line should have no token type
continue;
}
tokenMetadata = tokens.getMetadata(tokenIndex);
for (x = tokenStartIndex; x < tokenEndIndex; x++) {
// Only render lines that do not exceed maximum columns
if (x > ViewportRenderStrategy.maxSupportedColumns) {
break;
}
segment = contentSegmenter.getSegmentAtIndex(x);
if (segment === undefined) {
continue;
}
chars = segment;
if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) {
charWidth = this.glyphRasterizer.getTextMetrics(chars).width;
}
decorationStyleSetColor = undefined;
decorationStyleSetBold = undefined;
decorationStyleSetOpacity = undefined;
// Apply supported inline decoration styles to the cell metadata
for (decoration of lineData.inlineDecorations) {
// This is Range.strictContainsPosition except it works at the cell level,
// it's also inlined to avoid overhead.
if ((y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) ||
(y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) ||
(y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1)) {
continue;
}
const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.inlineClassName);
for (const rule of rules) {
for (const r of rule.style) {
const value = rule.styleMap.get(r)?.toString() ?? '';
switch (r) {
case 'color': {
// TODO: This parsing and error handling should move into canRender so fallback
// to DOM works
const parsedColor = Color.Format.CSS.parse(value);
if (!parsedColor) {
throw new BugIndicatingError('Invalid color format ' + value);
}
decorationStyleSetColor = parsedColor.toNumber32Bit();
break;
}
case 'font-weight': {
const parsedValue = parseCssFontWeight(value);
if (parsedValue >= 400) {
decorationStyleSetBold = true;
// TODO: Set bold (https://github.com/microsoft/vscode/issues/237584)
}
else {
decorationStyleSetBold = false;
// TODO: Set normal (https://github.com/microsoft/vscode/issues/237584)
}
break;
}
case 'opacity': {
const parsedValue = parseCssOpacity(value);
decorationStyleSetOpacity = parsedValue;
break;
}
default: throw new BugIndicatingError('Unexpected inline decoration style');
}
}
}
}
if (chars === ' ' || chars === '\t') {
// Zero out glyph to ensure it doesn't get rendered
cellIndex = ((y - 1) * ViewportRenderStrategy.maxSupportedColumns + x) * 6 /* Constants.IndicesPerCell */;
cellBuffer.fill(0, cellIndex, cellIndex + 6 /* CellBufferInfo.FloatsPerEntry */);
// Adjust xOffset for tab stops
if (chars === '\t') {
// Find the pixel offset between the current position and the next tab stop
const offsetBefore = x + tabXOffset;
tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize);
absoluteOffsetX += charWidth * (tabXOffset - offsetBefore);
// Convert back to offset excluding x and the current character
tabXOffset -= x + 1;
}
else {
absoluteOffsetX += charWidth;
}
continue;
}
const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity);
glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX);
absoluteOffsetY = Math.round(
// Top of layout box (includes line height)
viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] * dpr +
// Delta from top of layout box (includes line height) to top of the inline box (no line height)
Math.floor((viewportData.lineHeight * dpr - (glyph.fontBoundingBoxAscent + glyph.fontBoundingBoxDescent)) / 2) +
// Delta from top of inline box (no line height) to top of glyph origin. If the glyph was drawn
// with a top baseline for example, this ends up drawing the glyph correctly using the alphabetical
// baseline.
glyph.fontBoundingBoxAscent);
cellIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns + x) * 6 /* Constants.IndicesPerCell */;
cellBuffer[cellIndex + 0 /* CellBufferInfo.Offset_X */] = Math.floor(absoluteOffsetX);
cellBuffer[cellIndex + 1 /* CellBufferInfo.Offset_Y */] = absoluteOffsetY;
cellBuffer[cellIndex + 4 /* CellBufferInfo.GlyphIndex */] = glyph.glyphIndex;
cellBuffer[cellIndex + 5 /* CellBufferInfo.TextureIndex */] = glyph.pageIndex;
// Adjust the x pixel offset for the next character
absoluteOffsetX += charWidth;
}
tokenStartIndex = tokenEndIndex;
}
// Clear to end of line
fillStartIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns + tokenEndIndex) * 6 /* Constants.IndicesPerCell */;
fillEndIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns) * 6 /* Constants.IndicesPerCell */;
cellBuffer.fill(0, fillStartIndex, fillEndIndex);
}
const visibleObjectCount = (viewportData.endLineNumber - viewportData.startLineNumber + 1) * lineIndexCount;
// This render strategy always uploads the whole viewport
this._device.queue.writeBuffer(this._cellBindBuffer, 0, cellBuffer.buffer, 0, (viewportData.endLineNumber - viewportData.startLineNumber) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT);
this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1;
this._visibleObjectCount = visibleObjectCount;
return visibleObjectCount;
}
draw(pass, viewportData) {
if (this._visibleObjectCount <= 0) {
throw new BugIndicatingError('Attempt to draw 0 objects');
}
pass.draw(quadVertices.length / 2, this._visibleObjectCount);
}
}
function parseCssFontWeight(value) {
switch (value) {
case 'lighter':
case 'normal': return 400;
case 'bolder':
case 'bold': return 700;
}
return parseInt(value);
}
function parseCssOpacity(value) {
if (value.endsWith('%')) {
return parseFloat(value.substring(0, value.length - 1)) / 100;
}
if (value.match(/^\d+(?:\.\d*)/)) {
return parseFloat(value);
}
return 1;
}
export { ViewportRenderStrategy };
@@ -0,0 +1,116 @@
import { getActiveWindow } from '../../../base/browser/dom.js';
import { Disposable, toDisposable } from '../../../base/common/lifecycle.js';
import { ILogService } from '../../../platform/log/common/log.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
let TaskQueue = class TaskQueue extends Disposable {
constructor(_logService) {
super();
this._logService = _logService;
this._tasks = [];
this._i = 0;
this._register(toDisposable(() => this.clear()));
}
enqueue(task) {
this._tasks.push(task);
this._start();
}
clear() {
if (this._idleCallback) {
this._cancelCallback(this._idleCallback);
this._idleCallback = undefined;
}
this._i = 0;
this._tasks.length = 0;
}
_start() {
if (!this._idleCallback) {
this._idleCallback = this._requestCallback(this._process.bind(this));
}
}
_process(deadline) {
this._idleCallback = undefined;
let taskDuration = 0;
let longestTask = 0;
let lastDeadlineRemaining = deadline.timeRemaining();
let deadlineRemaining = 0;
while (this._i < this._tasks.length) {
taskDuration = Date.now();
if (!this._tasks[this._i]()) {
this._i++;
}
// other than performance.now, Date.now might not be stable (changes on wall clock changes),
// this is not an issue here as a clock change during a short running task is very unlikely
// in case it still happened and leads to negative duration, simply assume 1 msec
taskDuration = Math.max(1, Date.now() - taskDuration);
longestTask = Math.max(taskDuration, longestTask);
// Guess the following task will take a similar time to the longest task in this batch, allow
// additional room to try avoid exceeding the deadline
deadlineRemaining = deadline.timeRemaining();
if (longestTask * 1.5 > deadlineRemaining) {
// Warn when the time exceeding the deadline is over 20ms, if this happens in practice the
// task should be split into sub-tasks to ensure the UI remains responsive.
if (lastDeadlineRemaining - taskDuration < -20) {
this._logService.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(lastDeadlineRemaining - taskDuration))}ms`);
}
this._start();
return;
}
lastDeadlineRemaining = deadlineRemaining;
}
this.clear();
}
};
TaskQueue = __decorate([
__param(0, ILogService)
], TaskQueue);
/**
* A queue of that runs tasks over several tasks via setTimeout, trying to maintain above 60 frames
* per second. The tasks will run in the order they are enqueued, but they will run some time later,
* and care should be taken to ensure they're non-urgent and will not introduce race conditions.
*/
class PriorityTaskQueue extends TaskQueue {
_requestCallback(callback) {
return getActiveWindow().setTimeout(() => callback(this._createDeadline(16)));
}
_cancelCallback(identifier) {
getActiveWindow().clearTimeout(identifier);
}
_createDeadline(duration) {
const end = Date.now() + duration;
return {
timeRemaining: () => Math.max(0, end - Date.now())
};
}
}
class IdleTaskQueueInternal extends TaskQueue {
_requestCallback(callback) {
return getActiveWindow().requestIdleCallback(callback);
}
_cancelCallback(identifier) {
getActiveWindow().cancelIdleCallback(identifier);
}
}
/**
* A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's
* deadline given by the environment. The tasks will run in the order they are enqueued, but they
* will run some time later, and care should be taken to ensure they're non-urgent and will not
* introduce race conditions.
*
* This reverts to a {@link PriorityTaskQueue} if the environment does not support idle callbacks.
*/
const IdleTaskQueue = ('requestIdleCallback' in getActiveWindow()) ? IdleTaskQueueInternal : PriorityTaskQueue;
export { IdleTaskQueue, PriorityTaskQueue };
@@ -0,0 +1,241 @@
import { localize } from '../../../nls.js';
import { getActiveWindow, addDisposableListener } from '../../../base/browser/dom.js';
import { createFastDomNode } from '../../../base/browser/fastDomNode.js';
import { BugIndicatingError } from '../../../base/common/errors.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import '../../../base/common/observableInternal/index.js';
import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
import { TextureAtlas } from './atlas/textureAtlas.js';
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
import { Severity, INotificationService } from '../../../platform/notification/common/notification.js';
import { GPULifecycle } from './gpuDisposable.js';
import { ensureNonNullable, observeDevicePixelDimensions } from './gpuUtils.js';
import { RectangleRenderer } from './rectangleRenderer.js';
import { DecorationCssRuleExtractor } from './css/decorationCssRuleExtractor.js';
import { Event } from '../../../base/common/event.js';
import { DecorationStyleCache } from './css/decorationStyleCache.js';
import { runOnChange } from '../../../base/common/observableInternal/utils/runOnChange.js';
import { observableValue } from '../../../base/common/observableInternal/observables/observableValue.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var ViewGpuContext_1;
let ViewGpuContext = class ViewGpuContext extends Disposable {
static { ViewGpuContext_1 = this; }
static { this._decorationCssRuleExtractor = new DecorationCssRuleExtractor(); }
static get decorationCssRuleExtractor() {
return ViewGpuContext_1._decorationCssRuleExtractor;
}
static { this._decorationStyleCache = new DecorationStyleCache(); }
static get decorationStyleCache() {
return ViewGpuContext_1._decorationStyleCache;
}
/**
* The shared texture atlas to use across all views.
*
* @throws if called before the GPU device is resolved
*/
static get atlas() {
if (!ViewGpuContext_1._atlas) {
throw new BugIndicatingError('Cannot call ViewGpuContext.textureAtlas before device is resolved');
}
return ViewGpuContext_1._atlas;
}
/**
* The shared texture atlas to use across all views. This is a convenience alias for
* {@link ViewGpuContext.atlas}.
*
* @throws if called before the GPU device is resolved
*/
get atlas() {
return ViewGpuContext_1.atlas;
}
constructor(context, _instantiationService, _notificationService, configurationService) {
super();
this._instantiationService = _instantiationService;
this._notificationService = _notificationService;
this.configurationService = configurationService;
/**
* The hard cap for line columns rendered by the GPU renderer.
*/
this.maxGpuCols = 2000;
this.canvas = createFastDomNode(document.createElement('canvas'));
this.canvas.setClassName('editorCanvas');
// Adjust the canvas size to avoid drawing under the scroll bar
this._register(Event.runAndSubscribe(configurationService.onDidChangeConfiguration, e => {
if (!e || e.affectsConfiguration('editor.scrollbar.verticalScrollbarSize')) {
const verticalScrollbarSize = configurationService.getValue('editor').scrollbar?.verticalScrollbarSize ?? 14;
this.canvas.domNode.style.boxSizing = 'border-box';
this.canvas.domNode.style.paddingRight = `${verticalScrollbarSize}px`;
}
}));
this.ctx = ensureNonNullable(this.canvas.domNode.getContext('webgpu'));
// Request the GPU device, we only want to do this a single time per window as it's async
// and can delay the initial render.
if (!ViewGpuContext_1.device) {
ViewGpuContext_1.device = GPULifecycle.requestDevice((message) => {
const choices = [{
label: localize(78, "Use DOM-based rendering"),
run: () => this.configurationService.updateValue('editor.experimentalGpuAcceleration', 'off'),
}];
this._notificationService.prompt(Severity.Warning, message, choices);
}).then(ref => {
ViewGpuContext_1.deviceSync = ref.object;
if (!ViewGpuContext_1._atlas) {
ViewGpuContext_1._atlas = this._instantiationService.createInstance(TextureAtlas, ref.object.limits.maxTextureDimension2D, undefined, ViewGpuContext_1.decorationStyleCache);
}
return ref.object;
});
}
const dprObs = observableValue(this, getActiveWindow().devicePixelRatio);
this._register(addDisposableListener(getActiveWindow(), 'resize', () => {
dprObs.set(getActiveWindow().devicePixelRatio, undefined);
}));
this.devicePixelRatio = dprObs;
this._register(runOnChange(this.devicePixelRatio, () => ViewGpuContext_1.atlas?.clear()));
const canvasDevicePixelDimensions = observableValue(this, { width: this.canvas.domNode.width, height: this.canvas.domNode.height });
this._register(observeDevicePixelDimensions(this.canvas.domNode, getActiveWindow(), (width, height) => {
this.canvas.domNode.width = width;
this.canvas.domNode.height = height;
canvasDevicePixelDimensions.set({ width, height }, undefined);
}));
this.canvasDevicePixelDimensions = canvasDevicePixelDimensions;
const contentLeft = observableValue(this, 0);
this._register(this.configurationService.onDidChangeConfiguration(e => {
contentLeft.set(context.configuration.options.get(165 /* EditorOption.layoutInfo */).contentLeft, undefined);
}));
this.contentLeft = contentLeft;
this.rectangleRenderer = this._instantiationService.createInstance(RectangleRenderer, context, this.contentLeft, this.devicePixelRatio, this.canvas.domNode, this.ctx, ViewGpuContext_1.device);
}
/**
* This method determines which lines can be and are allowed to be rendered using the GPU
* renderer. Eventually this should trend all lines, except maybe exceptional cases like
* decorations that use class names.
*/
canRender(options, viewportData, lineNumber) {
const data = viewportData.getViewLineRenderingData(lineNumber);
// Check if the line has simple attributes that aren't supported
if (data.containsRTL ||
data.maxColumn > this.maxGpuCols) {
return false;
}
// Check if all inline decorations are supported
if (data.inlineDecorations.length > 0) {
let supported = true;
for (const decoration of data.inlineDecorations) {
if (decoration.type !== 0 /* InlineDecorationType.Regular */) {
supported = false;
break;
}
const styleRules = ViewGpuContext_1._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName);
supported &&= styleRules.every(rule => {
// Pseudo classes aren't supported currently
if (rule.selectorText.includes(':')) {
return false;
}
for (const r of rule.style) {
if (!supportsCssRule(r, rule.style)) {
return false;
}
}
return true;
});
if (!supported) {
break;
}
}
return supported;
}
return true;
}
/**
* Like {@link canRender} but returns detailed information about why the line cannot be rendered.
*/
canRenderDetailed(options, viewportData, lineNumber) {
const data = viewportData.getViewLineRenderingData(lineNumber);
const reasons = [];
if (data.containsRTL) {
reasons.push('containsRTL');
}
if (data.maxColumn > this.maxGpuCols) {
reasons.push('maxColumn > maxGpuCols');
}
if (data.inlineDecorations.length > 0) {
let supported = true;
const problemTypes = [];
const problemSelectors = [];
const problemRules = [];
for (const decoration of data.inlineDecorations) {
if (decoration.type !== 0 /* InlineDecorationType.Regular */) {
problemTypes.push(decoration.type);
supported = false;
continue;
}
const styleRules = ViewGpuContext_1._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName);
supported &&= styleRules.every(rule => {
// Pseudo classes aren't supported currently
if (rule.selectorText.includes(':')) {
problemSelectors.push(rule.selectorText);
return false;
}
for (const r of rule.style) {
if (!supportsCssRule(r, rule.style)) {
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
problemRules.push(`${r}: ${rule.style[r]}`);
return false;
}
}
return true;
});
if (!supported) {
continue;
}
}
if (problemTypes.length > 0) {
reasons.push(`inlineDecorations with unsupported types (${problemTypes.map(e => `\`${e}\``).join(', ')})`);
}
if (problemRules.length > 0) {
reasons.push(`inlineDecorations with unsupported CSS rules (${problemRules.map(e => `\`${e}\``).join(', ')})`);
}
if (problemSelectors.length > 0) {
reasons.push(`inlineDecorations with unsupported CSS selectors (${problemSelectors.map(e => `\`${e}\``).join(', ')})`);
}
}
return reasons;
}
};
ViewGpuContext = ViewGpuContext_1 = __decorate([
__param(1, IInstantiationService),
__param(2, INotificationService),
__param(3, IConfigurationService)
], ViewGpuContext);
/**
* A list of supported decoration CSS rules that can be used in the GPU renderer.
*/
const gpuSupportedDecorationCssRules = [
'color',
'font-weight',
'opacity',
];
function supportsCssRule(rule, style) {
if (!gpuSupportedDecorationCssRules.includes(rule)) {
return false;
}
// Check for values that aren't supported
switch (rule) {
default: return true;
}
}
export { ViewGpuContext };
@@ -0,0 +1,373 @@
import { equalsIfDefined, itemsEquals } from '../../base/common/equals.js';
import { Disposable, DisposableStore, toDisposable } from '../../base/common/lifecycle.js';
import '../../base/common/observableInternal/index.js';
import { LineRange } from '../common/core/ranges/lineRange.js';
import { OffsetRange } from '../common/core/ranges/offsetRange.js';
import { Position } from '../common/core/position.js';
import { Selection } from '../common/core/selection.js';
import { Point } from '../common/core/2d/point.js';
import { TransactionImpl } from '../../base/common/observableInternal/transaction.js';
import { derivedOpts, derivedWithSetter, derived } from '../../base/common/observableInternal/observables/derived.js';
import { observableValue } from '../../base/common/observableInternal/observables/observableValue.js';
import { observableFromEvent } from '../../base/common/observableInternal/observables/observableFromEvent.js';
import { observableValueOpts } from '../../base/common/observableInternal/observables/observableValueOpts.js';
import { observableSignal } from '../../base/common/observableInternal/observables/observableSignal.js';
import { DebugLocation } from '../../base/common/observableInternal/debugLocation.js';
import { autorunOpts, autorun } from '../../base/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.
*--------------------------------------------------------------------------------------------*/
/**
* Returns a facade for the code editor that provides observables for various states/events.
*/
function observableCodeEditor(editor) {
return ObservableCodeEditor.get(editor);
}
class ObservableCodeEditor extends Disposable {
static { this._map = new Map(); }
/**
* Make sure that editor is not disposed yet!
*/
static get(editor) {
let result = ObservableCodeEditor._map.get(editor);
if (!result) {
result = new ObservableCodeEditor(editor);
ObservableCodeEditor._map.set(editor, result);
const d = editor.onDidDispose(() => {
const item = ObservableCodeEditor._map.get(editor);
if (item) {
ObservableCodeEditor._map.delete(editor);
item.dispose();
d.dispose();
}
});
}
return result;
}
_beginUpdate() {
this._updateCounter++;
if (this._updateCounter === 1) {
this._currentTransaction = new TransactionImpl(() => {
/** @description Update editor state */
});
}
}
_endUpdate() {
this._updateCounter--;
if (this._updateCounter === 0) {
const t = this._currentTransaction;
this._currentTransaction = undefined;
t.finish();
}
}
constructor(editor) {
super();
this.editor = editor;
this._updateCounter = 0;
this._currentTransaction = undefined;
this._model = observableValue(this, this.editor.getModel());
this.model = this._model;
this.isReadonly = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(104 /* EditorOption.readOnly */));
this._versionId = observableValueOpts({ owner: this, lazy: true }, this.editor.getModel()?.getVersionId() ?? null);
this.versionId = this._versionId;
this._selections = observableValueOpts({ owner: this, equalsFn: equalsIfDefined(itemsEquals(Selection.selectionsEqual)), lazy: true }, this.editor.getSelections() ?? null);
this.selections = this._selections;
this.positions = derivedOpts({ owner: this, equalsFn: equalsIfDefined(itemsEquals(Position.equals)) }, reader => this.selections.read(reader)?.map(s => s.getStartPosition()) ?? null);
this.isFocused = observableFromEvent(this, e => {
const d1 = this.editor.onDidFocusEditorWidget(e);
const d2 = this.editor.onDidBlurEditorWidget(e);
return {
dispose() {
d1.dispose();
d2.dispose();
}
};
}, () => this.editor.hasWidgetFocus());
this.isTextFocused = observableFromEvent(this, e => {
const d1 = this.editor.onDidFocusEditorText(e);
const d2 = this.editor.onDidBlurEditorText(e);
return {
dispose() {
d1.dispose();
d2.dispose();
}
};
}, () => this.editor.hasTextFocus());
this.inComposition = observableFromEvent(this, e => {
const d1 = this.editor.onDidCompositionStart(() => {
e(undefined);
});
const d2 = this.editor.onDidCompositionEnd(() => {
e(undefined);
});
return {
dispose() {
d1.dispose();
d2.dispose();
}
};
}, () => this.editor.inComposition);
this.value = derivedWithSetter(this, reader => { this.versionId.read(reader); return this.model.read(reader)?.getValue() ?? ''; }, (value, tx) => {
const model = this.model.get();
if (model !== null) {
if (value !== model.getValue()) {
model.setValue(value);
}
}
});
this.valueIsEmpty = derived(this, reader => { this.versionId.read(reader); return this.editor.getModel()?.getValueLength() === 0; });
this.cursorSelection = derivedOpts({ owner: this, equalsFn: equalsIfDefined(Selection.selectionsEqual) }, reader => this.selections.read(reader)?.[0] ?? null);
this.cursorPosition = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.selections.read(reader)?.[0]?.getPosition() ?? null);
this.cursorLineNumber = derived(this, reader => this.cursorPosition.read(reader)?.lineNumber ?? null);
this.onDidType = observableSignal(this);
this.onDidPaste = observableSignal(this);
this.scrollTop = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollTop());
this.scrollLeft = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollLeft());
this.layoutInfo = observableFromEvent(this.editor.onDidLayoutChange, () => this.editor.getLayoutInfo());
this.layoutInfoContentLeft = this.layoutInfo.map(l => l.contentLeft);
this.layoutInfoDecorationsLeft = this.layoutInfo.map(l => l.decorationsLeft);
this.layoutInfoWidth = this.layoutInfo.map(l => l.width);
this.layoutInfoHeight = this.layoutInfo.map(l => l.height);
this.layoutInfoMinimap = this.layoutInfo.map(l => l.minimap);
this.layoutInfoVerticalScrollbarWidth = this.layoutInfo.map(l => l.verticalScrollbarWidth);
this.contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth());
this.contentHeight = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentHeight());
this._widgetCounter = 0;
this.openedPeekWidgets = observableValue(this, 0);
this._register(this.editor.onBeginUpdate(() => this._beginUpdate()));
this._register(this.editor.onEndUpdate(() => this._endUpdate()));
this._register(this.editor.onDidChangeModel(() => {
this._beginUpdate();
try {
this._model.set(this.editor.getModel(), this._currentTransaction);
this._forceUpdate();
}
finally {
this._endUpdate();
}
}));
this._register(this.editor.onDidType((e) => {
this._beginUpdate();
try {
this._forceUpdate();
this.onDidType.trigger(this._currentTransaction, e);
}
finally {
this._endUpdate();
}
}));
this._register(this.editor.onDidPaste((e) => {
this._beginUpdate();
try {
this._forceUpdate();
this.onDidPaste.trigger(this._currentTransaction, e);
}
finally {
this._endUpdate();
}
}));
this._register(this.editor.onDidChangeModelContent(e => {
this._beginUpdate();
try {
this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, e);
this._forceUpdate();
}
finally {
this._endUpdate();
}
}));
this._register(this.editor.onDidChangeCursorSelection(e => {
this._beginUpdate();
try {
this._selections.set(this.editor.getSelections(), this._currentTransaction, e);
this._forceUpdate();
}
finally {
this._endUpdate();
}
}));
this.domNode = derived(reader => {
this.model.read(reader);
return this.editor.getDomNode();
});
}
forceUpdate(cb) {
this._beginUpdate();
try {
this._forceUpdate();
if (!cb) {
return undefined;
}
return cb(this._currentTransaction);
}
finally {
this._endUpdate();
}
}
_forceUpdate() {
this._beginUpdate();
try {
this._model.set(this.editor.getModel(), this._currentTransaction);
this._versionId.set(this.editor.getModel()?.getVersionId() ?? null, this._currentTransaction, undefined);
this._selections.set(this.editor.getSelections(), this._currentTransaction, undefined);
}
finally {
this._endUpdate();
}
}
getOption(id, debugLocation = DebugLocation.ofCaller()) {
return observableFromEvent(this, cb => this.editor.onDidChangeConfiguration(e => {
if (e.hasChanged(id)) {
cb(undefined);
}
}), () => this.editor.getOption(id), debugLocation);
}
setDecorations(decorations) {
const d = new DisposableStore();
const decorationsCollection = this.editor.createDecorationsCollection();
d.add(autorunOpts({ owner: this, debugName: () => `Apply decorations from ${decorations.debugName}` }, reader => {
const d = decorations.read(reader);
decorationsCollection.set(d);
}));
d.add({
dispose: () => {
decorationsCollection.clear();
}
});
return d;
}
createOverlayWidget(widget) {
const overlayWidgetId = 'observableOverlayWidget' + (this._widgetCounter++);
const w = {
getDomNode: () => widget.domNode,
getPosition: () => widget.position.get(),
getId: () => overlayWidgetId,
allowEditorOverflow: widget.allowEditorOverflow,
getMinContentWidthInPx: () => widget.minContentWidthInPx.get(),
};
this.editor.addOverlayWidget(w);
const d = autorun(reader => {
widget.position.read(reader);
widget.minContentWidthInPx.read(reader);
this.editor.layoutOverlayWidget(w);
});
return toDisposable(() => {
d.dispose();
this.editor.removeOverlayWidget(w);
});
}
createContentWidget(widget) {
const contentWidgetId = 'observableContentWidget' + (this._widgetCounter++);
const w = {
getDomNode: () => widget.domNode,
getPosition: () => widget.position.get(),
getId: () => contentWidgetId,
allowEditorOverflow: widget.allowEditorOverflow,
};
this.editor.addContentWidget(w);
const d = autorun(reader => {
widget.position.read(reader);
this.editor.layoutContentWidget(w);
});
return toDisposable(() => {
d.dispose();
this.editor.removeContentWidget(w);
});
}
observeLineOffsetRange(lineRange, store) {
const start = this.observePosition(lineRange.map(r => new Position(r.startLineNumber, 1)), store);
const end = this.observePosition(lineRange.map(r => new Position(r.endLineNumberExclusive + 1, 1)), store);
return derived(reader => {
start.read(reader);
end.read(reader);
const range = lineRange.read(reader);
const lineCount = this.model.read(reader)?.getLineCount();
const s = ((typeof lineCount !== 'undefined' && range.startLineNumber > lineCount
? this.editor.getBottomForLineNumber(lineCount)
: this.editor.getTopForLineNumber(range.startLineNumber))
- this.scrollTop.read(reader));
const e = range.isEmpty ? s : (this.editor.getBottomForLineNumber(range.endLineNumberExclusive - 1) - this.scrollTop.read(reader));
return new OffsetRange(s, e);
});
}
observePosition(position, store) {
let pos = position.get();
const result = observableValueOpts({ owner: this, debugName: () => `topLeftOfPosition${pos?.toString()}`, equalsFn: equalsIfDefined(Point.equals) }, new Point(0, 0));
const contentWidgetId = `observablePositionWidget` + (this._widgetCounter++);
const domNode = document.createElement('div');
const w = {
getDomNode: () => domNode,
getPosition: () => {
return pos ? { preference: [0 /* ContentWidgetPositionPreference.EXACT */], position: position.get() } : null;
},
getId: () => contentWidgetId,
allowEditorOverflow: false,
afterRender: (position, coordinate) => {
const model = this._model.get();
if (model && pos && pos.lineNumber > model.getLineCount()) {
// the position is after the last line
result.set(new Point(0, this.editor.getBottomForLineNumber(model.getLineCount()) - this.scrollTop.get()), undefined);
}
else {
result.set(coordinate ? new Point(coordinate.left, coordinate.top) : null, undefined);
}
},
};
this.editor.addContentWidget(w);
store.add(autorun(reader => {
pos = position.read(reader);
this.editor.layoutContentWidget(w);
}));
store.add(toDisposable(() => {
this.editor.removeContentWidget(w);
}));
return result;
}
isTargetHovered(predicate, store) {
const isHovered = observableValue('isInjectedTextHovered', false);
store.add(this.editor.onMouseMove(e => {
const val = predicate(e);
isHovered.set(val, undefined);
}));
store.add(this.editor.onMouseLeave(E => {
isHovered.set(false, undefined);
}));
return isHovered;
}
observeLineHeightForPosition(position) {
return derived(reader => {
const pos = position instanceof Position ? position : position.read(reader);
if (pos === null) {
return null;
}
this.getOption(75 /* EditorOption.lineHeight */).read(reader);
return this.editor.getLineHeightForPosition(pos);
});
}
observeLineHeightForLine(lineNumber) {
if (typeof lineNumber === 'number') {
return this.observeLineHeightForPosition(new Position(lineNumber, 1));
}
return derived(reader => {
const line = lineNumber.read(reader);
if (line === null) {
return null;
}
return this.observeLineHeightForPosition(new Position(line, 1)).read(reader);
});
}
observeLineHeightsForLineRange(lineNumber) {
return derived(reader => {
const range = lineNumber instanceof LineRange ? lineNumber : lineNumber.read(reader);
const heights = [];
for (let i = range.startLineNumber; i < range.endLineNumberExclusive; i++) {
heights.push(this.observeLineHeightForLine(i).read(reader));
}
return heights;
});
}
}
export { ObservableCodeEditor, observableCodeEditor };
@@ -0,0 +1,129 @@
import { Emitter } from '../../../base/common/event.js';
import { Disposable, toDisposable } from '../../../base/common/lifecycle.js';
import { LinkedList } from '../../../base/common/linkedList.js';
import { IThemeService } from '../../../platform/theme/common/themeService.js';
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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
let AbstractCodeEditorService = class AbstractCodeEditorService extends Disposable {
constructor(_themeService) {
super();
this._themeService = _themeService;
this._onWillCreateCodeEditor = this._register(new Emitter());
this._onCodeEditorAdd = this._register(new Emitter());
this.onCodeEditorAdd = this._onCodeEditorAdd.event;
this._onCodeEditorRemove = this._register(new Emitter());
this.onCodeEditorRemove = this._onCodeEditorRemove.event;
this._onWillCreateDiffEditor = this._register(new Emitter());
this._onDiffEditorAdd = this._register(new Emitter());
this.onDiffEditorAdd = this._onDiffEditorAdd.event;
this._onDiffEditorRemove = this._register(new Emitter());
this.onDiffEditorRemove = this._onDiffEditorRemove.event;
this._decorationOptionProviders = new Map();
this._codeEditorOpenHandlers = new LinkedList();
this._modelProperties = new Map();
this._codeEditors = Object.create(null);
this._diffEditors = Object.create(null);
this._globalStyleSheet = null;
}
willCreateCodeEditor() {
this._onWillCreateCodeEditor.fire();
}
addCodeEditor(editor) {
this._codeEditors[editor.getId()] = editor;
this._onCodeEditorAdd.fire(editor);
}
removeCodeEditor(editor) {
if (delete this._codeEditors[editor.getId()]) {
this._onCodeEditorRemove.fire(editor);
}
}
listCodeEditors() {
return Object.keys(this._codeEditors).map(id => this._codeEditors[id]);
}
willCreateDiffEditor() {
this._onWillCreateDiffEditor.fire();
}
addDiffEditor(editor) {
this._diffEditors[editor.getId()] = editor;
this._onDiffEditorAdd.fire(editor);
}
removeDiffEditor(editor) {
if (delete this._diffEditors[editor.getId()]) {
this._onDiffEditorRemove.fire(editor);
}
}
listDiffEditors() {
return Object.keys(this._diffEditors).map(id => this._diffEditors[id]);
}
getFocusedCodeEditor() {
let editorWithWidgetFocus = null;
const editors = this.listCodeEditors();
for (const editor of editors) {
if (editor.hasTextFocus()) {
// bingo!
return editor;
}
if (editor.hasWidgetFocus()) {
editorWithWidgetFocus = editor;
}
}
return editorWithWidgetFocus;
}
removeDecorationType(key) {
const provider = this._decorationOptionProviders.get(key);
if (provider) {
provider.refCount--;
if (provider.refCount <= 0) {
this._decorationOptionProviders.delete(key);
provider.dispose();
this.listCodeEditors().forEach((ed) => ed.removeDecorationsByType(key));
}
}
}
setModelProperty(resource, key, value) {
const key1 = resource.toString();
let dest;
if (this._modelProperties.has(key1)) {
dest = this._modelProperties.get(key1);
}
else {
dest = new Map();
this._modelProperties.set(key1, dest);
}
dest.set(key, value);
}
getModelProperty(resource, key) {
const key1 = resource.toString();
if (this._modelProperties.has(key1)) {
const innerMap = this._modelProperties.get(key1);
return innerMap.get(key);
}
return undefined;
}
async openCodeEditor(input, source, sideBySide) {
for (const handler of this._codeEditorOpenHandlers) {
const candidate = await handler(input, source, sideBySide);
if (candidate !== null) {
return candidate;
}
}
return null;
}
registerCodeEditorOpenHandler(handler) {
const rm = this._codeEditorOpenHandlers.unshift(handler);
return toDisposable(rm);
}
};
AbstractCodeEditorService = __decorate([
__param(0, IThemeService)
], AbstractCodeEditorService);
export { AbstractCodeEditorService };
@@ -0,0 +1,76 @@
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
import { URI } from '../../../base/common/uri.js';
import { isObject } from '../../../base/common/types.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const IBulkEditService = createDecorator('IWorkspaceEditService');
class ResourceEdit {
constructor(metadata) {
this.metadata = metadata;
}
static convert(edit) {
return edit.edits.map(edit => {
if (ResourceTextEdit.is(edit)) {
return ResourceTextEdit.lift(edit);
}
if (ResourceFileEdit.is(edit)) {
return ResourceFileEdit.lift(edit);
}
throw new Error('Unsupported edit');
});
}
}
class ResourceTextEdit extends ResourceEdit {
static is(candidate) {
if (candidate instanceof ResourceTextEdit) {
return true;
}
return isObject(candidate)
&& URI.isUri(candidate.resource)
&& isObject(candidate.textEdit);
}
static lift(edit) {
if (edit instanceof ResourceTextEdit) {
return edit;
}
else {
return new ResourceTextEdit(edit.resource, edit.textEdit, edit.versionId, edit.metadata);
}
}
constructor(resource, textEdit, versionId = undefined, metadata) {
super(metadata);
this.resource = resource;
this.textEdit = textEdit;
this.versionId = versionId;
}
}
class ResourceFileEdit extends ResourceEdit {
static is(candidate) {
if (candidate instanceof ResourceFileEdit) {
return true;
}
else {
return isObject(candidate)
&& (Boolean(candidate.newResource) || Boolean(candidate.oldResource));
}
}
static lift(edit) {
if (edit instanceof ResourceFileEdit) {
return edit;
}
else {
return new ResourceFileEdit(edit.oldResource, edit.newResource, edit.options, edit.metadata);
}
}
constructor(oldResource, newResource, options = {}, metadata) {
super(metadata);
this.oldResource = oldResource;
this.newResource = newResource;
this.options = options;
}
}
export { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit };
@@ -0,0 +1,9 @@
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const ICodeEditorService = createDecorator('codeEditorService');
export { ICodeEditorService };
@@ -0,0 +1,366 @@
import { timeout } from '../../../base/common/async.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { logOnceWebWorkerWarning } from '../../../base/common/worker/webWorker.js';
import { createWebWorker } from '../../../base/browser/webWorkerFactory.js';
import { Range } from '../../common/core/range.js';
import { ILanguageConfigurationService } from '../../common/languages/languageConfigurationRegistry.js';
import { EditorWorker } from '../../common/services/editorWebWorker.js';
import { IModelService } from '../../common/services/model.js';
import { ITextResourceConfigurationService } from '../../common/services/textResourceConfiguration.js';
import { isNonEmptyArray } from '../../../base/common/arrays.js';
import { ILogService } from '../../../platform/log/common/log.js';
import { StopWatch } from '../../../base/common/stopwatch.js';
import { canceled } from '../../../base/common/errors.js';
import { ILanguageFeaturesService } from '../../common/services/languageFeatures.js';
import { MovedText } from '../../common/diff/linesDiffComputer.js';
import { LineRangeMapping, DetailedLineRangeMapping, RangeMapping } from '../../common/diff/rangeMapping.js';
import { LineRange } from '../../common/core/ranges/lineRange.js';
import { mainWindow } from '../../../base/browser/window.js';
import { WindowIntervalTimer } from '../../../base/browser/dom.js';
import { WorkerTextModelSyncClient } from '../../common/services/textModelSync/textModelSync.impl.js';
import { EditorWorkerHost } from '../../common/services/editorWorkerHost.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
/**
* Stop the worker if it was not needed for 5 min.
*/
const STOP_WORKER_DELTA_TIME_MS = 5 * 60 * 1000;
function canSyncModel(modelService, resource) {
const model = modelService.getModel(resource);
if (!model) {
return false;
}
if (model.isTooLargeForSyncing()) {
return false;
}
return true;
}
let EditorWorkerService = class EditorWorkerService extends Disposable {
constructor(workerDescriptor, modelService, configurationService, logService, _languageConfigurationService, languageFeaturesService) {
super();
this._languageConfigurationService = _languageConfigurationService;
this._modelService = modelService;
this._workerManager = this._register(new WorkerManager(workerDescriptor, this._modelService));
this._logService = logService;
// register default link-provider and default completions-provider
this._register(languageFeaturesService.linkProvider.register({ language: '*', hasAccessToAllModels: true }, {
provideLinks: async (model, token) => {
if (!canSyncModel(this._modelService, model.uri)) {
return Promise.resolve({ links: [] }); // File too large
}
const worker = await this._workerWithResources([model.uri]);
const links = await worker.$computeLinks(model.uri.toString());
return links && { links };
}
}));
this._register(languageFeaturesService.completionProvider.register('*', new WordBasedCompletionItemProvider(this._workerManager, configurationService, this._modelService, this._languageConfigurationService, this._logService)));
}
dispose() {
super.dispose();
}
canComputeUnicodeHighlights(uri) {
return canSyncModel(this._modelService, uri);
}
async computedUnicodeHighlights(uri, options, range) {
const worker = await this._workerWithResources([uri]);
return worker.$computeUnicodeHighlights(uri.toString(), options, range);
}
async computeDiff(original, modified, options, algorithm) {
const worker = await this._workerWithResources([original, modified], /* forceLargeModels */ true);
const result = await worker.$computeDiff(original.toString(), modified.toString(), options, algorithm);
if (!result) {
return null;
}
// Convert from space efficient JSON data to rich objects.
const diff = {
identical: result.identical,
quitEarly: result.quitEarly,
changes: toLineRangeMappings(result.changes),
moves: result.moves.map(m => new MovedText(new LineRangeMapping(new LineRange(m[0], m[1]), new LineRange(m[2], m[3])), toLineRangeMappings(m[4])))
};
return diff;
function toLineRangeMappings(changes) {
return changes.map((c) => new DetailedLineRangeMapping(new LineRange(c[0], c[1]), new LineRange(c[2], c[3]), c[4]?.map((c) => new RangeMapping(new Range(c[0], c[1], c[2], c[3]), new Range(c[4], c[5], c[6], c[7])))));
}
}
async computeMoreMinimalEdits(resource, edits, pretty = false) {
if (isNonEmptyArray(edits)) {
if (!canSyncModel(this._modelService, resource)) {
return Promise.resolve(edits); // File too large
}
const sw = StopWatch.create();
const result = this._workerWithResources([resource]).then(worker => worker.$computeMoreMinimalEdits(resource.toString(), edits, pretty));
result.finally(() => this._logService.trace('FORMAT#computeMoreMinimalEdits', resource.toString(true), sw.elapsed()));
return Promise.race([result, timeout(1000).then(() => edits)]);
}
else {
return Promise.resolve(undefined);
}
}
canNavigateValueSet(resource) {
return (canSyncModel(this._modelService, resource));
}
async navigateValueSet(resource, range, up) {
const model = this._modelService.getModel(resource);
if (!model) {
return null;
}
const wordDefRegExp = this._languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition();
const wordDef = wordDefRegExp.source;
const wordDefFlags = wordDefRegExp.flags;
const worker = await this._workerWithResources([resource]);
return worker.$navigateValueSet(resource.toString(), range, up, wordDef, wordDefFlags);
}
canComputeWordRanges(resource) {
return canSyncModel(this._modelService, resource);
}
async computeWordRanges(resource, range) {
const model = this._modelService.getModel(resource);
if (!model) {
return Promise.resolve(null);
}
const wordDefRegExp = this._languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition();
const wordDef = wordDefRegExp.source;
const wordDefFlags = wordDefRegExp.flags;
const worker = await this._workerWithResources([resource]);
return worker.$computeWordRanges(resource.toString(), range, wordDef, wordDefFlags);
}
async findSectionHeaders(uri, options) {
const worker = await this._workerWithResources([uri]);
return worker.$findSectionHeaders(uri.toString(), options);
}
async computeDefaultDocumentColors(uri) {
const worker = await this._workerWithResources([uri]);
return worker.$computeDefaultDocumentColors(uri.toString());
}
async _workerWithResources(resources, forceLargeModels = false) {
const worker = await this._workerManager.withWorker();
return await worker.workerWithSyncedResources(resources, forceLargeModels);
}
};
EditorWorkerService = __decorate([
__param(1, IModelService),
__param(2, ITextResourceConfigurationService),
__param(3, ILogService),
__param(4, ILanguageConfigurationService),
__param(5, ILanguageFeaturesService)
], EditorWorkerService);
class WordBasedCompletionItemProvider {
constructor(workerManager, configurationService, modelService, languageConfigurationService, logService) {
this.languageConfigurationService = languageConfigurationService;
this.logService = logService;
this._debugDisplayName = 'wordbasedCompletions';
this._workerManager = workerManager;
this._configurationService = configurationService;
this._modelService = modelService;
}
async provideCompletionItems(model, position) {
const config = this._configurationService.getValue(model.uri, position, 'editor');
if (config.wordBasedSuggestions === 'off') {
return undefined;
}
const models = [];
if (config.wordBasedSuggestions === 'currentDocument') {
// only current file and only if not too large
if (canSyncModel(this._modelService, model.uri)) {
models.push(model.uri);
}
}
else {
// either all files or files of same language
for (const candidate of this._modelService.getModels()) {
if (!canSyncModel(this._modelService, candidate.uri)) {
continue;
}
if (candidate === model) {
models.unshift(candidate.uri);
}
else if (config.wordBasedSuggestions === 'allDocuments' || candidate.getLanguageId() === model.getLanguageId()) {
models.push(candidate.uri);
}
}
}
if (models.length === 0) {
return undefined; // File too large, no other files
}
const wordDefRegExp = this.languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).getWordDefinition();
const word = model.getWordAtPosition(position);
const replace = !word ? Range.fromPositions(position) : new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn);
const insert = replace.setEndPosition(position.lineNumber, position.column);
// Trace logging about the word and replace/insert ranges
this.logService.trace('[WordBasedCompletionItemProvider]', `word: "${word?.word || ''}", wordDef: "${wordDefRegExp}", replace: [${replace.toString()}], insert: [${insert.toString()}]`);
const client = await this._workerManager.withWorker();
const data = await client.textualSuggest(models, word?.word, wordDefRegExp);
if (!data) {
return undefined;
}
return {
duration: data.duration,
suggestions: data.words.map((word) => {
return {
kind: 18 /* languages.CompletionItemKind.Text */,
label: word,
insertText: word,
range: { insert, replace }
};
}),
};
}
}
let WorkerManager = class WorkerManager extends Disposable {
constructor(_workerDescriptor, modelService) {
super();
this._workerDescriptor = _workerDescriptor;
this._modelService = modelService;
this._editorWorkerClient = null;
this._lastWorkerUsedTime = (new Date()).getTime();
const stopWorkerInterval = this._register(new WindowIntervalTimer());
stopWorkerInterval.cancelAndSet(() => this._checkStopIdleWorker(), Math.round(STOP_WORKER_DELTA_TIME_MS / 2), mainWindow);
this._register(this._modelService.onModelRemoved(_ => this._checkStopEmptyWorker()));
}
dispose() {
if (this._editorWorkerClient) {
this._editorWorkerClient.dispose();
this._editorWorkerClient = null;
}
super.dispose();
}
/**
* Check if the model service has no more models and stop the worker if that is the case.
*/
_checkStopEmptyWorker() {
if (!this._editorWorkerClient) {
return;
}
const models = this._modelService.getModels();
if (models.length === 0) {
// There are no more models => nothing possible for me to do
this._editorWorkerClient.dispose();
this._editorWorkerClient = null;
}
}
/**
* Check if the worker has been idle for a while and then stop it.
*/
_checkStopIdleWorker() {
if (!this._editorWorkerClient) {
return;
}
const timeSinceLastWorkerUsedTime = (new Date()).getTime() - this._lastWorkerUsedTime;
if (timeSinceLastWorkerUsedTime > STOP_WORKER_DELTA_TIME_MS) {
this._editorWorkerClient.dispose();
this._editorWorkerClient = null;
}
}
withWorker() {
this._lastWorkerUsedTime = (new Date()).getTime();
if (!this._editorWorkerClient) {
this._editorWorkerClient = new EditorWorkerClient(this._workerDescriptor, false, this._modelService);
}
return Promise.resolve(this._editorWorkerClient);
}
};
WorkerManager = __decorate([
__param(1, IModelService)
], WorkerManager);
class SynchronousWorkerClient {
constructor(instance) {
this._instance = instance;
this.proxy = this._instance;
}
dispose() {
this._instance.dispose();
}
setChannel(channel, handler) {
throw new Error(`Not supported`);
}
}
let EditorWorkerClient = class EditorWorkerClient extends Disposable {
constructor(_workerDescriptorOrWorker, keepIdleModels, modelService) {
super();
this._workerDescriptorOrWorker = _workerDescriptorOrWorker;
this._disposed = false;
this._modelService = modelService;
this._keepIdleModels = keepIdleModels;
this._worker = null;
this._modelManager = null;
}
// foreign host request
fhr(method, args) {
throw new Error(`Not implemented!`);
}
_getOrCreateWorker() {
if (!this._worker) {
try {
this._worker = this._register(createWebWorker(this._workerDescriptorOrWorker));
EditorWorkerHost.setChannel(this._worker, this._createEditorWorkerHost());
}
catch (err) {
logOnceWebWorkerWarning(err);
this._worker = this._createFallbackLocalWorker();
}
}
return this._worker;
}
async _getProxy() {
try {
const proxy = this._getOrCreateWorker().proxy;
await proxy.$ping();
return proxy;
}
catch (err) {
logOnceWebWorkerWarning(err);
this._worker = this._createFallbackLocalWorker();
return this._worker.proxy;
}
}
_createFallbackLocalWorker() {
return new SynchronousWorkerClient(new EditorWorker(null));
}
_createEditorWorkerHost() {
return {
$fhr: (method, args) => this.fhr(method, args)
};
}
_getOrCreateModelManager(proxy) {
if (!this._modelManager) {
this._modelManager = this._register(new WorkerTextModelSyncClient(proxy, this._modelService, this._keepIdleModels));
}
return this._modelManager;
}
async workerWithSyncedResources(resources, forceLargeModels = false) {
if (this._disposed) {
return Promise.reject(canceled());
}
const proxy = await this._getProxy();
this._getOrCreateModelManager(proxy).ensureSyncedResources(resources, forceLargeModels);
return proxy;
}
async textualSuggest(resources, leadingWord, wordDefRegExp) {
const proxy = await this.workerWithSyncedResources(resources);
const wordDef = wordDefRegExp.source;
const wordDefFlags = wordDefRegExp.flags;
return proxy.$textualSuggest(resources.map(r => r.toString()), leadingWord, wordDef, wordDefFlags);
}
dispose() {
super.dispose();
this._disposed = true;
}
};
EditorWorkerClient = __decorate([
__param(2, IModelService)
], EditorWorkerClient);
export { EditorWorkerClient, EditorWorkerService };
@@ -0,0 +1,170 @@
import { TimeoutTimer } from '../../../base/common/async.js';
import { BugIndicatingError } from '../../../base/common/errors.js';
import { Emitter } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { localize, localize2 } from '../../../nls.js';
import { Action2 } from '../../../platform/actions/common/actions.js';
import { RawContextKey, IContextKeyService, ContextKeyExpr } from '../../../platform/contextkey/common/contextkey.js';
import { registerSingleton } from '../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
import { IQuickInputService } from '../../../platform/quickinput/common/quickInput.js';
import { IStorageService } from '../../../platform/storage/common/storage.js';
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
const IInlineCompletionsService = createDecorator('IInlineCompletionsService');
const InlineCompletionsSnoozing = new RawContextKey('inlineCompletions.snoozed', false, localize(79, "Whether inline completions are currently snoozed"));
let InlineCompletionsService = class InlineCompletionsService extends Disposable {
get snoozeTimeLeft() {
if (this._snoozeTimeEnd === undefined) {
return 0;
}
return Math.max(0, this._snoozeTimeEnd - Date.now());
}
constructor(_contextKeyService, _telemetryService) {
super();
this._contextKeyService = _contextKeyService;
this._telemetryService = _telemetryService;
this._onDidChangeIsSnoozing = this._register(new Emitter());
this.onDidChangeIsSnoozing = this._onDidChangeIsSnoozing.event; // 5 minutes
this._snoozeTimeEnd = undefined;
this._recentCompletionIds = [];
this._timer = this._register(new TimeoutTimer());
const inlineCompletionsSnoozing = InlineCompletionsSnoozing.bindTo(this._contextKeyService);
this._register(this.onDidChangeIsSnoozing(() => inlineCompletionsSnoozing.set(this.isSnoozing())));
}
setSnoozeDuration(durationMs) {
if (durationMs < 0) {
throw new BugIndicatingError(`Invalid snooze duration: ${durationMs}. Duration must be non-negative.`);
}
if (durationMs === 0) {
this.cancelSnooze();
return;
}
const wasSnoozing = this.isSnoozing();
const timeLeft = this.snoozeTimeLeft;
this._snoozeTimeEnd = Date.now() + durationMs;
if (!wasSnoozing) {
this._onDidChangeIsSnoozing.fire(true);
}
this._timer.cancelAndSet(() => {
if (!this.isSnoozing()) {
this._onDidChangeIsSnoozing.fire(false);
}
else {
throw new BugIndicatingError('Snooze timer did not fire as expected');
}
}, this.snoozeTimeLeft + 1);
this._reportSnooze(durationMs - timeLeft, durationMs);
}
isSnoozing() {
return this.snoozeTimeLeft > 0;
}
cancelSnooze() {
if (this.isSnoozing()) {
this._reportSnooze(-this.snoozeTimeLeft, 0);
this._snoozeTimeEnd = undefined;
this._timer.cancel();
this._onDidChangeIsSnoozing.fire(false);
}
}
reportNewCompletion(requestUuid) {
this._lastCompletionId = requestUuid;
this._recentCompletionIds.unshift(requestUuid);
if (this._recentCompletionIds.length > 5) {
this._recentCompletionIds.pop();
}
}
_reportSnooze(deltaMs, totalMs) {
const deltaSeconds = Math.round(deltaMs / 1000);
const totalSeconds = Math.round(totalMs / 1000);
this._telemetryService.publicLog2('inlineCompletions.snooze', {
deltaSeconds,
totalSeconds,
lastCompletionId: this._lastCompletionId,
recentCompletionIds: this._recentCompletionIds,
});
}
};
InlineCompletionsService = __decorate([
__param(0, IContextKeyService),
__param(1, ITelemetryService)
], InlineCompletionsService);
registerSingleton(IInlineCompletionsService, InlineCompletionsService, 1 /* InstantiationType.Delayed */);
const snoozeInlineSuggestId = 'editor.action.inlineSuggest.snooze';
const cancelSnoozeInlineSuggestId = 'editor.action.inlineSuggest.cancelSnooze';
const LAST_SNOOZE_DURATION_KEY = 'inlineCompletions.lastSnoozeDuration';
class SnoozeInlineCompletion extends Action2 {
static { this.ID = snoozeInlineSuggestId; }
constructor() {
super({
id: SnoozeInlineCompletion.ID,
title: localize2(81, "Snooze Inline Suggestions"),
precondition: ContextKeyExpr.true(),
f1: true,
});
}
async run(accessor, ...args) {
const quickInputService = accessor.get(IQuickInputService);
const inlineCompletionsService = accessor.get(IInlineCompletionsService);
const storageService = accessor.get(IStorageService);
let durationMs;
if (args.length > 0 && typeof args[0] === 'number') {
durationMs = args[0] * 60_000;
}
if (!durationMs) {
durationMs = await this.getDurationFromUser(quickInputService, storageService);
}
if (durationMs) {
inlineCompletionsService.setSnoozeDuration(durationMs);
}
}
async getDurationFromUser(quickInputService, storageService) {
const lastSelectedDuration = storageService.getNumber(LAST_SNOOZE_DURATION_KEY, 0 /* StorageScope.PROFILE */, 300_000);
const items = [
{ label: '1 minute', id: '1', value: 60_000 },
{ label: '5 minutes', id: '5', value: 300_000 },
{ label: '10 minutes', id: '10', value: 600_000 },
{ label: '15 minutes', id: '15', value: 900_000 },
{ label: '30 minutes', id: '30', value: 1_800_000 },
{ label: '60 minutes', id: '60', value: 3_600_000 }
];
const picked = await quickInputService.pick(items, {
placeHolder: localize(80, "Select snooze duration for Inline Suggestions"),
activeItem: items.find(item => item.value === lastSelectedDuration),
});
if (picked) {
storageService.store(LAST_SNOOZE_DURATION_KEY, picked.value, 0 /* StorageScope.PROFILE */, 0 /* StorageTarget.USER */);
return picked.value;
}
return undefined;
}
}
class CancelSnoozeInlineCompletion extends Action2 {
static { this.ID = cancelSnoozeInlineSuggestId; }
constructor() {
super({
id: CancelSnoozeInlineCompletion.ID,
title: localize2(82, "Cancel Snooze Inline Suggestions"),
precondition: InlineCompletionsSnoozing,
f1: true,
});
}
async run(accessor) {
accessor.get(IInlineCompletionsService).cancelSnooze();
}
}
export { CancelSnoozeInlineCompletion, IInlineCompletionsService, InlineCompletionsService, SnoozeInlineCompletion };
@@ -0,0 +1,30 @@
import { IMarkerDecorationsService } from '../../common/services/markerDecorations.js';
import { registerEditorContribution } from '../editorExtensions.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
let MarkerDecorationsContribution = class MarkerDecorationsContribution {
static { this.ID = 'editor.contrib.markerDecorations'; }
constructor(_editor, _markerDecorationsService) {
// Doesn't do anything, just requires `IMarkerDecorationsService` to make sure it gets instantiated
}
dispose() {
}
};
MarkerDecorationsContribution = __decorate([
__param(1, IMarkerDecorationsService)
], MarkerDecorationsContribution);
registerEditorContribution(MarkerDecorationsContribution.ID, MarkerDecorationsContribution, 0 /* EditorContributionInstantiation.Eager */); // eager because it instantiates IMarkerDecorationsService which is responsible for rendering squiggles
export { MarkerDecorationsContribution };
@@ -0,0 +1,222 @@
import { windowOpenNoOpener } from '../../../base/browser/dom.js';
import { mainWindow } from '../../../base/browser/window.js';
import { CancellationToken } from '../../../base/common/cancellation.js';
import { LinkedList } from '../../../base/common/linkedList.js';
import { ResourceMap } from '../../../base/common/map.js';
import { parse } from '../../../base/common/marshalling.js';
import { matchesScheme, Schemas, matchesSomeScheme } from '../../../base/common/network.js';
import { normalizePath } from '../../../base/common/resources.js';
import { URI } from '../../../base/common/uri.js';
import { ICodeEditorService } from './codeEditorService.js';
import { ICommandService } from '../../../platform/commands/common/commands.js';
import { EditorOpenSource } from '../../../platform/editor/common/editor.js';
import { extractSelection } from '../../../platform/opener/common/opener.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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
let CommandOpener = class CommandOpener {
constructor(_commandService) {
this._commandService = _commandService;
}
async open(target, options) {
if (!matchesScheme(target, Schemas.command)) {
return false;
}
if (!options?.allowCommands) {
// silently ignore commands when command-links are disabled, also
// suppress other openers by returning TRUE
return true;
}
if (typeof target === 'string') {
target = URI.parse(target);
}
if (Array.isArray(options.allowCommands)) {
// Only allow specific commands
if (!options.allowCommands.includes(target.path)) {
// Suppress other openers by returning TRUE
return true;
}
}
// execute as command
let args = [];
try {
args = parse(decodeURIComponent(target.query));
}
catch {
// ignore and retry
try {
args = parse(target.query);
}
catch {
// ignore error
}
}
if (!Array.isArray(args)) {
args = [args];
}
await this._commandService.executeCommand(target.path, ...args);
return true;
}
};
CommandOpener = __decorate([
__param(0, ICommandService)
], CommandOpener);
let EditorOpener = class EditorOpener {
constructor(_editorService) {
this._editorService = _editorService;
}
async open(target, options) {
if (typeof target === 'string') {
target = URI.parse(target);
}
const { selection, uri } = extractSelection(target);
target = uri;
if (target.scheme === Schemas.file) {
target = normalizePath(target); // workaround for non-normalized paths (https://github.com/microsoft/vscode/issues/12954)
}
await this._editorService.openCodeEditor({
resource: target,
options: {
selection,
source: options?.fromUserGesture ? EditorOpenSource.USER : EditorOpenSource.API,
...options?.editorOptions
}
}, this._editorService.getFocusedCodeEditor(), options?.openToSide);
return true;
}
};
EditorOpener = __decorate([
__param(0, ICodeEditorService)
], EditorOpener);
let OpenerService = class OpenerService {
constructor(editorService, commandService) {
this._openers = new LinkedList();
this._validators = new LinkedList();
this._resolvers = new LinkedList();
this._resolvedUriTargets = new ResourceMap(uri => uri.with({ path: null, fragment: null, query: null }).toString());
this._externalOpeners = new LinkedList();
// Default external opener is going through window.open()
this._defaultExternalOpener = {
openExternal: async (href) => {
// ensure to open HTTP/HTTPS links into new windows
// to not trigger a navigation. Any other link is
// safe to be set as HREF to prevent a blank window
// from opening.
if (matchesSomeScheme(href, Schemas.http, Schemas.https)) {
windowOpenNoOpener(href);
}
else {
mainWindow.location.href = href;
}
return true;
}
};
// Default opener: any external, maito, http(s), command, and catch-all-editors
this._openers.push({
open: async (target, options) => {
if (options?.openExternal || matchesSomeScheme(target, Schemas.mailto, Schemas.http, Schemas.https, Schemas.vsls)) {
// open externally
await this._doOpenExternal(target, options);
return true;
}
return false;
}
});
this._openers.push(new CommandOpener(commandService));
this._openers.push(new EditorOpener(editorService));
}
registerOpener(opener) {
const remove = this._openers.unshift(opener);
return { dispose: remove };
}
async open(target, options) {
// check with contributed validators
if (!options?.skipValidation) {
const targetURI = typeof target === 'string' ? URI.parse(target) : target;
const validationTarget = this._resolvedUriTargets.get(targetURI) ?? target; // validate against the original URI that this URI resolves to, if one exists
for (const validator of this._validators) {
if (!(await validator.shouldOpen(validationTarget, options))) {
return false;
}
}
}
// check with contributed openers
for (const opener of this._openers) {
const handled = await opener.open(target, options);
if (handled) {
return true;
}
}
return false;
}
async resolveExternalUri(resource, options) {
for (const resolver of this._resolvers) {
try {
const result = await resolver.resolveExternalUri(resource, options);
if (result) {
if (!this._resolvedUriTargets.has(result.resolved)) {
this._resolvedUriTargets.set(result.resolved, resource);
}
return result;
}
}
catch {
// noop
}
}
throw new Error('Could not resolve external URI: ' + resource.toString());
}
async _doOpenExternal(resource, options) {
//todo@jrieken IExternalUriResolver should support `uri: URI | string`
const uri = typeof resource === 'string' ? URI.parse(resource) : resource;
let externalUri;
try {
externalUri = (await this.resolveExternalUri(uri, options)).resolved;
}
catch {
externalUri = uri;
}
let href;
if (typeof resource === 'string' && uri.toString() === externalUri.toString()) {
// open the url-string AS IS
href = resource;
}
else {
// open URI using the toString(noEncode)+encodeURI-trick
href = encodeURI(externalUri.toString(true));
}
if (options?.allowContributedOpeners) {
const preferredOpenerId = typeof options?.allowContributedOpeners === 'string' ? options?.allowContributedOpeners : undefined;
for (const opener of this._externalOpeners) {
const didOpen = await opener.openExternal(href, {
sourceUri: uri,
preferredOpenerId,
}, CancellationToken.None);
if (didOpen) {
return true;
}
}
}
return this._defaultExternalOpener.openExternal(href, { sourceUri: uri }, CancellationToken.None);
}
dispose() {
this._validators.clear();
}
};
OpenerService = __decorate([
__param(0, ICodeEditorService),
__param(1, ICommandService)
], OpenerService);
export { OpenerService };
@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class StableEditorScrollState {
static capture(editor) {
if (editor.getScrollTop() === 0 || editor.hasPendingScrollAnimation()) {
// Never mess with the scroll top if the editor is at the top of the file or if there is a pending scroll animation
return new StableEditorScrollState(editor.getScrollTop(), editor.getContentHeight(), null, 0, null);
}
let visiblePosition = null;
let visiblePositionScrollDelta = 0;
const visibleRanges = editor.getVisibleRanges();
if (visibleRanges.length > 0) {
visiblePosition = visibleRanges[0].getStartPosition();
const visiblePositionScrollTop = editor.getTopForPosition(visiblePosition.lineNumber, visiblePosition.column);
visiblePositionScrollDelta = editor.getScrollTop() - visiblePositionScrollTop;
}
return new StableEditorScrollState(editor.getScrollTop(), editor.getContentHeight(), visiblePosition, visiblePositionScrollDelta, editor.getPosition());
}
constructor(_initialScrollTop, _initialContentHeight, _visiblePosition, _visiblePositionScrollDelta, _cursorPosition) {
this._initialScrollTop = _initialScrollTop;
this._initialContentHeight = _initialContentHeight;
this._visiblePosition = _visiblePosition;
this._visiblePositionScrollDelta = _visiblePositionScrollDelta;
this._cursorPosition = _cursorPosition;
}
restore(editor) {
if (this._initialContentHeight === editor.getContentHeight() && this._initialScrollTop === editor.getScrollTop()) {
// The editor's content height and scroll top haven't changed, so we don't need to do anything
return;
}
if (this._visiblePosition) {
const visiblePositionScrollTop = editor.getTopForPosition(this._visiblePosition.lineNumber, this._visiblePosition.column);
editor.setScrollTop(visiblePositionScrollTop + this._visiblePositionScrollDelta);
}
}
restoreRelativeVerticalPositionOfCursor(editor) {
if (this._initialContentHeight === editor.getContentHeight() && this._initialScrollTop === editor.getScrollTop()) {
// The editor's content height and scroll top haven't changed, so we don't need to do anything
return;
}
const currentCursorPosition = editor.getPosition();
if (!this._cursorPosition || !currentCursorPosition) {
return;
}
const offset = editor.getTopForLineNumber(currentCursorPosition.lineNumber) - editor.getTopForLineNumber(this._cursorPosition.lineNumber);
editor.setScrollTop(editor.getScrollTop() + offset, 1 /* ScrollType.Immediate */);
}
}
export { StableEditorScrollState };
@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Registry for commands that can trigger Inline Edits (NES) when invoked.
*/
class TriggerInlineEditCommandsRegistry {
static { this.REGISTERED_COMMANDS = new Set(); }
static getRegisteredCommands() {
return [...TriggerInlineEditCommandsRegistry.REGISTERED_COMMANDS];
}
static registerCommand(commandId) {
TriggerInlineEditCommandsRegistry.REGISTERED_COMMANDS.add(commandId);
}
}
export { TriggerInlineEditCommandsRegistry };
+720
View File
@@ -0,0 +1,720 @@
import { runAtThisOrScheduleAtNextAnimationFrame, getWindow, trackFocus } from '../../base/browser/dom.js';
import { createFastDomNode } from '../../base/browser/fastDomNode.js';
import { inputLatency } from '../../base/browser/performance.js';
import { BugIndicatingError, onUnexpectedError } from '../../base/common/errors.js';
import { Disposable } from '../../base/common/lifecycle.js';
import { PointerHandlerLastRenderData } from './controller/mouseTarget.js';
import { PointerHandler } from './controller/pointerHandler.js';
import { RenderingContext } from './view/renderingContext.js';
import { ViewController } from './view/viewController.js';
import { ContentViewOverlays, MarginViewOverlays } from './view/viewOverlays.js';
import { PartFingerprints } from './view/viewPart.js';
import { ViewUserInputEvents } from './view/viewUserInputEvents.js';
import { BlockDecorations } from './viewParts/blockDecorations/blockDecorations.js';
import { ViewContentWidgets } from './viewParts/contentWidgets/contentWidgets.js';
import { CurrentLineHighlightOverlay, CurrentLineMarginHighlightOverlay } from './viewParts/currentLineHighlight/currentLineHighlight.js';
import { DecorationsOverlay } from './viewParts/decorations/decorations.js';
import { EditorScrollbar } from './viewParts/editorScrollbar/editorScrollbar.js';
import { GlyphMarginWidgets } from './viewParts/glyphMargin/glyphMargin.js';
import { IndentGuidesOverlay } from './viewParts/indentGuides/indentGuides.js';
import { LineNumbersOverlay } from './viewParts/lineNumbers/lineNumbers.js';
import { ViewLines } from './viewParts/viewLines/viewLines.js';
import { LinesDecorationsOverlay } from './viewParts/linesDecorations/linesDecorations.js';
import { Margin } from './viewParts/margin/margin.js';
import { MarginViewLineDecorationsOverlay } from './viewParts/marginDecorations/marginDecorations.js';
import { Minimap } from './viewParts/minimap/minimap.js';
import { ViewOverlayWidgets } from './viewParts/overlayWidgets/overlayWidgets.js';
import { DecorationsOverviewRuler } from './viewParts/overviewRuler/decorationsOverviewRuler.js';
import { OverviewRuler } from './viewParts/overviewRuler/overviewRuler.js';
import { Rulers } from './viewParts/rulers/rulers.js';
import { ScrollDecorationViewPart } from './viewParts/scrollDecoration/scrollDecoration.js';
import { SelectionsOverlay } from './viewParts/selections/selections.js';
import { ViewCursors } from './viewParts/viewCursors/viewCursors.js';
import { ViewZones } from './viewParts/viewZones/viewZones.js';
import { WhitespaceOverlay } from './viewParts/whitespace/whitespace.js';
import { Position } from '../common/core/position.js';
import { Range } from '../common/core/range.js';
import { Selection } from '../common/core/selection.js';
import { GlyphMarginLane } from '../common/model.js';
import { ViewEventHandler } from '../common/viewEventHandler.js';
import { ViewportData } from '../common/viewLayout/viewLinesViewportData.js';
import { ViewContext } from '../common/viewModel/viewContext.js';
import { IInstantiationService } from '../../platform/instantiation/common/instantiation.js';
import { getThemeTypeSelector } from '../../platform/theme/common/themeService.js';
import { ViewGpuContext } from './gpu/viewGpuContext.js';
import { ViewLinesGpu } from './viewParts/viewLinesGpu/viewLinesGpu.js';
import { TextAreaEditContext } from './controller/editContext/textArea/textAreaEditContext.js';
import { NativeEditContext } from './controller/editContext/native/nativeEditContext.js';
import { RulersGpu } from './viewParts/rulersGpu/rulersGpu.js';
import { GpuMarkOverlay } from './viewParts/gpuMark/gpuMark.js';
import { Emitter } from '../../base/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.
*--------------------------------------------------------------------------------------------*/
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 __param = (undefined && undefined.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
let View = class View extends ViewEventHandler {
constructor(editorContainer, ownerID, commandDelegate, configuration, colorTheme, model, userInputEvents, overflowWidgetsDomNode, _instantiationService) {
super();
this._instantiationService = _instantiationService;
// Actual mutable state
this._shouldRecomputeGlyphMarginLanes = false;
this._ownerID = ownerID;
this._widgetFocusTracker = this._register(new CodeEditorWidgetFocusTracker(editorContainer, overflowWidgetsDomNode));
this._register(this._widgetFocusTracker.onChange(() => {
this._context.viewModel.setHasWidgetFocus(this._widgetFocusTracker.hasFocus());
}));
this._selections = [new Selection(1, 1, 1, 1)];
this._renderAnimationFrame = null;
this._overflowGuardContainer = createFastDomNode(document.createElement('div'));
PartFingerprints.write(this._overflowGuardContainer, 3 /* PartFingerprint.OverflowGuard */);
this._overflowGuardContainer.setClassName('overflow-guard');
this._viewController = new ViewController(configuration, model, userInputEvents, commandDelegate);
// The view context is passed on to most classes (basically to reduce param. counts in ctors)
this._context = new ViewContext(configuration, colorTheme, model);
// Ensure the view is the first event handler in order to update the layout
this._context.addEventHandler(this);
this._viewParts = [];
// Keyboard handler
this._editContextEnabled = this._context.configuration.options.get(170 /* EditorOption.effectiveEditContext */);
this._accessibilitySupport = this._context.configuration.options.get(2 /* EditorOption.accessibilitySupport */);
this._editContext = this._instantiateEditContext();
this._viewParts.push(this._editContext);
// These two dom nodes must be constructed up front, since references are needed in the layout provider (scrolling & co.)
this._linesContent = createFastDomNode(document.createElement('div'));
this._linesContent.setClassName('lines-content' + ' monaco-editor-background');
this._linesContent.setPosition('absolute');
this.domNode = createFastDomNode(document.createElement('div'));
this.domNode.setClassName(this._getEditorClassName());
// Set role 'code' for better screen reader support https://github.com/microsoft/vscode/issues/93438
this.domNode.setAttribute('role', 'code');
if (this._context.configuration.options.get(46 /* EditorOption.experimentalGpuAcceleration */) === 'on') {
this._viewGpuContext = this._instantiationService.createInstance(ViewGpuContext, this._context);
}
this._scrollbar = new EditorScrollbar(this._context, this._linesContent, this.domNode, this._overflowGuardContainer);
this._viewParts.push(this._scrollbar);
// View Lines
this._viewLines = new ViewLines(this._context, this._viewGpuContext, this._linesContent);
if (this._viewGpuContext) {
this._viewLinesGpu = this._instantiationService.createInstance(ViewLinesGpu, this._context, this._viewGpuContext);
}
// View Zones
this._viewZones = new ViewZones(this._context);
this._viewParts.push(this._viewZones);
// Decorations overview ruler
const decorationsOverviewRuler = new DecorationsOverviewRuler(this._context);
this._viewParts.push(decorationsOverviewRuler);
const scrollDecoration = new ScrollDecorationViewPart(this._context);
this._viewParts.push(scrollDecoration);
const contentViewOverlays = new ContentViewOverlays(this._context);
this._viewParts.push(contentViewOverlays);
contentViewOverlays.addDynamicOverlay(new CurrentLineHighlightOverlay(this._context));
contentViewOverlays.addDynamicOverlay(new SelectionsOverlay(this._context));
contentViewOverlays.addDynamicOverlay(new IndentGuidesOverlay(this._context));
contentViewOverlays.addDynamicOverlay(new DecorationsOverlay(this._context));
contentViewOverlays.addDynamicOverlay(new WhitespaceOverlay(this._context));
const marginViewOverlays = new MarginViewOverlays(this._context);
this._viewParts.push(marginViewOverlays);
marginViewOverlays.addDynamicOverlay(new CurrentLineMarginHighlightOverlay(this._context));
marginViewOverlays.addDynamicOverlay(new MarginViewLineDecorationsOverlay(this._context));
marginViewOverlays.addDynamicOverlay(new LinesDecorationsOverlay(this._context));
marginViewOverlays.addDynamicOverlay(new LineNumbersOverlay(this._context));
if (this._viewGpuContext) {
marginViewOverlays.addDynamicOverlay(new GpuMarkOverlay(this._context, this._viewGpuContext));
}
// Glyph margin widgets
this._glyphMarginWidgets = new GlyphMarginWidgets(this._context);
this._viewParts.push(this._glyphMarginWidgets);
const margin = new Margin(this._context);
margin.getDomNode().appendChild(this._viewZones.marginDomNode);
margin.getDomNode().appendChild(marginViewOverlays.getDomNode());
margin.getDomNode().appendChild(this._glyphMarginWidgets.domNode);
this._viewParts.push(margin);
// Content widgets
this._contentWidgets = new ViewContentWidgets(this._context, this.domNode);
this._viewParts.push(this._contentWidgets);
this._viewCursors = new ViewCursors(this._context);
this._viewParts.push(this._viewCursors);
// Overlay widgets
this._overlayWidgets = new ViewOverlayWidgets(this._context, this.domNode);
this._viewParts.push(this._overlayWidgets);
const rulers = this._viewGpuContext
? new RulersGpu(this._context, this._viewGpuContext)
: new Rulers(this._context);
this._viewParts.push(rulers);
const blockOutline = new BlockDecorations(this._context);
this._viewParts.push(blockOutline);
const minimap = new Minimap(this._context);
this._viewParts.push(minimap);
// -------------- Wire dom nodes up
if (decorationsOverviewRuler) {
const overviewRulerData = this._scrollbar.getOverviewRulerLayoutInfo();
overviewRulerData.parent.insertBefore(decorationsOverviewRuler.getDomNode(), overviewRulerData.insertBefore);
}
this._linesContent.appendChild(contentViewOverlays.getDomNode());
if ('domNode' in rulers) {
this._linesContent.appendChild(rulers.domNode);
}
this._linesContent.appendChild(this._viewZones.domNode);
this._linesContent.appendChild(this._viewLines.getDomNode());
this._linesContent.appendChild(this._contentWidgets.domNode);
this._linesContent.appendChild(this._viewCursors.getDomNode());
this._overflowGuardContainer.appendChild(margin.getDomNode());
this._overflowGuardContainer.appendChild(this._scrollbar.getDomNode());
if (this._viewGpuContext) {
this._overflowGuardContainer.appendChild(this._viewGpuContext.canvas);
}
this._overflowGuardContainer.appendChild(scrollDecoration.getDomNode());
this._overflowGuardContainer.appendChild(this._overlayWidgets.getDomNode());
this._overflowGuardContainer.appendChild(minimap.getDomNode());
this._overflowGuardContainer.appendChild(blockOutline.domNode);
this.domNode.appendChild(this._overflowGuardContainer);
if (overflowWidgetsDomNode) {
overflowWidgetsDomNode.appendChild(this._contentWidgets.overflowingContentWidgetsDomNode.domNode);
overflowWidgetsDomNode.appendChild(this._overlayWidgets.overflowingOverlayWidgetsDomNode.domNode);
}
else {
this.domNode.appendChild(this._contentWidgets.overflowingContentWidgetsDomNode);
this.domNode.appendChild(this._overlayWidgets.overflowingOverlayWidgetsDomNode);
}
this._applyLayout();
// Pointer handler
this._pointerHandler = this._register(new PointerHandler(this._context, this._viewController, this._createPointerHandlerHelper()));
}
_instantiateEditContext() {
const usingExperimentalEditContext = this._context.configuration.options.get(170 /* EditorOption.effectiveEditContext */);
if (usingExperimentalEditContext) {
return this._instantiationService.createInstance(NativeEditContext, this._ownerID, this._context, this._overflowGuardContainer, this._viewController, this._createTextAreaHandlerHelper());
}
else {
return this._instantiationService.createInstance(TextAreaEditContext, this._context, this._overflowGuardContainer, this._viewController, this._createTextAreaHandlerHelper());
}
}
_updateEditContext() {
const editContextEnabled = this._context.configuration.options.get(170 /* EditorOption.effectiveEditContext */);
const accessibilitySupport = this._context.configuration.options.get(2 /* EditorOption.accessibilitySupport */);
if (this._editContextEnabled === editContextEnabled && this._accessibilitySupport === accessibilitySupport) {
return;
}
this._editContextEnabled = editContextEnabled;
this._accessibilitySupport = accessibilitySupport;
const isEditContextFocused = this._editContext.isFocused();
const indexOfEditContext = this._viewParts.indexOf(this._editContext);
this._editContext.dispose();
this._editContext = this._instantiateEditContext();
if (isEditContextFocused) {
this._editContext.focus();
}
if (indexOfEditContext !== -1) {
this._viewParts.splice(indexOfEditContext, 1, this._editContext);
}
}
_computeGlyphMarginLanes() {
const model = this._context.viewModel.model;
const laneModel = this._context.viewModel.glyphLanes;
let glyphs = [];
let maxLineNumber = 0;
// Add all margin decorations
glyphs = glyphs.concat(model.getAllMarginDecorations().map((decoration) => {
const lane = decoration.options.glyphMargin?.position ?? GlyphMarginLane.Center;
maxLineNumber = Math.max(maxLineNumber, decoration.range.endLineNumber);
return { range: decoration.range, lane, persist: decoration.options.glyphMargin?.persistLane };
}));
// Add all glyph margin widgets
glyphs = glyphs.concat(this._glyphMarginWidgets.getWidgets().map((widget) => {
const range = model.validateRange(widget.preference.range);
maxLineNumber = Math.max(maxLineNumber, range.endLineNumber);
return { range, lane: widget.preference.lane };
}));
// Sorted by their start position
glyphs.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range));
laneModel.reset(maxLineNumber);
for (const glyph of glyphs) {
laneModel.push(glyph.lane, glyph.range, glyph.persist);
}
return laneModel;
}
_createPointerHandlerHelper() {
return {
viewDomNode: this.domNode.domNode,
linesContentDomNode: this._linesContent.domNode,
viewLinesDomNode: this._viewLines.getDomNode().domNode,
viewLinesGpu: this._viewLinesGpu,
focusTextArea: () => {
this.focus();
},
dispatchTextAreaEvent: (event) => {
this._editContext.domNode.domNode.dispatchEvent(event);
},
getLastRenderData: () => {
const lastViewCursorsRenderData = this._viewCursors.getLastRenderData() || [];
const lastTextareaPosition = this._editContext.getLastRenderData();
return new PointerHandlerLastRenderData(lastViewCursorsRenderData, lastTextareaPosition);
},
renderNow: () => {
this.render(true, false);
},
shouldSuppressMouseDownOnViewZone: (viewZoneId) => {
return this._viewZones.shouldSuppressMouseDownOnViewZone(viewZoneId);
},
shouldSuppressMouseDownOnWidget: (widgetId) => {
return this._contentWidgets.shouldSuppressMouseDownOnWidget(widgetId);
},
getPositionFromDOMInfo: (spanNode, offset) => {
this._flushAccumulatedAndRenderNow();
return this._viewLines.getPositionFromDOMInfo(spanNode, offset);
},
visibleRangeForPosition: (lineNumber, column) => {
this._flushAccumulatedAndRenderNow();
const position = new Position(lineNumber, column);
return this._viewLines.visibleRangeForPosition(position) ?? this._viewLinesGpu?.visibleRangeForPosition(position) ?? null;
},
getLineWidth: (lineNumber) => {
this._flushAccumulatedAndRenderNow();
if (this._viewLinesGpu) {
const result = this._viewLinesGpu.getLineWidth(lineNumber);
if (result !== undefined) {
return result;
}
}
return this._viewLines.getLineWidth(lineNumber);
}
};
}
_createTextAreaHandlerHelper() {
return {
visibleRangeForPosition: (position) => {
this._flushAccumulatedAndRenderNow();
return this._viewLines.visibleRangeForPosition(position);
},
linesVisibleRangesForRange: (range, includeNewLines) => {
this._flushAccumulatedAndRenderNow();
return this._viewLines.linesVisibleRangesForRange(range, includeNewLines);
}
};
}
_applyLayout() {
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this.domNode.setWidth(layoutInfo.width);
this.domNode.setHeight(layoutInfo.height);
this._overflowGuardContainer.setWidth(layoutInfo.width);
this._overflowGuardContainer.setHeight(layoutInfo.height);
// https://stackoverflow.com/questions/38905916/content-in-google-chrome-larger-than-16777216-px-not-being-rendered
this._linesContent.setWidth(16777216);
this._linesContent.setHeight(16777216);
}
_getEditorClassName() {
const focused = this._editContext.isFocused() ? ' focused' : '';
return this._context.configuration.options.get(162 /* EditorOption.editorClassName */) + ' ' + getThemeTypeSelector(this._context.theme.type) + focused;
}
// --- begin event handlers
handleEvents(events) {
super.handleEvents(events);
this._scheduleRender();
}
onConfigurationChanged(e) {
this.domNode.setClassName(this._getEditorClassName());
this._updateEditContext();
this._applyLayout();
return false;
}
onCursorStateChanged(e) {
this._selections = e.selections;
return false;
}
onDecorationsChanged(e) {
if (e.affectsGlyphMargin) {
this._shouldRecomputeGlyphMarginLanes = true;
}
return false;
}
onFocusChanged(e) {
this.domNode.setClassName(this._getEditorClassName());
return false;
}
onThemeChanged(e) {
this._context.theme.update(e.theme);
this.domNode.setClassName(this._getEditorClassName());
return false;
}
// --- end event handlers
dispose() {
if (this._renderAnimationFrame !== null) {
this._renderAnimationFrame.dispose();
this._renderAnimationFrame = null;
}
this._contentWidgets.overflowingContentWidgetsDomNode.domNode.remove();
this._overlayWidgets.overflowingOverlayWidgetsDomNode.domNode.remove();
this._context.removeEventHandler(this);
this._viewGpuContext?.dispose();
this._viewLines.dispose();
this._viewLinesGpu?.dispose();
// Destroy view parts
for (const viewPart of this._viewParts) {
viewPart.dispose();
}
super.dispose();
}
_scheduleRender() {
if (this._store.isDisposed) {
throw new BugIndicatingError();
}
if (this._renderAnimationFrame === null) {
// TODO: workaround fix for https://github.com/microsoft/vscode/issues/229825
if (this._editContext instanceof NativeEditContext) {
this._editContext.setEditContextOnDomNode();
}
const rendering = this._createCoordinatedRendering();
this._renderAnimationFrame = EditorRenderingCoordinator.INSTANCE.scheduleCoordinatedRendering({
window: getWindow(this.domNode?.domNode),
prepareRenderText: () => {
if (this._store.isDisposed) {
throw new BugIndicatingError();
}
try {
return rendering.prepareRenderText();
}
finally {
this._renderAnimationFrame = null;
}
},
renderText: () => {
if (this._store.isDisposed) {
throw new BugIndicatingError();
}
return rendering.renderText();
},
prepareRender: (viewParts, ctx) => {
if (this._store.isDisposed) {
throw new BugIndicatingError();
}
return rendering.prepareRender(viewParts, ctx);
},
render: (viewParts, ctx) => {
if (this._store.isDisposed) {
throw new BugIndicatingError();
}
return rendering.render(viewParts, ctx);
}
});
}
}
_flushAccumulatedAndRenderNow() {
const rendering = this._createCoordinatedRendering();
safeInvokeNoArg(() => rendering.prepareRenderText());
const data = safeInvokeNoArg(() => rendering.renderText());
if (data) {
const [viewParts, ctx] = data;
safeInvokeNoArg(() => rendering.prepareRender(viewParts, ctx));
safeInvokeNoArg(() => rendering.render(viewParts, ctx));
}
}
_getViewPartsToRender() {
const result = [];
let resultLen = 0;
for (const viewPart of this._viewParts) {
if (viewPart.shouldRender()) {
result[resultLen++] = viewPart;
}
}
return result;
}
_createCoordinatedRendering() {
return {
prepareRenderText: () => {
if (this._shouldRecomputeGlyphMarginLanes) {
this._shouldRecomputeGlyphMarginLanes = false;
const model = this._computeGlyphMarginLanes();
this._context.configuration.setGlyphMarginDecorationLaneCount(model.requiredLanes);
}
inputLatency.onRenderStart();
},
renderText: () => {
if (!this.domNode.domNode.isConnected) {
return null;
}
let viewPartsToRender = this._getViewPartsToRender();
if (!this._viewLines.shouldRender() && viewPartsToRender.length === 0) {
// Nothing to render
return null;
}
const partialViewportData = this._context.viewLayout.getLinesViewportData();
this._context.viewModel.setViewport(partialViewportData.startLineNumber, partialViewportData.endLineNumber, partialViewportData.centeredLineNumber);
const viewportData = new ViewportData(this._selections, partialViewportData, this._context.viewLayout.getWhitespaceViewportData(), this._context.viewModel);
if (this._contentWidgets.shouldRender()) {
// Give the content widgets a chance to set their max width before a possible synchronous layout
this._contentWidgets.onBeforeRender(viewportData);
}
if (this._viewLines.shouldRender()) {
this._viewLines.renderText(viewportData);
this._viewLines.onDidRender();
// Rendering of viewLines might cause scroll events to occur, so collect view parts to render again
viewPartsToRender = this._getViewPartsToRender();
}
if (this._viewLinesGpu?.shouldRender()) {
this._viewLinesGpu.renderText(viewportData);
this._viewLinesGpu.onDidRender();
}
return [viewPartsToRender, new RenderingContext(this._context.viewLayout, viewportData, this._viewLines, this._viewLinesGpu)];
},
prepareRender: (viewPartsToRender, ctx) => {
for (const viewPart of viewPartsToRender) {
viewPart.prepareRender(ctx);
}
},
render: (viewPartsToRender, ctx) => {
for (const viewPart of viewPartsToRender) {
viewPart.render(ctx);
viewPart.onDidRender();
}
}
};
}
// --- BEGIN CodeEditor helpers
delegateVerticalScrollbarPointerDown(browserEvent) {
this._scrollbar.delegateVerticalScrollbarPointerDown(browserEvent);
}
delegateScrollFromMouseWheelEvent(browserEvent) {
this._scrollbar.delegateScrollFromMouseWheelEvent(browserEvent);
}
restoreState(scrollPosition) {
this._context.viewModel.viewLayout.setScrollPosition({
scrollTop: scrollPosition.scrollTop,
scrollLeft: scrollPosition.scrollLeft
}, 1 /* ScrollType.Immediate */);
this._context.viewModel.visibleLinesStabilized();
}
getOffsetForColumn(modelLineNumber, modelColumn) {
const modelPosition = this._context.viewModel.model.validatePosition({
lineNumber: modelLineNumber,
column: modelColumn
});
const viewPosition = this._context.viewModel.coordinatesConverter.convertModelPositionToViewPosition(modelPosition);
this._flushAccumulatedAndRenderNow();
const visibleRange = this._viewLines.visibleRangeForPosition(new Position(viewPosition.lineNumber, viewPosition.column));
if (!visibleRange) {
return -1;
}
return visibleRange.left;
}
getTargetAtClientPoint(clientX, clientY) {
const mouseTarget = this._pointerHandler.getTargetAtClientPoint(clientX, clientY);
if (!mouseTarget) {
return null;
}
return ViewUserInputEvents.convertViewToModelMouseTarget(mouseTarget, this._context.viewModel.coordinatesConverter);
}
createOverviewRuler(cssClassName) {
return new OverviewRuler(this._context, cssClassName);
}
change(callback) {
this._viewZones.changeViewZones(callback);
this._scheduleRender();
}
render(now, everything) {
if (everything) {
// Force everything to render...
this._viewLines.forceShouldRender();
for (const viewPart of this._viewParts) {
viewPart.forceShouldRender();
}
}
if (now) {
this._flushAccumulatedAndRenderNow();
}
else {
this._scheduleRender();
}
}
writeScreenReaderContent(reason) {
this._editContext.writeScreenReaderContent(reason);
}
focus() {
this._editContext.focus();
}
isFocused() {
return this._editContext.isFocused();
}
isWidgetFocused() {
return this._widgetFocusTracker.hasFocus();
}
setAriaOptions(options) {
this._editContext.setAriaOptions(options);
}
addContentWidget(widgetData) {
this._contentWidgets.addWidget(widgetData.widget);
this.layoutContentWidget(widgetData);
this._scheduleRender();
}
layoutContentWidget(widgetData) {
this._contentWidgets.setWidgetPosition(widgetData.widget, widgetData.position?.position ?? null, widgetData.position?.secondaryPosition ?? null, widgetData.position?.preference ?? null, widgetData.position?.positionAffinity ?? null);
this._scheduleRender();
}
removeContentWidget(widgetData) {
this._contentWidgets.removeWidget(widgetData.widget);
this._scheduleRender();
}
addOverlayWidget(widgetData) {
this._overlayWidgets.addWidget(widgetData.widget);
this.layoutOverlayWidget(widgetData);
this._scheduleRender();
}
layoutOverlayWidget(widgetData) {
const shouldRender = this._overlayWidgets.setWidgetPosition(widgetData.widget, widgetData.position);
if (shouldRender) {
this._scheduleRender();
}
}
removeOverlayWidget(widgetData) {
this._overlayWidgets.removeWidget(widgetData.widget);
this._scheduleRender();
}
addGlyphMarginWidget(widgetData) {
this._glyphMarginWidgets.addWidget(widgetData.widget);
this._shouldRecomputeGlyphMarginLanes = true;
this._scheduleRender();
}
layoutGlyphMarginWidget(widgetData) {
const newPreference = widgetData.position;
const shouldRender = this._glyphMarginWidgets.setWidgetPosition(widgetData.widget, newPreference);
if (shouldRender) {
this._shouldRecomputeGlyphMarginLanes = true;
this._scheduleRender();
}
}
removeGlyphMarginWidget(widgetData) {
this._glyphMarginWidgets.removeWidget(widgetData.widget);
this._shouldRecomputeGlyphMarginLanes = true;
this._scheduleRender();
}
};
View = __decorate([
__param(8, IInstantiationService)
], View);
function safeInvokeNoArg(func) {
try {
return func();
}
catch (e) {
onUnexpectedError(e);
return null;
}
}
class EditorRenderingCoordinator {
static { this.INSTANCE = new EditorRenderingCoordinator(); }
constructor() {
this._coordinatedRenderings = [];
this._animationFrameRunners = new Map();
}
scheduleCoordinatedRendering(rendering) {
this._coordinatedRenderings.push(rendering);
this._scheduleRender(rendering.window);
return {
dispose: () => {
const renderingIndex = this._coordinatedRenderings.indexOf(rendering);
if (renderingIndex === -1) {
return;
}
this._coordinatedRenderings.splice(renderingIndex, 1);
if (this._coordinatedRenderings.length === 0) {
// There are no more renderings to coordinate => cancel animation frames
for (const [_, disposable] of this._animationFrameRunners) {
disposable.dispose();
}
this._animationFrameRunners.clear();
}
}
};
}
_scheduleRender(window) {
if (!this._animationFrameRunners.has(window)) {
const runner = () => {
this._animationFrameRunners.delete(window);
this._onRenderScheduled();
};
this._animationFrameRunners.set(window, runAtThisOrScheduleAtNextAnimationFrame(window, runner, 100));
}
}
_onRenderScheduled() {
const coordinatedRenderings = this._coordinatedRenderings.slice(0);
this._coordinatedRenderings = [];
for (const rendering of coordinatedRenderings) {
safeInvokeNoArg(() => rendering.prepareRenderText());
}
const datas = [];
for (let i = 0, len = coordinatedRenderings.length; i < len; i++) {
const rendering = coordinatedRenderings[i];
datas[i] = safeInvokeNoArg(() => rendering.renderText());
}
for (let i = 0, len = coordinatedRenderings.length; i < len; i++) {
const rendering = coordinatedRenderings[i];
const data = datas[i];
if (!data) {
continue;
}
const [viewParts, ctx] = data;
safeInvokeNoArg(() => rendering.prepareRender(viewParts, ctx));
}
for (let i = 0, len = coordinatedRenderings.length; i < len; i++) {
const rendering = coordinatedRenderings[i];
const data = datas[i];
if (!data) {
continue;
}
const [viewParts, ctx] = data;
safeInvokeNoArg(() => rendering.render(viewParts, ctx));
}
}
}
class CodeEditorWidgetFocusTracker extends Disposable {
constructor(domElement, overflowWidgetsDomNode) {
super();
this._onChange = this._register(new Emitter());
this.onChange = this._onChange.event;
this._hadFocus = undefined;
this._hasDomElementFocus = false;
this._domFocusTracker = this._register(trackFocus(domElement));
this._overflowWidgetsDomNodeHasFocus = false;
this._register(this._domFocusTracker.onDidFocus(() => {
this._hasDomElementFocus = true;
this._update();
}));
this._register(this._domFocusTracker.onDidBlur(() => {
this._hasDomElementFocus = false;
this._update();
}));
if (overflowWidgetsDomNode) {
this._overflowWidgetsDomNode = this._register(trackFocus(overflowWidgetsDomNode));
this._register(this._overflowWidgetsDomNode.onDidFocus(() => {
this._overflowWidgetsDomNodeHasFocus = true;
this._update();
}));
this._register(this._overflowWidgetsDomNode.onDidBlur(() => {
this._overflowWidgetsDomNodeHasFocus = false;
this._update();
}));
}
}
_update() {
const focused = this._hasDomElementFocus || this._overflowWidgetsDomNodeHasFocus;
if (this._hadFocus !== focused) {
this._hadFocus = focused;
this._onChange.fire(undefined);
}
}
hasFocus() {
return this._hadFocus ?? false;
}
}
export { View };
@@ -0,0 +1,301 @@
import { createTrustedTypesPolicy } from '../../../base/browser/trustedTypes.js';
import { firstNonWhitespaceIndex, isFullWidthCharacter } from '../../../base/common/strings.js';
import { assertReturnsDefined } from '../../../base/common/types.js';
import { applyFontInfo } from '../config/domFontInfo.js';
import { StringBuilder } from '../../common/core/stringBuilder.js';
import { ModelLineProjectionData } from '../../common/modelLineProjectionData.js';
import { LineInjectedText } from '../../common/textModelEvents.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const ttPolicy = createTrustedTypesPolicy('domLineBreaksComputer', { createHTML: value => value });
class DOMLineBreaksComputerFactory {
static create(targetWindow) {
return new DOMLineBreaksComputerFactory(new WeakRef(targetWindow));
}
constructor(targetWindow) {
this.targetWindow = targetWindow;
}
createLineBreaksComputer(fontInfo, tabSize, wrappingColumn, wrappingIndent, wordBreak, wrapOnEscapedLineFeeds) {
const requests = [];
const injectedTexts = [];
return {
addRequest: (lineText, injectedText, previousLineBreakData) => {
requests.push(lineText);
injectedTexts.push(injectedText);
},
finalize: () => {
return createLineBreaks(assertReturnsDefined(this.targetWindow.deref()), requests, fontInfo, tabSize, wrappingColumn, wrappingIndent, wordBreak, injectedTexts);
}
};
}
}
function createLineBreaks(targetWindow, requests, fontInfo, tabSize, firstLineBreakColumn, wrappingIndent, wordBreak, injectedTextsPerLine) {
function createEmptyLineBreakWithPossiblyInjectedText(requestIdx) {
const injectedTexts = injectedTextsPerLine[requestIdx];
if (injectedTexts) {
const lineText = LineInjectedText.applyInjectedText(requests[requestIdx], injectedTexts);
const injectionOptions = injectedTexts.map(t => t.options);
const injectionOffsets = injectedTexts.map(text => text.column - 1);
// creating a `LineBreakData` with an invalid `breakOffsetsVisibleColumn` is OK
// because `breakOffsetsVisibleColumn` will never be used because it contains injected text
return new ModelLineProjectionData(injectionOffsets, injectionOptions, [lineText.length], [], 0);
}
else {
return null;
}
}
if (firstLineBreakColumn === -1) {
const result = [];
for (let i = 0, len = requests.length; i < len; i++) {
result[i] = createEmptyLineBreakWithPossiblyInjectedText(i);
}
return result;
}
const overallWidth = Math.round(firstLineBreakColumn * fontInfo.typicalHalfwidthCharacterWidth);
const additionalIndent = (wrappingIndent === 3 /* WrappingIndent.DeepIndent */ ? 2 : wrappingIndent === 2 /* WrappingIndent.Indent */ ? 1 : 0);
const additionalIndentSize = Math.round(tabSize * additionalIndent);
const additionalIndentLength = Math.ceil(fontInfo.spaceWidth * additionalIndentSize);
const containerDomNode = document.createElement('div');
applyFontInfo(containerDomNode, fontInfo);
const sb = new StringBuilder(10000);
const firstNonWhitespaceIndices = [];
const wrappedTextIndentLengths = [];
const renderLineContents = [];
const allCharOffsets = [];
const allVisibleColumns = [];
for (let i = 0; i < requests.length; i++) {
const lineContent = LineInjectedText.applyInjectedText(requests[i], injectedTextsPerLine[i]);
let firstNonWhitespaceIndex$1 = 0;
let wrappedTextIndentLength = 0;
let width = overallWidth;
if (wrappingIndent !== 0 /* WrappingIndent.None */) {
firstNonWhitespaceIndex$1 = firstNonWhitespaceIndex(lineContent);
if (firstNonWhitespaceIndex$1 === -1) {
// all whitespace line
firstNonWhitespaceIndex$1 = 0;
}
else {
// Track existing indent
for (let i = 0; i < firstNonWhitespaceIndex$1; i++) {
const charWidth = (lineContent.charCodeAt(i) === 9 /* CharCode.Tab */
? (tabSize - (wrappedTextIndentLength % tabSize))
: 1);
wrappedTextIndentLength += charWidth;
}
const indentWidth = Math.ceil(fontInfo.spaceWidth * wrappedTextIndentLength);
// Force sticking to beginning of line if no character would fit except for the indentation
if (indentWidth + fontInfo.typicalFullwidthCharacterWidth > overallWidth) {
firstNonWhitespaceIndex$1 = 0;
wrappedTextIndentLength = 0;
}
else {
width = overallWidth - indentWidth;
}
}
}
const renderLineContent = lineContent.substr(firstNonWhitespaceIndex$1);
const tmp = renderLine(renderLineContent, wrappedTextIndentLength, tabSize, width, sb, additionalIndentLength);
firstNonWhitespaceIndices[i] = firstNonWhitespaceIndex$1;
wrappedTextIndentLengths[i] = wrappedTextIndentLength;
renderLineContents[i] = renderLineContent;
allCharOffsets[i] = tmp[0];
allVisibleColumns[i] = tmp[1];
}
const html = sb.build();
const trustedhtml = ttPolicy?.createHTML(html) ?? html;
containerDomNode.innerHTML = trustedhtml;
containerDomNode.style.position = 'absolute';
containerDomNode.style.top = '10000';
if (wordBreak === 'keepAll') {
// word-break: keep-all; overflow-wrap: anywhere
containerDomNode.style.wordBreak = 'keep-all';
containerDomNode.style.overflowWrap = 'anywhere';
}
else {
// overflow-wrap: break-word
containerDomNode.style.wordBreak = 'inherit';
containerDomNode.style.overflowWrap = 'break-word';
}
targetWindow.document.body.appendChild(containerDomNode);
const range = document.createRange();
const lineDomNodes = Array.prototype.slice.call(containerDomNode.children, 0);
const result = [];
for (let i = 0; i < requests.length; i++) {
const lineDomNode = lineDomNodes[i];
const breakOffsets = readLineBreaks(range, lineDomNode, renderLineContents[i], allCharOffsets[i]);
if (breakOffsets === null) {
result[i] = createEmptyLineBreakWithPossiblyInjectedText(i);
continue;
}
const firstNonWhitespaceIndex = firstNonWhitespaceIndices[i];
const wrappedTextIndentLength = wrappedTextIndentLengths[i] + additionalIndentSize;
const visibleColumns = allVisibleColumns[i];
const breakOffsetsVisibleColumn = [];
for (let j = 0, len = breakOffsets.length; j < len; j++) {
breakOffsetsVisibleColumn[j] = visibleColumns[breakOffsets[j]];
}
if (firstNonWhitespaceIndex !== 0) {
// All break offsets are relative to the renderLineContent, make them absolute again
for (let j = 0, len = breakOffsets.length; j < len; j++) {
breakOffsets[j] += firstNonWhitespaceIndex;
}
}
let injectionOptions;
let injectionOffsets;
const curInjectedTexts = injectedTextsPerLine[i];
if (curInjectedTexts) {
injectionOptions = curInjectedTexts.map(t => t.options);
injectionOffsets = curInjectedTexts.map(text => text.column - 1);
}
else {
injectionOptions = null;
injectionOffsets = null;
}
result[i] = new ModelLineProjectionData(injectionOffsets, injectionOptions, breakOffsets, breakOffsetsVisibleColumn, wrappedTextIndentLength);
}
containerDomNode.remove();
return result;
}
function renderLine(lineContent, initialVisibleColumn, tabSize, width, sb, wrappingIndentLength) {
if (wrappingIndentLength !== 0) {
const hangingOffset = String(wrappingIndentLength);
sb.appendString('<div style="text-indent: -');
sb.appendString(hangingOffset);
sb.appendString('px; padding-left: ');
sb.appendString(hangingOffset);
sb.appendString('px; box-sizing: border-box; width:');
}
else {
sb.appendString('<div style="width:');
}
sb.appendString(String(width));
sb.appendString('px;">');
// if (containsRTL) {
// sb.appendASCIIString('" dir="ltr');
// }
const len = lineContent.length;
let visibleColumn = initialVisibleColumn;
let charOffset = 0;
const charOffsets = [];
const visibleColumns = [];
let nextCharCode = (0 < len ? lineContent.charCodeAt(0) : 0 /* CharCode.Null */);
sb.appendString('<span>');
for (let charIndex = 0; charIndex < len; charIndex++) {
if (charIndex !== 0 && charIndex % 16384 /* Constants.SPAN_MODULO_LIMIT */ === 0) {
sb.appendString('</span><span>');
}
charOffsets[charIndex] = charOffset;
visibleColumns[charIndex] = visibleColumn;
const charCode = nextCharCode;
nextCharCode = (charIndex + 1 < len ? lineContent.charCodeAt(charIndex + 1) : 0 /* CharCode.Null */);
let producedCharacters = 1;
let charWidth = 1;
switch (charCode) {
case 9 /* CharCode.Tab */:
producedCharacters = (tabSize - (visibleColumn % tabSize));
charWidth = producedCharacters;
for (let space = 1; space <= producedCharacters; space++) {
if (space < producedCharacters) {
sb.appendCharCode(0xA0); // &nbsp;
}
else {
sb.appendASCIICharCode(32 /* CharCode.Space */);
}
}
break;
case 32 /* CharCode.Space */:
if (nextCharCode === 32 /* CharCode.Space */) {
sb.appendCharCode(0xA0); // &nbsp;
}
else {
sb.appendASCIICharCode(32 /* CharCode.Space */);
}
break;
case 60 /* CharCode.LessThan */:
sb.appendString('&lt;');
break;
case 62 /* CharCode.GreaterThan */:
sb.appendString('&gt;');
break;
case 38 /* CharCode.Ampersand */:
sb.appendString('&amp;');
break;
case 0 /* CharCode.Null */:
sb.appendString('&#00;');
break;
case 65279 /* CharCode.UTF8_BOM */:
case 8232 /* CharCode.LINE_SEPARATOR */:
case 8233 /* CharCode.PARAGRAPH_SEPARATOR */:
case 133 /* CharCode.NEXT_LINE */:
sb.appendCharCode(0xFFFD);
break;
default:
if (isFullWidthCharacter(charCode)) {
charWidth++;
}
if (charCode < 32) {
sb.appendCharCode(9216 + charCode);
}
else {
sb.appendCharCode(charCode);
}
}
charOffset += producedCharacters;
visibleColumn += charWidth;
}
sb.appendString('</span>');
charOffsets[lineContent.length] = charOffset;
visibleColumns[lineContent.length] = visibleColumn;
sb.appendString('</div>');
return [charOffsets, visibleColumns];
}
function readLineBreaks(range, lineDomNode, lineContent, charOffsets) {
if (lineContent.length <= 1) {
return null;
}
const spans = Array.prototype.slice.call(lineDomNode.children, 0);
const breakOffsets = [];
try {
discoverBreaks(range, spans, charOffsets, 0, null, lineContent.length - 1, null, breakOffsets);
}
catch (err) {
console.log(err);
return null;
}
if (breakOffsets.length === 0) {
return null;
}
breakOffsets.push(lineContent.length);
return breakOffsets;
}
function discoverBreaks(range, spans, charOffsets, low, lowRects, high, highRects, result) {
if (low === high) {
return;
}
lowRects = lowRects || readClientRect(range, spans, charOffsets[low], charOffsets[low + 1]);
highRects = highRects || readClientRect(range, spans, charOffsets[high], charOffsets[high + 1]);
if (Math.abs(lowRects[0].top - highRects[0].top) <= 0.1) {
// same line
return;
}
// there is at least one line break between these two offsets
if (low + 1 === high) {
// the two characters are adjacent, so the line break must be exactly between them
result.push(high);
return;
}
const mid = low + ((high - low) / 2) | 0;
const midRects = readClientRect(range, spans, charOffsets[mid], charOffsets[mid + 1]);
discoverBreaks(range, spans, charOffsets, low, lowRects, mid, midRects, result);
discoverBreaks(range, spans, charOffsets, mid, midRects, high, highRects, result);
}
function readClientRect(range, spans, startOffset, endOffset) {
range.setStart(spans[(startOffset / 16384 /* Constants.SPAN_MODULO_LIMIT */) | 0].firstChild, startOffset % 16384 /* Constants.SPAN_MODULO_LIMIT */);
range.setEnd(spans[(endOffset / 16384 /* Constants.SPAN_MODULO_LIMIT */) | 0].firstChild, endOffset % 16384 /* Constants.SPAN_MODULO_LIMIT */);
return range.getClientRects();
}
export { DOMLineBreaksComputerFactory };
@@ -0,0 +1,10 @@
import { ViewEventHandler } from '../../common/viewEventHandler.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class DynamicViewOverlay extends ViewEventHandler {
}
export { DynamicViewOverlay };
@@ -0,0 +1,118 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class RestrictedRenderingContext {
constructor(viewLayout, viewportData) {
this._restrictedRenderingContextBrand = undefined;
this._viewLayout = viewLayout;
this.viewportData = viewportData;
this.scrollWidth = this._viewLayout.getScrollWidth();
this.scrollHeight = this._viewLayout.getScrollHeight();
this.visibleRange = this.viewportData.visibleRange;
this.bigNumbersDelta = this.viewportData.bigNumbersDelta;
const vInfo = this._viewLayout.getCurrentViewport();
this.scrollTop = vInfo.top;
this.scrollLeft = vInfo.left;
this.viewportWidth = vInfo.width;
this.viewportHeight = vInfo.height;
}
getScrolledTopFromAbsoluteTop(absoluteTop) {
return absoluteTop - this.scrollTop;
}
getVerticalOffsetForLineNumber(lineNumber, includeViewZones) {
return this._viewLayout.getVerticalOffsetForLineNumber(lineNumber, includeViewZones);
}
getVerticalOffsetAfterLineNumber(lineNumber, includeViewZones) {
return this._viewLayout.getVerticalOffsetAfterLineNumber(lineNumber, includeViewZones);
}
getLineHeightForLineNumber(lineNumber) {
return this._viewLayout.getLineHeightForLineNumber(lineNumber);
}
getDecorationsInViewport() {
return this.viewportData.getDecorationsInViewport();
}
}
class RenderingContext extends RestrictedRenderingContext {
constructor(viewLayout, viewportData, viewLines, viewLinesGpu) {
super(viewLayout, viewportData);
this._renderingContextBrand = undefined;
this._viewLines = viewLines;
this._viewLinesGpu = viewLinesGpu;
}
linesVisibleRangesForRange(range, includeNewLines) {
const domRanges = this._viewLines.linesVisibleRangesForRange(range, includeNewLines);
if (!this._viewLinesGpu) {
return domRanges ?? null;
}
const gpuRanges = this._viewLinesGpu.linesVisibleRangesForRange(range, includeNewLines);
if (!domRanges) {
return gpuRanges;
}
if (!gpuRanges) {
return domRanges;
}
return domRanges.concat(gpuRanges).sort((a, b) => a.lineNumber - b.lineNumber);
}
visibleRangeForPosition(position) {
return this._viewLines.visibleRangeForPosition(position) ?? this._viewLinesGpu?.visibleRangeForPosition(position) ?? null;
}
}
class LineVisibleRanges {
constructor(outsideRenderedLine, lineNumber, ranges,
/**
* Indicates if the requested range does not end in this line, but continues on the next line.
*/
continuesOnNextLine) {
this.outsideRenderedLine = outsideRenderedLine;
this.lineNumber = lineNumber;
this.ranges = ranges;
this.continuesOnNextLine = continuesOnNextLine;
}
}
class HorizontalRange {
static from(ranges) {
const result = new Array(ranges.length);
for (let i = 0, len = ranges.length; i < len; i++) {
const range = ranges[i];
result[i] = new HorizontalRange(range.left, range.width);
}
return result;
}
constructor(left, width) {
this._horizontalRangeBrand = undefined;
this.left = Math.round(left);
this.width = Math.round(width);
}
toString() {
return `[${this.left},${this.width}]`;
}
}
class FloatHorizontalRange {
constructor(left, width) {
this._floatHorizontalRangeBrand = undefined;
this.left = left;
this.width = width;
}
toString() {
return `[${this.left},${this.width}]`;
}
static compare(a, b) {
return a.left - b.left;
}
}
class HorizontalPosition {
constructor(outsideRenderedLine, left) {
this.outsideRenderedLine = outsideRenderedLine;
this.originalLeft = left;
this.left = Math.round(this.originalLeft);
}
}
class VisibleRanges {
constructor(outsideRenderedLine, ranges) {
this.outsideRenderedLine = outsideRenderedLine;
this.ranges = ranges;
}
}
export { FloatHorizontalRange, HorizontalPosition, HorizontalRange, LineVisibleRanges, RenderingContext, RestrictedRenderingContext, VisibleRanges };
@@ -0,0 +1,274 @@
import { CoreNavigationCommands } from '../coreCommands.js';
import { Position } from '../../common/core/position.js';
import { isLinux } from '../../../base/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 ViewController {
constructor(configuration, viewModel, userInputEvents, commandDelegate) {
this.configuration = configuration;
this.viewModel = viewModel;
this.userInputEvents = userInputEvents;
this.commandDelegate = commandDelegate;
}
paste(text, pasteOnNewLine, multicursorText, mode) {
this.commandDelegate.paste(text, pasteOnNewLine, multicursorText, mode);
}
type(text) {
this.commandDelegate.type(text);
}
compositionType(text, replacePrevCharCnt, replaceNextCharCnt, positionDelta) {
this.commandDelegate.compositionType(text, replacePrevCharCnt, replaceNextCharCnt, positionDelta);
}
compositionStart() {
this.commandDelegate.startComposition();
}
compositionEnd() {
this.commandDelegate.endComposition();
}
cut() {
this.commandDelegate.cut();
}
setSelection(modelSelection) {
CoreNavigationCommands.SetSelection.runCoreEditorCommand(this.viewModel, {
source: 'keyboard',
selection: modelSelection
});
}
_validateViewColumn(viewPosition) {
const minColumn = this.viewModel.getLineMinColumn(viewPosition.lineNumber);
if (viewPosition.column < minColumn) {
return new Position(viewPosition.lineNumber, minColumn);
}
return viewPosition;
}
_hasMulticursorModifier(data) {
switch (this.configuration.options.get(86 /* EditorOption.multiCursorModifier */)) {
case 'altKey':
return data.altKey;
case 'ctrlKey':
return data.ctrlKey;
case 'metaKey':
return data.metaKey;
default:
return false;
}
}
_hasNonMulticursorModifier(data) {
switch (this.configuration.options.get(86 /* EditorOption.multiCursorModifier */)) {
case 'altKey':
return data.ctrlKey || data.metaKey;
case 'ctrlKey':
return data.altKey || data.metaKey;
case 'metaKey':
return data.ctrlKey || data.altKey;
default:
return false;
}
}
dispatchMouse(data) {
const options = this.configuration.options;
const selectionClipboardIsOn = (isLinux && options.get(121 /* EditorOption.selectionClipboard */));
const columnSelection = options.get(28 /* EditorOption.columnSelection */);
const scrollOnMiddleClick = options.get(171 /* EditorOption.scrollOnMiddleClick */);
if (data.middleButton && !selectionClipboardIsOn) {
if (scrollOnMiddleClick) ;
else {
this._columnSelect(data.position, data.mouseColumn, data.inSelectionMode);
}
}
else if (data.startedOnLineNumbers) {
// If the dragging started on the gutter, then have operations work on the entire line
if (this._hasMulticursorModifier(data)) {
if (data.inSelectionMode) {
this._lastCursorLineSelect(data.position, data.revealType);
}
else {
this._createCursor(data.position, true);
}
}
else {
if (data.inSelectionMode) {
this._lineSelectDrag(data.position, data.revealType);
}
else {
this._lineSelect(data.position, data.revealType);
}
}
}
else if (data.mouseDownCount >= 4) {
this._selectAll();
}
else if (data.mouseDownCount === 3) {
if (this._hasMulticursorModifier(data)) {
if (data.inSelectionMode) {
this._lastCursorLineSelectDrag(data.position, data.revealType);
}
else {
this._lastCursorLineSelect(data.position, data.revealType);
}
}
else {
if (data.inSelectionMode) {
this._lineSelectDrag(data.position, data.revealType);
}
else {
this._lineSelect(data.position, data.revealType);
}
}
}
else if (data.mouseDownCount === 2) {
if (!data.onInjectedText) {
if (this._hasMulticursorModifier(data)) {
this._lastCursorWordSelect(data.position, data.revealType);
}
else {
if (data.inSelectionMode) {
this._wordSelectDrag(data.position, data.revealType);
}
else {
this._wordSelect(data.position, data.revealType);
}
}
}
}
else {
if (this._hasMulticursorModifier(data)) {
if (!this._hasNonMulticursorModifier(data)) {
if (data.shiftKey) {
this._columnSelect(data.position, data.mouseColumn, true);
}
else {
// Do multi-cursor operations only when purely alt is pressed
if (data.inSelectionMode) {
this._lastCursorMoveToSelect(data.position, data.revealType);
}
else {
this._createCursor(data.position, false);
}
}
}
}
else {
if (data.inSelectionMode) {
if (data.altKey) {
this._columnSelect(data.position, data.mouseColumn, true);
}
else {
if (columnSelection) {
this._columnSelect(data.position, data.mouseColumn, true);
}
else {
this._moveToSelect(data.position, data.revealType);
}
}
}
else {
this.moveTo(data.position, data.revealType);
}
}
}
}
_usualArgs(viewPosition, revealType) {
viewPosition = this._validateViewColumn(viewPosition);
return {
source: 'mouse',
position: this._convertViewToModelPosition(viewPosition),
viewPosition,
revealType
};
}
moveTo(viewPosition, revealType) {
CoreNavigationCommands.MoveTo.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType));
}
_moveToSelect(viewPosition, revealType) {
CoreNavigationCommands.MoveToSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType));
}
_columnSelect(viewPosition, mouseColumn, doColumnSelect) {
viewPosition = this._validateViewColumn(viewPosition);
CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(this.viewModel, {
source: 'mouse',
position: this._convertViewToModelPosition(viewPosition),
viewPosition: viewPosition,
mouseColumn: mouseColumn,
doColumnSelect: doColumnSelect
});
}
_createCursor(viewPosition, wholeLine) {
viewPosition = this._validateViewColumn(viewPosition);
CoreNavigationCommands.CreateCursor.runCoreEditorCommand(this.viewModel, {
source: 'mouse',
position: this._convertViewToModelPosition(viewPosition),
viewPosition: viewPosition,
wholeLine: wholeLine
});
}
_lastCursorMoveToSelect(viewPosition, revealType) {
CoreNavigationCommands.LastCursorMoveToSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType));
}
_wordSelect(viewPosition, revealType) {
CoreNavigationCommands.WordSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType));
}
_wordSelectDrag(viewPosition, revealType) {
CoreNavigationCommands.WordSelectDrag.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType));
}
_lastCursorWordSelect(viewPosition, revealType) {
CoreNavigationCommands.LastCursorWordSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType));
}
_lineSelect(viewPosition, revealType) {
CoreNavigationCommands.LineSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType));
}
_lineSelectDrag(viewPosition, revealType) {
CoreNavigationCommands.LineSelectDrag.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType));
}
_lastCursorLineSelect(viewPosition, revealType) {
CoreNavigationCommands.LastCursorLineSelect.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType));
}
_lastCursorLineSelectDrag(viewPosition, revealType) {
CoreNavigationCommands.LastCursorLineSelectDrag.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType));
}
_selectAll() {
CoreNavigationCommands.SelectAll.runCoreEditorCommand(this.viewModel, { source: 'mouse' });
}
// ----------------------
_convertViewToModelPosition(viewPosition) {
return this.viewModel.coordinatesConverter.convertViewPositionToModelPosition(viewPosition);
}
emitKeyDown(e) {
this.userInputEvents.emitKeyDown(e);
}
emitKeyUp(e) {
this.userInputEvents.emitKeyUp(e);
}
emitContextMenu(e) {
this.userInputEvents.emitContextMenu(e);
}
emitMouseMove(e) {
this.userInputEvents.emitMouseMove(e);
}
emitMouseLeave(e) {
this.userInputEvents.emitMouseLeave(e);
}
emitMouseUp(e) {
this.userInputEvents.emitMouseUp(e);
}
emitMouseDown(e) {
this.userInputEvents.emitMouseDown(e);
}
emitMouseDrag(e) {
this.userInputEvents.emitMouseDrag(e);
}
emitMouseDrop(e) {
this.userInputEvents.emitMouseDrop(e);
}
emitMouseDropCanceled() {
this.userInputEvents.emitMouseDropCanceled();
}
emitMouseWheel(e) {
this.userInputEvents.emitMouseWheel(e);
}
}
export { ViewController };
@@ -0,0 +1,478 @@
import { createFastDomNode } from '../../../base/browser/fastDomNode.js';
import { createTrustedTypesPolicy } from '../../../base/browser/trustedTypes.js';
import { BugIndicatingError } from '../../../base/common/errors.js';
import { StringBuilder } from '../../common/core/stringBuilder.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class RenderedLinesCollection {
constructor(_lineFactory) {
this._lineFactory = _lineFactory;
this._set(1, []);
}
flush() {
this._set(1, []);
}
_set(rendLineNumberStart, lines) {
this._lines = lines;
this._rendLineNumberStart = rendLineNumberStart;
}
_get() {
return {
rendLineNumberStart: this._rendLineNumberStart,
lines: this._lines
};
}
/**
* @returns Inclusive line number that is inside this collection
*/
getStartLineNumber() {
return this._rendLineNumberStart;
}
/**
* @returns Inclusive line number that is inside this collection
*/
getEndLineNumber() {
return this._rendLineNumberStart + this._lines.length - 1;
}
getCount() {
return this._lines.length;
}
getLine(lineNumber) {
const lineIndex = lineNumber - this._rendLineNumberStart;
if (lineIndex < 0 || lineIndex >= this._lines.length) {
throw new BugIndicatingError('Illegal value for lineNumber');
}
return this._lines[lineIndex];
}
/**
* @returns Lines that were removed from this collection
*/
onLinesDeleted(deleteFromLineNumber, deleteToLineNumber) {
if (this.getCount() === 0) {
// no lines
return null;
}
const startLineNumber = this.getStartLineNumber();
const endLineNumber = this.getEndLineNumber();
if (deleteToLineNumber < startLineNumber) {
// deleting above the viewport
const deleteCnt = deleteToLineNumber - deleteFromLineNumber + 1;
this._rendLineNumberStart -= deleteCnt;
return null;
}
if (deleteFromLineNumber > endLineNumber) {
// deleted below the viewport
return null;
}
// Record what needs to be deleted
let deleteStartIndex = 0;
let deleteCount = 0;
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
const lineIndex = lineNumber - this._rendLineNumberStart;
if (deleteFromLineNumber <= lineNumber && lineNumber <= deleteToLineNumber) {
// this is a line to be deleted
if (deleteCount === 0) {
// this is the first line to be deleted
deleteStartIndex = lineIndex;
deleteCount = 1;
}
else {
deleteCount++;
}
}
}
// Adjust this._rendLineNumberStart for lines deleted above
if (deleteFromLineNumber < startLineNumber) {
// Something was deleted above
let deleteAboveCount = 0;
if (deleteToLineNumber < startLineNumber) {
// the entire deleted lines are above
deleteAboveCount = deleteToLineNumber - deleteFromLineNumber + 1;
}
else {
deleteAboveCount = startLineNumber - deleteFromLineNumber;
}
this._rendLineNumberStart -= deleteAboveCount;
}
const deleted = this._lines.splice(deleteStartIndex, deleteCount);
return deleted;
}
onLinesChanged(changeFromLineNumber, changeCount) {
const changeToLineNumber = changeFromLineNumber + changeCount - 1;
if (this.getCount() === 0) {
// no lines
return false;
}
const startLineNumber = this.getStartLineNumber();
const endLineNumber = this.getEndLineNumber();
let someoneNotified = false;
for (let changedLineNumber = changeFromLineNumber; changedLineNumber <= changeToLineNumber; changedLineNumber++) {
if (changedLineNumber >= startLineNumber && changedLineNumber <= endLineNumber) {
// Notify the line
this._lines[changedLineNumber - this._rendLineNumberStart].onContentChanged();
someoneNotified = true;
}
}
return someoneNotified;
}
onLinesInserted(insertFromLineNumber, insertToLineNumber) {
if (this.getCount() === 0) {
// no lines
return null;
}
const insertCnt = insertToLineNumber - insertFromLineNumber + 1;
const startLineNumber = this.getStartLineNumber();
const endLineNumber = this.getEndLineNumber();
if (insertFromLineNumber <= startLineNumber) {
// inserting above the viewport
this._rendLineNumberStart += insertCnt;
return null;
}
if (insertFromLineNumber > endLineNumber) {
// inserting below the viewport
return null;
}
if (insertCnt + insertFromLineNumber > endLineNumber) {
// insert inside the viewport in such a way that all remaining lines are pushed outside
const deleted = this._lines.splice(insertFromLineNumber - this._rendLineNumberStart, endLineNumber - insertFromLineNumber + 1);
return deleted;
}
// insert inside the viewport, push out some lines, but not all remaining lines
const newLines = [];
for (let i = 0; i < insertCnt; i++) {
newLines[i] = this._lineFactory.createLine();
}
const insertIndex = insertFromLineNumber - this._rendLineNumberStart;
const beforeLines = this._lines.slice(0, insertIndex);
const afterLines = this._lines.slice(insertIndex, this._lines.length - insertCnt);
const deletedLines = this._lines.slice(this._lines.length - insertCnt, this._lines.length);
this._lines = beforeLines.concat(newLines).concat(afterLines);
return deletedLines;
}
onTokensChanged(ranges) {
if (this.getCount() === 0) {
// no lines
return false;
}
const startLineNumber = this.getStartLineNumber();
const endLineNumber = this.getEndLineNumber();
let notifiedSomeone = false;
for (let i = 0, len = ranges.length; i < len; i++) {
const rng = ranges[i];
if (rng.toLineNumber < startLineNumber || rng.fromLineNumber > endLineNumber) {
// range outside viewport
continue;
}
const from = Math.max(startLineNumber, rng.fromLineNumber);
const to = Math.min(endLineNumber, rng.toLineNumber);
for (let lineNumber = from; lineNumber <= to; lineNumber++) {
const lineIndex = lineNumber - this._rendLineNumberStart;
this._lines[lineIndex].onTokensChanged();
notifiedSomeone = true;
}
}
return notifiedSomeone;
}
}
class VisibleLinesCollection {
constructor(_viewContext, _lineFactory) {
this._viewContext = _viewContext;
this._lineFactory = _lineFactory;
this.domNode = this._createDomNode();
this._linesCollection = new RenderedLinesCollection(this._lineFactory);
}
_createDomNode() {
const domNode = createFastDomNode(document.createElement('div'));
domNode.setClassName('view-layer');
domNode.setPosition('absolute');
domNode.domNode.setAttribute('role', 'presentation');
domNode.domNode.setAttribute('aria-hidden', 'true');
return domNode;
}
// ---- begin view event handlers
onConfigurationChanged(e) {
if (e.hasChanged(165 /* EditorOption.layoutInfo */)) {
return true;
}
return false;
}
onFlushed(e, flushDom) {
// No need to clear the dom node because a full .innerHTML will occur in
// ViewLayerRenderer._render, however the fallback mechanism in the
// GPU renderer may cause this to be necessary as the .innerHTML call
// may not happen depending on the new state, leaving stale DOM nodes
// around.
if (flushDom) {
const start = this._linesCollection.getStartLineNumber();
const end = this._linesCollection.getEndLineNumber();
for (let i = start; i <= end; i++) {
this._linesCollection.getLine(i).getDomNode()?.remove();
}
}
this._linesCollection.flush();
return true;
}
onLinesChanged(e) {
return this._linesCollection.onLinesChanged(e.fromLineNumber, e.count);
}
onLinesDeleted(e) {
const deleted = this._linesCollection.onLinesDeleted(e.fromLineNumber, e.toLineNumber);
if (deleted) {
// Remove from DOM
for (let i = 0, len = deleted.length; i < len; i++) {
const lineDomNode = deleted[i].getDomNode();
lineDomNode?.remove();
}
}
return true;
}
onLinesInserted(e) {
const deleted = this._linesCollection.onLinesInserted(e.fromLineNumber, e.toLineNumber);
if (deleted) {
// Remove from DOM
for (let i = 0, len = deleted.length; i < len; i++) {
const lineDomNode = deleted[i].getDomNode();
lineDomNode?.remove();
}
}
return true;
}
onScrollChanged(e) {
return e.scrollTopChanged;
}
onTokensChanged(e) {
return this._linesCollection.onTokensChanged(e.ranges);
}
onZonesChanged(e) {
return true;
}
// ---- end view event handlers
getStartLineNumber() {
return this._linesCollection.getStartLineNumber();
}
getEndLineNumber() {
return this._linesCollection.getEndLineNumber();
}
getVisibleLine(lineNumber) {
return this._linesCollection.getLine(lineNumber);
}
renderLines(viewportData) {
const inp = this._linesCollection._get();
const renderer = new ViewLayerRenderer(this.domNode.domNode, this._lineFactory, viewportData, this._viewContext);
const ctx = {
rendLineNumberStart: inp.rendLineNumberStart,
lines: inp.lines,
linesLength: inp.lines.length
};
// Decide if this render will do a single update (single large .innerHTML) or many updates (inserting/removing dom nodes)
const resCtx = renderer.render(ctx, viewportData.startLineNumber, viewportData.endLineNumber, viewportData.relativeVerticalOffset);
this._linesCollection._set(resCtx.rendLineNumberStart, resCtx.lines);
}
}
class ViewLayerRenderer {
static { this._ttPolicy = createTrustedTypesPolicy('editorViewLayer', { createHTML: value => value }); }
constructor(_domNode, _lineFactory, _viewportData, _viewContext) {
this._domNode = _domNode;
this._lineFactory = _lineFactory;
this._viewportData = _viewportData;
this._viewContext = _viewContext;
}
render(inContext, startLineNumber, stopLineNumber, deltaTop) {
const ctx = {
rendLineNumberStart: inContext.rendLineNumberStart,
lines: inContext.lines.slice(0),
linesLength: inContext.linesLength
};
if ((ctx.rendLineNumberStart + ctx.linesLength - 1 < startLineNumber) || (stopLineNumber < ctx.rendLineNumberStart)) {
// There is no overlap whatsoever
ctx.rendLineNumberStart = startLineNumber;
ctx.linesLength = stopLineNumber - startLineNumber + 1;
ctx.lines = [];
for (let x = startLineNumber; x <= stopLineNumber; x++) {
ctx.lines[x - startLineNumber] = this._lineFactory.createLine();
}
this._finishRendering(ctx, true, deltaTop);
return ctx;
}
// Update lines which will remain untouched
this._renderUntouchedLines(ctx, Math.max(startLineNumber - ctx.rendLineNumberStart, 0), Math.min(stopLineNumber - ctx.rendLineNumberStart, ctx.linesLength - 1), deltaTop, startLineNumber);
if (ctx.rendLineNumberStart > startLineNumber) {
// Insert lines before
const fromLineNumber = startLineNumber;
const toLineNumber = Math.min(stopLineNumber, ctx.rendLineNumberStart - 1);
if (fromLineNumber <= toLineNumber) {
this._insertLinesBefore(ctx, fromLineNumber, toLineNumber, deltaTop, startLineNumber);
ctx.linesLength += toLineNumber - fromLineNumber + 1;
}
}
else if (ctx.rendLineNumberStart < startLineNumber) {
// Remove lines before
const removeCnt = Math.min(ctx.linesLength, startLineNumber - ctx.rendLineNumberStart);
if (removeCnt > 0) {
this._removeLinesBefore(ctx, removeCnt);
ctx.linesLength -= removeCnt;
}
}
ctx.rendLineNumberStart = startLineNumber;
if (ctx.rendLineNumberStart + ctx.linesLength - 1 < stopLineNumber) {
// Insert lines after
const fromLineNumber = ctx.rendLineNumberStart + ctx.linesLength;
const toLineNumber = stopLineNumber;
if (fromLineNumber <= toLineNumber) {
this._insertLinesAfter(ctx, fromLineNumber, toLineNumber, deltaTop, startLineNumber);
ctx.linesLength += toLineNumber - fromLineNumber + 1;
}
}
else if (ctx.rendLineNumberStart + ctx.linesLength - 1 > stopLineNumber) {
// Remove lines after
const fromLineNumber = Math.max(0, stopLineNumber - ctx.rendLineNumberStart + 1);
const toLineNumber = ctx.linesLength - 1;
const removeCnt = toLineNumber - fromLineNumber + 1;
if (removeCnt > 0) {
this._removeLinesAfter(ctx, removeCnt);
ctx.linesLength -= removeCnt;
}
}
this._finishRendering(ctx, false, deltaTop);
return ctx;
}
_renderUntouchedLines(ctx, startIndex, endIndex, deltaTop, deltaLN) {
const rendLineNumberStart = ctx.rendLineNumberStart;
const lines = ctx.lines;
for (let i = startIndex; i <= endIndex; i++) {
const lineNumber = rendLineNumberStart + i;
lines[i].layoutLine(lineNumber, deltaTop[lineNumber - deltaLN], this._lineHeightForLineNumber(lineNumber));
}
}
_insertLinesBefore(ctx, fromLineNumber, toLineNumber, deltaTop, deltaLN) {
const newLines = [];
let newLinesLen = 0;
for (let lineNumber = fromLineNumber; lineNumber <= toLineNumber; lineNumber++) {
newLines[newLinesLen++] = this._lineFactory.createLine();
}
ctx.lines = newLines.concat(ctx.lines);
}
_removeLinesBefore(ctx, removeCount) {
for (let i = 0; i < removeCount; i++) {
const lineDomNode = ctx.lines[i].getDomNode();
lineDomNode?.remove();
}
ctx.lines.splice(0, removeCount);
}
_insertLinesAfter(ctx, fromLineNumber, toLineNumber, deltaTop, deltaLN) {
const newLines = [];
let newLinesLen = 0;
for (let lineNumber = fromLineNumber; lineNumber <= toLineNumber; lineNumber++) {
newLines[newLinesLen++] = this._lineFactory.createLine();
}
ctx.lines = ctx.lines.concat(newLines);
}
_removeLinesAfter(ctx, removeCount) {
const removeIndex = ctx.linesLength - removeCount;
for (let i = 0; i < removeCount; i++) {
const lineDomNode = ctx.lines[removeIndex + i].getDomNode();
lineDomNode?.remove();
}
ctx.lines.splice(removeIndex, removeCount);
}
_finishRenderingNewLines(ctx, domNodeIsEmpty, newLinesHTML, wasNew) {
if (ViewLayerRenderer._ttPolicy) {
newLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(newLinesHTML);
}
const lastChild = this._domNode.lastChild;
if (domNodeIsEmpty || !lastChild) {
this._domNode.innerHTML = newLinesHTML; // explains the ugly casts -> https://github.com/microsoft/vscode/issues/106396#issuecomment-692625393;
}
else {
lastChild.insertAdjacentHTML('afterend', newLinesHTML);
}
let currChild = this._domNode.lastChild;
for (let i = ctx.linesLength - 1; i >= 0; i--) {
const line = ctx.lines[i];
if (wasNew[i]) {
line.setDomNode(currChild);
currChild = currChild.previousSibling;
}
}
}
_finishRenderingInvalidLines(ctx, invalidLinesHTML, wasInvalid) {
const hugeDomNode = document.createElement('div');
if (ViewLayerRenderer._ttPolicy) {
invalidLinesHTML = ViewLayerRenderer._ttPolicy.createHTML(invalidLinesHTML);
}
hugeDomNode.innerHTML = invalidLinesHTML;
for (let i = 0; i < ctx.linesLength; i++) {
const line = ctx.lines[i];
if (wasInvalid[i]) {
const source = hugeDomNode.firstChild;
const lineDomNode = line.getDomNode();
lineDomNode.replaceWith(source);
line.setDomNode(source);
}
}
}
static { this._sb = new StringBuilder(100000); }
_finishRendering(ctx, domNodeIsEmpty, deltaTop) {
const sb = ViewLayerRenderer._sb;
const linesLength = ctx.linesLength;
const lines = ctx.lines;
const rendLineNumberStart = ctx.rendLineNumberStart;
const wasNew = [];
{
sb.reset();
let hadNewLine = false;
for (let i = 0; i < linesLength; i++) {
const line = lines[i];
wasNew[i] = false;
const lineDomNode = line.getDomNode();
if (lineDomNode) {
// line is not new
continue;
}
const renderedLineNumber = i + rendLineNumberStart;
const renderResult = line.renderLine(renderedLineNumber, deltaTop[i], this._lineHeightForLineNumber(renderedLineNumber), this._viewportData, sb);
if (!renderResult) {
// line does not need rendering
continue;
}
wasNew[i] = true;
hadNewLine = true;
}
if (hadNewLine) {
this._finishRenderingNewLines(ctx, domNodeIsEmpty, sb.build(), wasNew);
}
}
{
sb.reset();
let hadInvalidLine = false;
const wasInvalid = [];
for (let i = 0; i < linesLength; i++) {
const line = lines[i];
wasInvalid[i] = false;
if (wasNew[i]) {
// line was new
continue;
}
const renderedLineNumber = i + rendLineNumberStart;
const renderResult = line.renderLine(renderedLineNumber, deltaTop[i], this._lineHeightForLineNumber(renderedLineNumber), this._viewportData, sb);
if (!renderResult) {
// line does not need rendering
continue;
}
wasInvalid[i] = true;
hadInvalidLine = true;
}
if (hadInvalidLine) {
this._finishRenderingInvalidLines(ctx, sb.build(), wasInvalid);
}
}
}
_lineHeightForLineNumber(lineNumber) {
return this._viewContext.viewLayout.getLineHeightForLineNumber(lineNumber);
}
}
export { RenderedLinesCollection, VisibleLinesCollection };
@@ -0,0 +1,204 @@
import { createFastDomNode } from '../../../base/browser/fastDomNode.js';
import { applyFontInfo } from '../config/domFontInfo.js';
import { VisibleLinesCollection } from './viewLayer.js';
import { ViewPart } from './viewPart.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class ViewOverlays extends ViewPart {
constructor(context) {
super(context);
this._dynamicOverlays = [];
this._isFocused = false;
this._visibleLines = new VisibleLinesCollection(this._context, {
createLine: () => new ViewOverlayLine(this._dynamicOverlays)
});
this.domNode = this._visibleLines.domNode;
const options = this._context.configuration.options;
const fontInfo = options.get(59 /* EditorOption.fontInfo */);
applyFontInfo(this.domNode, fontInfo);
this.domNode.setClassName('view-overlays');
}
shouldRender() {
if (super.shouldRender()) {
return true;
}
for (let i = 0, len = this._dynamicOverlays.length; i < len; i++) {
const dynamicOverlay = this._dynamicOverlays[i];
if (dynamicOverlay.shouldRender()) {
return true;
}
}
return false;
}
dispose() {
super.dispose();
for (let i = 0, len = this._dynamicOverlays.length; i < len; i++) {
const dynamicOverlay = this._dynamicOverlays[i];
dynamicOverlay.dispose();
}
this._dynamicOverlays = [];
}
getDomNode() {
return this.domNode;
}
addDynamicOverlay(overlay) {
this._dynamicOverlays.push(overlay);
}
// ----- event handlers
onConfigurationChanged(e) {
this._visibleLines.onConfigurationChanged(e);
const options = this._context.configuration.options;
const fontInfo = options.get(59 /* EditorOption.fontInfo */);
applyFontInfo(this.domNode, fontInfo);
return true;
}
onFlushed(e) {
return this._visibleLines.onFlushed(e);
}
onFocusChanged(e) {
this._isFocused = e.isFocused;
return true;
}
onLinesChanged(e) {
return this._visibleLines.onLinesChanged(e);
}
onLinesDeleted(e) {
return this._visibleLines.onLinesDeleted(e);
}
onLinesInserted(e) {
return this._visibleLines.onLinesInserted(e);
}
onScrollChanged(e) {
return this._visibleLines.onScrollChanged(e) || true;
}
onTokensChanged(e) {
return this._visibleLines.onTokensChanged(e);
}
onZonesChanged(e) {
return this._visibleLines.onZonesChanged(e);
}
// ----- end event handlers
prepareRender(ctx) {
const toRender = this._dynamicOverlays.filter(overlay => overlay.shouldRender());
for (let i = 0, len = toRender.length; i < len; i++) {
const dynamicOverlay = toRender[i];
dynamicOverlay.prepareRender(ctx);
dynamicOverlay.onDidRender();
}
}
render(ctx) {
// Overwriting to bypass `shouldRender` flag
this._viewOverlaysRender(ctx);
this.domNode.toggleClassName('focused', this._isFocused);
}
_viewOverlaysRender(ctx) {
this._visibleLines.renderLines(ctx.viewportData);
}
}
class ViewOverlayLine {
constructor(dynamicOverlays) {
this._dynamicOverlays = dynamicOverlays;
this._domNode = null;
this._renderedContent = null;
}
getDomNode() {
if (!this._domNode) {
return null;
}
return this._domNode.domNode;
}
setDomNode(domNode) {
this._domNode = createFastDomNode(domNode);
}
onContentChanged() {
// Nothing
}
onTokensChanged() {
// Nothing
}
renderLine(lineNumber, deltaTop, lineHeight, viewportData, sb) {
let result = '';
for (let i = 0, len = this._dynamicOverlays.length; i < len; i++) {
const dynamicOverlay = this._dynamicOverlays[i];
result += dynamicOverlay.render(viewportData.startLineNumber, lineNumber);
}
if (this._renderedContent === result) {
// No rendering needed
return false;
}
this._renderedContent = result;
sb.appendString('<div style="top:');
sb.appendString(String(deltaTop));
sb.appendString('px;height:');
sb.appendString(String(lineHeight));
sb.appendString('px;line-height:');
sb.appendString(String(lineHeight));
sb.appendString('px;">');
sb.appendString(result);
sb.appendString('</div>');
return true;
}
layoutLine(lineNumber, deltaTop, lineHeight) {
if (this._domNode) {
this._domNode.setTop(deltaTop);
this._domNode.setHeight(lineHeight);
this._domNode.setLineHeight(lineHeight);
}
}
}
class ContentViewOverlays extends ViewOverlays {
constructor(context) {
super(context);
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this._contentWidth = layoutInfo.contentWidth;
this.domNode.setHeight(0);
}
// --- begin event handlers
onConfigurationChanged(e) {
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this._contentWidth = layoutInfo.contentWidth;
return super.onConfigurationChanged(e) || true;
}
onScrollChanged(e) {
return super.onScrollChanged(e) || e.scrollWidthChanged;
}
// --- end event handlers
_viewOverlaysRender(ctx) {
super._viewOverlaysRender(ctx);
this.domNode.setWidth(Math.max(ctx.scrollWidth, this._contentWidth));
}
}
class MarginViewOverlays extends ViewOverlays {
constructor(context) {
super(context);
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this._contentLeft = layoutInfo.contentLeft;
this.domNode.setClassName('margin-view-overlays');
this.domNode.setWidth(1);
applyFontInfo(this.domNode, options.get(59 /* EditorOption.fontInfo */));
}
onConfigurationChanged(e) {
const options = this._context.configuration.options;
applyFontInfo(this.domNode, options.get(59 /* EditorOption.fontInfo */));
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this._contentLeft = layoutInfo.contentLeft;
return super.onConfigurationChanged(e) || true;
}
onScrollChanged(e) {
return super.onScrollChanged(e) || e.scrollHeightChanged;
}
_viewOverlaysRender(ctx) {
super._viewOverlaysRender(ctx);
const height = Math.min(ctx.scrollHeight, 1000000);
this.domNode.setHeight(height);
this.domNode.setWidth(this._contentLeft);
}
}
export { ContentViewOverlays, MarginViewOverlays, ViewOverlayLine, ViewOverlays };
@@ -0,0 +1,49 @@
import { ViewEventHandler } from '../../common/viewEventHandler.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class ViewPart extends ViewEventHandler {
constructor(context) {
super();
this._context = context;
this._context.addEventHandler(this);
}
dispose() {
this._context.removeEventHandler(this);
super.dispose();
}
}
class PartFingerprints {
static write(target, partId) {
target.setAttribute('data-mprt', String(partId));
}
static read(target) {
const r = target.getAttribute('data-mprt');
if (r === null) {
return 0 /* PartFingerprint.None */;
}
return parseInt(r, 10);
}
static collect(child, stopAt) {
const result = [];
let resultLen = 0;
while (child && child !== child.ownerDocument.body) {
if (child === stopAt) {
break;
}
if (child.nodeType === child.ELEMENT_NODE) {
result[resultLen++] = this.read(child);
}
child = child.parentElement;
}
const r = new Uint8Array(resultLen);
for (let i = 0; i < resultLen; i++) {
r[i] = result[resultLen - i - 1];
}
return r;
}
}
export { PartFingerprints, ViewPart };
@@ -0,0 +1,91 @@
import { Position } from '../../common/core/position.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class ViewUserInputEvents {
constructor(coordinatesConverter) {
this.onKeyDown = null;
this.onKeyUp = null;
this.onContextMenu = null;
this.onMouseMove = null;
this.onMouseLeave = null;
this.onMouseDown = null;
this.onMouseUp = null;
this.onMouseDrag = null;
this.onMouseDrop = null;
this.onMouseDropCanceled = null;
this.onMouseWheel = null;
this._coordinatesConverter = coordinatesConverter;
}
emitKeyDown(e) {
this.onKeyDown?.(e);
}
emitKeyUp(e) {
this.onKeyUp?.(e);
}
emitContextMenu(e) {
this.onContextMenu?.(this._convertViewToModelMouseEvent(e));
}
emitMouseMove(e) {
this.onMouseMove?.(this._convertViewToModelMouseEvent(e));
}
emitMouseLeave(e) {
this.onMouseLeave?.(this._convertViewToModelMouseEvent(e));
}
emitMouseDown(e) {
this.onMouseDown?.(this._convertViewToModelMouseEvent(e));
}
emitMouseUp(e) {
this.onMouseUp?.(this._convertViewToModelMouseEvent(e));
}
emitMouseDrag(e) {
this.onMouseDrag?.(this._convertViewToModelMouseEvent(e));
}
emitMouseDrop(e) {
this.onMouseDrop?.(this._convertViewToModelMouseEvent(e));
}
emitMouseDropCanceled() {
this.onMouseDropCanceled?.();
}
emitMouseWheel(e) {
this.onMouseWheel?.(e);
}
_convertViewToModelMouseEvent(e) {
if (e.target) {
return {
event: e.event,
target: this._convertViewToModelMouseTarget(e.target)
};
}
return e;
}
_convertViewToModelMouseTarget(target) {
return ViewUserInputEvents.convertViewToModelMouseTarget(target, this._coordinatesConverter);
}
static convertViewToModelMouseTarget(target, coordinatesConverter) {
const result = { ...target };
if (result.position) {
result.position = coordinatesConverter.convertViewPositionToModelPosition(result.position);
}
if (result.range) {
result.range = coordinatesConverter.convertViewRangeToModelRange(result.range);
}
if (result.type === 5 /* MouseTargetType.GUTTER_VIEW_ZONE */ || result.type === 8 /* MouseTargetType.CONTENT_VIEW_ZONE */) {
result.detail = this.convertViewToModelViewZoneData(result.detail, coordinatesConverter);
}
return result;
}
static convertViewToModelViewZoneData(data, coordinatesConverter) {
return {
viewZoneId: data.viewZoneId,
positionBefore: data.positionBefore ? coordinatesConverter.convertViewPositionToModelPosition(data.positionBefore) : data.positionBefore,
positionAfter: data.positionAfter ? coordinatesConverter.convertViewPositionToModelPosition(data.positionAfter) : data.positionAfter,
position: coordinatesConverter.convertViewPositionToModelPosition(data.position),
afterLineNumber: coordinatesConverter.convertViewPositionToModelPosition(new Position(data.afterLineNumber, 1)).lineNumber,
};
}
}
export { ViewUserInputEvents };
@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .blockDecorations-container {
position: absolute;
top: 0;
pointer-events: none;
}
.monaco-editor .blockDecorations-block {
position: absolute;
box-sizing: border-box;
}
@@ -0,0 +1,97 @@
import { createFastDomNode } from '../../../../base/browser/fastDomNode.js';
import './blockDecorations.css';
import { ViewPart } from '../../view/viewPart.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class BlockDecorations extends ViewPart {
constructor(context) {
super(context);
this.blocks = [];
this.contentWidth = -1;
this.contentLeft = 0;
this.domNode = createFastDomNode(document.createElement('div'));
this.domNode.setAttribute('role', 'presentation');
this.domNode.setAttribute('aria-hidden', 'true');
this.domNode.setClassName('blockDecorations-container');
this.update();
}
update() {
let didChange = false;
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
const newContentWidth = layoutInfo.contentWidth - layoutInfo.verticalScrollbarWidth;
if (this.contentWidth !== newContentWidth) {
this.contentWidth = newContentWidth;
didChange = true;
}
const newContentLeft = layoutInfo.contentLeft;
if (this.contentLeft !== newContentLeft) {
this.contentLeft = newContentLeft;
didChange = true;
}
return didChange;
}
dispose() {
super.dispose();
}
// --- begin event handlers
onConfigurationChanged(e) {
return this.update();
}
onScrollChanged(e) {
return e.scrollTopChanged || e.scrollLeftChanged;
}
onDecorationsChanged(e) {
return true;
}
onZonesChanged(e) {
return true;
}
// --- end event handlers
prepareRender(ctx) {
// Nothing to read
}
render(ctx) {
let count = 0;
const decorations = ctx.getDecorationsInViewport();
for (const decoration of decorations) {
if (!decoration.options.blockClassName) {
continue;
}
let block = this.blocks[count];
if (!block) {
block = this.blocks[count] = createFastDomNode(document.createElement('div'));
this.domNode.appendChild(block);
}
let top;
let bottom;
if (decoration.options.blockIsAfterEnd) {
// range must be empty
top = ctx.getVerticalOffsetAfterLineNumber(decoration.range.endLineNumber, false);
bottom = ctx.getVerticalOffsetAfterLineNumber(decoration.range.endLineNumber, true);
}
else {
top = ctx.getVerticalOffsetForLineNumber(decoration.range.startLineNumber, true);
bottom = decoration.range.isEmpty() && !decoration.options.blockDoesNotCollapse
? ctx.getVerticalOffsetForLineNumber(decoration.range.startLineNumber, false)
: ctx.getVerticalOffsetAfterLineNumber(decoration.range.endLineNumber, true);
}
const [paddingTop, paddingRight, paddingBottom, paddingLeft] = decoration.options.blockPadding ?? [0, 0, 0, 0];
block.setClassName('blockDecorations-block ' + decoration.options.blockClassName);
block.setLeft(this.contentLeft - paddingLeft);
block.setWidth(this.contentWidth + paddingLeft + paddingRight);
block.setTop(top - ctx.scrollTop - paddingTop);
block.setHeight(bottom - top + paddingTop + paddingBottom);
count++;
}
for (let i = count; i < this.blocks.length; i++) {
this.blocks[i].domNode.remove();
}
this.blocks.length = count;
}
}
export { BlockDecorations };
@@ -0,0 +1,495 @@
import { getDomNodePagePosition, getClientArea } from '../../../../base/browser/dom.js';
import { createFastDomNode } from '../../../../base/browser/fastDomNode.js';
import { ViewPart, PartFingerprints } from '../../view/viewPart.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* This view part is responsible for rendering the content widgets, which are
* used for rendering elements that are associated to an editor position,
* such as suggestions or the parameter hints.
*/
class ViewContentWidgets extends ViewPart {
constructor(context, viewDomNode) {
super(context);
this._viewDomNode = viewDomNode;
this._widgets = {};
this.domNode = createFastDomNode(document.createElement('div'));
PartFingerprints.write(this.domNode, 1 /* PartFingerprint.ContentWidgets */);
this.domNode.setClassName('contentWidgets');
this.domNode.setPosition('absolute');
this.domNode.setTop(0);
this.overflowingContentWidgetsDomNode = createFastDomNode(document.createElement('div'));
PartFingerprints.write(this.overflowingContentWidgetsDomNode, 2 /* PartFingerprint.OverflowingContentWidgets */);
this.overflowingContentWidgetsDomNode.setClassName('overflowingContentWidgets');
}
dispose() {
super.dispose();
this._widgets = {};
}
// --- begin event handlers
onConfigurationChanged(e) {
const keys = Object.keys(this._widgets);
for (const widgetId of keys) {
this._widgets[widgetId].onConfigurationChanged(e);
}
return true;
}
onDecorationsChanged(e) {
// true for inline decorations that can end up relayouting text
return true;
}
onFlushed(e) {
return true;
}
onLineMappingChanged(e) {
this._updateAnchorsViewPositions();
return true;
}
onLinesChanged(e) {
this._updateAnchorsViewPositions();
return true;
}
onLinesDeleted(e) {
this._updateAnchorsViewPositions();
return true;
}
onLinesInserted(e) {
this._updateAnchorsViewPositions();
return true;
}
onScrollChanged(e) {
return true;
}
onZonesChanged(e) {
return true;
}
// ---- end view event handlers
_updateAnchorsViewPositions() {
const keys = Object.keys(this._widgets);
for (const widgetId of keys) {
this._widgets[widgetId].updateAnchorViewPosition();
}
}
addWidget(_widget) {
const myWidget = new Widget(this._context, this._viewDomNode, _widget);
this._widgets[myWidget.id] = myWidget;
if (myWidget.allowEditorOverflow) {
this.overflowingContentWidgetsDomNode.appendChild(myWidget.domNode);
}
else {
this.domNode.appendChild(myWidget.domNode);
}
this.setShouldRender();
}
setWidgetPosition(widget, primaryAnchor, secondaryAnchor, preference, affinity) {
const myWidget = this._widgets[widget.getId()];
myWidget.setPosition(primaryAnchor, secondaryAnchor, preference, affinity);
this.setShouldRender();
}
removeWidget(widget) {
const widgetId = widget.getId();
if (this._widgets.hasOwnProperty(widgetId)) {
const myWidget = this._widgets[widgetId];
delete this._widgets[widgetId];
const domNode = myWidget.domNode.domNode;
domNode.remove();
domNode.removeAttribute('monaco-visible-content-widget');
this.setShouldRender();
}
}
shouldSuppressMouseDownOnWidget(widgetId) {
if (this._widgets.hasOwnProperty(widgetId)) {
return this._widgets[widgetId].suppressMouseDown;
}
return false;
}
onBeforeRender(viewportData) {
const keys = Object.keys(this._widgets);
for (const widgetId of keys) {
this._widgets[widgetId].onBeforeRender(viewportData);
}
}
prepareRender(ctx) {
const keys = Object.keys(this._widgets);
for (const widgetId of keys) {
this._widgets[widgetId].prepareRender(ctx);
}
}
render(ctx) {
const keys = Object.keys(this._widgets);
for (const widgetId of keys) {
this._widgets[widgetId].render(ctx);
}
}
}
class Widget {
constructor(context, viewDomNode, actual) {
this._primaryAnchor = new PositionPair(null, null);
this._secondaryAnchor = new PositionPair(null, null);
this._context = context;
this._viewDomNode = viewDomNode;
this._actual = actual;
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
const allowOverflow = options.get(4 /* EditorOption.allowOverflow */);
this.domNode = createFastDomNode(this._actual.getDomNode());
this.id = this._actual.getId();
this.allowEditorOverflow = (this._actual.allowEditorOverflow || false) && allowOverflow;
this.suppressMouseDown = this._actual.suppressMouseDown || false;
this._fixedOverflowWidgets = options.get(51 /* EditorOption.fixedOverflowWidgets */);
this._contentWidth = layoutInfo.contentWidth;
this._contentLeft = layoutInfo.contentLeft;
this._affinity = null;
this._preference = [];
this._cachedDomNodeOffsetWidth = -1;
this._cachedDomNodeOffsetHeight = -1;
this._maxWidth = this._getMaxWidth();
this._isVisible = false;
this._renderData = null;
this.domNode.setPosition((this._fixedOverflowWidgets && this.allowEditorOverflow) ? 'fixed' : 'absolute');
this.domNode.setDisplay('none');
this.domNode.setVisibility('hidden');
this.domNode.setAttribute('widgetId', this.id);
this.domNode.setMaxWidth(this._maxWidth);
}
onConfigurationChanged(e) {
const options = this._context.configuration.options;
if (e.hasChanged(165 /* EditorOption.layoutInfo */)) {
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this._contentLeft = layoutInfo.contentLeft;
this._contentWidth = layoutInfo.contentWidth;
this._maxWidth = this._getMaxWidth();
}
}
updateAnchorViewPosition() {
this._setPosition(this._affinity, this._primaryAnchor.modelPosition, this._secondaryAnchor.modelPosition);
}
_setPosition(affinity, primaryAnchor, secondaryAnchor) {
this._affinity = affinity;
this._primaryAnchor = getValidPositionPair(primaryAnchor, this._context.viewModel, this._affinity);
this._secondaryAnchor = getValidPositionPair(secondaryAnchor, this._context.viewModel, this._affinity);
function getValidPositionPair(position, viewModel, affinity) {
if (!position) {
return new PositionPair(null, null);
}
// Do not trust that widgets give a valid position
const validModelPosition = viewModel.model.validatePosition(position);
if (viewModel.coordinatesConverter.modelPositionIsVisible(validModelPosition)) {
const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(validModelPosition, affinity ?? undefined);
return new PositionPair(position, viewPosition);
}
return new PositionPair(position, null);
}
}
_getMaxWidth() {
const elDocument = this.domNode.domNode.ownerDocument;
const elWindow = elDocument.defaultView;
return (this.allowEditorOverflow
? elWindow?.innerWidth || elDocument.documentElement.offsetWidth || elDocument.body.offsetWidth
: this._contentWidth);
}
setPosition(primaryAnchor, secondaryAnchor, preference, affinity) {
this._setPosition(affinity, primaryAnchor, secondaryAnchor);
this._preference = preference;
if (this._primaryAnchor.viewPosition && this._preference && this._preference.length > 0) {
// this content widget would like to be visible if possible
// we change it from `display:none` to `display:block` even if it
// might be outside the viewport such that we can measure its size
// in `prepareRender`
this.domNode.setDisplay('block');
}
else {
this.domNode.setDisplay('none');
}
this._cachedDomNodeOffsetWidth = -1;
this._cachedDomNodeOffsetHeight = -1;
}
_layoutBoxInViewport(anchor, width, height, ctx) {
// Our visible box is split horizontally by the current line => 2 boxes
// a) the box above the line
const aboveLineTop = anchor.top;
const heightAvailableAboveLine = aboveLineTop;
// b) the box under the line
const underLineTop = anchor.top + anchor.height;
const heightAvailableUnderLine = ctx.viewportHeight - underLineTop;
const aboveTop = aboveLineTop - height;
const fitsAbove = (heightAvailableAboveLine >= height);
const belowTop = underLineTop;
const fitsBelow = (heightAvailableUnderLine >= height);
// And its left
let left = anchor.left;
if (left + width > ctx.scrollLeft + ctx.viewportWidth) {
left = ctx.scrollLeft + ctx.viewportWidth - width;
}
if (left < ctx.scrollLeft) {
left = ctx.scrollLeft;
}
return { fitsAbove, aboveTop, fitsBelow, belowTop, left };
}
_layoutHorizontalSegmentInPage(windowSize, domNodePosition, left, width) {
// Leave some clearance to the left/right
const LEFT_PADDING = 15;
const RIGHT_PADDING = 15;
// Initially, the limits are defined as the dom node limits
const MIN_LIMIT = Math.max(LEFT_PADDING, domNodePosition.left - width);
const MAX_LIMIT = Math.min(domNodePosition.left + domNodePosition.width + width, windowSize.width - RIGHT_PADDING);
const elDocument = this._viewDomNode.domNode.ownerDocument;
const elWindow = elDocument.defaultView;
let absoluteLeft = domNodePosition.left + left - (elWindow?.scrollX ?? 0);
if (absoluteLeft + width > MAX_LIMIT) {
const delta = absoluteLeft - (MAX_LIMIT - width);
absoluteLeft -= delta;
left -= delta;
}
if (absoluteLeft < MIN_LIMIT) {
const delta = absoluteLeft - MIN_LIMIT;
absoluteLeft -= delta;
left -= delta;
}
return [left, absoluteLeft];
}
_layoutBoxInPage(anchor, width, height, ctx) {
const aboveTop = anchor.top - height;
const belowTop = anchor.top + anchor.height;
const domNodePosition = getDomNodePagePosition(this._viewDomNode.domNode);
const elDocument = this._viewDomNode.domNode.ownerDocument;
const elWindow = elDocument.defaultView;
const absoluteAboveTop = domNodePosition.top + aboveTop - (elWindow?.scrollY ?? 0);
const absoluteBelowTop = domNodePosition.top + belowTop - (elWindow?.scrollY ?? 0);
const windowSize = getClientArea(elDocument.body);
const [left, absoluteAboveLeft] = this._layoutHorizontalSegmentInPage(windowSize, domNodePosition, anchor.left - ctx.scrollLeft + this._contentLeft, width);
// Leave some clearance to the top/bottom
const TOP_PADDING = 22;
const BOTTOM_PADDING = 22;
const fitsAbove = (absoluteAboveTop >= TOP_PADDING);
const fitsBelow = (absoluteBelowTop + height <= windowSize.height - BOTTOM_PADDING);
if (this._fixedOverflowWidgets) {
return {
fitsAbove,
aboveTop: Math.max(absoluteAboveTop, TOP_PADDING),
fitsBelow,
belowTop: absoluteBelowTop,
left: absoluteAboveLeft
};
}
return { fitsAbove, aboveTop, fitsBelow, belowTop, left };
}
_prepareRenderWidgetAtExactPositionOverflowing(topLeft) {
return new Coordinate(topLeft.top, topLeft.left + this._contentLeft);
}
/**
* Compute the coordinates above and below the primary and secondary anchors.
* The content widget *must* touch the primary anchor.
* The content widget should touch if possible the secondary anchor.
*/
_getAnchorsCoordinates(ctx) {
const primary = getCoordinates(this._primaryAnchor.viewPosition, this._affinity);
const secondaryViewPosition = (this._secondaryAnchor.viewPosition?.lineNumber === this._primaryAnchor.viewPosition?.lineNumber ? this._secondaryAnchor.viewPosition : null);
const secondary = getCoordinates(secondaryViewPosition, this._affinity);
return { primary, secondary };
function getCoordinates(position, affinity) {
if (!position) {
return null;
}
const horizontalPosition = ctx.visibleRangeForPosition(position);
if (!horizontalPosition) {
return null;
}
// Left-align widgets that should appear :before content
const left = (position.column === 1 && affinity === 3 /* PositionAffinity.LeftOfInjectedText */ ? 0 : horizontalPosition.left);
const top = ctx.getVerticalOffsetForLineNumber(position.lineNumber) - ctx.scrollTop;
const lineHeight = ctx.getLineHeightForLineNumber(position.lineNumber);
return new AnchorCoordinate(top, left, lineHeight);
}
}
_reduceAnchorCoordinates(primary, secondary, width) {
if (!secondary) {
return primary;
}
const fontInfo = this._context.configuration.options.get(59 /* EditorOption.fontInfo */);
let left = secondary.left;
if (left < primary.left) {
left = Math.max(left, primary.left - width + fontInfo.typicalFullwidthCharacterWidth);
}
else {
left = Math.min(left, primary.left + width - fontInfo.typicalFullwidthCharacterWidth);
}
return new AnchorCoordinate(primary.top, left, primary.height);
}
_prepareRenderWidget(ctx) {
if (!this._preference || this._preference.length === 0) {
return null;
}
const { primary, secondary } = this._getAnchorsCoordinates(ctx);
if (!primary) {
return {
kind: 'offViewport',
preserveFocus: this.domNode.domNode.contains(this.domNode.domNode.ownerDocument.activeElement)
};
// return null;
}
if (this._cachedDomNodeOffsetWidth === -1 || this._cachedDomNodeOffsetHeight === -1) {
let preferredDimensions = null;
if (typeof this._actual.beforeRender === 'function') {
preferredDimensions = safeInvoke(this._actual.beforeRender, this._actual);
}
if (preferredDimensions) {
this._cachedDomNodeOffsetWidth = preferredDimensions.width;
this._cachedDomNodeOffsetHeight = preferredDimensions.height;
}
else {
const domNode = this.domNode.domNode;
const clientRect = domNode.getBoundingClientRect();
this._cachedDomNodeOffsetWidth = Math.round(clientRect.width);
this._cachedDomNodeOffsetHeight = Math.round(clientRect.height);
}
}
const anchor = this._reduceAnchorCoordinates(primary, secondary, this._cachedDomNodeOffsetWidth);
let placement;
if (this.allowEditorOverflow) {
placement = this._layoutBoxInPage(anchor, this._cachedDomNodeOffsetWidth, this._cachedDomNodeOffsetHeight, ctx);
}
else {
placement = this._layoutBoxInViewport(anchor, this._cachedDomNodeOffsetWidth, this._cachedDomNodeOffsetHeight, ctx);
}
// Do two passes, first for perfect fit, second picks first option
for (let pass = 1; pass <= 2; pass++) {
for (const pref of this._preference) {
// placement
if (pref === 1 /* ContentWidgetPositionPreference.ABOVE */) {
if (!placement) {
// Widget outside of viewport
return null;
}
if (pass === 2 || placement.fitsAbove) {
return {
kind: 'inViewport',
coordinate: new Coordinate(placement.aboveTop, placement.left),
position: 1 /* ContentWidgetPositionPreference.ABOVE */
};
}
}
else if (pref === 2 /* ContentWidgetPositionPreference.BELOW */) {
if (!placement) {
// Widget outside of viewport
return null;
}
if (pass === 2 || placement.fitsBelow) {
return {
kind: 'inViewport',
coordinate: new Coordinate(placement.belowTop, placement.left),
position: 2 /* ContentWidgetPositionPreference.BELOW */
};
}
}
else {
if (this.allowEditorOverflow) {
return {
kind: 'inViewport',
coordinate: this._prepareRenderWidgetAtExactPositionOverflowing(new Coordinate(anchor.top, anchor.left)),
position: 0 /* ContentWidgetPositionPreference.EXACT */
};
}
else {
return {
kind: 'inViewport',
coordinate: new Coordinate(anchor.top, anchor.left),
position: 0 /* ContentWidgetPositionPreference.EXACT */
};
}
}
}
}
return null;
}
/**
* On this first pass, we ensure that the content widget (if it is in the viewport) has the max width set correctly.
*/
onBeforeRender(viewportData) {
if (!this._primaryAnchor.viewPosition || !this._preference) {
return;
}
if (this._primaryAnchor.viewPosition.lineNumber < viewportData.startLineNumber || this._primaryAnchor.viewPosition.lineNumber > viewportData.endLineNumber) {
// Outside of viewport
return;
}
this.domNode.setMaxWidth(this._maxWidth);
}
prepareRender(ctx) {
this._renderData = this._prepareRenderWidget(ctx);
}
render(ctx) {
if (!this._renderData || this._renderData.kind === 'offViewport') {
// This widget should be invisible
if (this._isVisible) {
this.domNode.removeAttribute('monaco-visible-content-widget');
this._isVisible = false;
if (this._renderData?.kind === 'offViewport' && this._renderData.preserveFocus) {
// widget wants to be shown, but it is outside of the viewport and it
// has focus which we need to preserve
this.domNode.setTop(-1e3);
}
else {
this.domNode.setVisibility('hidden');
}
}
if (typeof this._actual.afterRender === 'function') {
safeInvoke(this._actual.afterRender, this._actual, null, null);
}
return;
}
// This widget should be visible
if (this.allowEditorOverflow) {
this.domNode.setTop(this._renderData.coordinate.top);
this.domNode.setLeft(this._renderData.coordinate.left);
}
else {
this.domNode.setTop(this._renderData.coordinate.top + ctx.scrollTop - ctx.bigNumbersDelta);
this.domNode.setLeft(this._renderData.coordinate.left);
}
if (!this._isVisible) {
this.domNode.setVisibility('inherit');
this.domNode.setAttribute('monaco-visible-content-widget', 'true');
this._isVisible = true;
}
if (typeof this._actual.afterRender === 'function') {
safeInvoke(this._actual.afterRender, this._actual, this._renderData.position, this._renderData.coordinate);
}
}
}
class PositionPair {
constructor(modelPosition, viewPosition) {
this.modelPosition = modelPosition;
this.viewPosition = viewPosition;
}
}
class Coordinate {
constructor(top, left) {
this.top = top;
this.left = left;
this._coordinateBrand = undefined;
}
}
class AnchorCoordinate {
constructor(top, left, height) {
this.top = top;
this.left = left;
this.height = height;
this._anchorCoordinateBrand = undefined;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function safeInvoke(fn, thisArg, ...args) {
try {
return fn.call(thisArg, ...args);
}
catch {
// ignore
return null;
}
}
export { ViewContentWidgets };
@@ -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-editor .view-overlays .current-line {
display: block;
position: absolute;
left: 0;
top: 0;
box-sizing: border-box;
height: 100%;
}
.monaco-editor .margin-view-overlays .current-line {
display: block;
position: absolute;
left: 0;
top: 0;
box-sizing: border-box;
height: 100%;
}
.monaco-editor
.margin-view-overlays
.current-line.current-line-margin.current-line-margin-both {
border-right: 0;
}
@@ -0,0 +1,205 @@
import './currentLineHighlight.css';
import { DynamicViewOverlay } from '../../view/dynamicViewOverlay.js';
import { editorLineHighlight, editorLineHighlightBorder } from '../../../common/core/editorColorRegistry.js';
import { equals } from '../../../../base/common/arrays.js';
import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';
import { Selection } from '../../../common/core/selection.js';
import { isHighContrast } from '../../../../platform/theme/common/theme.js';
import { Position } from '../../../common/core/position.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class AbstractLineHighlightOverlay extends DynamicViewOverlay {
constructor(context) {
super();
this._context = context;
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this._renderLineHighlight = options.get(110 /* EditorOption.renderLineHighlight */);
this._renderLineHighlightOnlyWhenFocus = options.get(111 /* EditorOption.renderLineHighlightOnlyWhenFocus */);
this._wordWrap = layoutInfo.isViewportWrapping;
this._contentLeft = layoutInfo.contentLeft;
this._contentWidth = layoutInfo.contentWidth;
this._selectionIsEmpty = true;
this._focused = false;
this._cursorLineNumbers = [1];
this._selections = [new Selection(1, 1, 1, 1)];
this._renderData = null;
this._context.addEventHandler(this);
}
dispose() {
this._context.removeEventHandler(this);
super.dispose();
}
_readFromSelections() {
let hasChanged = false;
const lineNumbers = new Set();
for (const selection of this._selections) {
lineNumbers.add(selection.positionLineNumber);
}
const cursorsLineNumbers = Array.from(lineNumbers);
cursorsLineNumbers.sort((a, b) => a - b);
if (!equals(this._cursorLineNumbers, cursorsLineNumbers)) {
this._cursorLineNumbers = cursorsLineNumbers;
hasChanged = true;
}
const selectionIsEmpty = this._selections.every(s => s.isEmpty());
if (this._selectionIsEmpty !== selectionIsEmpty) {
this._selectionIsEmpty = selectionIsEmpty;
hasChanged = true;
}
return hasChanged;
}
// --- begin event handlers
onThemeChanged(e) {
return this._readFromSelections();
}
onConfigurationChanged(e) {
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this._renderLineHighlight = options.get(110 /* EditorOption.renderLineHighlight */);
this._renderLineHighlightOnlyWhenFocus = options.get(111 /* EditorOption.renderLineHighlightOnlyWhenFocus */);
this._wordWrap = layoutInfo.isViewportWrapping;
this._contentLeft = layoutInfo.contentLeft;
this._contentWidth = layoutInfo.contentWidth;
return true;
}
onCursorStateChanged(e) {
this._selections = e.selections;
return this._readFromSelections();
}
onFlushed(e) {
return true;
}
onLinesDeleted(e) {
return true;
}
onLinesInserted(e) {
return true;
}
onScrollChanged(e) {
return e.scrollWidthChanged || e.scrollTopChanged;
}
onZonesChanged(e) {
return true;
}
onFocusChanged(e) {
if (!this._renderLineHighlightOnlyWhenFocus) {
return false;
}
this._focused = e.isFocused;
return true;
}
// --- end event handlers
prepareRender(ctx) {
if (!this._shouldRenderThis()) {
this._renderData = null;
return;
}
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
// initialize renderData
const renderData = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
const lineIndex = lineNumber - visibleStartLineNumber;
renderData[lineIndex] = '';
}
if (this._wordWrap) {
// do a first pass to render wrapped lines
const renderedLineWrapped = this._renderOne(ctx, false);
for (const cursorLineNumber of this._cursorLineNumbers) {
const coordinatesConverter = this._context.viewModel.coordinatesConverter;
const modelLineNumber = coordinatesConverter.convertViewPositionToModelPosition(new Position(cursorLineNumber, 1)).lineNumber;
const firstViewLineNumber = coordinatesConverter.convertModelPositionToViewPosition(new Position(modelLineNumber, 1)).lineNumber;
const lastViewLineNumber = coordinatesConverter.convertModelPositionToViewPosition(new Position(modelLineNumber, this._context.viewModel.model.getLineMaxColumn(modelLineNumber))).lineNumber;
const firstLine = Math.max(firstViewLineNumber, visibleStartLineNumber);
const lastLine = Math.min(lastViewLineNumber, visibleEndLineNumber);
for (let lineNumber = firstLine; lineNumber <= lastLine; lineNumber++) {
const lineIndex = lineNumber - visibleStartLineNumber;
renderData[lineIndex] = renderedLineWrapped;
}
}
}
// do a second pass to render exact lines
const renderedLineExact = this._renderOne(ctx, true);
for (const cursorLineNumber of this._cursorLineNumbers) {
if (cursorLineNumber < visibleStartLineNumber || cursorLineNumber > visibleEndLineNumber) {
continue;
}
const lineIndex = cursorLineNumber - visibleStartLineNumber;
renderData[lineIndex] = renderedLineExact;
}
this._renderData = renderData;
}
render(startLineNumber, lineNumber) {
if (!this._renderData) {
return '';
}
const lineIndex = lineNumber - startLineNumber;
if (lineIndex >= this._renderData.length) {
return '';
}
return this._renderData[lineIndex];
}
_shouldRenderInMargin() {
return ((this._renderLineHighlight === 'gutter' || this._renderLineHighlight === 'all')
&& (!this._renderLineHighlightOnlyWhenFocus || this._focused));
}
_shouldRenderInContent() {
return ((this._renderLineHighlight === 'line' || this._renderLineHighlight === 'all')
&& this._selectionIsEmpty
&& (!this._renderLineHighlightOnlyWhenFocus || this._focused));
}
}
/**
* Emphasizes the current line by drawing a border around it.
*/
class CurrentLineHighlightOverlay extends AbstractLineHighlightOverlay {
_renderOne(ctx, exact) {
const className = 'current-line' + (this._shouldRenderInMargin() ? ' current-line-both' : '') + (exact ? ' current-line-exact' : '');
return `<div class="${className}" style="width:${Math.max(ctx.scrollWidth, this._contentWidth)}px;"></div>`;
}
_shouldRenderThis() {
return this._shouldRenderInContent();
}
_shouldRenderOther() {
return this._shouldRenderInMargin();
}
}
/**
* Emphasizes the current line margin/gutter by drawing a border around it.
*/
class CurrentLineMarginHighlightOverlay extends AbstractLineHighlightOverlay {
_renderOne(ctx, exact) {
const className = 'current-line' + (this._shouldRenderInMargin() ? ' current-line-margin' : '') + (this._shouldRenderOther() ? ' current-line-margin-both' : '') + (this._shouldRenderInMargin() && exact ? ' current-line-exact-margin' : '');
return `<div class="${className}" style="width:${this._contentLeft}px"></div>`;
}
_shouldRenderThis() {
return true;
}
_shouldRenderOther() {
return this._shouldRenderInContent();
}
}
registerThemingParticipant((theme, collector) => {
const lineHighlight = theme.getColor(editorLineHighlight);
if (lineHighlight) {
collector.addRule(`.monaco-editor .view-overlays .current-line { background-color: ${lineHighlight}; }`);
collector.addRule(`.monaco-editor .margin-view-overlays .current-line-margin { background-color: ${lineHighlight}; border: none; }`);
}
if (!lineHighlight || lineHighlight.isTransparent() || theme.defines(editorLineHighlightBorder)) {
const lineHighlightBorder = theme.getColor(editorLineHighlightBorder);
if (lineHighlightBorder) {
collector.addRule(`.monaco-editor .view-overlays .current-line-exact { border: 2px solid ${lineHighlightBorder}; }`);
collector.addRule(`.monaco-editor .margin-view-overlays .current-line-exact-margin { border: 2px solid ${lineHighlightBorder}; }`);
if (isHighContrast(theme.type)) {
collector.addRule(`.monaco-editor .view-overlays .current-line-exact { border-width: 1px; }`);
collector.addRule(`.monaco-editor .margin-view-overlays .current-line-exact-margin { border-width: 1px; }`);
}
}
}
});
export { AbstractLineHighlightOverlay, CurrentLineHighlightOverlay, CurrentLineMarginHighlightOverlay };
@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*
Keeping name short for faster parsing.
cdr = core decorations rendering (div)
*/
.monaco-editor .lines-content .cdr {
position: absolute;
height: 100%;
}
@@ -0,0 +1,196 @@
import './decorations.css';
import { DynamicViewOverlay } from '../../view/dynamicViewOverlay.js';
import { HorizontalRange } from '../../view/renderingContext.js';
import { Range } from '../../../common/core/range.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class DecorationsOverlay extends DynamicViewOverlay {
constructor(context) {
super();
this._context = context;
const options = this._context.configuration.options;
this._typicalHalfwidthCharacterWidth = options.get(59 /* EditorOption.fontInfo */).typicalHalfwidthCharacterWidth;
this._renderResult = null;
this._context.addEventHandler(this);
}
dispose() {
this._context.removeEventHandler(this);
this._renderResult = null;
super.dispose();
}
// --- begin event handlers
onConfigurationChanged(e) {
const options = this._context.configuration.options;
this._typicalHalfwidthCharacterWidth = options.get(59 /* EditorOption.fontInfo */).typicalHalfwidthCharacterWidth;
return true;
}
onDecorationsChanged(e) {
return true;
}
onFlushed(e) {
return true;
}
onLinesChanged(e) {
return true;
}
onLinesDeleted(e) {
return true;
}
onLinesInserted(e) {
return true;
}
onScrollChanged(e) {
return e.scrollTopChanged || e.scrollWidthChanged;
}
onZonesChanged(e) {
return true;
}
// --- end event handlers
prepareRender(ctx) {
const _decorations = ctx.getDecorationsInViewport();
// Keep only decorations with `className`
let decorations = [];
let decorationsLen = 0;
for (let i = 0, len = _decorations.length; i < len; i++) {
const d = _decorations[i];
if (d.options.className) {
decorations[decorationsLen++] = d;
}
}
// Sort decorations for consistent render output
decorations = decorations.sort((a, b) => {
if (a.options.zIndex < b.options.zIndex) {
return -1;
}
if (a.options.zIndex > b.options.zIndex) {
return 1;
}
const aClassName = a.options.className;
const bClassName = b.options.className;
if (aClassName < bClassName) {
return -1;
}
if (aClassName > bClassName) {
return 1;
}
return Range.compareRangesUsingStarts(a.range, b.range);
});
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
const output = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
const lineIndex = lineNumber - visibleStartLineNumber;
output[lineIndex] = '';
}
// Render first whole line decorations and then regular decorations
this._renderWholeLineDecorations(ctx, decorations, output);
this._renderNormalDecorations(ctx, decorations, output);
this._renderResult = output;
}
_renderWholeLineDecorations(ctx, decorations, output) {
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
for (let i = 0, lenI = decorations.length; i < lenI; i++) {
const d = decorations[i];
if (!d.options.isWholeLine) {
continue;
}
const decorationOutput = ('<div class="cdr '
+ d.options.className
+ '" style="left:0;width:100%;"></div>');
const startLineNumber = Math.max(d.range.startLineNumber, visibleStartLineNumber);
const endLineNumber = Math.min(d.range.endLineNumber, visibleEndLineNumber);
for (let j = startLineNumber; j <= endLineNumber; j++) {
const lineIndex = j - visibleStartLineNumber;
output[lineIndex] += decorationOutput;
}
}
}
_renderNormalDecorations(ctx, decorations, output) {
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
let prevClassName = null;
let prevShowIfCollapsed = false;
let prevRange = null;
let prevShouldFillLineOnLineBreak = false;
for (let i = 0, lenI = decorations.length; i < lenI; i++) {
const d = decorations[i];
if (d.options.isWholeLine) {
continue;
}
const className = d.options.className;
const showIfCollapsed = Boolean(d.options.showIfCollapsed);
let range = d.range;
if (showIfCollapsed && range.endColumn === 1 && range.endLineNumber !== range.startLineNumber) {
range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber - 1, this._context.viewModel.getLineMaxColumn(range.endLineNumber - 1));
}
if (prevClassName === className && prevShowIfCollapsed === showIfCollapsed && Range.areIntersectingOrTouching(prevRange, range)) {
// merge into previous decoration
prevRange = Range.plusRange(prevRange, range);
continue;
}
// flush previous decoration
if (prevClassName !== null) {
this._renderNormalDecoration(ctx, prevRange, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, visibleStartLineNumber, output);
}
prevClassName = className;
prevShowIfCollapsed = showIfCollapsed;
prevRange = range;
prevShouldFillLineOnLineBreak = d.options.shouldFillLineOnLineBreak ?? false;
}
if (prevClassName !== null) {
this._renderNormalDecoration(ctx, prevRange, prevClassName, prevShouldFillLineOnLineBreak, prevShowIfCollapsed, visibleStartLineNumber, output);
}
}
_renderNormalDecoration(ctx, range, className, shouldFillLineOnLineBreak, showIfCollapsed, visibleStartLineNumber, output) {
const linesVisibleRanges = ctx.linesVisibleRangesForRange(range, /*TODO@Alex*/ className === 'findMatch');
if (!linesVisibleRanges) {
return;
}
for (let j = 0, lenJ = linesVisibleRanges.length; j < lenJ; j++) {
const lineVisibleRanges = linesVisibleRanges[j];
if (lineVisibleRanges.outsideRenderedLine) {
continue;
}
const lineIndex = lineVisibleRanges.lineNumber - visibleStartLineNumber;
if (showIfCollapsed && lineVisibleRanges.ranges.length === 1) {
const singleVisibleRange = lineVisibleRanges.ranges[0];
if (singleVisibleRange.width < this._typicalHalfwidthCharacterWidth) {
// collapsed/very small range case => make the decoration visible by expanding its width
// expand its size on both sides (both to the left and to the right, keeping it centered)
const center = Math.round(singleVisibleRange.left + singleVisibleRange.width / 2);
const left = Math.max(0, Math.round(center - this._typicalHalfwidthCharacterWidth / 2));
lineVisibleRanges.ranges[0] = new HorizontalRange(left, this._typicalHalfwidthCharacterWidth);
}
}
for (let k = 0, lenK = lineVisibleRanges.ranges.length; k < lenK; k++) {
const expandToLeft = shouldFillLineOnLineBreak && lineVisibleRanges.continuesOnNextLine && lenK === 1;
const visibleRange = lineVisibleRanges.ranges[k];
const decorationOutput = ('<div class="cdr '
+ className
+ '" style="left:'
+ String(visibleRange.left)
+ 'px;width:'
+ (expandToLeft ?
'100%;' :
(String(visibleRange.width) + 'px;'))
+ '"></div>');
output[lineIndex] += decorationOutput;
}
}
}
render(startLineNumber, lineNumber) {
if (!this._renderResult) {
return '';
}
const lineIndex = lineNumber - startLineNumber;
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
return '';
}
return this._renderResult[lineIndex];
}
}
export { DecorationsOverlay };
@@ -0,0 +1,151 @@
import { addDisposableListener } from '../../../../base/browser/dom.js';
import { createFastDomNode } from '../../../../base/browser/fastDomNode.js';
import { SmoothScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
import { ViewPart, PartFingerprints } from '../../view/viewPart.js';
import { getThemeTypeSelector } from '../../../../platform/theme/common/themeService.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* The editor scrollbar built on VS Code's scrollable element that sits beside
* the minimap.
*/
class EditorScrollbar extends ViewPart {
constructor(context, linesContent, viewDomNode, overflowGuardDomNode) {
super(context);
const options = this._context.configuration.options;
const scrollbar = options.get(117 /* EditorOption.scrollbar */);
const mouseWheelScrollSensitivity = options.get(83 /* EditorOption.mouseWheelScrollSensitivity */);
const fastScrollSensitivity = options.get(49 /* EditorOption.fastScrollSensitivity */);
const scrollPredominantAxis = options.get(120 /* EditorOption.scrollPredominantAxis */);
const inertialScroll = options.get(158 /* EditorOption.inertialScroll */);
const scrollbarOptions = {
listenOnDomNode: viewDomNode.domNode,
className: 'editor-scrollable' + ' ' + getThemeTypeSelector(context.theme.type),
useShadows: false,
lazyRender: true,
vertical: scrollbar.vertical,
horizontal: scrollbar.horizontal,
verticalHasArrows: scrollbar.verticalHasArrows,
horizontalHasArrows: scrollbar.horizontalHasArrows,
verticalScrollbarSize: scrollbar.verticalScrollbarSize,
verticalSliderSize: scrollbar.verticalSliderSize,
horizontalScrollbarSize: scrollbar.horizontalScrollbarSize,
horizontalSliderSize: scrollbar.horizontalSliderSize,
handleMouseWheel: scrollbar.handleMouseWheel,
alwaysConsumeMouseWheel: scrollbar.alwaysConsumeMouseWheel,
arrowSize: scrollbar.arrowSize,
mouseWheelScrollSensitivity: mouseWheelScrollSensitivity,
fastScrollSensitivity: fastScrollSensitivity,
scrollPredominantAxis: scrollPredominantAxis,
scrollByPage: scrollbar.scrollByPage,
inertialScroll: inertialScroll,
};
this.scrollbar = this._register(new SmoothScrollableElement(linesContent.domNode, scrollbarOptions, this._context.viewLayout.getScrollable()));
PartFingerprints.write(this.scrollbar.getDomNode(), 6 /* PartFingerprint.ScrollableElement */);
this.scrollbarDomNode = createFastDomNode(this.scrollbar.getDomNode());
this.scrollbarDomNode.setPosition('absolute');
this._setLayout();
// When having a zone widget that calls .focus() on one of its dom elements,
// the browser will try desperately to reveal that dom node, unexpectedly
// changing the .scrollTop of this.linesContent
const onBrowserDesperateReveal = (domNode, lookAtScrollTop, lookAtScrollLeft) => {
const newScrollPosition = {};
{
const deltaTop = domNode.scrollTop;
if (deltaTop) {
newScrollPosition.scrollTop = this._context.viewLayout.getCurrentScrollTop() + deltaTop;
domNode.scrollTop = 0;
}
}
if (lookAtScrollLeft) {
const deltaLeft = domNode.scrollLeft;
if (deltaLeft) {
newScrollPosition.scrollLeft = this._context.viewLayout.getCurrentScrollLeft() + deltaLeft;
domNode.scrollLeft = 0;
}
}
this._context.viewModel.viewLayout.setScrollPosition(newScrollPosition, 1 /* ScrollType.Immediate */);
};
// I've seen this happen both on the view dom node & on the lines content dom node.
this._register(addDisposableListener(viewDomNode.domNode, 'scroll', (e) => onBrowserDesperateReveal(viewDomNode.domNode, true, true)));
this._register(addDisposableListener(linesContent.domNode, 'scroll', (e) => onBrowserDesperateReveal(linesContent.domNode, true, false)));
this._register(addDisposableListener(overflowGuardDomNode.domNode, 'scroll', (e) => onBrowserDesperateReveal(overflowGuardDomNode.domNode, true, false)));
this._register(addDisposableListener(this.scrollbarDomNode.domNode, 'scroll', (e) => onBrowserDesperateReveal(this.scrollbarDomNode.domNode, true, false)));
}
dispose() {
super.dispose();
}
_setLayout() {
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this.scrollbarDomNode.setLeft(layoutInfo.contentLeft);
const minimap = options.get(81 /* EditorOption.minimap */);
const side = minimap.side;
if (side === 'right') {
this.scrollbarDomNode.setWidth(layoutInfo.contentWidth + layoutInfo.minimap.minimapWidth);
}
else {
this.scrollbarDomNode.setWidth(layoutInfo.contentWidth);
}
this.scrollbarDomNode.setHeight(layoutInfo.height);
}
getOverviewRulerLayoutInfo() {
return this.scrollbar.getOverviewRulerLayoutInfo();
}
getDomNode() {
return this.scrollbarDomNode;
}
delegateVerticalScrollbarPointerDown(browserEvent) {
this.scrollbar.delegateVerticalScrollbarPointerDown(browserEvent);
}
delegateScrollFromMouseWheelEvent(browserEvent) {
this.scrollbar.delegateScrollFromMouseWheelEvent(browserEvent);
}
// --- begin event handlers
onConfigurationChanged(e) {
if (e.hasChanged(117 /* EditorOption.scrollbar */)
|| e.hasChanged(83 /* EditorOption.mouseWheelScrollSensitivity */)
|| e.hasChanged(49 /* EditorOption.fastScrollSensitivity */)) {
const options = this._context.configuration.options;
const scrollbar = options.get(117 /* EditorOption.scrollbar */);
const mouseWheelScrollSensitivity = options.get(83 /* EditorOption.mouseWheelScrollSensitivity */);
const fastScrollSensitivity = options.get(49 /* EditorOption.fastScrollSensitivity */);
const scrollPredominantAxis = options.get(120 /* EditorOption.scrollPredominantAxis */);
const newOpts = {
vertical: scrollbar.vertical,
horizontal: scrollbar.horizontal,
verticalScrollbarSize: scrollbar.verticalScrollbarSize,
horizontalScrollbarSize: scrollbar.horizontalScrollbarSize,
scrollByPage: scrollbar.scrollByPage,
handleMouseWheel: scrollbar.handleMouseWheel,
mouseWheelScrollSensitivity: mouseWheelScrollSensitivity,
fastScrollSensitivity: fastScrollSensitivity,
scrollPredominantAxis: scrollPredominantAxis
};
this.scrollbar.updateOptions(newOpts);
}
if (e.hasChanged(165 /* EditorOption.layoutInfo */)) {
this._setLayout();
}
return true;
}
onScrollChanged(e) {
return true;
}
onThemeChanged(e) {
this.scrollbar.updateClassName('editor-scrollable' + ' ' + getThemeTypeSelector(this._context.theme.type));
return true;
}
// --- end event handlers
prepareRender(ctx) {
// Nothing to do
}
render(ctx) {
this.scrollbar.renderNow();
}
}
export { EditorScrollbar };
@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .glyph-margin {
position: absolute;
top: 0;
}
/*
Keeping name short for faster parsing.
cgmr = core glyph margin rendering (div)
*/
.monaco-editor .glyph-margin-widgets .cgmr {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
}
/*
Ensure spinning icons are pixel-perfectly centered and avoid wobble.
This is only applied to icons that spin to avoid unnecessary
GPU layers and blurry subpixel AA.
*/
.monaco-editor .glyph-margin-widgets .cgmr.codicon-modifier-spin::before {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
@@ -0,0 +1,403 @@
import { createFastDomNode } from '../../../../base/browser/fastDomNode.js';
import { ArrayQueue } from '../../../../base/common/arrays.js';
import './glyphMargin.css';
import { DynamicViewOverlay } from '../../view/dynamicViewOverlay.js';
import { ViewPart } from '../../view/viewPart.js';
import { Position } from '../../../common/core/position.js';
import { Range } from '../../../common/core/range.js';
import { GlyphMarginLane } from '../../../common/model.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Represents a decoration that should be shown along the lines from `startLineNumber` to `endLineNumber`.
* This can end up producing multiple `LineDecorationToRender`.
*/
class DecorationToRender {
constructor(startLineNumber, endLineNumber, className, tooltip, zIndex) {
this.startLineNumber = startLineNumber;
this.endLineNumber = endLineNumber;
this.className = className;
this.tooltip = tooltip;
this._decorationToRenderBrand = undefined;
this.zIndex = zIndex ?? 0;
}
}
/**
* A decoration that should be shown along a line.
*/
class LineDecorationToRender {
constructor(className, zIndex, tooltip) {
this.className = className;
this.zIndex = zIndex;
this.tooltip = tooltip;
}
}
/**
* Decorations to render on a visible line.
*/
class VisibleLineDecorationsToRender {
constructor() {
this.decorations = [];
}
add(decoration) {
this.decorations.push(decoration);
}
getDecorations() {
return this.decorations;
}
}
class DedupOverlay extends DynamicViewOverlay {
/**
* Returns an array with an element for each visible line number.
*/
_render(visibleStartLineNumber, visibleEndLineNumber, decorations) {
const output = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
const lineIndex = lineNumber - visibleStartLineNumber;
output[lineIndex] = new VisibleLineDecorationsToRender();
}
if (decorations.length === 0) {
return output;
}
// Sort decorations by className, then by startLineNumber and then by endLineNumber
decorations.sort((a, b) => {
if (a.className === b.className) {
if (a.startLineNumber === b.startLineNumber) {
return a.endLineNumber - b.endLineNumber;
}
return a.startLineNumber - b.startLineNumber;
}
return (a.className < b.className ? -1 : 1);
});
let prevClassName = null;
let prevEndLineIndex = 0;
for (let i = 0, len = decorations.length; i < len; i++) {
const d = decorations[i];
const className = d.className;
const zIndex = d.zIndex;
let startLineIndex = Math.max(d.startLineNumber, visibleStartLineNumber) - visibleStartLineNumber;
const endLineIndex = Math.min(d.endLineNumber, visibleEndLineNumber) - visibleStartLineNumber;
if (prevClassName === className) {
// Here we avoid rendering the same className multiple times on the same line
startLineIndex = Math.max(prevEndLineIndex + 1, startLineIndex);
prevEndLineIndex = Math.max(prevEndLineIndex, endLineIndex);
}
else {
prevClassName = className;
prevEndLineIndex = endLineIndex;
}
for (let i = startLineIndex; i <= prevEndLineIndex; i++) {
output[i].add(new LineDecorationToRender(className, zIndex, d.tooltip));
}
}
return output;
}
}
class GlyphMarginWidgets extends ViewPart {
constructor(context) {
super(context);
this._widgets = {};
this._context = context;
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this.domNode = createFastDomNode(document.createElement('div'));
this.domNode.setClassName('glyph-margin-widgets');
this.domNode.setPosition('absolute');
this.domNode.setTop(0);
this._lineHeight = options.get(75 /* EditorOption.lineHeight */);
this._glyphMargin = options.get(66 /* EditorOption.glyphMargin */);
this._glyphMarginLeft = layoutInfo.glyphMarginLeft;
this._glyphMarginWidth = layoutInfo.glyphMarginWidth;
this._glyphMarginDecorationLaneCount = layoutInfo.glyphMarginDecorationLaneCount;
this._managedDomNodes = [];
this._decorationGlyphsToRender = [];
}
dispose() {
this._managedDomNodes = [];
this._decorationGlyphsToRender = [];
this._widgets = {};
super.dispose();
}
getWidgets() {
return Object.values(this._widgets);
}
// --- begin event handlers
onConfigurationChanged(e) {
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this._lineHeight = options.get(75 /* EditorOption.lineHeight */);
this._glyphMargin = options.get(66 /* EditorOption.glyphMargin */);
this._glyphMarginLeft = layoutInfo.glyphMarginLeft;
this._glyphMarginWidth = layoutInfo.glyphMarginWidth;
this._glyphMarginDecorationLaneCount = layoutInfo.glyphMarginDecorationLaneCount;
return true;
}
onDecorationsChanged(e) {
return true;
}
onFlushed(e) {
return true;
}
onLinesChanged(e) {
return true;
}
onLinesDeleted(e) {
return true;
}
onLinesInserted(e) {
return true;
}
onScrollChanged(e) {
return e.scrollTopChanged;
}
onZonesChanged(e) {
return true;
}
// --- end event handlers
// --- begin widget management
addWidget(widget) {
const domNode = createFastDomNode(widget.getDomNode());
this._widgets[widget.getId()] = {
widget: widget,
preference: widget.getPosition(),
domNode: domNode,
renderInfo: null
};
domNode.setPosition('absolute');
domNode.setDisplay('none');
domNode.setAttribute('widgetId', widget.getId());
this.domNode.appendChild(domNode);
this.setShouldRender();
}
setWidgetPosition(widget, preference) {
const myWidget = this._widgets[widget.getId()];
if (myWidget.preference.lane === preference.lane
&& myWidget.preference.zIndex === preference.zIndex
&& Range.equalsRange(myWidget.preference.range, preference.range)) {
return false;
}
myWidget.preference = preference;
this.setShouldRender();
return true;
}
removeWidget(widget) {
const widgetId = widget.getId();
if (this._widgets[widgetId]) {
const widgetData = this._widgets[widgetId];
const domNode = widgetData.domNode.domNode;
delete this._widgets[widgetId];
domNode.remove();
this.setShouldRender();
}
}
// --- end widget management
_collectDecorationBasedGlyphRenderRequest(ctx, requests) {
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
const decorations = ctx.getDecorationsInViewport();
for (const d of decorations) {
const glyphMarginClassName = d.options.glyphMarginClassName;
if (!glyphMarginClassName) {
continue;
}
const startLineNumber = Math.max(d.range.startLineNumber, visibleStartLineNumber);
const endLineNumber = Math.min(d.range.endLineNumber, visibleEndLineNumber);
const lane = d.options.glyphMargin?.position ?? GlyphMarginLane.Center;
const zIndex = d.options.zIndex ?? 0;
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
const modelPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber, 0));
const laneIndex = this._context.viewModel.glyphLanes.getLanesAtLine(modelPosition.lineNumber).indexOf(lane);
requests.push(new DecorationBasedGlyphRenderRequest(lineNumber, laneIndex, zIndex, glyphMarginClassName));
}
}
}
_collectWidgetBasedGlyphRenderRequest(ctx, requests) {
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
for (const widget of Object.values(this._widgets)) {
const range = widget.preference.range;
const { startLineNumber, endLineNumber } = this._context.viewModel.coordinatesConverter.convertModelRangeToViewRange(Range.lift(range));
if (!startLineNumber || !endLineNumber || endLineNumber < visibleStartLineNumber || startLineNumber > visibleEndLineNumber) {
// The widget is not in the viewport
continue;
}
// The widget is in the viewport, find a good line for it
const widgetLineNumber = Math.max(startLineNumber, visibleStartLineNumber);
const modelPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(widgetLineNumber, 0));
const laneIndex = this._context.viewModel.glyphLanes.getLanesAtLine(modelPosition.lineNumber).indexOf(widget.preference.lane);
requests.push(new WidgetBasedGlyphRenderRequest(widgetLineNumber, laneIndex, widget.preference.zIndex, widget));
}
}
_collectSortedGlyphRenderRequests(ctx) {
const requests = [];
this._collectDecorationBasedGlyphRenderRequest(ctx, requests);
this._collectWidgetBasedGlyphRenderRequest(ctx, requests);
// sort requests by lineNumber ASC, lane ASC, zIndex DESC, type DESC (widgets first), className ASC
// don't change this sort unless you understand `prepareRender` below.
requests.sort((a, b) => {
if (a.lineNumber === b.lineNumber) {
if (a.laneIndex === b.laneIndex) {
if (a.zIndex === b.zIndex) {
if (b.type === a.type) {
if (a.type === 0 /* GlyphRenderRequestType.Decoration */ && b.type === 0 /* GlyphRenderRequestType.Decoration */) {
return (a.className < b.className ? -1 : 1);
}
return 0;
}
return b.type - a.type;
}
return b.zIndex - a.zIndex;
}
return a.laneIndex - b.laneIndex;
}
return a.lineNumber - b.lineNumber;
});
return requests;
}
/**
* Will store render information in each widget's renderInfo and in `_decorationGlyphsToRender`.
*/
prepareRender(ctx) {
if (!this._glyphMargin) {
this._decorationGlyphsToRender = [];
return;
}
for (const widget of Object.values(this._widgets)) {
widget.renderInfo = null;
}
const requests = new ArrayQueue(this._collectSortedGlyphRenderRequests(ctx));
const decorationGlyphsToRender = [];
while (requests.length > 0) {
const first = requests.peek();
if (!first) {
// not possible
break;
}
// Requests are sorted by lineNumber and lane, so we read all requests for this particular location
const requestsAtLocation = requests.takeWhile((el) => el.lineNumber === first.lineNumber && el.laneIndex === first.laneIndex);
if (!requestsAtLocation || requestsAtLocation.length === 0) {
// not possible
break;
}
const winner = requestsAtLocation[0];
if (winner.type === 0 /* GlyphRenderRequestType.Decoration */) {
// combine all decorations with the same z-index
const classNames = [];
// requests are sorted by zIndex, type, and className so we can dedup className by looking at the previous one
for (const request of requestsAtLocation) {
if (request.zIndex !== winner.zIndex || request.type !== winner.type) {
break;
}
if (classNames.length === 0 || classNames[classNames.length - 1] !== request.className) {
classNames.push(request.className);
}
}
decorationGlyphsToRender.push(winner.accept(classNames.join(' '))); // TODO@joyceerhl Implement overflow for remaining decorations
}
else {
// widgets cannot be combined
winner.widget.renderInfo = {
lineNumber: winner.lineNumber,
laneIndex: winner.laneIndex,
};
}
}
this._decorationGlyphsToRender = decorationGlyphsToRender;
}
render(ctx) {
if (!this._glyphMargin) {
for (const widget of Object.values(this._widgets)) {
widget.domNode.setDisplay('none');
}
while (this._managedDomNodes.length > 0) {
const domNode = this._managedDomNodes.pop();
domNode?.domNode.remove();
}
return;
}
const width = (Math.round(this._glyphMarginWidth / this._glyphMarginDecorationLaneCount));
// Render widgets
for (const widget of Object.values(this._widgets)) {
if (!widget.renderInfo) {
// this widget is not visible
widget.domNode.setDisplay('none');
}
else {
const top = ctx.viewportData.relativeVerticalOffset[widget.renderInfo.lineNumber - ctx.viewportData.startLineNumber];
const left = this._glyphMarginLeft + widget.renderInfo.laneIndex * this._lineHeight;
widget.domNode.setDisplay('block');
widget.domNode.setTop(top);
widget.domNode.setLeft(left);
widget.domNode.setWidth(width);
widget.domNode.setHeight(this._lineHeight);
}
}
// Render decorations, reusing previous dom nodes as possible
for (let i = 0; i < this._decorationGlyphsToRender.length; i++) {
const dec = this._decorationGlyphsToRender[i];
const decLineNumber = dec.lineNumber;
const top = ctx.viewportData.relativeVerticalOffset[decLineNumber - ctx.viewportData.startLineNumber];
const left = this._glyphMarginLeft + dec.laneIndex * this._lineHeight;
let domNode;
if (i < this._managedDomNodes.length) {
domNode = this._managedDomNodes[i];
}
else {
domNode = createFastDomNode(document.createElement('div'));
this._managedDomNodes.push(domNode);
this.domNode.appendChild(domNode);
}
const lineHeight = this._context.viewLayout.getLineHeightForLineNumber(decLineNumber);
domNode.setClassName(`cgmr codicon ` + dec.combinedClassName);
domNode.setPosition(`absolute`);
domNode.setTop(top);
domNode.setLeft(left);
domNode.setWidth(width);
domNode.setHeight(lineHeight);
}
// remove extra dom nodes
while (this._managedDomNodes.length > this._decorationGlyphsToRender.length) {
const domNode = this._managedDomNodes.pop();
domNode?.domNode.remove();
}
}
}
/**
* A request to render a decoration in the glyph margin at a certain location.
*/
class DecorationBasedGlyphRenderRequest {
constructor(lineNumber, laneIndex, zIndex, className) {
this.lineNumber = lineNumber;
this.laneIndex = laneIndex;
this.zIndex = zIndex;
this.className = className;
this.type = 0 /* GlyphRenderRequestType.Decoration */;
}
accept(combinedClassName) {
return new DecorationBasedGlyph(this.lineNumber, this.laneIndex, combinedClassName);
}
}
/**
* A request to render a widget in the glyph margin at a certain location.
*/
class WidgetBasedGlyphRenderRequest {
constructor(lineNumber, laneIndex, zIndex, widget) {
this.lineNumber = lineNumber;
this.laneIndex = laneIndex;
this.zIndex = zIndex;
this.widget = widget;
this.type = 1 /* GlyphRenderRequestType.Widget */;
}
}
class DecorationBasedGlyph {
constructor(lineNumber, laneIndex, combinedClassName) {
this.lineNumber = lineNumber;
this.laneIndex = laneIndex;
this.combinedClassName = combinedClassName;
}
}
export { DecorationToRender, DedupOverlay, GlyphMarginWidgets, LineDecorationToRender, VisibleLineDecorationsToRender };
@@ -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-editor .margin-view-overlays .gpu-mark {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
display: inline-block;
border-left: solid 2px var(--vscode-editorWarning-foreground);
opacity: 0.2;
transition: background-color 0.1s linear;
}
.monaco-editor .margin-view-overlays .gpu-mark:hover {
background-color: var(--vscode-editorWarning-foreground)
}
@@ -0,0 +1,80 @@
import { DynamicViewOverlay } from '../../view/dynamicViewOverlay.js';
import { ViewLineOptions } from '../viewLines/viewLineOptions.js';
import './gpuMark.css';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* A mark on lines to make identification of GPU-rendered lines vs DOM easier.
*/
class GpuMarkOverlay extends DynamicViewOverlay {
static { this.CLASS_NAME = 'gpu-mark'; }
constructor(context, _viewGpuContext) {
super();
this._viewGpuContext = _viewGpuContext;
this._context = context;
this._renderResult = null;
this._context.addEventHandler(this);
}
dispose() {
this._context.removeEventHandler(this);
this._renderResult = null;
super.dispose();
}
// --- begin event handlers
onConfigurationChanged(e) {
return true;
}
onCursorStateChanged(e) {
return true;
}
onFlushed(e) {
return true;
}
onLinesChanged(e) {
return true;
}
onLinesDeleted(e) {
return true;
}
onLinesInserted(e) {
return true;
}
onScrollChanged(e) {
return e.scrollTopChanged;
}
onZonesChanged(e) {
return true;
}
onDecorationsChanged(e) {
return true;
}
// --- end event handlers
prepareRender(ctx) {
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
const viewportData = ctx.viewportData;
const options = new ViewLineOptions(this._context.configuration, this._context.theme.type);
const output = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
const lineIndex = lineNumber - visibleStartLineNumber;
const cannotRenderReasons = this._viewGpuContext.canRenderDetailed(options, viewportData, lineNumber);
output[lineIndex] = cannotRenderReasons.length ? `<div class="${GpuMarkOverlay.CLASS_NAME}" title="Cannot render on GPU: ${cannotRenderReasons.join(', ')}"></div>` : '';
}
this._renderResult = output;
}
render(startLineNumber, lineNumber) {
if (!this._renderResult) {
return '';
}
const lineIndex = lineNumber - startLineNumber;
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
return '';
}
return this._renderResult[lineIndex];
}
}
export { GpuMarkOverlay };
@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .lines-content .core-guide {
position: absolute;
box-sizing: border-box;
height: 100%;
}
@@ -0,0 +1,254 @@
import './indentGuides.css';
import { DynamicViewOverlay } from '../../view/dynamicViewOverlay.js';
import { editorBracketPairGuideActiveBackground1, editorBracketPairGuideBackground1, editorBracketHighlightingForeground1, editorBracketPairGuideActiveBackground2, editorBracketPairGuideBackground2, editorBracketHighlightingForeground2, editorBracketPairGuideActiveBackground3, editorBracketPairGuideBackground3, editorBracketHighlightingForeground3, editorBracketPairGuideActiveBackground4, editorBracketPairGuideBackground4, editorBracketHighlightingForeground4, editorBracketPairGuideActiveBackground5, editorBracketPairGuideBackground5, editorBracketHighlightingForeground5, editorBracketPairGuideActiveBackground6, editorBracketPairGuideBackground6, editorBracketHighlightingForeground6, editorActiveIndentGuide1, editorIndentGuide1, editorActiveIndentGuide2, editorIndentGuide2, editorActiveIndentGuide3, editorIndentGuide3, editorActiveIndentGuide4, editorIndentGuide4, editorActiveIndentGuide5, editorIndentGuide5, editorActiveIndentGuide6, editorIndentGuide6 } from '../../../common/core/editorColorRegistry.js';
import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';
import { Position } from '../../../common/core/position.js';
import { ArrayQueue } from '../../../../base/common/arrays.js';
import { isDefined } from '../../../../base/common/types.js';
import { BracketPairGuidesClassNames } from '../../../common/model/guidesTextModelPart.js';
import { HorizontalGuidesState, IndentGuide } from '../../../common/textModelGuides.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Indent guides are vertical lines that help identify the indentation level of
* the code.
*/
class IndentGuidesOverlay extends DynamicViewOverlay {
constructor(context) {
super();
this._context = context;
this._primaryPosition = null;
const options = this._context.configuration.options;
const wrappingInfo = options.get(166 /* EditorOption.wrappingInfo */);
const fontInfo = options.get(59 /* EditorOption.fontInfo */);
this._spaceWidth = fontInfo.spaceWidth;
this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth);
this._bracketPairGuideOptions = options.get(22 /* EditorOption.guides */);
this._renderResult = null;
this._context.addEventHandler(this);
}
dispose() {
this._context.removeEventHandler(this);
this._renderResult = null;
super.dispose();
}
// --- begin event handlers
onConfigurationChanged(e) {
const options = this._context.configuration.options;
const wrappingInfo = options.get(166 /* EditorOption.wrappingInfo */);
const fontInfo = options.get(59 /* EditorOption.fontInfo */);
this._spaceWidth = fontInfo.spaceWidth;
this._maxIndentLeft = wrappingInfo.wrappingColumn === -1 ? -1 : (wrappingInfo.wrappingColumn * fontInfo.typicalHalfwidthCharacterWidth);
this._bracketPairGuideOptions = options.get(22 /* EditorOption.guides */);
return true;
}
onCursorStateChanged(e) {
const selection = e.selections[0];
const newPosition = selection.getPosition();
if (!this._primaryPosition?.equals(newPosition)) {
this._primaryPosition = newPosition;
return true;
}
return false;
}
onDecorationsChanged(e) {
// true for inline decorations
return true;
}
onFlushed(e) {
return true;
}
onLinesChanged(e) {
return true;
}
onLinesDeleted(e) {
return true;
}
onLinesInserted(e) {
return true;
}
onScrollChanged(e) {
return e.scrollTopChanged; // || e.scrollWidthChanged;
}
onZonesChanged(e) {
return true;
}
onLanguageConfigurationChanged(e) {
return true;
}
// --- end event handlers
prepareRender(ctx) {
if (!this._bracketPairGuideOptions.indentation && this._bracketPairGuideOptions.bracketPairs === false) {
this._renderResult = null;
return;
}
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
const scrollWidth = ctx.scrollWidth;
const activeCursorPosition = this._primaryPosition;
const indents = this.getGuidesByLine(visibleStartLineNumber, Math.min(visibleEndLineNumber + 1, this._context.viewModel.getLineCount()), activeCursorPosition);
const output = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
const lineIndex = lineNumber - visibleStartLineNumber;
const indent = indents[lineIndex];
let result = '';
const leftOffset = ctx.visibleRangeForPosition(new Position(lineNumber, 1))?.left ?? 0;
for (const guide of indent) {
const left = guide.column === -1
? leftOffset + (guide.visibleColumn - 1) * this._spaceWidth
: ctx.visibleRangeForPosition(new Position(lineNumber, guide.column)).left;
if (left > scrollWidth || (this._maxIndentLeft > 0 && left > this._maxIndentLeft)) {
break;
}
const className = guide.horizontalLine ? (guide.horizontalLine.top ? 'horizontal-top' : 'horizontal-bottom') : 'vertical';
const width = guide.horizontalLine
? (ctx.visibleRangeForPosition(new Position(lineNumber, guide.horizontalLine.endColumn))?.left ?? (left + this._spaceWidth)) - left
: this._spaceWidth;
result += `<div class="core-guide ${guide.className} ${className}" style="left:${left}px;width:${width}px"></div>`;
}
output[lineIndex] = result;
}
this._renderResult = output;
}
getGuidesByLine(visibleStartLineNumber, visibleEndLineNumber, activeCursorPosition) {
const bracketGuides = this._bracketPairGuideOptions.bracketPairs !== false
? this._context.viewModel.getBracketGuidesInRangeByLine(visibleStartLineNumber, visibleEndLineNumber, activeCursorPosition, {
highlightActive: this._bracketPairGuideOptions.highlightActiveBracketPair,
horizontalGuides: this._bracketPairGuideOptions.bracketPairsHorizontal === true
? HorizontalGuidesState.Enabled
: this._bracketPairGuideOptions.bracketPairsHorizontal === 'active'
? HorizontalGuidesState.EnabledForActive
: HorizontalGuidesState.Disabled,
includeInactive: this._bracketPairGuideOptions.bracketPairs === true,
})
: null;
const indentGuides = this._bracketPairGuideOptions.indentation
? this._context.viewModel.getLinesIndentGuides(visibleStartLineNumber, visibleEndLineNumber)
: null;
let activeIndentStartLineNumber = 0;
let activeIndentEndLineNumber = 0;
let activeIndentLevel = 0;
if (this._bracketPairGuideOptions.highlightActiveIndentation !== false && activeCursorPosition) {
const activeIndentInfo = this._context.viewModel.getActiveIndentGuide(activeCursorPosition.lineNumber, visibleStartLineNumber, visibleEndLineNumber);
activeIndentStartLineNumber = activeIndentInfo.startLineNumber;
activeIndentEndLineNumber = activeIndentInfo.endLineNumber;
activeIndentLevel = activeIndentInfo.indent;
}
const { indentSize } = this._context.viewModel.model.getOptions();
const result = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
const lineGuides = new Array();
result.push(lineGuides);
const bracketGuidesInLine = bracketGuides ? bracketGuides[lineNumber - visibleStartLineNumber] : [];
const bracketGuidesInLineQueue = new ArrayQueue(bracketGuidesInLine);
const indentGuidesInLine = indentGuides ? indentGuides[lineNumber - visibleStartLineNumber] : 0;
for (let indentLvl = 1; indentLvl <= indentGuidesInLine; indentLvl++) {
const indentGuide = (indentLvl - 1) * indentSize + 1;
const isActive =
// Disable active indent guide if there are bracket guides.
(this._bracketPairGuideOptions.highlightActiveIndentation === 'always' || bracketGuidesInLine.length === 0) &&
activeIndentStartLineNumber <= lineNumber &&
lineNumber <= activeIndentEndLineNumber &&
indentLvl === activeIndentLevel;
lineGuides.push(...bracketGuidesInLineQueue.takeWhile(g => g.visibleColumn < indentGuide) || []);
const peeked = bracketGuidesInLineQueue.peek();
if (!peeked || peeked.visibleColumn !== indentGuide || peeked.horizontalLine) {
lineGuides.push(new IndentGuide(indentGuide, -1, `core-guide-indent lvl-${(indentLvl - 1) % 30}` + (isActive ? ' indent-active' : ''), null, -1, -1));
}
}
lineGuides.push(...bracketGuidesInLineQueue.takeWhile(g => true) || []);
}
return result;
}
render(startLineNumber, lineNumber) {
if (!this._renderResult) {
return '';
}
const lineIndex = lineNumber - startLineNumber;
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
return '';
}
return this._renderResult[lineIndex];
}
}
function transparentToUndefined(color) {
if (color && color.isTransparent()) {
return undefined;
}
return color;
}
registerThemingParticipant((theme, collector) => {
const colors = [
{ bracketColor: editorBracketHighlightingForeground1, guideColor: editorBracketPairGuideBackground1, guideColorActive: editorBracketPairGuideActiveBackground1 },
{ bracketColor: editorBracketHighlightingForeground2, guideColor: editorBracketPairGuideBackground2, guideColorActive: editorBracketPairGuideActiveBackground2 },
{ bracketColor: editorBracketHighlightingForeground3, guideColor: editorBracketPairGuideBackground3, guideColorActive: editorBracketPairGuideActiveBackground3 },
{ bracketColor: editorBracketHighlightingForeground4, guideColor: editorBracketPairGuideBackground4, guideColorActive: editorBracketPairGuideActiveBackground4 },
{ bracketColor: editorBracketHighlightingForeground5, guideColor: editorBracketPairGuideBackground5, guideColorActive: editorBracketPairGuideActiveBackground5 },
{ bracketColor: editorBracketHighlightingForeground6, guideColor: editorBracketPairGuideBackground6, guideColorActive: editorBracketPairGuideActiveBackground6 }
];
const colorProvider = new BracketPairGuidesClassNames();
const indentColors = [
{ indentColor: editorIndentGuide1, indentColorActive: editorActiveIndentGuide1 },
{ indentColor: editorIndentGuide2, indentColorActive: editorActiveIndentGuide2 },
{ indentColor: editorIndentGuide3, indentColorActive: editorActiveIndentGuide3 },
{ indentColor: editorIndentGuide4, indentColorActive: editorActiveIndentGuide4 },
{ indentColor: editorIndentGuide5, indentColorActive: editorActiveIndentGuide5 },
{ indentColor: editorIndentGuide6, indentColorActive: editorActiveIndentGuide6 },
];
const colorValues = colors
.map(c => {
const bracketColor = theme.getColor(c.bracketColor);
const guideColor = theme.getColor(c.guideColor);
const guideColorActive = theme.getColor(c.guideColorActive);
const effectiveGuideColor = transparentToUndefined(transparentToUndefined(guideColor) ?? bracketColor?.transparent(0.3));
const effectiveGuideColorActive = transparentToUndefined(transparentToUndefined(guideColorActive) ?? bracketColor);
if (!effectiveGuideColor || !effectiveGuideColorActive) {
return undefined;
}
return {
guideColor: effectiveGuideColor,
guideColorActive: effectiveGuideColorActive,
};
})
.filter(isDefined);
const indentColorValues = indentColors
.map(c => {
const indentColor = theme.getColor(c.indentColor);
const indentColorActive = theme.getColor(c.indentColorActive);
const effectiveIndentColor = transparentToUndefined(indentColor);
const effectiveIndentColorActive = transparentToUndefined(indentColorActive);
if (!effectiveIndentColor || !effectiveIndentColorActive) {
return undefined;
}
return {
indentColor: effectiveIndentColor,
indentColorActive: effectiveIndentColorActive,
};
})
.filter(isDefined);
if (colorValues.length > 0) {
for (let level = 0; level < 30; level++) {
const colors = colorValues[level % colorValues.length];
collector.addRule(`.monaco-editor .${colorProvider.getInlineClassNameOfLevel(level).replace(/ /g, '.')} { --guide-color: ${colors.guideColor}; --guide-color-active: ${colors.guideColorActive}; }`);
}
collector.addRule(`.monaco-editor .vertical { box-shadow: 1px 0 0 0 var(--guide-color) inset; }`);
collector.addRule(`.monaco-editor .horizontal-top { border-top: 1px solid var(--guide-color); }`);
collector.addRule(`.monaco-editor .horizontal-bottom { border-bottom: 1px solid var(--guide-color); }`);
collector.addRule(`.monaco-editor .vertical.${colorProvider.activeClassName} { box-shadow: 1px 0 0 0 var(--guide-color-active) inset; }`);
collector.addRule(`.monaco-editor .horizontal-top.${colorProvider.activeClassName} { border-top: 1px solid var(--guide-color-active); }`);
collector.addRule(`.monaco-editor .horizontal-bottom.${colorProvider.activeClassName} { border-bottom: 1px solid var(--guide-color-active); }`);
}
if (indentColorValues.length > 0) {
for (let level = 0; level < 30; level++) {
const colors = indentColorValues[level % indentColorValues.length];
collector.addRule(`.monaco-editor .lines-content .core-guide-indent.lvl-${level} { --indent-color: ${colors.indentColor}; --indent-color-active: ${colors.indentColorActive}; }`);
}
collector.addRule(`.monaco-editor .lines-content .core-guide-indent { box-shadow: 1px 0 0 0 var(--indent-color) inset; }`);
collector.addRule(`.monaco-editor .lines-content .core-guide-indent.indent-active { box-shadow: 1px 0 0 0 var(--indent-color-active) inset; }`);
}
});
export { IndentGuidesOverlay };
@@ -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.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .margin-view-overlays .line-numbers {
bottom: 0;
font-variant-numeric: tabular-nums;
position: absolute;
text-align: right;
display: inline-block;
vertical-align: middle;
box-sizing: border-box;
cursor: default;
}
.monaco-editor .relative-current-line-number {
text-align: left;
display: inline-block;
width: 100%;
}
.monaco-editor .margin-view-overlays .line-numbers.lh-odd {
margin-top: 1px;
}
.monaco-editor .line-numbers {
color: var(--vscode-editorLineNumber-foreground);
}
.monaco-editor .line-numbers.active-line-number {
color: var(--vscode-editorLineNumber-activeForeground);
}
@@ -0,0 +1,184 @@
import './lineNumbers.css';
import { isLinux } from '../../../../base/common/platform.js';
import { DynamicViewOverlay } from '../../view/dynamicViewOverlay.js';
import { Position } from '../../../common/core/position.js';
import { Range } from '../../../common/core/range.js';
import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';
import { editorLineNumbers, editorDimmedLineNumber } from '../../../common/core/editorColorRegistry.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Renders line numbers to the left of the main view lines content.
*/
class LineNumbersOverlay extends DynamicViewOverlay {
static { this.CLASS_NAME = 'line-numbers'; }
constructor(context) {
super();
this._context = context;
this._readConfig();
this._lastCursorModelPosition = new Position(1, 1);
this._renderResult = null;
this._activeModelLineNumber = 1;
this._context.addEventHandler(this);
}
_readConfig() {
const options = this._context.configuration.options;
this._lineHeight = options.get(75 /* EditorOption.lineHeight */);
const lineNumbers = options.get(76 /* EditorOption.lineNumbers */);
this._renderLineNumbers = lineNumbers.renderType;
this._renderCustomLineNumbers = lineNumbers.renderFn;
this._renderFinalNewline = options.get(109 /* EditorOption.renderFinalNewline */);
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this._lineNumbersLeft = layoutInfo.lineNumbersLeft;
this._lineNumbersWidth = layoutInfo.lineNumbersWidth;
}
dispose() {
this._context.removeEventHandler(this);
this._renderResult = null;
super.dispose();
}
// --- begin event handlers
onConfigurationChanged(e) {
this._readConfig();
return true;
}
onCursorStateChanged(e) {
const primaryViewPosition = e.selections[0].getPosition();
this._lastCursorModelPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(primaryViewPosition);
let shouldRender = false;
if (this._activeModelLineNumber !== this._lastCursorModelPosition.lineNumber) {
this._activeModelLineNumber = this._lastCursorModelPosition.lineNumber;
shouldRender = true;
}
if (this._renderLineNumbers === 2 /* RenderLineNumbersType.Relative */ || this._renderLineNumbers === 3 /* RenderLineNumbersType.Interval */) {
shouldRender = true;
}
return shouldRender;
}
onFlushed(e) {
return true;
}
onLinesChanged(e) {
return true;
}
onLinesDeleted(e) {
return true;
}
onLinesInserted(e) {
return true;
}
onScrollChanged(e) {
return e.scrollTopChanged;
}
onZonesChanged(e) {
return true;
}
onDecorationsChanged(e) {
return e.affectsLineNumber;
}
// --- end event handlers
_getLineRenderLineNumber(viewLineNumber) {
const modelPosition = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(viewLineNumber, 1));
if (modelPosition.column !== 1) {
return '';
}
const modelLineNumber = modelPosition.lineNumber;
if (this._renderCustomLineNumbers) {
return this._renderCustomLineNumbers(modelLineNumber);
}
if (this._renderLineNumbers === 2 /* RenderLineNumbersType.Relative */) {
const diff = Math.abs(this._lastCursorModelPosition.lineNumber - modelLineNumber);
if (diff === 0) {
return '<span class="relative-current-line-number">' + modelLineNumber + '</span>';
}
return String(diff);
}
if (this._renderLineNumbers === 3 /* RenderLineNumbersType.Interval */) {
if (this._lastCursorModelPosition.lineNumber === modelLineNumber) {
return String(modelLineNumber);
}
if (modelLineNumber % 10 === 0) {
return String(modelLineNumber);
}
const finalLineNumber = this._context.viewModel.getLineCount();
if (modelLineNumber === finalLineNumber) {
return String(modelLineNumber);
}
return '';
}
return String(modelLineNumber);
}
prepareRender(ctx) {
if (this._renderLineNumbers === 0 /* RenderLineNumbersType.Off */) {
this._renderResult = null;
return;
}
const lineHeightClassName = (isLinux ? (this._lineHeight % 2 === 0 ? ' lh-even' : ' lh-odd') : '');
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
const lineNoDecorations = this._context.viewModel.getDecorationsInViewport(ctx.visibleRange).filter(d => !!d.options.lineNumberClassName);
lineNoDecorations.sort((a, b) => Range.compareRangesUsingEnds(a.range, b.range));
let decorationStartIndex = 0;
const lineCount = this._context.viewModel.getLineCount();
const output = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
const lineIndex = lineNumber - visibleStartLineNumber;
const modelLineNumber = this._context.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber, 1)).lineNumber;
let renderLineNumber = this._getLineRenderLineNumber(lineNumber);
let extraClassNames = '';
// skip decorations whose end positions we've already passed
while (decorationStartIndex < lineNoDecorations.length && lineNoDecorations[decorationStartIndex].range.endLineNumber < lineNumber) {
decorationStartIndex++;
}
for (let i = decorationStartIndex; i < lineNoDecorations.length; i++) {
const { range, options } = lineNoDecorations[i];
if (range.startLineNumber <= lineNumber) {
extraClassNames += ' ' + options.lineNumberClassName;
}
}
if (!renderLineNumber && !extraClassNames) {
output[lineIndex] = '';
continue;
}
if (lineNumber === lineCount && this._context.viewModel.getLineLength(lineNumber) === 0) {
// this is the last line
if (this._renderFinalNewline === 'off') {
renderLineNumber = '';
}
if (this._renderFinalNewline === 'dimmed') {
extraClassNames += ' dimmed-line-number';
}
}
if (modelLineNumber === this._activeModelLineNumber) {
extraClassNames += ' active-line-number';
}
output[lineIndex] = (`<div class="${LineNumbersOverlay.CLASS_NAME}${lineHeightClassName}${extraClassNames}" style="left:${this._lineNumbersLeft}px;width:${this._lineNumbersWidth}px;">${renderLineNumber}</div>`);
}
this._renderResult = output;
}
render(startLineNumber, lineNumber) {
if (!this._renderResult) {
return '';
}
const lineIndex = lineNumber - startLineNumber;
if (lineIndex < 0 || lineIndex >= this._renderResult.length) {
return '';
}
return this._renderResult[lineIndex];
}
}
registerThemingParticipant((theme, collector) => {
const editorLineNumbersColor = theme.getColor(editorLineNumbers);
const editorDimmedLineNumberColor = theme.getColor(editorDimmedLineNumber);
if (editorDimmedLineNumberColor) {
collector.addRule(`.monaco-editor .line-numbers.dimmed-line-number { color: ${editorDimmedLineNumberColor}; }`);
}
else if (editorLineNumbersColor) {
collector.addRule(`.monaco-editor .line-numbers.dimmed-line-number { color: ${editorLineNumbersColor.transparent(0.4)}; }`);
}
});
export { LineNumbersOverlay };
@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .lines-decorations {
position: absolute;
top: 0;
background: white;
}
/*
Keeping name short for faster parsing.
cldr = core lines decorations rendering (div)
*/
.monaco-editor .margin-view-overlays .cldr {
position: absolute;
height: 100%;
}
@@ -0,0 +1,104 @@
import './linesDecorations.css';
import { DedupOverlay, DecorationToRender } from '../glyphMargin/glyphMargin.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class LinesDecorationsOverlay extends DedupOverlay {
constructor(context) {
super();
this._context = context;
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this._decorationsLeft = layoutInfo.decorationsLeft;
this._decorationsWidth = layoutInfo.decorationsWidth;
this._renderResult = null;
this._context.addEventHandler(this);
}
dispose() {
this._context.removeEventHandler(this);
this._renderResult = null;
super.dispose();
}
// --- begin event handlers
onConfigurationChanged(e) {
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this._decorationsLeft = layoutInfo.decorationsLeft;
this._decorationsWidth = layoutInfo.decorationsWidth;
return true;
}
onDecorationsChanged(e) {
return true;
}
onFlushed(e) {
return true;
}
onLinesChanged(e) {
return true;
}
onLinesDeleted(e) {
return true;
}
onLinesInserted(e) {
return true;
}
onScrollChanged(e) {
return e.scrollTopChanged;
}
onZonesChanged(e) {
return true;
}
// --- end event handlers
_getDecorations(ctx) {
const decorations = ctx.getDecorationsInViewport();
const r = [];
let rLen = 0;
for (let i = 0, len = decorations.length; i < len; i++) {
const d = decorations[i];
const linesDecorationsClassName = d.options.linesDecorationsClassName;
const zIndex = d.options.zIndex;
if (linesDecorationsClassName) {
r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, linesDecorationsClassName, d.options.linesDecorationsTooltip ?? null, zIndex);
}
const firstLineDecorationClassName = d.options.firstLineDecorationClassName;
if (firstLineDecorationClassName) {
r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.startLineNumber, firstLineDecorationClassName, d.options.linesDecorationsTooltip ?? null, zIndex);
}
}
return r;
}
prepareRender(ctx) {
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
const toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, this._getDecorations(ctx));
const left = this._decorationsLeft.toString();
const width = this._decorationsWidth.toString();
const common = '" style="left:' + left + 'px;width:' + width + 'px;"></div>';
const output = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
const lineIndex = lineNumber - visibleStartLineNumber;
const decorations = toRender[lineIndex].getDecorations();
let lineOutput = '';
for (const decoration of decorations) {
let addition = '<div class="cldr ' + decoration.className;
if (decoration.tooltip !== null) {
addition += '" title="' + decoration.tooltip; // The tooltip is already escaped.
}
addition += common;
lineOutput += addition;
}
output[lineIndex] = lineOutput;
}
this._renderResult = output;
}
render(startLineNumber, lineNumber) {
if (!this._renderResult) {
return '';
}
return this._renderResult[lineNumber - startLineNumber];
}
}
export { LinesDecorationsOverlay };
@@ -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-editor .margin {
background-color: var(--vscode-editorGutter-background);
}
@@ -0,0 +1,71 @@
import './margin.css';
import { createFastDomNode } from '../../../../base/browser/fastDomNode.js';
import { ViewPart } from '../../view/viewPart.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Margin is a vertical strip located on the left of the editor's content area.
* It is used for various features such as line numbers, folding markers, and
* decorations that provide additional information about the lines of code.
*/
class Margin extends ViewPart {
static { this.CLASS_NAME = 'glyph-margin'; }
static { this.OUTER_CLASS_NAME = 'margin'; }
constructor(context) {
super(context);
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this._canUseLayerHinting = !options.get(39 /* EditorOption.disableLayerHinting */);
this._contentLeft = layoutInfo.contentLeft;
this._glyphMarginLeft = layoutInfo.glyphMarginLeft;
this._glyphMarginWidth = layoutInfo.glyphMarginWidth;
this._domNode = createFastDomNode(document.createElement('div'));
this._domNode.setClassName(Margin.OUTER_CLASS_NAME);
this._domNode.setPosition('absolute');
this._domNode.setAttribute('role', 'presentation');
this._domNode.setAttribute('aria-hidden', 'true');
this._glyphMarginBackgroundDomNode = createFastDomNode(document.createElement('div'));
this._glyphMarginBackgroundDomNode.setClassName(Margin.CLASS_NAME);
this._domNode.appendChild(this._glyphMarginBackgroundDomNode);
}
dispose() {
super.dispose();
}
getDomNode() {
return this._domNode;
}
// --- begin event handlers
onConfigurationChanged(e) {
const options = this._context.configuration.options;
const layoutInfo = options.get(165 /* EditorOption.layoutInfo */);
this._canUseLayerHinting = !options.get(39 /* EditorOption.disableLayerHinting */);
this._contentLeft = layoutInfo.contentLeft;
this._glyphMarginLeft = layoutInfo.glyphMarginLeft;
this._glyphMarginWidth = layoutInfo.glyphMarginWidth;
return true;
}
onScrollChanged(e) {
return super.onScrollChanged(e) || e.scrollTopChanged;
}
// --- end event handlers
prepareRender(ctx) {
// Nothing to read
}
render(ctx) {
this._domNode.setLayerHinting(this._canUseLayerHinting);
this._domNode.setContain('strict');
const adjustedScrollTop = ctx.scrollTop - ctx.bigNumbersDelta;
this._domNode.setTop(-adjustedScrollTop);
const height = Math.min(ctx.scrollHeight, 1000000);
this._domNode.setHeight(height);
this._domNode.setWidth(this._contentLeft);
this._glyphMarginBackgroundDomNode.setLeft(this._glyphMarginLeft);
this._glyphMarginBackgroundDomNode.setWidth(this._glyphMarginWidth);
this._glyphMarginBackgroundDomNode.setHeight(height);
}
}
export { Margin };
@@ -0,0 +1,15 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*
Keeping name short for faster parsing.
cmdr = core margin decorations rendering (div)
*/
.monaco-editor .margin-view-overlays .cmdr {
position: absolute;
left: 0;
width: 100%;
height: 100%;
}
@@ -0,0 +1,84 @@
import './marginDecorations.css';
import { DedupOverlay, DecorationToRender } from '../glyphMargin/glyphMargin.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class MarginViewLineDecorationsOverlay extends DedupOverlay {
constructor(context) {
super();
this._context = context;
this._renderResult = null;
this._context.addEventHandler(this);
}
dispose() {
this._context.removeEventHandler(this);
this._renderResult = null;
super.dispose();
}
// --- begin event handlers
onConfigurationChanged(e) {
return true;
}
onDecorationsChanged(e) {
return true;
}
onFlushed(e) {
return true;
}
onLinesChanged(e) {
return true;
}
onLinesDeleted(e) {
return true;
}
onLinesInserted(e) {
return true;
}
onScrollChanged(e) {
return e.scrollTopChanged;
}
onZonesChanged(e) {
return true;
}
// --- end event handlers
_getDecorations(ctx) {
const decorations = ctx.getDecorationsInViewport();
const r = [];
let rLen = 0;
for (let i = 0, len = decorations.length; i < len; i++) {
const d = decorations[i];
const marginClassName = d.options.marginClassName;
const zIndex = d.options.zIndex;
if (marginClassName) {
r[rLen++] = new DecorationToRender(d.range.startLineNumber, d.range.endLineNumber, marginClassName, null, zIndex);
}
}
return r;
}
prepareRender(ctx) {
const visibleStartLineNumber = ctx.visibleRange.startLineNumber;
const visibleEndLineNumber = ctx.visibleRange.endLineNumber;
const toRender = this._render(visibleStartLineNumber, visibleEndLineNumber, this._getDecorations(ctx));
const output = [];
for (let lineNumber = visibleStartLineNumber; lineNumber <= visibleEndLineNumber; lineNumber++) {
const lineIndex = lineNumber - visibleStartLineNumber;
const decorations = toRender[lineIndex].getDecorations();
let lineOutput = '';
for (const decoration of decorations) {
lineOutput += '<div class="cmdr ' + decoration.className + '" style=""></div>';
}
output[lineIndex] = lineOutput;
}
this._renderResult = output;
}
render(startLineNumber, lineNumber) {
if (!this._renderResult) {
return '';
}
return this._renderResult[lineNumber - startLineNumber];
}
}
export { MarginViewLineDecorationsOverlay };
@@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* START cover the case that slider is visible on mouseover */
.monaco-editor .minimap.slider-mouseover .minimap-slider {
opacity: 0;
transition: opacity 100ms linear;
}
.monaco-editor .minimap.slider-mouseover:hover .minimap-slider {
opacity: 1;
}
.monaco-editor .minimap.slider-mouseover .minimap-slider.active {
opacity: 1;
}
/* END cover the case that slider is visible on mouseover */
.monaco-editor .minimap-slider .minimap-slider-horizontal {
background: var(--vscode-minimapSlider-background);
}
.monaco-editor .minimap-slider:hover .minimap-slider-horizontal {
background: var(--vscode-minimapSlider-hoverBackground);
}
.monaco-editor .minimap-slider.active .minimap-slider-horizontal {
background: var(--vscode-minimapSlider-activeBackground);
}
.monaco-editor .minimap-shadow-visible {
box-shadow: var(--vscode-scrollbar-shadow) -6px 0 6px -6px inset;
}
.monaco-editor .minimap-shadow-hidden {
position: absolute;
width: 0;
}
.monaco-editor .minimap-shadow-visible {
position: absolute;
left: -6px;
width: 6px;
pointer-events: none;
}
.monaco-editor.no-minimap-shadow .minimap-shadow-visible {
position: absolute;
left: -1px;
width: 1px;
}
/* 0.5s fade in/out for the minimap */
.minimap.minimap-autohide-mouseover,
.minimap.minimap-autohide-scroll {
opacity: 0;
transition: opacity 0.5s;
}
.minimap.minimap-autohide-scroll{
pointer-events: none;
}
.minimap.minimap-autohide-mouseover:hover,
.minimap.minimap-autohide-scroll.active {
opacity: 1;
pointer-events: auto;
}
.monaco-editor .minimap {
z-index: 5;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,90 @@
import { getCharIndex } from './minimapCharSheet.js';
import { toUint8 } from '../../../../base/common/uint.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
class MinimapCharRenderer {
constructor(charData, scale) {
this.scale = scale;
this._minimapCharRendererBrand = undefined;
this.charDataNormal = MinimapCharRenderer.soften(charData, 12 / 15);
this.charDataLight = MinimapCharRenderer.soften(charData, 50 / 60);
}
static soften(input, ratio) {
const result = new Uint8ClampedArray(input.length);
for (let i = 0, len = input.length; i < len; i++) {
result[i] = toUint8(input[i] * ratio);
}
return result;
}
renderChar(target, dx, dy, chCode, color, foregroundAlpha, backgroundColor, backgroundAlpha, fontScale, useLighterFont, force1pxHeight) {
const charWidth = 1 /* Constants.BASE_CHAR_WIDTH */ * this.scale;
const charHeight = 2 /* Constants.BASE_CHAR_HEIGHT */ * this.scale;
const renderHeight = (force1pxHeight ? 1 : charHeight);
if (dx + charWidth > target.width || dy + renderHeight > target.height) {
console.warn('bad render request outside image data');
return;
}
const charData = useLighterFont ? this.charDataLight : this.charDataNormal;
const charIndex = getCharIndex(chCode, fontScale);
const destWidth = target.width * 4 /* Constants.RGBA_CHANNELS_CNT */;
const backgroundR = backgroundColor.r;
const backgroundG = backgroundColor.g;
const backgroundB = backgroundColor.b;
const deltaR = color.r - backgroundR;
const deltaG = color.g - backgroundG;
const deltaB = color.b - backgroundB;
const destAlpha = Math.max(foregroundAlpha, backgroundAlpha);
const dest = target.data;
let sourceOffset = charIndex * charWidth * charHeight;
let row = dy * destWidth + dx * 4 /* Constants.RGBA_CHANNELS_CNT */;
for (let y = 0; y < renderHeight; y++) {
let column = row;
for (let x = 0; x < charWidth; x++) {
const c = (charData[sourceOffset++] / 255) * (foregroundAlpha / 255);
dest[column++] = backgroundR + deltaR * c;
dest[column++] = backgroundG + deltaG * c;
dest[column++] = backgroundB + deltaB * c;
dest[column++] = destAlpha;
}
row += destWidth;
}
}
blockRenderChar(target, dx, dy, color, foregroundAlpha, backgroundColor, backgroundAlpha, force1pxHeight) {
const charWidth = 1 /* Constants.BASE_CHAR_WIDTH */ * this.scale;
const charHeight = 2 /* Constants.BASE_CHAR_HEIGHT */ * this.scale;
const renderHeight = (force1pxHeight ? 1 : charHeight);
if (dx + charWidth > target.width || dy + renderHeight > target.height) {
console.warn('bad render request outside image data');
return;
}
const destWidth = target.width * 4 /* Constants.RGBA_CHANNELS_CNT */;
const c = 0.5 * (foregroundAlpha / 255);
const backgroundR = backgroundColor.r;
const backgroundG = backgroundColor.g;
const backgroundB = backgroundColor.b;
const deltaR = color.r - backgroundR;
const deltaG = color.g - backgroundG;
const deltaB = color.b - backgroundB;
const colorR = backgroundR + deltaR * c;
const colorG = backgroundG + deltaG * c;
const colorB = backgroundB + deltaB * c;
const destAlpha = Math.max(foregroundAlpha, backgroundAlpha);
const dest = target.data;
let row = dy * destWidth + dx * 4 /* Constants.RGBA_CHANNELS_CNT */;
for (let y = 0; y < renderHeight; y++) {
let column = row;
for (let x = 0; x < charWidth; x++) {
dest[column++] = colorR;
dest[column++] = colorG;
dest[column++] = colorB;
dest[column++] = destAlpha;
}
row += destWidth;
}
}
}
export { MinimapCharRenderer };
@@ -0,0 +1,136 @@
import { MinimapCharRenderer } from './minimapCharRenderer.js';
import { allCharCodes } from './minimapCharSheet.js';
import { prebakedMiniMaps } from './minimapPreBaked.js';
import { toUint8 } from '../../../../base/common/uint.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Creates character renderers. It takes a 'scale' that determines how large
* characters should be drawn. Using this, it draws data into a canvas and
* then downsamples the characters as necessary for the current display.
* This makes rendering more efficient, rather than drawing a full (tiny)
* font, or downsampling in real-time.
*/
class MinimapCharRendererFactory {
/**
* Creates a new character renderer factory with the given scale.
*/
static create(scale, fontFamily) {
// renderers are immutable. By default we'll 'create' a new minimap
// character renderer whenever we switch editors, no need to do extra work.
if (this.lastCreated && scale === this.lastCreated.scale && fontFamily === this.lastFontFamily) {
return this.lastCreated;
}
let factory;
if (prebakedMiniMaps[scale]) {
factory = new MinimapCharRenderer(prebakedMiniMaps[scale](), scale);
}
else {
factory = MinimapCharRendererFactory.createFromSampleData(MinimapCharRendererFactory.createSampleData(fontFamily).data, scale);
}
this.lastFontFamily = fontFamily;
this.lastCreated = factory;
return factory;
}
/**
* Creates the font sample data, writing to a canvas.
*/
static createSampleData(fontFamily) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.style.height = `${16 /* Constants.SAMPLED_CHAR_HEIGHT */}px`;
canvas.height = 16 /* Constants.SAMPLED_CHAR_HEIGHT */;
canvas.width = 96 /* Constants.CHAR_COUNT */ * 10 /* Constants.SAMPLED_CHAR_WIDTH */;
canvas.style.width = 96 /* Constants.CHAR_COUNT */ * 10 /* Constants.SAMPLED_CHAR_WIDTH */ + 'px';
ctx.fillStyle = '#ffffff';
ctx.font = `bold ${16 /* Constants.SAMPLED_CHAR_HEIGHT */}px ${fontFamily}`;
ctx.textBaseline = 'middle';
let x = 0;
for (const code of allCharCodes) {
ctx.fillText(String.fromCharCode(code), x, 16 /* Constants.SAMPLED_CHAR_HEIGHT */ / 2);
x += 10 /* Constants.SAMPLED_CHAR_WIDTH */;
}
return ctx.getImageData(0, 0, 96 /* Constants.CHAR_COUNT */ * 10 /* Constants.SAMPLED_CHAR_WIDTH */, 16 /* Constants.SAMPLED_CHAR_HEIGHT */);
}
/**
* Creates a character renderer from the canvas sample data.
*/
static createFromSampleData(source, scale) {
const expectedLength = 16 /* Constants.SAMPLED_CHAR_HEIGHT */ * 10 /* Constants.SAMPLED_CHAR_WIDTH */ * 4 /* Constants.RGBA_CHANNELS_CNT */ * 96 /* Constants.CHAR_COUNT */;
if (source.length !== expectedLength) {
throw new Error('Unexpected source in MinimapCharRenderer');
}
const charData = MinimapCharRendererFactory._downsample(source, scale);
return new MinimapCharRenderer(charData, scale);
}
static _downsampleChar(source, sourceOffset, dest, destOffset, scale) {
const width = 1 /* Constants.BASE_CHAR_WIDTH */ * scale;
const height = 2 /* Constants.BASE_CHAR_HEIGHT */ * scale;
let targetIndex = destOffset;
let brightest = 0;
// This is essentially an ad-hoc rescaling algorithm. Standard approaches
// like bicubic interpolation are awesome for scaling between image sizes,
// but don't work so well when scaling to very small pixel values, we end
// up with blurry, indistinct forms.
//
// The approach taken here is simply mapping each source pixel to the target
// pixels, and taking the weighted values for all pixels in each, and then
// averaging them out. Finally we apply an intensity boost in _downsample,
// since when scaling to the smallest pixel sizes there's more black space
// which causes characters to be much less distinct.
for (let y = 0; y < height; y++) {
// 1. For this destination pixel, get the source pixels we're sampling
// from (x1, y1) to the next pixel (x2, y2)
const sourceY1 = (y / height) * 16 /* Constants.SAMPLED_CHAR_HEIGHT */;
const sourceY2 = ((y + 1) / height) * 16 /* Constants.SAMPLED_CHAR_HEIGHT */;
for (let x = 0; x < width; x++) {
const sourceX1 = (x / width) * 10 /* Constants.SAMPLED_CHAR_WIDTH */;
const sourceX2 = ((x + 1) / width) * 10 /* Constants.SAMPLED_CHAR_WIDTH */;
// 2. Sample all of them, summing them up and weighting them. Similar
// to bilinear interpolation.
let value = 0;
let samples = 0;
for (let sy = sourceY1; sy < sourceY2; sy++) {
const sourceRow = sourceOffset + Math.floor(sy) * 3840 /* Constants.RGBA_SAMPLED_ROW_WIDTH */;
const yBalance = 1 - (sy - Math.floor(sy));
for (let sx = sourceX1; sx < sourceX2; sx++) {
const xBalance = 1 - (sx - Math.floor(sx));
const sourceIndex = sourceRow + Math.floor(sx) * 4 /* Constants.RGBA_CHANNELS_CNT */;
const weight = xBalance * yBalance;
samples += weight;
value += ((source[sourceIndex] * source[sourceIndex + 3]) / 255) * weight;
}
}
const final = value / samples;
brightest = Math.max(brightest, final);
dest[targetIndex++] = toUint8(final);
}
}
return brightest;
}
static _downsample(data, scale) {
const pixelsPerCharacter = 2 /* Constants.BASE_CHAR_HEIGHT */ * scale * 1 /* Constants.BASE_CHAR_WIDTH */ * scale;
const resultLen = pixelsPerCharacter * 96 /* Constants.CHAR_COUNT */;
const result = new Uint8ClampedArray(resultLen);
let resultOffset = 0;
let sourceOffset = 0;
let brightest = 0;
for (let charIndex = 0; charIndex < 96 /* Constants.CHAR_COUNT */; charIndex++) {
brightest = Math.max(brightest, this._downsampleChar(data, sourceOffset, result, resultOffset, scale));
resultOffset += pixelsPerCharacter;
sourceOffset += 10 /* Constants.SAMPLED_CHAR_WIDTH */ * 4 /* Constants.RGBA_CHANNELS_CNT */;
}
if (brightest > 0) {
const adjust = 255 / brightest;
for (let i = 0; i < resultLen; i++) {
result[i] *= adjust;
}
}
return result;
}
}
export { MinimapCharRendererFactory };
@@ -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.
*--------------------------------------------------------------------------------------------*/
const allCharCodes = (() => {
const v = [];
for (let i = 32 /* Constants.START_CH_CODE */; i <= 126 /* Constants.END_CH_CODE */; i++) {
v.push(i);
}
v.push(65533 /* Constants.UNKNOWN_CODE */);
return v;
})();
const getCharIndex = (chCode, fontScale) => {
chCode -= 32 /* Constants.START_CH_CODE */;
if (chCode < 0 || chCode > 96 /* Constants.CHAR_COUNT */) {
if (fontScale <= 2) {
// for smaller scales, we can get away with using any ASCII character...
return (chCode + 96 /* Constants.CHAR_COUNT */) % 96 /* Constants.CHAR_COUNT */;
}
return 96 /* Constants.CHAR_COUNT */ - 1; // unknown symbol
}
return chCode;
};
export { allCharCodes, getCharIndex };
@@ -0,0 +1,52 @@
import { createSingleCallFunction } from '../../../../base/common/functional.js';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const charTable = {
'0': 0,
'1': 1,
'2': 2,
'3': 3,
'4': 4,
'5': 5,
'6': 6,
'7': 7,
'8': 8,
'9': 9,
A: 10,
B: 11,
C: 12,
D: 13,
E: 14,
F: 15
};
const decodeData = (str) => {
const output = new Uint8ClampedArray(str.length / 2);
for (let i = 0; i < str.length; i += 2) {
output[i >> 1] = (charTable[str[i]] << 4) | (charTable[str[i + 1]] & 0xF);
}
return output;
};
/*
const encodeData = (data: Uint8ClampedArray, length: string) => {
const chars = '0123456789ABCDEF';
let output = '';
for (let i = 0; i < data.length; i++) {
output += chars[data[i] >> 4] + chars[data[i] & 0xf];
}
return output;
};
*/
/**
* Map of minimap scales to prebaked sample data at those scales. We don't
* sample much larger data, because then font family becomes visible, which
* is use-configurable.
*/
const prebakedMiniMaps = {
1: createSingleCallFunction(() => decodeData('0000511D6300CF609C709645A78432005642574171487021003C451900274D35D762755E8B629C5BA856AF57BA649530C167D1512A272A3F6038604460398526BCA2A968DB6F8957C768BE5FBE2FB467CF5D8D5B795DC7625B5DFF50DE64C466DB2FC47CD860A65E9A2EB96CB54CE06DA763AB2EA26860524D3763536601005116008177A8705E53AB738E6A982F88BAA35B5F5B626D9C636B449B737E5B7B678598869A662F6B5B8542706C704C80736A607578685B70594A49715A4522E792')),
2: createSingleCallFunction(() => decodeData('000000000000000055394F383D2800008B8B1F210002000081B1CBCBCC820000847AAF6B9AAF2119BE08B8881AD60000A44FD07DCCF107015338130C00000000385972265F390B406E2437634B4B48031B12B8A0847000001E15B29A402F0000000000004B33460B00007A752C2A0000000000004D3900000084394B82013400ABA5CFC7AD9C0302A45A3E5A98AB000089A43382D97900008BA54AA087A70A0248A6A7AE6DBE0000BF6F94987EA40A01A06DCFA7A7A9030496C32F77891D0000A99FB1A0AFA80603B29AB9CA75930D010C0948354D3900000C0948354F37460D0028BE673D8400000000AF9D7B6E00002B007AA8933400007AA642675C2700007984CFB9C3985B768772A8A6B7B20000CAAECAAFC4B700009F94A6009F840009D09F9BA4CA9C0000CC8FC76DC87F0000C991C472A2000000A894A48CA7B501079BA2C9C69BA20000B19A5D3FA89000005CA6009DA2960901B0A7F0669FB200009D009E00B7890000DAD0F5D092820000D294D4C48BD10000B5A7A4A3B1A50402CAB6CBA6A2000000B5A7A4A3B1A8044FCDADD19D9CB00000B7778F7B8AAE0803C9AB5D3F5D3F00009EA09EA0BAB006039EA0989A8C7900009B9EF4D6B7C00000A9A7816CACA80000ABAC84705D3F000096DA635CDC8C00006F486F266F263D4784006124097B00374F6D2D6D2D6D4A3A95872322000000030000000000008D8939130000000000002E22A5C9CBC70600AB25C0B5C9B400061A2DB04CA67001082AA6BEBEBFC606002321DACBC19E03087AA08B6768380000282FBAC0B8CA7A88AD25BBA5A29900004C396C5894A6000040485A6E356E9442A32CD17EADA70000B4237923628600003E2DE9C1D7B500002F25BBA5A2990000231DB6AFB4A804023025C0B5CAB588062B2CBDBEC0C706882435A75CA20000002326BD6A82A908048B4B9A5A668000002423A09CB4BB060025259C9D8A7900001C1FCAB2C7C700002A2A9387ABA200002626A4A47D6E9D14333163A0C87500004B6F9C2D643A257049364936493647358A34438355497F1A0000A24C1D590000D38DFFBDD4CD3126'))
};
export { prebakedMiniMaps };

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