50b641106abed82e3a683cc16bf96a9d706521b1ab353b7282e23007c19b62d7b56b8c08e546b6f22f520bf8e09ac75c7c126e494a0054569f83e3bc94d2ec 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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 deduceInput(previousState, currentState, couldBeEmojiInput) {
  60. if (!previousState) {
  61. // This is the EMPTY state
  62. return {
  63. text: '',
  64. replacePrevCharCnt: 0,
  65. replaceNextCharCnt: 0,
  66. positionDelta: 0
  67. };
  68. }
  69. if (_debugComposition) {
  70. console.log('------------------------deduceInput');
  71. console.log(`PREVIOUS STATE: ${previousState.toString()}`);
  72. console.log(`CURRENT STATE: ${currentState.toString()}`);
  73. }
  74. const prefixLength = Math.min(strings.commonPrefixLength(previousState.value, currentState.value), previousState.selectionStart, currentState.selectionStart);
  75. const suffixLength = Math.min(strings.commonSuffixLength(previousState.value, currentState.value), previousState.value.length - previousState.selectionEnd, currentState.value.length - currentState.selectionEnd);
  76. const previousValue = previousState.value.substring(prefixLength, previousState.value.length - suffixLength);
  77. const currentValue = currentState.value.substring(prefixLength, currentState.value.length - suffixLength);
  78. const previousSelectionStart = previousState.selectionStart - prefixLength;
  79. const previousSelectionEnd = previousState.selectionEnd - prefixLength;
  80. const currentSelectionStart = currentState.selectionStart - prefixLength;
  81. const currentSelectionEnd = currentState.selectionEnd - prefixLength;
  82. if (_debugComposition) {
  83. console.log(`AFTER DIFFING PREVIOUS STATE: <${previousValue}>, selectionStart: ${previousSelectionStart}, selectionEnd: ${previousSelectionEnd}`);
  84. console.log(`AFTER DIFFING CURRENT STATE: <${currentValue}>, selectionStart: ${currentSelectionStart}, selectionEnd: ${currentSelectionEnd}`);
  85. }
  86. if (currentSelectionStart === currentSelectionEnd) {
  87. // no current selection
  88. const replacePreviousCharacters = (previousState.selectionStart - prefixLength);
  89. if (_debugComposition) {
  90. console.log(`REMOVE PREVIOUS: ${replacePreviousCharacters} chars`);
  91. }
  92. return {
  93. text: currentValue,
  94. replacePrevCharCnt: replacePreviousCharacters,
  95. replaceNextCharCnt: 0,
  96. positionDelta: 0
  97. };
  98. }
  99. // there is a current selection => composition case
  100. const replacePreviousCharacters = previousSelectionEnd - previousSelectionStart;
  101. return {
  102. text: currentValue,
  103. replacePrevCharCnt: replacePreviousCharacters,
  104. replaceNextCharCnt: 0,
  105. positionDelta: 0
  106. };
  107. }
  108. static deduceAndroidCompositionInput(previousState, currentState) {
  109. if (!previousState) {
  110. // This is the EMPTY state
  111. return {
  112. text: '',
  113. replacePrevCharCnt: 0,
  114. replaceNextCharCnt: 0,
  115. positionDelta: 0
  116. };
  117. }
  118. if (_debugComposition) {
  119. console.log('------------------------deduceAndroidCompositionInput');
  120. console.log(`PREVIOUS STATE: ${previousState.toString()}`);
  121. console.log(`CURRENT STATE: ${currentState.toString()}`);
  122. }
  123. if (previousState.value === currentState.value) {
  124. return {
  125. text: '',
  126. replacePrevCharCnt: 0,
  127. replaceNextCharCnt: 0,
  128. positionDelta: currentState.selectionEnd - previousState.selectionEnd
  129. };
  130. }
  131. const prefixLength = Math.min(strings.commonPrefixLength(previousState.value, currentState.value), previousState.selectionEnd);
  132. const suffixLength = Math.min(strings.commonSuffixLength(previousState.value, currentState.value), previousState.value.length - previousState.selectionEnd);
  133. const previousValue = previousState.value.substring(prefixLength, previousState.value.length - suffixLength);
  134. const currentValue = currentState.value.substring(prefixLength, currentState.value.length - suffixLength);
  135. const previousSelectionStart = previousState.selectionStart - prefixLength;
  136. const previousSelectionEnd = previousState.selectionEnd - prefixLength;
  137. const currentSelectionStart = currentState.selectionStart - prefixLength;
  138. const currentSelectionEnd = currentState.selectionEnd - prefixLength;
  139. if (_debugComposition) {
  140. console.log(`AFTER DIFFING PREVIOUS STATE: <${previousValue}>, selectionStart: ${previousSelectionStart}, selectionEnd: ${previousSelectionEnd}`);
  141. console.log(`AFTER DIFFING CURRENT STATE: <${currentValue}>, selectionStart: ${currentSelectionStart}, selectionEnd: ${currentSelectionEnd}`);
  142. }
  143. return {
  144. text: currentValue,
  145. replacePrevCharCnt: previousSelectionEnd,
  146. replaceNextCharCnt: previousValue.length - previousSelectionEnd,
  147. positionDelta: currentSelectionEnd - currentValue.length
  148. };
  149. }
  150. }
  151. TextAreaState.EMPTY = new TextAreaState('', 0, 0, null, null);
  152. export class PagedScreenReaderStrategy {
  153. static _getPageOfLine(lineNumber, linesPerPage) {
  154. return Math.floor((lineNumber - 1) / linesPerPage);
  155. }
  156. static _getRangeForPage(page, linesPerPage) {
  157. const offset = page * linesPerPage;
  158. const startLineNumber = offset + 1;
  159. const endLineNumber = offset + linesPerPage;
  160. return new Range(startLineNumber, 1, endLineNumber + 1, 1);
  161. }
  162. static fromEditorSelection(previousState, model, selection, linesPerPage, trimLongText) {
  163. const selectionStartPage = PagedScreenReaderStrategy._getPageOfLine(selection.startLineNumber, linesPerPage);
  164. const selectionStartPageRange = PagedScreenReaderStrategy._getRangeForPage(selectionStartPage, linesPerPage);
  165. const selectionEndPage = PagedScreenReaderStrategy._getPageOfLine(selection.endLineNumber, linesPerPage);
  166. const selectionEndPageRange = PagedScreenReaderStrategy._getRangeForPage(selectionEndPage, linesPerPage);
  167. const pretextRange = selectionStartPageRange.intersectRanges(new Range(1, 1, selection.startLineNumber, selection.startColumn));
  168. let pretext = model.getValueInRange(pretextRange, 1 /* EndOfLinePreference.LF */);
  169. const lastLine = model.getLineCount();
  170. const lastLineMaxColumn = model.getLineMaxColumn(lastLine);
  171. const posttextRange = selectionEndPageRange.intersectRanges(new Range(selection.endLineNumber, selection.endColumn, lastLine, lastLineMaxColumn));
  172. let posttext = model.getValueInRange(posttextRange, 1 /* EndOfLinePreference.LF */);
  173. let text;
  174. if (selectionStartPage === selectionEndPage || selectionStartPage + 1 === selectionEndPage) {
  175. // take full selection
  176. text = model.getValueInRange(selection, 1 /* EndOfLinePreference.LF */);
  177. }
  178. else {
  179. const selectionRange1 = selectionStartPageRange.intersectRanges(selection);
  180. const selectionRange2 = selectionEndPageRange.intersectRanges(selection);
  181. text = (model.getValueInRange(selectionRange1, 1 /* EndOfLinePreference.LF */)
  182. + String.fromCharCode(8230)
  183. + model.getValueInRange(selectionRange2, 1 /* EndOfLinePreference.LF */));
  184. }
  185. // Chromium handles very poorly text even of a few thousand chars
  186. // Cut text to avoid stalling the entire UI
  187. if (trimLongText) {
  188. const LIMIT_CHARS = 500;
  189. if (pretext.length > LIMIT_CHARS) {
  190. pretext = pretext.substring(pretext.length - LIMIT_CHARS, pretext.length);
  191. }
  192. if (posttext.length > LIMIT_CHARS) {
  193. posttext = posttext.substring(0, LIMIT_CHARS);
  194. }
  195. if (text.length > 2 * LIMIT_CHARS) {
  196. text = text.substring(0, LIMIT_CHARS) + String.fromCharCode(8230) + text.substring(text.length - LIMIT_CHARS, text.length);
  197. }
  198. }
  199. return new TextAreaState(pretext + text + posttext, pretext.length, pretext.length + text.length, new Position(selection.startLineNumber, selection.startColumn), new Position(selection.endLineNumber, selection.endColumn));
  200. }
  201. }