5582ce4ad89ac284bae723326f36572dc7c1a9919efc49b5988f091fbf46fe191e122b2de9a2a035264c5ea0426f8d8d648bc0462ceadbf635ab107e58b6a0 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import './textAreaHandler.css';
  6. import * as nls from '../../../nls.js';
  7. import * as browser from '../../../base/browser/browser.js';
  8. import { createFastDomNode } from '../../../base/browser/fastDomNode.js';
  9. import * as platform from '../../../base/common/platform.js';
  10. import * as strings from '../../../base/common/strings.js';
  11. import { applyFontInfo } from '../config/domFontInfo.js';
  12. import { CopyOptions, TextAreaInput, TextAreaWrapper } from './textAreaInput.js';
  13. import { PagedScreenReaderStrategy, TextAreaState, _debugComposition } from './textAreaState.js';
  14. import { PartFingerprints, ViewPart } from '../view/viewPart.js';
  15. import { LineNumbersOverlay } from '../viewParts/lineNumbers/lineNumbers.js';
  16. import { Margin } from '../viewParts/margin/margin.js';
  17. import { EditorOptions } from '../../common/config/editorOptions.js';
  18. import { getMapForWordSeparators } from '../../common/core/wordCharacterClassifier.js';
  19. import { Position } from '../../common/core/position.js';
  20. import { Range } from '../../common/core/range.js';
  21. import { Selection } from '../../common/core/selection.js';
  22. import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from '../../../base/browser/ui/mouseCursor/mouseCursor.js';
  23. import { TokenizationRegistry } from '../../common/languages.js';
  24. import { Color } from '../../../base/common/color.js';
  25. class VisibleTextAreaData {
  26. constructor(_context, modelLineNumber, distanceToModelLineStart, widthOfHiddenLineTextBefore, distanceToModelLineEnd) {
  27. this._context = _context;
  28. this.modelLineNumber = modelLineNumber;
  29. this.distanceToModelLineStart = distanceToModelLineStart;
  30. this.widthOfHiddenLineTextBefore = widthOfHiddenLineTextBefore;
  31. this.distanceToModelLineEnd = distanceToModelLineEnd;
  32. this._visibleTextAreaBrand = undefined;
  33. this.startPosition = null;
  34. this.endPosition = null;
  35. this.visibleTextareaStart = null;
  36. this.visibleTextareaEnd = null;
  37. /**
  38. * When doing composition, the currently composed text might be split up into
  39. * multiple tokens, then merged again into a single token, etc. Here we attempt
  40. * to keep the presentation of the <textarea> stable by using the previous used
  41. * style if multiple tokens come into play. This avoids flickering.
  42. */
  43. this._previousPresentation = null;
  44. }
  45. prepareRender(visibleRangeProvider) {
  46. const startModelPosition = new Position(this.modelLineNumber, this.distanceToModelLineStart + 1);
  47. const endModelPosition = new Position(this.modelLineNumber, this._context.viewModel.model.getLineMaxColumn(this.modelLineNumber) - this.distanceToModelLineEnd);
  48. this.startPosition = this._context.viewModel.coordinatesConverter.convertModelPositionToViewPosition(startModelPosition);
  49. this.endPosition = this._context.viewModel.coordinatesConverter.convertModelPositionToViewPosition(endModelPosition);
  50. if (this.startPosition.lineNumber === this.endPosition.lineNumber) {
  51. this.visibleTextareaStart = visibleRangeProvider.visibleRangeForPosition(this.startPosition);
  52. this.visibleTextareaEnd = visibleRangeProvider.visibleRangeForPosition(this.endPosition);
  53. }
  54. else {
  55. // TODO: what if the view positions are not on the same line?
  56. this.visibleTextareaStart = null;
  57. this.visibleTextareaEnd = null;
  58. }
  59. }
  60. definePresentation(tokenPresentation) {
  61. if (!this._previousPresentation) {
  62. // To avoid flickering, once set, always reuse a presentation throughout the entire IME session
  63. if (tokenPresentation) {
  64. this._previousPresentation = tokenPresentation;
  65. }
  66. else {
  67. this._previousPresentation = {
  68. foreground: 1 /* ColorId.DefaultForeground */,
  69. italic: false,
  70. bold: false,
  71. underline: false,
  72. strikethrough: false,
  73. };
  74. }
  75. }
  76. return this._previousPresentation;
  77. }
  78. }
  79. const canUseZeroSizeTextarea = (browser.isFirefox);
  80. export class TextAreaHandler extends ViewPart {
  81. constructor(context, viewController, visibleRangeProvider) {
  82. super(context);
  83. // --- end view API
  84. this._primaryCursorPosition = new Position(1, 1);
  85. this._primaryCursorVisibleRange = null;
  86. this._viewController = viewController;
  87. this._visibleRangeProvider = visibleRangeProvider;
  88. this._scrollLeft = 0;
  89. this._scrollTop = 0;
  90. const options = this._context.configuration.options;
  91. const layoutInfo = options.get(133 /* EditorOption.layoutInfo */);
  92. this._setAccessibilityOptions(options);
  93. this._contentLeft = layoutInfo.contentLeft;
  94. this._contentWidth = layoutInfo.contentWidth;
  95. this._contentHeight = layoutInfo.height;
  96. this._fontInfo = options.get(46 /* EditorOption.fontInfo */);
  97. this._lineHeight = options.get(61 /* EditorOption.lineHeight */);
  98. this._emptySelectionClipboard = options.get(33 /* EditorOption.emptySelectionClipboard */);
  99. this._copyWithSyntaxHighlighting = options.get(21 /* EditorOption.copyWithSyntaxHighlighting */);
  100. this._visibleTextArea = null;
  101. this._selections = [new Selection(1, 1, 1, 1)];
  102. this._modelSelections = [new Selection(1, 1, 1, 1)];
  103. this._lastRenderPosition = null;
  104. // Text Area (The focus will always be in the textarea when the cursor is blinking)
  105. this.textArea = createFastDomNode(document.createElement('textarea'));
  106. PartFingerprints.write(this.textArea, 6 /* PartFingerprint.TextArea */);
  107. this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`);
  108. this.textArea.setAttribute('wrap', 'off');
  109. this.textArea.setAttribute('autocorrect', 'off');
  110. this.textArea.setAttribute('autocapitalize', 'off');
  111. this.textArea.setAttribute('autocomplete', 'off');
  112. this.textArea.setAttribute('spellcheck', 'false');
  113. this.textArea.setAttribute('aria-label', this._getAriaLabel(options));
  114. this.textArea.setAttribute('tabindex', String(options.get(114 /* EditorOption.tabIndex */)));
  115. this.textArea.setAttribute('role', 'textbox');
  116. this.textArea.setAttribute('aria-roledescription', nls.localize('editor', "editor"));
  117. this.textArea.setAttribute('aria-multiline', 'true');
  118. this.textArea.setAttribute('aria-haspopup', 'false');
  119. this.textArea.setAttribute('aria-autocomplete', 'both');
  120. if (options.get(30 /* EditorOption.domReadOnly */) && options.get(83 /* EditorOption.readOnly */)) {
  121. this.textArea.setAttribute('readonly', 'true');
  122. }
  123. this.textAreaCover = createFastDomNode(document.createElement('div'));
  124. this.textAreaCover.setPosition('absolute');
  125. const simpleModel = {
  126. getLineCount: () => {
  127. return this._context.viewModel.getLineCount();
  128. },
  129. getLineMaxColumn: (lineNumber) => {
  130. return this._context.viewModel.getLineMaxColumn(lineNumber);
  131. },
  132. getValueInRange: (range, eol) => {
  133. return this._context.viewModel.getValueInRange(range, eol);
  134. }
  135. };
  136. const textAreaInputHost = {
  137. getDataToCopy: () => {
  138. const rawTextToCopy = this._context.viewModel.getPlainTextToCopy(this._modelSelections, this._emptySelectionClipboard, platform.isWindows);
  139. const newLineCharacter = this._context.viewModel.model.getEOL();
  140. const isFromEmptySelection = (this._emptySelectionClipboard && this._modelSelections.length === 1 && this._modelSelections[0].isEmpty());
  141. const multicursorText = (Array.isArray(rawTextToCopy) ? rawTextToCopy : null);
  142. const text = (Array.isArray(rawTextToCopy) ? rawTextToCopy.join(newLineCharacter) : rawTextToCopy);
  143. let html = undefined;
  144. let mode = null;
  145. if (CopyOptions.forceCopyWithSyntaxHighlighting || (this._copyWithSyntaxHighlighting && text.length < 65536)) {
  146. const richText = this._context.viewModel.getRichTextToCopy(this._modelSelections, this._emptySelectionClipboard);
  147. if (richText) {
  148. html = richText.html;
  149. mode = richText.mode;
  150. }
  151. }
  152. return {
  153. isFromEmptySelection,
  154. multicursorText,
  155. text,
  156. html,
  157. mode
  158. };
  159. },
  160. getScreenReaderContent: (currentState) => {
  161. if (this._accessibilitySupport === 1 /* AccessibilitySupport.Disabled */) {
  162. // We know for a fact that a screen reader is not attached
  163. // On OSX, we write the character before the cursor to allow for "long-press" composition
  164. // Also on OSX, we write the word before the cursor to allow for the Accessibility Keyboard to give good hints
  165. const selection = this._selections[0];
  166. if (platform.isMacintosh && selection.isEmpty()) {
  167. const position = selection.getStartPosition();
  168. let textBefore = this._getWordBeforePosition(position);
  169. if (textBefore.length === 0) {
  170. textBefore = this._getCharacterBeforePosition(position);
  171. }
  172. if (textBefore.length > 0) {
  173. return new TextAreaState(textBefore, textBefore.length, textBefore.length, position, position);
  174. }
  175. }
  176. // on Safari, document.execCommand('cut') and document.execCommand('copy') will just not work
  177. // if the textarea has no content selected. So if there is an editor selection, ensure something
  178. // is selected in the textarea.
  179. if (browser.isSafari && !selection.isEmpty()) {
  180. const placeholderText = 'vscode-placeholder';
  181. return new TextAreaState(placeholderText, 0, placeholderText.length, null, null);
  182. }
  183. return TextAreaState.EMPTY;
  184. }
  185. if (browser.isAndroid) {
  186. // when tapping in the editor on a word, Android enters composition mode.
  187. // in the `compositionstart` event we cannot clear the textarea, because
  188. // it then forgets to ever send a `compositionend`.
  189. // we therefore only write the current word in the textarea
  190. const selection = this._selections[0];
  191. if (selection.isEmpty()) {
  192. const position = selection.getStartPosition();
  193. const [wordAtPosition, positionOffsetInWord] = this._getAndroidWordAtPosition(position);
  194. if (wordAtPosition.length > 0) {
  195. return new TextAreaState(wordAtPosition, positionOffsetInWord, positionOffsetInWord, position, position);
  196. }
  197. }
  198. return TextAreaState.EMPTY;
  199. }
  200. return PagedScreenReaderStrategy.fromEditorSelection(currentState, simpleModel, this._selections[0], this._accessibilityPageSize, this._accessibilitySupport === 0 /* AccessibilitySupport.Unknown */);
  201. },
  202. deduceModelPosition: (viewAnchorPosition, deltaOffset, lineFeedCnt) => {
  203. return this._context.viewModel.deduceModelPositionRelativeToViewPosition(viewAnchorPosition, deltaOffset, lineFeedCnt);
  204. }
  205. };
  206. const textAreaWrapper = this._register(new TextAreaWrapper(this.textArea.domNode));
  207. this._textAreaInput = this._register(new TextAreaInput(textAreaInputHost, textAreaWrapper, platform.OS, browser));
  208. this._register(this._textAreaInput.onKeyDown((e) => {
  209. this._viewController.emitKeyDown(e);
  210. }));
  211. this._register(this._textAreaInput.onKeyUp((e) => {
  212. this._viewController.emitKeyUp(e);
  213. }));
  214. this._register(this._textAreaInput.onPaste((e) => {
  215. let pasteOnNewLine = false;
  216. let multicursorText = null;
  217. let mode = null;
  218. if (e.metadata) {
  219. pasteOnNewLine = (this._emptySelectionClipboard && !!e.metadata.isFromEmptySelection);
  220. multicursorText = (typeof e.metadata.multicursorText !== 'undefined' ? e.metadata.multicursorText : null);
  221. mode = e.metadata.mode;
  222. }
  223. this._viewController.paste(e.text, pasteOnNewLine, multicursorText, mode);
  224. }));
  225. this._register(this._textAreaInput.onCut(() => {
  226. this._viewController.cut();
  227. }));
  228. this._register(this._textAreaInput.onType((e) => {
  229. if (e.replacePrevCharCnt || e.replaceNextCharCnt || e.positionDelta) {
  230. // must be handled through the new command
  231. if (_debugComposition) {
  232. console.log(` => compositionType: <<${e.text}>>, ${e.replacePrevCharCnt}, ${e.replaceNextCharCnt}, ${e.positionDelta}`);
  233. }
  234. this._viewController.compositionType(e.text, e.replacePrevCharCnt, e.replaceNextCharCnt, e.positionDelta);
  235. }
  236. else {
  237. if (_debugComposition) {
  238. console.log(` => type: <<${e.text}>>`);
  239. }
  240. this._viewController.type(e.text);
  241. }
  242. }));
  243. this._register(this._textAreaInput.onSelectionChangeRequest((modelSelection) => {
  244. this._viewController.setSelection(modelSelection);
  245. }));
  246. this._register(this._textAreaInput.onCompositionStart((e) => {
  247. // The textarea might contain some content when composition starts.
  248. //
  249. // When we make the textarea visible, it always has a height of 1 line,
  250. // so we don't need to worry too much about content on lines above or below
  251. // the selection.
  252. //
  253. // However, the text on the current line needs to be made visible because
  254. // some IME methods allow to move to other glyphs on the current line
  255. // (by pressing arrow keys).
  256. //
  257. // (1) The textarea might contain only some parts of the current line,
  258. // like the word before the selection. Also, the content inside the textarea
  259. // can grow or shrink as composition occurs. We therefore anchor the textarea
  260. // in terms of distance to a certain line start and line end.
  261. //
  262. // (2) Also, we should not make \t characters visible, because their rendering
  263. // inside the <textarea> will not align nicely with our rendering. We therefore
  264. // will hide (if necessary) some of the leading text on the current line.
  265. const ta = this.textArea.domNode;
  266. const modelSelection = this._modelSelections[0];
  267. const { distanceToModelLineStart, widthOfHiddenTextBefore } = (() => {
  268. // Find the text that is on the current line before the selection
  269. const textBeforeSelection = ta.value.substring(0, Math.min(ta.selectionStart, ta.selectionEnd));
  270. const lineFeedOffset1 = textBeforeSelection.lastIndexOf('\n');
  271. const lineTextBeforeSelection = textBeforeSelection.substring(lineFeedOffset1 + 1);
  272. // We now search to see if we should hide some part of it (if it contains \t)
  273. const tabOffset1 = lineTextBeforeSelection.lastIndexOf('\t');
  274. const desiredVisibleBeforeCharCount = lineTextBeforeSelection.length - tabOffset1 - 1;
  275. const startModelPosition = modelSelection.getStartPosition();
  276. const visibleBeforeCharCount = Math.min(startModelPosition.column - 1, desiredVisibleBeforeCharCount);
  277. const distanceToModelLineStart = startModelPosition.column - 1 - visibleBeforeCharCount;
  278. const hiddenLineTextBefore = lineTextBeforeSelection.substring(0, lineTextBeforeSelection.length - visibleBeforeCharCount);
  279. const widthOfHiddenTextBefore = measureText(hiddenLineTextBefore, this._fontInfo);
  280. return { distanceToModelLineStart, widthOfHiddenTextBefore };
  281. })();
  282. const { distanceToModelLineEnd } = (() => {
  283. // Find the text that is on the current line after the selection
  284. const textAfterSelection = ta.value.substring(Math.max(ta.selectionStart, ta.selectionEnd));
  285. const lineFeedOffset2 = textAfterSelection.indexOf('\n');
  286. const lineTextAfterSelection = lineFeedOffset2 === -1 ? textAfterSelection : textAfterSelection.substring(0, lineFeedOffset2);
  287. const tabOffset2 = lineTextAfterSelection.indexOf('\t');
  288. const desiredVisibleAfterCharCount = (tabOffset2 === -1 ? lineTextAfterSelection.length : lineTextAfterSelection.length - tabOffset2 - 1);
  289. const endModelPosition = modelSelection.getEndPosition();
  290. const visibleAfterCharCount = Math.min(this._context.viewModel.model.getLineMaxColumn(endModelPosition.lineNumber) - endModelPosition.column, desiredVisibleAfterCharCount);
  291. const distanceToModelLineEnd = this._context.viewModel.model.getLineMaxColumn(endModelPosition.lineNumber) - endModelPosition.column - visibleAfterCharCount;
  292. return { distanceToModelLineEnd };
  293. })();
  294. // Scroll to reveal the location in the editor where composition occurs
  295. this._context.viewModel.revealRange('keyboard', true, Range.fromPositions(this._selections[0].getStartPosition()), 0 /* viewEvents.VerticalRevealType.Simple */, 1 /* ScrollType.Immediate */);
  296. this._visibleTextArea = new VisibleTextAreaData(this._context, modelSelection.startLineNumber, distanceToModelLineStart, widthOfHiddenTextBefore, distanceToModelLineEnd);
  297. this._visibleTextArea.prepareRender(this._visibleRangeProvider);
  298. this._render();
  299. // Show the textarea
  300. this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ime-input`);
  301. this._viewController.compositionStart();
  302. this._context.viewModel.onCompositionStart();
  303. }));
  304. this._register(this._textAreaInput.onCompositionUpdate((e) => {
  305. if (!this._visibleTextArea) {
  306. return;
  307. }
  308. this._visibleTextArea.prepareRender(this._visibleRangeProvider);
  309. this._render();
  310. }));
  311. this._register(this._textAreaInput.onCompositionEnd(() => {
  312. this._visibleTextArea = null;
  313. this._render();
  314. this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`);
  315. this._viewController.compositionEnd();
  316. this._context.viewModel.onCompositionEnd();
  317. }));
  318. this._register(this._textAreaInput.onFocus(() => {
  319. this._context.viewModel.setHasFocus(true);
  320. }));
  321. this._register(this._textAreaInput.onBlur(() => {
  322. this._context.viewModel.setHasFocus(false);
  323. }));
  324. }
  325. dispose() {
  326. super.dispose();
  327. }
  328. _getAndroidWordAtPosition(position) {
  329. const ANDROID_WORD_SEPARATORS = '`~!@#$%^&*()-=+[{]}\\|;:",.<>/?';
  330. const lineContent = this._context.viewModel.getLineContent(position.lineNumber);
  331. const wordSeparators = getMapForWordSeparators(ANDROID_WORD_SEPARATORS);
  332. let goingLeft = true;
  333. let startColumn = position.column;
  334. let goingRight = true;
  335. let endColumn = position.column;
  336. let distance = 0;
  337. while (distance < 50 && (goingLeft || goingRight)) {
  338. if (goingLeft && startColumn <= 1) {
  339. goingLeft = false;
  340. }
  341. if (goingLeft) {
  342. const charCode = lineContent.charCodeAt(startColumn - 2);
  343. const charClass = wordSeparators.get(charCode);
  344. if (charClass !== 0 /* WordCharacterClass.Regular */) {
  345. goingLeft = false;
  346. }
  347. else {
  348. startColumn--;
  349. }
  350. }
  351. if (goingRight && endColumn > lineContent.length) {
  352. goingRight = false;
  353. }
  354. if (goingRight) {
  355. const charCode = lineContent.charCodeAt(endColumn - 1);
  356. const charClass = wordSeparators.get(charCode);
  357. if (charClass !== 0 /* WordCharacterClass.Regular */) {
  358. goingRight = false;
  359. }
  360. else {
  361. endColumn++;
  362. }
  363. }
  364. distance++;
  365. }
  366. return [lineContent.substring(startColumn - 1, endColumn - 1), position.column - startColumn];
  367. }
  368. _getWordBeforePosition(position) {
  369. const lineContent = this._context.viewModel.getLineContent(position.lineNumber);
  370. const wordSeparators = getMapForWordSeparators(this._context.configuration.options.get(119 /* EditorOption.wordSeparators */));
  371. let column = position.column;
  372. let distance = 0;
  373. while (column > 1) {
  374. const charCode = lineContent.charCodeAt(column - 2);
  375. const charClass = wordSeparators.get(charCode);
  376. if (charClass !== 0 /* WordCharacterClass.Regular */ || distance > 50) {
  377. return lineContent.substring(column - 1, position.column - 1);
  378. }
  379. distance++;
  380. column--;
  381. }
  382. return lineContent.substring(0, position.column - 1);
  383. }
  384. _getCharacterBeforePosition(position) {
  385. if (position.column > 1) {
  386. const lineContent = this._context.viewModel.getLineContent(position.lineNumber);
  387. const charBefore = lineContent.charAt(position.column - 2);
  388. if (!strings.isHighSurrogate(charBefore.charCodeAt(0))) {
  389. return charBefore;
  390. }
  391. }
  392. return '';
  393. }
  394. _getAriaLabel(options) {
  395. const accessibilitySupport = options.get(2 /* EditorOption.accessibilitySupport */);
  396. if (accessibilitySupport === 1 /* AccessibilitySupport.Disabled */) {
  397. return nls.localize('accessibilityOffAriaLabel', "The editor is not accessible at this time. Press {0} for options.", platform.isLinux ? 'Shift+Alt+F1' : 'Alt+F1');
  398. }
  399. return options.get(4 /* EditorOption.ariaLabel */);
  400. }
  401. _setAccessibilityOptions(options) {
  402. this._accessibilitySupport = options.get(2 /* EditorOption.accessibilitySupport */);
  403. const accessibilityPageSize = options.get(3 /* EditorOption.accessibilityPageSize */);
  404. if (this._accessibilitySupport === 2 /* AccessibilitySupport.Enabled */ && accessibilityPageSize === EditorOptions.accessibilityPageSize.defaultValue) {
  405. // 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
  406. this._accessibilityPageSize = 500;
  407. }
  408. else {
  409. this._accessibilityPageSize = accessibilityPageSize;
  410. }
  411. }
  412. // --- begin event handlers
  413. onConfigurationChanged(e) {
  414. const options = this._context.configuration.options;
  415. const layoutInfo = options.get(133 /* EditorOption.layoutInfo */);
  416. this._setAccessibilityOptions(options);
  417. this._contentLeft = layoutInfo.contentLeft;
  418. this._contentWidth = layoutInfo.contentWidth;
  419. this._contentHeight = layoutInfo.height;
  420. this._fontInfo = options.get(46 /* EditorOption.fontInfo */);
  421. this._lineHeight = options.get(61 /* EditorOption.lineHeight */);
  422. this._emptySelectionClipboard = options.get(33 /* EditorOption.emptySelectionClipboard */);
  423. this._copyWithSyntaxHighlighting = options.get(21 /* EditorOption.copyWithSyntaxHighlighting */);
  424. this.textArea.setAttribute('aria-label', this._getAriaLabel(options));
  425. this.textArea.setAttribute('tabindex', String(options.get(114 /* EditorOption.tabIndex */)));
  426. if (e.hasChanged(30 /* EditorOption.domReadOnly */) || e.hasChanged(83 /* EditorOption.readOnly */)) {
  427. if (options.get(30 /* EditorOption.domReadOnly */) && options.get(83 /* EditorOption.readOnly */)) {
  428. this.textArea.setAttribute('readonly', 'true');
  429. }
  430. else {
  431. this.textArea.removeAttribute('readonly');
  432. }
  433. }
  434. if (e.hasChanged(2 /* EditorOption.accessibilitySupport */)) {
  435. this._textAreaInput.writeScreenReaderContent('strategy changed');
  436. }
  437. return true;
  438. }
  439. onCursorStateChanged(e) {
  440. this._selections = e.selections.slice(0);
  441. this._modelSelections = e.modelSelections.slice(0);
  442. this._textAreaInput.writeScreenReaderContent('selection changed');
  443. return true;
  444. }
  445. onDecorationsChanged(e) {
  446. // true for inline decorations that can end up relayouting text
  447. return true;
  448. }
  449. onFlushed(e) {
  450. return true;
  451. }
  452. onLinesChanged(e) {
  453. return true;
  454. }
  455. onLinesDeleted(e) {
  456. return true;
  457. }
  458. onLinesInserted(e) {
  459. return true;
  460. }
  461. onScrollChanged(e) {
  462. this._scrollLeft = e.scrollLeft;
  463. this._scrollTop = e.scrollTop;
  464. return true;
  465. }
  466. onZonesChanged(e) {
  467. return true;
  468. }
  469. // --- end event handlers
  470. // --- begin view API
  471. isFocused() {
  472. return this._textAreaInput.isFocused();
  473. }
  474. focusTextArea() {
  475. this._textAreaInput.focusTextArea();
  476. }
  477. getLastRenderData() {
  478. return this._lastRenderPosition;
  479. }
  480. setAriaOptions(options) {
  481. if (options.activeDescendant) {
  482. this.textArea.setAttribute('aria-haspopup', 'true');
  483. this.textArea.setAttribute('aria-autocomplete', 'list');
  484. this.textArea.setAttribute('aria-activedescendant', options.activeDescendant);
  485. }
  486. else {
  487. this.textArea.setAttribute('aria-haspopup', 'false');
  488. this.textArea.setAttribute('aria-autocomplete', 'both');
  489. this.textArea.removeAttribute('aria-activedescendant');
  490. }
  491. if (options.role) {
  492. this.textArea.setAttribute('role', options.role);
  493. }
  494. }
  495. prepareRender(ctx) {
  496. var _a;
  497. this._primaryCursorPosition = new Position(this._selections[0].positionLineNumber, this._selections[0].positionColumn);
  498. this._primaryCursorVisibleRange = ctx.visibleRangeForPosition(this._primaryCursorPosition);
  499. (_a = this._visibleTextArea) === null || _a === void 0 ? void 0 : _a.prepareRender(ctx);
  500. }
  501. render(ctx) {
  502. this._textAreaInput.writeScreenReaderContent('render');
  503. this._render();
  504. }
  505. _render() {
  506. if (this._visibleTextArea) {
  507. // The text area is visible for composition reasons
  508. const visibleStart = this._visibleTextArea.visibleTextareaStart;
  509. const visibleEnd = this._visibleTextArea.visibleTextareaEnd;
  510. const startPosition = this._visibleTextArea.startPosition;
  511. const endPosition = this._visibleTextArea.endPosition;
  512. if (startPosition && endPosition && visibleStart && visibleEnd && visibleEnd.left >= this._scrollLeft && visibleStart.left <= this._scrollLeft + this._contentWidth) {
  513. const top = (this._context.viewLayout.getVerticalOffsetForLineNumber(this._primaryCursorPosition.lineNumber) - this._scrollTop);
  514. const lineCount = this._newlinecount(this.textArea.domNode.value.substr(0, this.textArea.domNode.selectionStart));
  515. let scrollLeft = this._visibleTextArea.widthOfHiddenLineTextBefore;
  516. let left = (this._contentLeft + visibleStart.left - this._scrollLeft);
  517. // See https://github.com/microsoft/vscode/issues/141725#issuecomment-1050670841
  518. // Here we are adding +1 to avoid flickering that might be caused by having a width that is too small.
  519. // This could be caused by rounding errors that might only show up with certain font families.
  520. // In other words, a pixel might be lost when doing something like
  521. // `Math.round(end) - Math.round(start)`
  522. // vs
  523. // `Math.round(end - start)`
  524. let width = visibleEnd.left - visibleStart.left + 1;
  525. if (left < this._contentLeft) {
  526. // the textarea would be rendered on top of the margin,
  527. // so reduce its width. We use the same technique as
  528. // for hiding text before
  529. const delta = (this._contentLeft - left);
  530. left += delta;
  531. scrollLeft += delta;
  532. width -= delta;
  533. }
  534. if (width > this._contentWidth) {
  535. // the textarea would be wider than the content width,
  536. // so reduce its width.
  537. width = this._contentWidth;
  538. }
  539. // Try to render the textarea with the color/font style to match the text under it
  540. const viewLineData = this._context.viewModel.getViewLineData(startPosition.lineNumber);
  541. const startTokenIndex = viewLineData.tokens.findTokenIndexAtOffset(startPosition.column - 1);
  542. const endTokenIndex = viewLineData.tokens.findTokenIndexAtOffset(endPosition.column - 1);
  543. const textareaSpansSingleToken = (startTokenIndex === endTokenIndex);
  544. const presentation = this._visibleTextArea.definePresentation((textareaSpansSingleToken ? viewLineData.tokens.getPresentation(startTokenIndex) : null));
  545. this.textArea.domNode.scrollTop = lineCount * this._lineHeight;
  546. this.textArea.domNode.scrollLeft = scrollLeft;
  547. this._doRender({
  548. lastRenderPosition: null,
  549. top: top,
  550. left: left,
  551. width: width,
  552. height: this._lineHeight,
  553. useCover: false,
  554. color: (TokenizationRegistry.getColorMap() || [])[presentation.foreground],
  555. italic: presentation.italic,
  556. bold: presentation.bold,
  557. underline: presentation.underline,
  558. strikethrough: presentation.strikethrough
  559. });
  560. }
  561. return;
  562. }
  563. if (!this._primaryCursorVisibleRange) {
  564. // The primary cursor is outside the viewport => place textarea to the top left
  565. this._renderAtTopLeft();
  566. return;
  567. }
  568. const left = this._contentLeft + this._primaryCursorVisibleRange.left - this._scrollLeft;
  569. if (left < this._contentLeft || left > this._contentLeft + this._contentWidth) {
  570. // cursor is outside the viewport
  571. this._renderAtTopLeft();
  572. return;
  573. }
  574. const top = this._context.viewLayout.getVerticalOffsetForLineNumber(this._selections[0].positionLineNumber) - this._scrollTop;
  575. if (top < 0 || top > this._contentHeight) {
  576. // cursor is outside the viewport
  577. this._renderAtTopLeft();
  578. return;
  579. }
  580. // The primary cursor is in the viewport (at least vertically) => place textarea on the cursor
  581. if (platform.isMacintosh) {
  582. // For the popup emoji input, we will make the text area as high as the line height
  583. // We will also make the fontSize and lineHeight the correct dimensions to help with the placement of these pickers
  584. this._doRender({
  585. lastRenderPosition: this._primaryCursorPosition,
  586. top: top,
  587. left: left,
  588. width: (canUseZeroSizeTextarea ? 0 : 1),
  589. height: this._lineHeight,
  590. useCover: false
  591. });
  592. // In case the textarea contains a word, we're going to try to align the textarea's cursor
  593. // with our cursor by scrolling the textarea as much as possible
  594. this.textArea.domNode.scrollLeft = this._primaryCursorVisibleRange.left;
  595. const lineCount = this._newlinecount(this.textArea.domNode.value.substr(0, this.textArea.domNode.selectionStart));
  596. this.textArea.domNode.scrollTop = lineCount * this._lineHeight;
  597. return;
  598. }
  599. this._doRender({
  600. lastRenderPosition: this._primaryCursorPosition,
  601. top: top,
  602. left: left,
  603. width: (canUseZeroSizeTextarea ? 0 : 1),
  604. height: (canUseZeroSizeTextarea ? 0 : 1),
  605. useCover: false
  606. });
  607. }
  608. _newlinecount(text) {
  609. let result = 0;
  610. let startIndex = -1;
  611. do {
  612. startIndex = text.indexOf('\n', startIndex + 1);
  613. if (startIndex === -1) {
  614. break;
  615. }
  616. result++;
  617. } while (true);
  618. return result;
  619. }
  620. _renderAtTopLeft() {
  621. // (in WebKit the textarea is 1px by 1px because it cannot handle input to a 0x0 textarea)
  622. // specifically, when doing Korean IME, setting the textarea to 0x0 breaks IME badly.
  623. this._doRender({
  624. lastRenderPosition: null,
  625. top: 0,
  626. left: 0,
  627. width: (canUseZeroSizeTextarea ? 0 : 1),
  628. height: (canUseZeroSizeTextarea ? 0 : 1),
  629. useCover: true
  630. });
  631. }
  632. _doRender(renderData) {
  633. this._lastRenderPosition = renderData.lastRenderPosition;
  634. const ta = this.textArea;
  635. const tac = this.textAreaCover;
  636. applyFontInfo(ta, this._fontInfo);
  637. ta.setTop(renderData.top);
  638. ta.setLeft(renderData.left);
  639. ta.setWidth(renderData.width);
  640. ta.setHeight(renderData.height);
  641. ta.setColor(renderData.color ? Color.Format.CSS.formatHex(renderData.color) : '');
  642. ta.setFontStyle(renderData.italic ? 'italic' : '');
  643. if (renderData.bold) {
  644. // fontWeight is also set by `applyFontInfo`, so only overwrite it if necessary
  645. ta.setFontWeight('bold');
  646. }
  647. ta.setTextDecoration(`${renderData.underline ? ' underline' : ''}${renderData.strikethrough ? ' line-through' : ''}`);
  648. tac.setTop(renderData.useCover ? renderData.top : 0);
  649. tac.setLeft(renderData.useCover ? renderData.left : 0);
  650. tac.setWidth(renderData.useCover ? renderData.width : 0);
  651. tac.setHeight(renderData.useCover ? renderData.height : 0);
  652. const options = this._context.configuration.options;
  653. if (options.get(52 /* EditorOption.glyphMargin */)) {
  654. tac.setClassName('monaco-editor-background textAreaCover ' + Margin.OUTER_CLASS_NAME);
  655. }
  656. else {
  657. if (options.get(62 /* EditorOption.lineNumbers */).renderType !== 0 /* RenderLineNumbersType.Off */) {
  658. tac.setClassName('monaco-editor-background textAreaCover ' + LineNumbersOverlay.CLASS_NAME);
  659. }
  660. else {
  661. tac.setClassName('monaco-editor-background textAreaCover');
  662. }
  663. }
  664. }
  665. }
  666. function measureText(text, fontInfo) {
  667. if (text.length === 0) {
  668. return 0;
  669. }
  670. const container = document.createElement('div');
  671. container.style.position = 'absolute';
  672. container.style.top = '-50000px';
  673. container.style.width = '50000px';
  674. const regularDomNode = document.createElement('span');
  675. applyFontInfo(regularDomNode, fontInfo);
  676. regularDomNode.style.whiteSpace = 'pre'; // just like the textarea
  677. regularDomNode.append(text);
  678. container.appendChild(regularDomNode);
  679. document.body.appendChild(container);
  680. const res = regularDomNode.offsetWidth;
  681. document.body.removeChild(container);
  682. return res;
  683. }