| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683 |
- /*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
- 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';
- 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);
- export class TextAreaHandler extends ViewPart {
- constructor(context, viewController, visibleRangeProvider) {
- super(context);
- // --- end view API
- 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(133 /* EditorOption.layoutInfo */);
- this._setAccessibilityOptions(options);
- this._contentLeft = layoutInfo.contentLeft;
- this._contentWidth = layoutInfo.contentWidth;
- this._contentHeight = layoutInfo.height;
- this._fontInfo = options.get(46 /* EditorOption.fontInfo */);
- this._lineHeight = options.get(61 /* EditorOption.lineHeight */);
- this._emptySelectionClipboard = options.get(33 /* EditorOption.emptySelectionClipboard */);
- this._copyWithSyntaxHighlighting = options.get(21 /* 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, 6 /* PartFingerprint.TextArea */);
- this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`);
- this.textArea.setAttribute('wrap', 'off');
- 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('tabindex', String(options.get(114 /* 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-haspopup', 'false');
- this.textArea.setAttribute('aria-autocomplete', 'both');
- if (options.get(30 /* EditorOption.domReadOnly */) && options.get(83 /* EditorOption.readOnly */)) {
- this.textArea.setAttribute('readonly', 'true');
- }
- 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);
- }
- };
- 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: (currentState) => {
- 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, position, position);
- }
- }
- // 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, null);
- }
- 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, position, position);
- }
- }
- return TextAreaState.EMPTY;
- }
- return PagedScreenReaderStrategy.fromEditorSelection(currentState, 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(new TextAreaInput(textAreaInputHost, textAreaWrapper, platform.OS, browser));
- 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 widthOfHiddenTextBefore = measureText(hiddenLineTextBefore, this._fontInfo);
- 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);
- 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;
- 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);
- }));
- }
- 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(119 /* 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 */) {
- return nls.localize('accessibilityOffAriaLabel', "The editor is not accessible at this time. Press {0} for options.", platform.isLinux ? 'Shift+Alt+F1' : 'Alt+F1');
- }
- 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;
- }
- }
- // --- begin event handlers
- onConfigurationChanged(e) {
- const options = this._context.configuration.options;
- const layoutInfo = options.get(133 /* EditorOption.layoutInfo */);
- this._setAccessibilityOptions(options);
- this._contentLeft = layoutInfo.contentLeft;
- this._contentWidth = layoutInfo.contentWidth;
- this._contentHeight = layoutInfo.height;
- this._fontInfo = options.get(46 /* EditorOption.fontInfo */);
- this._lineHeight = options.get(61 /* EditorOption.lineHeight */);
- this._emptySelectionClipboard = options.get(33 /* EditorOption.emptySelectionClipboard */);
- this._copyWithSyntaxHighlighting = options.get(21 /* EditorOption.copyWithSyntaxHighlighting */);
- this.textArea.setAttribute('aria-label', this._getAriaLabel(options));
- this.textArea.setAttribute('tabindex', String(options.get(114 /* EditorOption.tabIndex */)));
- if (e.hasChanged(30 /* EditorOption.domReadOnly */) || e.hasChanged(83 /* EditorOption.readOnly */)) {
- if (options.get(30 /* EditorOption.domReadOnly */) && options.get(83 /* EditorOption.readOnly */)) {
- this.textArea.setAttribute('readonly', 'true');
- }
- else {
- this.textArea.removeAttribute('readonly');
- }
- }
- if (e.hasChanged(2 /* EditorOption.accessibilitySupport */)) {
- this._textAreaInput.writeScreenReaderContent('strategy changed');
- }
- return true;
- }
- onCursorStateChanged(e) {
- this._selections = e.selections.slice(0);
- this._modelSelections = e.modelSelections.slice(0);
- this._textAreaInput.writeScreenReaderContent('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);
- }
- }
- prepareRender(ctx) {
- var _a;
- this._primaryCursorPosition = new Position(this._selections[0].positionLineNumber, this._selections[0].positionColumn);
- this._primaryCursorVisibleRange = ctx.visibleRangeForPosition(this._primaryCursorPosition);
- (_a = this._visibleTextArea) === null || _a === void 0 ? void 0 : _a.prepareRender(ctx);
- }
- render(ctx) {
- this._textAreaInput.writeScreenReaderContent('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) {
- // 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: top,
- left: left,
- width: (canUseZeroSizeTextarea ? 0 : 1),
- 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._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: left,
- width: (canUseZeroSizeTextarea ? 0 : 1),
- 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: (canUseZeroSizeTextarea ? 0 : 1),
- 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(52 /* EditorOption.glyphMargin */)) {
- tac.setClassName('monaco-editor-background textAreaCover ' + Margin.OUTER_CLASS_NAME);
- }
- else {
- if (options.get(62 /* EditorOption.lineNumbers */).renderType !== 0 /* RenderLineNumbersType.Off */) {
- tac.setClassName('monaco-editor-background textAreaCover ' + LineNumbersOverlay.CLASS_NAME);
- }
- else {
- tac.setClassName('monaco-editor-background textAreaCover');
- }
- }
- }
- }
- function measureText(text, fontInfo) {
- if (text.length === 0) {
- return 0;
- }
- const container = document.createElement('div');
- container.style.position = 'absolute';
- container.style.top = '-50000px';
- container.style.width = '50000px';
- const regularDomNode = document.createElement('span');
- applyFontInfo(regularDomNode, fontInfo);
- regularDomNode.style.whiteSpace = 'pre'; // just like the textarea
- regularDomNode.append(text);
- container.appendChild(regularDomNode);
- document.body.appendChild(container);
- const res = regularDomNode.offsetWidth;
- document.body.removeChild(container);
- return res;
- }
|