textAreaState.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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 * as strings from '../../../base/common/strings.js';
  6. import { Position } from '../../common/core/position.js';
  7. import { Range } from '../../common/core/range.js';
  8. export const _debugComposition = false;
  9. export class TextAreaState {
  10. constructor(value, selectionStart, selectionEnd, selectionStartPosition, selectionEndPosition) {
  11. this.value = value;
  12. this.selectionStart = selectionStart;
  13. this.selectionEnd = selectionEnd;
  14. this.selectionStartPosition = selectionStartPosition;
  15. this.selectionEndPosition = selectionEndPosition;
  16. }
  17. toString() {
  18. return '[ <' + this.value + '>, selectionStart: ' + this.selectionStart + ', selectionEnd: ' + this.selectionEnd + ']';
  19. }
  20. static readFromTextArea(textArea) {
  21. return new TextAreaState(textArea.getValue(), textArea.getSelectionStart(), textArea.getSelectionEnd(), null, null);
  22. }
  23. collapseSelection() {
  24. return new TextAreaState(this.value, this.value.length, this.value.length, null, null);
  25. }
  26. writeToTextArea(reason, textArea, select) {
  27. if (_debugComposition) {
  28. console.log('writeToTextArea ' + reason + ': ' + this.toString());
  29. }
  30. textArea.setValue(reason, this.value);
  31. if (select) {
  32. textArea.setSelectionRange(reason, this.selectionStart, this.selectionEnd);
  33. }
  34. }
  35. deduceEditorPosition(offset) {
  36. if (offset <= this.selectionStart) {
  37. const str = this.value.substring(offset, this.selectionStart);
  38. return this._finishDeduceEditorPosition(this.selectionStartPosition, str, -1);
  39. }
  40. if (offset >= this.selectionEnd) {
  41. const str = this.value.substring(this.selectionEnd, offset);
  42. return this._finishDeduceEditorPosition(this.selectionEndPosition, str, 1);
  43. }
  44. const str1 = this.value.substring(this.selectionStart, offset);
  45. if (str1.indexOf(String.fromCharCode(8230)) === -1) {
  46. return this._finishDeduceEditorPosition(this.selectionStartPosition, str1, 1);
  47. }
  48. const str2 = this.value.substring(offset, this.selectionEnd);
  49. return this._finishDeduceEditorPosition(this.selectionEndPosition, str2, -1);
  50. }
  51. _finishDeduceEditorPosition(anchor, deltaText, signum) {
  52. let lineFeedCnt = 0;
  53. let lastLineFeedIndex = -1;
  54. while ((lastLineFeedIndex = deltaText.indexOf('\n', lastLineFeedIndex + 1)) !== -1) {
  55. lineFeedCnt++;
  56. }
  57. return [anchor, signum * deltaText.length, lineFeedCnt];
  58. }
  59. static selectedText(text) {
  60. return new TextAreaState(text, 0, text.length, null, null);
  61. }
  62. static deduceInput(previousState, currentState, couldBeEmojiInput) {
  63. if (!previousState) {
  64. // This is the EMPTY state
  65. return {
  66. text: '',
  67. replacePrevCharCnt: 0,
  68. replaceNextCharCnt: 0,
  69. positionDelta: 0
  70. };
  71. }
  72. if (_debugComposition) {
  73. console.log('------------------------deduceInput');
  74. console.log('PREVIOUS STATE: ' + previousState.toString());
  75. console.log('CURRENT STATE: ' + currentState.toString());
  76. }
  77. let previousValue = previousState.value;
  78. let previousSelectionStart = previousState.selectionStart;
  79. let previousSelectionEnd = previousState.selectionEnd;
  80. let currentValue = currentState.value;
  81. let currentSelectionStart = currentState.selectionStart;
  82. let currentSelectionEnd = currentState.selectionEnd;
  83. // Strip the previous suffix from the value (without interfering with the current selection)
  84. const previousSuffix = previousValue.substring(previousSelectionEnd);
  85. const currentSuffix = currentValue.substring(currentSelectionEnd);
  86. const suffixLength = strings.commonSuffixLength(previousSuffix, currentSuffix);
  87. currentValue = currentValue.substring(0, currentValue.length - suffixLength);
  88. previousValue = previousValue.substring(0, previousValue.length - suffixLength);
  89. const previousPrefix = previousValue.substring(0, previousSelectionStart);
  90. const currentPrefix = currentValue.substring(0, currentSelectionStart);
  91. const prefixLength = strings.commonPrefixLength(previousPrefix, currentPrefix);
  92. currentValue = currentValue.substring(prefixLength);
  93. previousValue = previousValue.substring(prefixLength);
  94. currentSelectionStart -= prefixLength;
  95. previousSelectionStart -= prefixLength;
  96. currentSelectionEnd -= prefixLength;
  97. previousSelectionEnd -= prefixLength;
  98. if (_debugComposition) {
  99. console.log('AFTER DIFFING PREVIOUS STATE: <' + previousValue + '>, selectionStart: ' + previousSelectionStart + ', selectionEnd: ' + previousSelectionEnd);
  100. console.log('AFTER DIFFING CURRENT STATE: <' + currentValue + '>, selectionStart: ' + currentSelectionStart + ', selectionEnd: ' + currentSelectionEnd);
  101. }
  102. if (couldBeEmojiInput && currentSelectionStart === currentSelectionEnd && previousValue.length > 0) {
  103. // on OSX, emojis from the emoji picker are inserted at random locations
  104. // the only hints we can use is that the selection is immediately after the inserted emoji
  105. // and that none of the old text has been deleted
  106. let potentialEmojiInput = null;
  107. if (currentSelectionStart === currentValue.length) {
  108. // emoji potentially inserted "somewhere" after the previous selection => it should appear at the end of `currentValue`
  109. if (currentValue.startsWith(previousValue)) {
  110. // only if all of the old text is accounted for
  111. potentialEmojiInput = currentValue.substring(previousValue.length);
  112. }
  113. }
  114. else {
  115. // emoji potentially inserted "somewhere" before the previous selection => it should appear at the start of `currentValue`
  116. if (currentValue.endsWith(previousValue)) {
  117. // only if all of the old text is accounted for
  118. potentialEmojiInput = currentValue.substring(0, currentValue.length - previousValue.length);
  119. }
  120. }
  121. if (potentialEmojiInput !== null && potentialEmojiInput.length > 0) {
  122. // now we check that this is indeed an emoji
  123. // emojis can grow quite long, so a length check is of no help
  124. // e.g. 1F3F4 E0067 E0062 E0065 E006E E0067 E007F -- flag of England
  125. // Oftentimes, emojis use Variation Selector-16 (U+FE0F), so that is a good hint
  126. // http://emojipedia.org/variation-selector-16/
  127. // > An invisible codepoint which specifies that the preceding character
  128. // > should be displayed with emoji presentation. Only required if the
  129. // > preceding character defaults to text presentation.
  130. if (/\uFE0F/.test(potentialEmojiInput) || strings.containsEmoji(potentialEmojiInput)) {
  131. return {
  132. text: potentialEmojiInput,
  133. replacePrevCharCnt: 0,
  134. replaceNextCharCnt: 0,
  135. positionDelta: 0
  136. };
  137. }
  138. }
  139. }
  140. if (currentSelectionStart === currentSelectionEnd) {
  141. // composition accept case (noticed in FF + Japanese)
  142. // [blahblah] => blahblah|
  143. if (previousValue === currentValue
  144. && previousSelectionStart === 0
  145. && previousSelectionEnd === previousValue.length
  146. && currentSelectionStart === currentValue.length
  147. && currentValue.indexOf('\n') === -1) {
  148. if (strings.containsFullWidthCharacter(currentValue)) {
  149. return {
  150. text: '',
  151. replacePrevCharCnt: 0,
  152. replaceNextCharCnt: 0,
  153. positionDelta: 0
  154. };
  155. }
  156. }
  157. // no current selection
  158. const replacePreviousCharacters = (previousPrefix.length - prefixLength);
  159. if (_debugComposition) {
  160. console.log('REMOVE PREVIOUS: ' + (previousPrefix.length - prefixLength) + ' chars');
  161. }
  162. return {
  163. text: currentValue,
  164. replacePrevCharCnt: replacePreviousCharacters,
  165. replaceNextCharCnt: 0,
  166. positionDelta: 0
  167. };
  168. }
  169. // there is a current selection => composition case
  170. const replacePreviousCharacters = previousSelectionEnd - previousSelectionStart;
  171. return {
  172. text: currentValue,
  173. replacePrevCharCnt: replacePreviousCharacters,
  174. replaceNextCharCnt: 0,
  175. positionDelta: 0
  176. };
  177. }
  178. static deduceAndroidCompositionInput(previousState, currentState) {
  179. if (!previousState) {
  180. // This is the EMPTY state
  181. return {
  182. text: '',
  183. replacePrevCharCnt: 0,
  184. replaceNextCharCnt: 0,
  185. positionDelta: 0
  186. };
  187. }
  188. if (_debugComposition) {
  189. console.log('------------------------deduceAndroidCompositionInput');
  190. console.log('PREVIOUS STATE: ' + previousState.toString());
  191. console.log('CURRENT STATE: ' + currentState.toString());
  192. }
  193. if (previousState.value === currentState.value) {
  194. return {
  195. text: '',
  196. replacePrevCharCnt: 0,
  197. replaceNextCharCnt: 0,
  198. positionDelta: currentState.selectionEnd - previousState.selectionEnd
  199. };
  200. }
  201. const prefixLength = Math.min(strings.commonPrefixLength(previousState.value, currentState.value), previousState.selectionEnd);
  202. const suffixLength = Math.min(strings.commonSuffixLength(previousState.value, currentState.value), previousState.value.length - previousState.selectionEnd);
  203. const previousValue = previousState.value.substring(prefixLength, previousState.value.length - suffixLength);
  204. const currentValue = currentState.value.substring(prefixLength, currentState.value.length - suffixLength);
  205. const previousSelectionStart = previousState.selectionStart - prefixLength;
  206. const previousSelectionEnd = previousState.selectionEnd - prefixLength;
  207. const currentSelectionStart = currentState.selectionStart - prefixLength;
  208. const currentSelectionEnd = currentState.selectionEnd - prefixLength;
  209. if (_debugComposition) {
  210. console.log('AFTER DIFFING PREVIOUS STATE: <' + previousValue + '>, selectionStart: ' + previousSelectionStart + ', selectionEnd: ' + previousSelectionEnd);
  211. console.log('AFTER DIFFING CURRENT STATE: <' + currentValue + '>, selectionStart: ' + currentSelectionStart + ', selectionEnd: ' + currentSelectionEnd);
  212. }
  213. return {
  214. text: currentValue,
  215. replacePrevCharCnt: previousSelectionEnd,
  216. replaceNextCharCnt: previousValue.length - previousSelectionEnd,
  217. positionDelta: currentSelectionEnd - currentValue.length
  218. };
  219. }
  220. }
  221. TextAreaState.EMPTY = new TextAreaState('', 0, 0, null, null);
  222. export class PagedScreenReaderStrategy {
  223. static _getPageOfLine(lineNumber, linesPerPage) {
  224. return Math.floor((lineNumber - 1) / linesPerPage);
  225. }
  226. static _getRangeForPage(page, linesPerPage) {
  227. const offset = page * linesPerPage;
  228. const startLineNumber = offset + 1;
  229. const endLineNumber = offset + linesPerPage;
  230. return new Range(startLineNumber, 1, endLineNumber + 1, 1);
  231. }
  232. static fromEditorSelection(previousState, model, selection, linesPerPage, trimLongText) {
  233. const selectionStartPage = PagedScreenReaderStrategy._getPageOfLine(selection.startLineNumber, linesPerPage);
  234. const selectionStartPageRange = PagedScreenReaderStrategy._getRangeForPage(selectionStartPage, linesPerPage);
  235. const selectionEndPage = PagedScreenReaderStrategy._getPageOfLine(selection.endLineNumber, linesPerPage);
  236. const selectionEndPageRange = PagedScreenReaderStrategy._getRangeForPage(selectionEndPage, linesPerPage);
  237. const pretextRange = selectionStartPageRange.intersectRanges(new Range(1, 1, selection.startLineNumber, selection.startColumn));
  238. let pretext = model.getValueInRange(pretextRange, 1 /* LF */);
  239. const lastLine = model.getLineCount();
  240. const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
  241. const posttextRange = selectionEndPageRange.intersectRanges(new Range(selection.endLineNumber, selection.endColumn, lastLine, lastLineMaxColumn));
  242. let posttext = model.getValueInRange(posttextRange, 1 /* LF */);
  243. let text;
  244. if (selectionStartPage === selectionEndPage || selectionStartPage + 1 === selectionEndPage) {
  245. // take full selection
  246. text = model.getValueInRange(selection, 1 /* LF */);
  247. }
  248. else {
  249. const selectionRange1 = selectionStartPageRange.intersectRanges(selection);
  250. const selectionRange2 = selectionEndPageRange.intersectRanges(selection);
  251. text = (model.getValueInRange(selectionRange1, 1 /* LF */)
  252. + String.fromCharCode(8230)
  253. + model.getValueInRange(selectionRange2, 1 /* LF */));
  254. }
  255. // Chromium handles very poorly text even of a few thousand chars
  256. // Cut text to avoid stalling the entire UI
  257. if (trimLongText) {
  258. const LIMIT_CHARS = 500;
  259. if (pretext.length > LIMIT_CHARS) {
  260. pretext = pretext.substring(pretext.length - LIMIT_CHARS, pretext.length);
  261. }
  262. if (posttext.length > LIMIT_CHARS) {
  263. posttext = posttext.substring(0, LIMIT_CHARS);
  264. }
  265. if (text.length > 2 * LIMIT_CHARS) {
  266. text = text.substring(0, LIMIT_CHARS) + String.fromCharCode(8230) + text.substring(text.length - LIMIT_CHARS, text.length);
  267. }
  268. }
  269. return new TextAreaState(pretext + text + posttext, pretext.length, pretext.length + text.length, new Position(selection.startLineNumber, selection.startColumn), new Position(selection.endLineNumber, selection.endColumn));
  270. }
  271. }