Version 1.0
This commit is contained in:
Generated
Vendored
+104
@@ -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 };
|
||||
+28
@@ -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 };
|
||||
Generated
Vendored
+292
@@ -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 };
|
||||
Generated
Vendored
+107
@@ -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 };
|
||||
Generated
Vendored
+206
@@ -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 };
|
||||
Generated
Vendored
+220
@@ -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 };
|
||||
+29
@@ -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 };
|
||||
Generated
Vendored
+174
@@ -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 };
|
||||
Generated
Vendored
+85
@@ -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 };
|
||||
Generated
Vendored
+10
@@ -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 };
|
||||
Generated
Vendored
+16
@@ -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 };
|
||||
Generated
Vendored
+50
@@ -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);
|
||||
}
|
||||
Generated
Vendored
+518
@@ -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 };
|
||||
Generated
Vendored
+23
@@ -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 };
|
||||
Generated
Vendored
+85
@@ -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 };
|
||||
Generated
Vendored
+337
@@ -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 };
|
||||
Generated
Vendored
+194
@@ -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 };
|
||||
Generated
Vendored
+175
@@ -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 };
|
||||
Generated
Vendored
+111
@@ -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 };
|
||||
Generated
Vendored
+38
@@ -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);
|
||||
}
|
||||
Generated
Vendored
+720
@@ -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 };
|
||||
Generated
Vendored
+588
@@ -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 };
|
||||
Generated
Vendored
+160
@@ -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 };
|
||||
Generated
Vendored
+539
@@ -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 };
|
||||
Generated
Vendored
+978
@@ -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 };
|
||||
Generated
Vendored
+132
@@ -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 };
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {}
|
||||
+1731
File diff suppressed because it is too large
Load Diff
+80
@@ -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 };
|
||||
+53
@@ -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
@@ -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 };
|
||||
+489
@@ -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 };
|
||||
Generated
Vendored
+179
@@ -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 };
|
||||
Generated
Vendored
+110
@@ -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 };
|
||||
Generated
Vendored
+129
@@ -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 };
|
||||
Generated
Vendored
+346
@@ -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 };
|
||||
Generated
Vendored
+45
@@ -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 };
|
||||
Generated
Vendored
+52
@@ -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 };
|
||||
Generated
Vendored
+73
@@ -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 };
|
||||
Generated
Vendored
+40
@@ -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 };
|
||||
Generated
Vendored
+9
@@ -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;
|
||||
}
|
||||
+48
@@ -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 };
|
||||
+58
@@ -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 };
|
||||
Generated
Vendored
+102
@@ -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 };
|
||||
Generated
Vendored
+276
@@ -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 };
|
||||
Generated
Vendored
+214
@@ -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 };
|
||||
Generated
Vendored
+68
@@ -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 };
|
||||
Generated
Vendored
+19
@@ -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 };
|
||||
Generated
Vendored
+414
@@ -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 };
|
||||
Generated
Vendored
+93
@@ -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 };
|
||||
Generated
Vendored
+316
@@ -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 };
|
||||
+116
@@ -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 };
|
||||
+241
@@ -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 };
|
||||
Generated
Vendored
+373
@@ -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 };
|
||||
Generated
Vendored
+129
@@ -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 };
|
||||
Generated
Vendored
+76
@@ -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 };
|
||||
Generated
Vendored
+9
@@ -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 };
|
||||
Generated
Vendored
+366
@@ -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 };
|
||||
Generated
Vendored
+170
@@ -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 };
|
||||
Generated
Vendored
+30
@@ -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 };
|
||||
Generated
Vendored
+222
@@ -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 };
|
||||
+52
@@ -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 };
|
||||
Generated
Vendored
+18
@@ -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
@@ -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 };
|
||||
Generated
Vendored
+301
@@ -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); //
|
||||
}
|
||||
else {
|
||||
sb.appendASCIICharCode(32 /* CharCode.Space */);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 32 /* CharCode.Space */:
|
||||
if (nextCharCode === 32 /* CharCode.Space */) {
|
||||
sb.appendCharCode(0xA0); //
|
||||
}
|
||||
else {
|
||||
sb.appendASCIICharCode(32 /* CharCode.Space */);
|
||||
}
|
||||
break;
|
||||
case 60 /* CharCode.LessThan */:
|
||||
sb.appendString('<');
|
||||
break;
|
||||
case 62 /* CharCode.GreaterThan */:
|
||||
sb.appendString('>');
|
||||
break;
|
||||
case 38 /* CharCode.Ampersand */:
|
||||
sb.appendString('&');
|
||||
break;
|
||||
case 0 /* CharCode.Null */:
|
||||
sb.appendString('�');
|
||||
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 };
|
||||
Generated
Vendored
+10
@@ -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 };
|
||||
Generated
Vendored
+118
@@ -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 };
|
||||
+274
@@ -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 };
|
||||
+478
@@ -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 };
|
||||
+204
@@ -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 };
|
||||
+49
@@ -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 };
|
||||
Generated
Vendored
+91
@@ -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 };
|
||||
Generated
Vendored
+15
@@ -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;
|
||||
}
|
||||
Generated
Vendored
+97
@@ -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 };
|
||||
frontend/node_modules/monaco-editor/esm/vs/editor/browser/viewParts/contentWidgets/contentWidgets.js
Generated
Vendored
+495
@@ -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 };
|
||||
Generated
Vendored
+28
@@ -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;
|
||||
}
|
||||
Generated
Vendored
+205
@@ -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 };
|
||||
Generated
Vendored
+13
@@ -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%;
|
||||
}
|
||||
Generated
Vendored
+196
@@ -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 };
|
||||
Generated
Vendored
+151
@@ -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 };
|
||||
Generated
Vendored
+32
@@ -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%);
|
||||
}
|
||||
Generated
Vendored
+403
@@ -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 };
|
||||
Generated
Vendored
+20
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+80
@@ -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 };
|
||||
Generated
Vendored
+10
@@ -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%;
|
||||
}
|
||||
Generated
Vendored
+254
@@ -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 };
|
||||
Generated
Vendored
+33
@@ -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);
|
||||
}
|
||||
Generated
Vendored
+184
@@ -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 };
|
||||
Generated
Vendored
+18
@@ -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%;
|
||||
}
|
||||
Generated
Vendored
+104
@@ -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 };
|
||||
Generated
Vendored
+8
@@ -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);
|
||||
}
|
||||
Generated
Vendored
+71
@@ -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 };
|
||||
Generated
Vendored
+15
@@ -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%;
|
||||
}
|
||||
Generated
Vendored
+84
@@ -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 };
|
||||
Generated
Vendored
+63
@@ -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;
|
||||
}
|
||||
Generated
Vendored
+1662
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+90
@@ -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 };
|
||||
Generated
Vendored
+136
@@ -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 };
|
||||
Generated
Vendored
+25
@@ -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 };
|
||||
Generated
Vendored
+52
@@ -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
Reference in New Issue
Block a user