monaco-editor-core
Version:
A browser based code editor
778 lines (777 loc) • 43.9 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
import './textAreaHandler.css';
import * as nls from '../../../nls.js';
import * as browser from '../../../base/browser/browser.js';
import { createFastDomNode } from '../../../base/browser/fastDomNode.js';
import * as platform from '../../../base/common/platform.js';
import * as strings from '../../../base/common/strings.js';
import { applyFontInfo } from '../config/domFontInfo.js';
import { CopyOptions, TextAreaInput, TextAreaWrapper } from './textAreaInput.js';
import { PagedScreenReaderStrategy, TextAreaState, _debugComposition } from './textAreaState.js';
import { PartFingerprints, ViewPart } 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 { getMapForWordSeparators } from '../../common/core/wordCharacterClassifier.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';
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 = (browser.isFirefox);
let TextAreaHandler = class TextAreaHandler extends ViewPart {
constructor(context, 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(146 /* EditorOption.layoutInfo */);
this._setAccessibilityOptions(options);
this._contentLeft = layoutInfo.contentLeft;
this._contentWidth = layoutInfo.contentWidth;
this._contentHeight = layoutInfo.height;
this._fontInfo = options.get(50 /* EditorOption.fontInfo */);
this._lineHeight = options.get(67 /* EditorOption.lineHeight */);
this._emptySelectionClipboard = options.get(37 /* EditorOption.emptySelectionClipboard */);
this._copyWithSyntaxHighlighting = options.get(25 /* 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', this._getAriaLabel(options));
this.textArea.setAttribute('aria-required', options.get(5 /* EditorOption.ariaRequired */) ? 'true' : 'false');
this.textArea.setAttribute('tabindex', String(options.get(125 /* EditorOption.tabIndex */)));
this.textArea.setAttribute('role', 'textbox');
this.textArea.setAttribute('aria-roledescription', nls.localize('editor', "editor"));
this.textArea.setAttribute('aria-multiline', 'true');
this.textArea.setAttribute('aria-autocomplete', options.get(92 /* EditorOption.readOnly */) ? 'none' : 'both');
this._ensureReadOnlyAttribute();
this.textAreaCover = createFastDomNode(document.createElement('div'));
this.textAreaCover.setPosition('absolute');
const simpleModel = {
getLineCount: () => {
return this._context.viewModel.getLineCount();
},
getLineMaxColumn: (lineNumber) => {
return this._context.viewModel.getLineMaxColumn(lineNumber);
},
getValueInRange: (range, eol) => {
return this._context.viewModel.getValueInRange(range, eol);
},
getValueLengthInRange: (range, eol) => {
return this._context.viewModel.getValueLengthInRange(range, eol);
},
modifyPosition: (position, offset) => {
return this._context.viewModel.modifyPosition(position, offset);
}
};
const textAreaInputHost = {
getDataToCopy: () => {
const rawTextToCopy = this._context.viewModel.getPlainTextToCopy(this._modelSelections, this._emptySelectionClipboard, platform.isWindows);
const newLineCharacter = this._context.viewModel.model.getEOL();
const isFromEmptySelection = (this._emptySelectionClipboard && this._modelSelections.length === 1 && this._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 || (this._copyWithSyntaxHighlighting && text.length < 65536)) {
const richText = this._context.viewModel.getRichTextToCopy(this._modelSelections, this._emptySelectionClipboard);
if (richText) {
html = richText.html;
mode = richText.mode;
}
}
return {
isFromEmptySelection,
multicursorText,
text,
html,
mode
};
},
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 (platform.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 (platform.isMacintosh && !selection.isEmpty() && simpleModel.getValueLengthInRange(selection, 0 /* EndOfLinePreference.TextDefined */) < LIMIT_CHARS) {
const text = simpleModel.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 (browser.isSafari && !selection.isEmpty()) {
const placeholderText = 'vscode-placeholder';
return new TextAreaState(placeholderText, 0, placeholderText.length, null, undefined);
}
return TextAreaState.EMPTY;
}
if (browser.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;
}
return PagedScreenReaderStrategy.fromEditorSelection(simpleModel, this._selections[0], this._accessibilityPageSize, this._accessibilitySupport === 0 /* AccessibilitySupport.Unknown */);
},
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, platform.OS, {
isAndroid: browser.isAndroid,
isChrome: browser.isChrome,
isFirefox: browser.isFirefox,
isSafari: browser.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) {
// must be handled through the new command
if (_debugComposition) {
console.log(` => compositionType: <<${e.text}>>, ${e.replacePrevCharCnt}, ${e.replaceNextCharCnt}, ${e.positionDelta}`);
}
this._viewController.compositionType(e.text, e.replacePrevCharCnt, e.replaceNextCharCnt, e.positionDelta);
}
else {
if (_debugComposition) {
console.log(` => type: <<${e.text}>>`);
}
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();
}));
}
writeScreenReaderContent(reason) {
this._textAreaInput.writeNativeTextAreaContent(reason);
}
dispose() {
super.dispose();
}
_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(132 /* 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 (!strings.isHighSurrogate(charBefore.charCodeAt(0))) {
return charBefore;
}
}
return '';
}
_getAriaLabel(options) {
const accessibilitySupport = options.get(2 /* EditorOption.accessibilitySupport */);
if (accessibilitySupport === 1 /* AccessibilitySupport.Disabled */) {
const toggleKeybindingLabel = this._keybindingService.lookupKeybinding('editor.action.toggleScreenReaderAccessibilityMode')?.getAriaLabel();
const runCommandKeybindingLabel = this._keybindingService.lookupKeybinding('workbench.action.showCommands')?.getAriaLabel();
const keybindingEditorKeybindingLabel = this._keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings')?.getAriaLabel();
const editorNotAccessibleMessage = nls.localize('accessibilityModeOff', "The editor is not accessible at this time.");
if (toggleKeybindingLabel) {
return nls.localize('accessibilityOffAriaLabel', "{0} To enable screen reader optimized mode, use {1}", editorNotAccessibleMessage, toggleKeybindingLabel);
}
else if (runCommandKeybindingLabel) {
return nls.localize('accessibilityOffAriaLabelNoKb', "{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 nls.localize('accessibilityOffAriaLabelNoKbs', "{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(4 /* EditorOption.ariaLabel */);
}
_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(146 /* EditorOption.layoutInfo */);
const wrappingColumn = layoutInfo.wrappingColumn;
if (wrappingColumn !== -1 && this._accessibilitySupport !== 1 /* AccessibilitySupport.Disabled */) {
const fontInfo = options.get(50 /* 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(146 /* EditorOption.layoutInfo */);
this._setAccessibilityOptions(options);
this._contentLeft = layoutInfo.contentLeft;
this._contentWidth = layoutInfo.contentWidth;
this._contentHeight = layoutInfo.height;
this._fontInfo = options.get(50 /* EditorOption.fontInfo */);
this._lineHeight = options.get(67 /* EditorOption.lineHeight */);
this._emptySelectionClipboard = options.get(37 /* EditorOption.emptySelectionClipboard */);
this._copyWithSyntaxHighlighting = options.get(25 /* 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', this._getAriaLabel(options));
this.textArea.setAttribute('aria-required', options.get(5 /* EditorOption.ariaRequired */) ? 'true' : 'false');
this.textArea.setAttribute('tabindex', String(options.get(125 /* EditorOption.tabIndex */)));
if (e.hasChanged(34 /* EditorOption.domReadOnly */) || e.hasChanged(92 /* 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();
}
focusTextArea() {
this._textAreaInput.focusTextArea();
}
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(34 /* EditorOption.domReadOnly */) && options.get(92 /* 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 = this._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 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 * this._lineHeight;
this.textArea.domNode.scrollLeft = scrollLeft;
this._doRender({
lastRenderPosition: null,
top: top,
left: left,
width: width,
height: this._lineHeight,
useCover: false,
color: (TokenizationRegistry.getColorMap() || [])[presentation.foreground],
italic: presentation.italic,
bold: presentation.bold,
underline: presentation.underline,
strikethrough: presentation.strikethrough
});
}
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 (platform.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
this._doRender({
lastRenderPosition: this._primaryCursorPosition,
top,
left: this._textAreaWrapping ? this._contentLeft : left,
width: this._textAreaWidth,
height: this._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 ?? this._newlinecount(this.textArea.domNode.value.substr(0, this.textArea.domNode.selectionStart));
this.textArea.domNode.scrollTop = lineCount * this._lineHeight;
return;
}
this._doRender({
lastRenderPosition: this._primaryCursorPosition,
top: top,
left: this._textAreaWrapping ? this._contentLeft : left,
width: this._textAreaWidth,
height: (canUseZeroSizeTextarea ? 0 : 1),
useCover: false
});
}
_newlinecount(text) {
let result = 0;
let startIndex = -1;
do {
startIndex = text.indexOf('\n', startIndex + 1);
if (startIndex === -1) {
break;
}
result++;
} while (true);
return result;
}
_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.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(57 /* EditorOption.glyphMargin */)) {
tac.setClassName('monaco-editor-background textAreaCover ' + Margin.OUTER_CLASS_NAME);
}
else {
if (options.get(68 /* EditorOption.lineNumbers */).renderType !== 0 /* RenderLineNumbersType.Off */) {
tac.setClassName('monaco-editor-background textAreaCover ' + LineNumbersOverlay.CLASS_NAME);
}
else {
tac.setClassName('monaco-editor-background textAreaCover');
}
}
}
};
TextAreaHandler = __decorate([
__param(3, IKeybindingService),
__param(4, IInstantiationService)
], TextAreaHandler);
export { TextAreaHandler };
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;
}