fef5480171ee780d5ba6b947287c14347708c6165bfefebf6de40d9cfe7a9af608e9df4d251ed5237fb2531c468a935c11cb4df5118d928b9f6e1f613aad34 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958
  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 { onUnexpectedError } from '../../../base/common/errors.js';
  6. import * as strings from '../../../base/common/strings.js';
  7. import { ReplaceCommand, ReplaceCommandWithOffsetCursorState, ReplaceCommandWithoutChangingPosition, ReplaceCommandThatPreservesSelection } from '../commands/replaceCommand.js';
  8. import { ShiftCommand } from '../commands/shiftCommand.js';
  9. import { CompositionSurroundSelectionCommand, SurroundSelectionCommand } from '../commands/surroundSelectionCommand.js';
  10. import { EditOperationResult, isQuote } from '../cursorCommon.js';
  11. import { getMapForWordSeparators } from '../core/wordCharacterClassifier.js';
  12. import { Range } from '../core/range.js';
  13. import { Position } from '../core/position.js';
  14. import { IndentAction } from '../languages/languageConfiguration.js';
  15. import { getIndentationAtPosition } from '../languages/languageConfigurationRegistry.js';
  16. import { createScopedLineTokens } from '../languages/supports.js';
  17. import { getIndentActionForType, getIndentForEnter, getInheritIndentForLine } from '../languages/autoIndent.js';
  18. import { getEnterAction } from '../languages/enterAction.js';
  19. export class TypeOperations {
  20. static indent(config, model, selections) {
  21. if (model === null || selections === null) {
  22. return [];
  23. }
  24. const commands = [];
  25. for (let i = 0, len = selections.length; i < len; i++) {
  26. commands[i] = new ShiftCommand(selections[i], {
  27. isUnshift: false,
  28. tabSize: config.tabSize,
  29. indentSize: config.indentSize,
  30. insertSpaces: config.insertSpaces,
  31. useTabStops: config.useTabStops,
  32. autoIndent: config.autoIndent
  33. }, config.languageConfigurationService);
  34. }
  35. return commands;
  36. }
  37. static outdent(config, model, selections) {
  38. const commands = [];
  39. for (let i = 0, len = selections.length; i < len; i++) {
  40. commands[i] = new ShiftCommand(selections[i], {
  41. isUnshift: true,
  42. tabSize: config.tabSize,
  43. indentSize: config.indentSize,
  44. insertSpaces: config.insertSpaces,
  45. useTabStops: config.useTabStops,
  46. autoIndent: config.autoIndent
  47. }, config.languageConfigurationService);
  48. }
  49. return commands;
  50. }
  51. static shiftIndent(config, indentation, count) {
  52. count = count || 1;
  53. return ShiftCommand.shiftIndent(indentation, indentation.length + count, config.tabSize, config.indentSize, config.insertSpaces);
  54. }
  55. static unshiftIndent(config, indentation, count) {
  56. count = count || 1;
  57. return ShiftCommand.unshiftIndent(indentation, indentation.length + count, config.tabSize, config.indentSize, config.insertSpaces);
  58. }
  59. static _distributedPaste(config, model, selections, text) {
  60. const commands = [];
  61. for (let i = 0, len = selections.length; i < len; i++) {
  62. commands[i] = new ReplaceCommand(selections[i], text[i]);
  63. }
  64. return new EditOperationResult(0 /* EditOperationType.Other */, commands, {
  65. shouldPushStackElementBefore: true,
  66. shouldPushStackElementAfter: true
  67. });
  68. }
  69. static _simplePaste(config, model, selections, text, pasteOnNewLine) {
  70. const commands = [];
  71. for (let i = 0, len = selections.length; i < len; i++) {
  72. const selection = selections[i];
  73. const position = selection.getPosition();
  74. if (pasteOnNewLine && !selection.isEmpty()) {
  75. pasteOnNewLine = false;
  76. }
  77. if (pasteOnNewLine && text.indexOf('\n') !== text.length - 1) {
  78. pasteOnNewLine = false;
  79. }
  80. if (pasteOnNewLine) {
  81. // Paste entire line at the beginning of line
  82. const typeSelection = new Range(position.lineNumber, 1, position.lineNumber, 1);
  83. commands[i] = new ReplaceCommandThatPreservesSelection(typeSelection, text, selection, true);
  84. }
  85. else {
  86. commands[i] = new ReplaceCommand(selection, text);
  87. }
  88. }
  89. return new EditOperationResult(0 /* EditOperationType.Other */, commands, {
  90. shouldPushStackElementBefore: true,
  91. shouldPushStackElementAfter: true
  92. });
  93. }
  94. static _distributePasteToCursors(config, selections, text, pasteOnNewLine, multicursorText) {
  95. if (pasteOnNewLine) {
  96. return null;
  97. }
  98. if (selections.length === 1) {
  99. return null;
  100. }
  101. if (multicursorText && multicursorText.length === selections.length) {
  102. return multicursorText;
  103. }
  104. if (config.multiCursorPaste === 'spread') {
  105. // Try to spread the pasted text in case the line count matches the cursor count
  106. // Remove trailing \n if present
  107. if (text.charCodeAt(text.length - 1) === 10 /* CharCode.LineFeed */) {
  108. text = text.substr(0, text.length - 1);
  109. }
  110. // Remove trailing \r if present
  111. if (text.charCodeAt(text.length - 1) === 13 /* CharCode.CarriageReturn */) {
  112. text = text.substr(0, text.length - 1);
  113. }
  114. const lines = strings.splitLines(text);
  115. if (lines.length === selections.length) {
  116. return lines;
  117. }
  118. }
  119. return null;
  120. }
  121. static paste(config, model, selections, text, pasteOnNewLine, multicursorText) {
  122. const distributedPaste = this._distributePasteToCursors(config, selections, text, pasteOnNewLine, multicursorText);
  123. if (distributedPaste) {
  124. selections = selections.sort(Range.compareRangesUsingStarts);
  125. return this._distributedPaste(config, model, selections, distributedPaste);
  126. }
  127. else {
  128. return this._simplePaste(config, model, selections, text, pasteOnNewLine);
  129. }
  130. }
  131. static _goodIndentForLine(config, model, lineNumber) {
  132. let action = null;
  133. let indentation = '';
  134. const expectedIndentAction = getInheritIndentForLine(config.autoIndent, model, lineNumber, false, config.languageConfigurationService);
  135. if (expectedIndentAction) {
  136. action = expectedIndentAction.action;
  137. indentation = expectedIndentAction.indentation;
  138. }
  139. else if (lineNumber > 1) {
  140. let lastLineNumber;
  141. for (lastLineNumber = lineNumber - 1; lastLineNumber >= 1; lastLineNumber--) {
  142. const lineText = model.getLineContent(lastLineNumber);
  143. const nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineText);
  144. if (nonWhitespaceIdx >= 0) {
  145. break;
  146. }
  147. }
  148. if (lastLineNumber < 1) {
  149. // No previous line with content found
  150. return null;
  151. }
  152. const maxColumn = model.getLineMaxColumn(lastLineNumber);
  153. const expectedEnterAction = getEnterAction(config.autoIndent, model, new Range(lastLineNumber, maxColumn, lastLineNumber, maxColumn), config.languageConfigurationService);
  154. if (expectedEnterAction) {
  155. indentation = expectedEnterAction.indentation + expectedEnterAction.appendText;
  156. }
  157. }
  158. if (action) {
  159. if (action === IndentAction.Indent) {
  160. indentation = TypeOperations.shiftIndent(config, indentation);
  161. }
  162. if (action === IndentAction.Outdent) {
  163. indentation = TypeOperations.unshiftIndent(config, indentation);
  164. }
  165. indentation = config.normalizeIndentation(indentation);
  166. }
  167. if (!indentation) {
  168. return null;
  169. }
  170. return indentation;
  171. }
  172. static _replaceJumpToNextIndent(config, model, selection, insertsAutoWhitespace) {
  173. let typeText = '';
  174. const position = selection.getStartPosition();
  175. if (config.insertSpaces) {
  176. const visibleColumnFromColumn = config.visibleColumnFromColumn(model, position);
  177. const indentSize = config.indentSize;
  178. const spacesCnt = indentSize - (visibleColumnFromColumn % indentSize);
  179. for (let i = 0; i < spacesCnt; i++) {
  180. typeText += ' ';
  181. }
  182. }
  183. else {
  184. typeText = '\t';
  185. }
  186. return new ReplaceCommand(selection, typeText, insertsAutoWhitespace);
  187. }
  188. static tab(config, model, selections) {
  189. const commands = [];
  190. for (let i = 0, len = selections.length; i < len; i++) {
  191. const selection = selections[i];
  192. if (selection.isEmpty()) {
  193. const lineText = model.getLineContent(selection.startLineNumber);
  194. if (/^\s*$/.test(lineText) && model.tokenization.isCheapToTokenize(selection.startLineNumber)) {
  195. let goodIndent = this._goodIndentForLine(config, model, selection.startLineNumber);
  196. goodIndent = goodIndent || '\t';
  197. const possibleTypeText = config.normalizeIndentation(goodIndent);
  198. if (!lineText.startsWith(possibleTypeText)) {
  199. commands[i] = new ReplaceCommand(new Range(selection.startLineNumber, 1, selection.startLineNumber, lineText.length + 1), possibleTypeText, true);
  200. continue;
  201. }
  202. }
  203. commands[i] = this._replaceJumpToNextIndent(config, model, selection, true);
  204. }
  205. else {
  206. if (selection.startLineNumber === selection.endLineNumber) {
  207. const lineMaxColumn = model.getLineMaxColumn(selection.startLineNumber);
  208. if (selection.startColumn !== 1 || selection.endColumn !== lineMaxColumn) {
  209. // This is a single line selection that is not the entire line
  210. commands[i] = this._replaceJumpToNextIndent(config, model, selection, false);
  211. continue;
  212. }
  213. }
  214. commands[i] = new ShiftCommand(selection, {
  215. isUnshift: false,
  216. tabSize: config.tabSize,
  217. indentSize: config.indentSize,
  218. insertSpaces: config.insertSpaces,
  219. useTabStops: config.useTabStops,
  220. autoIndent: config.autoIndent
  221. }, config.languageConfigurationService);
  222. }
  223. }
  224. return commands;
  225. }
  226. static compositionType(prevEditOperationType, config, model, selections, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta) {
  227. const commands = selections.map(selection => this._compositionType(model, selection, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta));
  228. return new EditOperationResult(4 /* EditOperationType.TypingOther */, commands, {
  229. shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, 4 /* EditOperationType.TypingOther */),
  230. shouldPushStackElementAfter: false
  231. });
  232. }
  233. static _compositionType(model, selection, text, replacePrevCharCnt, replaceNextCharCnt, positionDelta) {
  234. if (!selection.isEmpty()) {
  235. // looks like https://github.com/microsoft/vscode/issues/2773
  236. // where a cursor operation occurred before a canceled composition
  237. // => ignore composition
  238. return null;
  239. }
  240. const pos = selection.getPosition();
  241. const startColumn = Math.max(1, pos.column - replacePrevCharCnt);
  242. const endColumn = Math.min(model.getLineMaxColumn(pos.lineNumber), pos.column + replaceNextCharCnt);
  243. const range = new Range(pos.lineNumber, startColumn, pos.lineNumber, endColumn);
  244. const oldText = model.getValueInRange(range);
  245. if (oldText === text && positionDelta === 0) {
  246. // => ignore composition that doesn't do anything
  247. return null;
  248. }
  249. return new ReplaceCommandWithOffsetCursorState(range, text, 0, positionDelta);
  250. }
  251. static _typeCommand(range, text, keepPosition) {
  252. if (keepPosition) {
  253. return new ReplaceCommandWithoutChangingPosition(range, text, true);
  254. }
  255. else {
  256. return new ReplaceCommand(range, text, true);
  257. }
  258. }
  259. static _enter(config, model, keepPosition, range) {
  260. if (config.autoIndent === 0 /* EditorAutoIndentStrategy.None */) {
  261. return TypeOperations._typeCommand(range, '\n', keepPosition);
  262. }
  263. if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber) || config.autoIndent === 1 /* EditorAutoIndentStrategy.Keep */) {
  264. const lineText = model.getLineContent(range.startLineNumber);
  265. const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1);
  266. return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition);
  267. }
  268. const r = getEnterAction(config.autoIndent, model, range, config.languageConfigurationService);
  269. if (r) {
  270. if (r.indentAction === IndentAction.None) {
  271. // Nothing special
  272. return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition);
  273. }
  274. else if (r.indentAction === IndentAction.Indent) {
  275. // Indent once
  276. return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(r.indentation + r.appendText), keepPosition);
  277. }
  278. else if (r.indentAction === IndentAction.IndentOutdent) {
  279. // Ultra special
  280. const normalIndent = config.normalizeIndentation(r.indentation);
  281. const increasedIndent = config.normalizeIndentation(r.indentation + r.appendText);
  282. const typeText = '\n' + increasedIndent + '\n' + normalIndent;
  283. if (keepPosition) {
  284. return new ReplaceCommandWithoutChangingPosition(range, typeText, true);
  285. }
  286. else {
  287. return new ReplaceCommandWithOffsetCursorState(range, typeText, -1, increasedIndent.length - normalIndent.length, true);
  288. }
  289. }
  290. else if (r.indentAction === IndentAction.Outdent) {
  291. const actualIndentation = TypeOperations.unshiftIndent(config, r.indentation);
  292. return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(actualIndentation + r.appendText), keepPosition);
  293. }
  294. }
  295. const lineText = model.getLineContent(range.startLineNumber);
  296. const indentation = strings.getLeadingWhitespace(lineText).substring(0, range.startColumn - 1);
  297. if (config.autoIndent >= 4 /* EditorAutoIndentStrategy.Full */) {
  298. const ir = getIndentForEnter(config.autoIndent, model, range, {
  299. unshiftIndent: (indent) => {
  300. return TypeOperations.unshiftIndent(config, indent);
  301. },
  302. shiftIndent: (indent) => {
  303. return TypeOperations.shiftIndent(config, indent);
  304. },
  305. normalizeIndentation: (indent) => {
  306. return config.normalizeIndentation(indent);
  307. }
  308. }, config.languageConfigurationService);
  309. if (ir) {
  310. let oldEndViewColumn = config.visibleColumnFromColumn(model, range.getEndPosition());
  311. const oldEndColumn = range.endColumn;
  312. const newLineContent = model.getLineContent(range.endLineNumber);
  313. const firstNonWhitespace = strings.firstNonWhitespaceIndex(newLineContent);
  314. if (firstNonWhitespace >= 0) {
  315. range = range.setEndPosition(range.endLineNumber, Math.max(range.endColumn, firstNonWhitespace + 1));
  316. }
  317. else {
  318. range = range.setEndPosition(range.endLineNumber, model.getLineMaxColumn(range.endLineNumber));
  319. }
  320. if (keepPosition) {
  321. return new ReplaceCommandWithoutChangingPosition(range, '\n' + config.normalizeIndentation(ir.afterEnter), true);
  322. }
  323. else {
  324. let offset = 0;
  325. if (oldEndColumn <= firstNonWhitespace + 1) {
  326. if (!config.insertSpaces) {
  327. oldEndViewColumn = Math.ceil(oldEndViewColumn / config.indentSize);
  328. }
  329. offset = Math.min(oldEndViewColumn + 1 - config.normalizeIndentation(ir.afterEnter).length - 1, 0);
  330. }
  331. return new ReplaceCommandWithOffsetCursorState(range, '\n' + config.normalizeIndentation(ir.afterEnter), 0, offset, true);
  332. }
  333. }
  334. }
  335. return TypeOperations._typeCommand(range, '\n' + config.normalizeIndentation(indentation), keepPosition);
  336. }
  337. static _isAutoIndentType(config, model, selections) {
  338. if (config.autoIndent < 4 /* EditorAutoIndentStrategy.Full */) {
  339. return false;
  340. }
  341. for (let i = 0, len = selections.length; i < len; i++) {
  342. if (!model.tokenization.isCheapToTokenize(selections[i].getEndPosition().lineNumber)) {
  343. return false;
  344. }
  345. }
  346. return true;
  347. }
  348. static _runAutoIndentType(config, model, range, ch) {
  349. const currentIndentation = getIndentationAtPosition(model, range.startLineNumber, range.startColumn);
  350. const actualIndentation = getIndentActionForType(config.autoIndent, model, range, ch, {
  351. shiftIndent: (indentation) => {
  352. return TypeOperations.shiftIndent(config, indentation);
  353. },
  354. unshiftIndent: (indentation) => {
  355. return TypeOperations.unshiftIndent(config, indentation);
  356. },
  357. }, config.languageConfigurationService);
  358. if (actualIndentation === null) {
  359. return null;
  360. }
  361. if (actualIndentation !== config.normalizeIndentation(currentIndentation)) {
  362. const firstNonWhitespace = model.getLineFirstNonWhitespaceColumn(range.startLineNumber);
  363. if (firstNonWhitespace === 0) {
  364. return TypeOperations._typeCommand(new Range(range.startLineNumber, 1, range.endLineNumber, range.endColumn), config.normalizeIndentation(actualIndentation) + ch, false);
  365. }
  366. else {
  367. return TypeOperations._typeCommand(new Range(range.startLineNumber, 1, range.endLineNumber, range.endColumn), config.normalizeIndentation(actualIndentation) +
  368. model.getLineContent(range.startLineNumber).substring(firstNonWhitespace - 1, range.startColumn - 1) + ch, false);
  369. }
  370. }
  371. return null;
  372. }
  373. static _isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch) {
  374. if (config.autoClosingOvertype === 'never') {
  375. return false;
  376. }
  377. if (!config.autoClosingPairs.autoClosingPairsCloseSingleChar.has(ch)) {
  378. return false;
  379. }
  380. for (let i = 0, len = selections.length; i < len; i++) {
  381. const selection = selections[i];
  382. if (!selection.isEmpty()) {
  383. return false;
  384. }
  385. const position = selection.getPosition();
  386. const lineText = model.getLineContent(position.lineNumber);
  387. const afterCharacter = lineText.charAt(position.column - 1);
  388. if (afterCharacter !== ch) {
  389. return false;
  390. }
  391. // Do not over-type quotes after a backslash
  392. const chIsQuote = isQuote(ch);
  393. const beforeCharacter = position.column > 2 ? lineText.charCodeAt(position.column - 2) : 0 /* CharCode.Null */;
  394. if (beforeCharacter === 92 /* CharCode.Backslash */ && chIsQuote) {
  395. return false;
  396. }
  397. // Must over-type a closing character typed by the editor
  398. if (config.autoClosingOvertype === 'auto') {
  399. let found = false;
  400. for (let j = 0, lenJ = autoClosedCharacters.length; j < lenJ; j++) {
  401. const autoClosedCharacter = autoClosedCharacters[j];
  402. if (position.lineNumber === autoClosedCharacter.startLineNumber && position.column === autoClosedCharacter.startColumn) {
  403. found = true;
  404. break;
  405. }
  406. }
  407. if (!found) {
  408. return false;
  409. }
  410. }
  411. }
  412. return true;
  413. }
  414. static _runAutoClosingOvertype(prevEditOperationType, config, model, selections, ch) {
  415. const commands = [];
  416. for (let i = 0, len = selections.length; i < len; i++) {
  417. const selection = selections[i];
  418. const position = selection.getPosition();
  419. const typeSelection = new Range(position.lineNumber, position.column, position.lineNumber, position.column + 1);
  420. commands[i] = new ReplaceCommand(typeSelection, ch);
  421. }
  422. return new EditOperationResult(4 /* EditOperationType.TypingOther */, commands, {
  423. shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, 4 /* EditOperationType.TypingOther */),
  424. shouldPushStackElementAfter: false
  425. });
  426. }
  427. static _isBeforeClosingBrace(config, lineAfter) {
  428. // If the start of lineAfter can be interpretted as both a starting or ending brace, default to returning false
  429. const nextChar = lineAfter.charAt(0);
  430. const potentialStartingBraces = config.autoClosingPairs.autoClosingPairsOpenByStart.get(nextChar) || [];
  431. const potentialClosingBraces = config.autoClosingPairs.autoClosingPairsCloseByStart.get(nextChar) || [];
  432. const isBeforeStartingBrace = potentialStartingBraces.some(x => lineAfter.startsWith(x.open));
  433. const isBeforeClosingBrace = potentialClosingBraces.some(x => lineAfter.startsWith(x.close));
  434. return !isBeforeStartingBrace && isBeforeClosingBrace;
  435. }
  436. /**
  437. * Determine if typing `ch` at all `positions` in the `model` results in an
  438. * auto closing open sequence being typed.
  439. *
  440. * Auto closing open sequences can consist of multiple characters, which
  441. * can lead to ambiguities. In such a case, the longest auto-closing open
  442. * sequence is returned.
  443. */
  444. static _findAutoClosingPairOpen(config, model, positions, ch) {
  445. const candidates = config.autoClosingPairs.autoClosingPairsOpenByEnd.get(ch);
  446. if (!candidates) {
  447. return null;
  448. }
  449. // Determine which auto-closing pair it is
  450. let result = null;
  451. for (const candidate of candidates) {
  452. if (result === null || candidate.open.length > result.open.length) {
  453. let candidateIsMatch = true;
  454. for (const position of positions) {
  455. const relevantText = model.getValueInRange(new Range(position.lineNumber, position.column - candidate.open.length + 1, position.lineNumber, position.column));
  456. if (relevantText + ch !== candidate.open) {
  457. candidateIsMatch = false;
  458. break;
  459. }
  460. }
  461. if (candidateIsMatch) {
  462. result = candidate;
  463. }
  464. }
  465. }
  466. return result;
  467. }
  468. /**
  469. * Find another auto-closing pair that is contained by the one passed in.
  470. *
  471. * e.g. when having [(,)] and [(*,*)] as auto-closing pairs
  472. * this method will find [(,)] as a containment pair for [(*,*)]
  473. */
  474. static _findContainedAutoClosingPair(config, pair) {
  475. if (pair.open.length <= 1) {
  476. return null;
  477. }
  478. const lastChar = pair.close.charAt(pair.close.length - 1);
  479. // get candidates with the same last character as close
  480. const candidates = config.autoClosingPairs.autoClosingPairsCloseByEnd.get(lastChar) || [];
  481. let result = null;
  482. for (const candidate of candidates) {
  483. if (candidate.open !== pair.open && pair.open.includes(candidate.open) && pair.close.endsWith(candidate.close)) {
  484. if (!result || candidate.open.length > result.open.length) {
  485. result = candidate;
  486. }
  487. }
  488. }
  489. return result;
  490. }
  491. static _getAutoClosingPairClose(config, model, selections, ch, chIsAlreadyTyped) {
  492. const chIsQuote = isQuote(ch);
  493. const autoCloseConfig = (chIsQuote ? config.autoClosingQuotes : config.autoClosingBrackets);
  494. const shouldAutoCloseBefore = (chIsQuote ? config.shouldAutoCloseBefore.quote : config.shouldAutoCloseBefore.bracket);
  495. if (autoCloseConfig === 'never') {
  496. return null;
  497. }
  498. for (const selection of selections) {
  499. if (!selection.isEmpty()) {
  500. return null;
  501. }
  502. }
  503. // This method is called both when typing (regularly) and when composition ends
  504. // This means that we need to work with a text buffer where sometimes `ch` is not
  505. // there (it is being typed right now) or with a text buffer where `ch` has already been typed
  506. //
  507. // In order to avoid adding checks for `chIsAlreadyTyped` in all places, we will work
  508. // with two conceptual positions, the position before `ch` and the position after `ch`
  509. //
  510. const positions = selections.map((s) => {
  511. const position = s.getPosition();
  512. if (chIsAlreadyTyped) {
  513. return { lineNumber: position.lineNumber, beforeColumn: position.column - ch.length, afterColumn: position.column };
  514. }
  515. else {
  516. return { lineNumber: position.lineNumber, beforeColumn: position.column, afterColumn: position.column };
  517. }
  518. });
  519. // Find the longest auto-closing open pair in case of multiple ending in `ch`
  520. // e.g. when having [f","] and [","], it picks [f","] if the character before is f
  521. const pair = this._findAutoClosingPairOpen(config, model, positions.map(p => new Position(p.lineNumber, p.beforeColumn)), ch);
  522. if (!pair) {
  523. return null;
  524. }
  525. // Sometimes, it is possible to have two auto-closing pairs that have a containment relationship
  526. // e.g. when having [(,)] and [(*,*)]
  527. // - when typing (, the resulting state is (|)
  528. // - when typing *, the desired resulting state is (*|*), not (*|*))
  529. const containedPair = this._findContainedAutoClosingPair(config, pair);
  530. const containedPairClose = containedPair ? containedPair.close : '';
  531. let isContainedPairPresent = true;
  532. for (const position of positions) {
  533. const { lineNumber, beforeColumn, afterColumn } = position;
  534. const lineText = model.getLineContent(lineNumber);
  535. const lineBefore = lineText.substring(0, beforeColumn - 1);
  536. const lineAfter = lineText.substring(afterColumn - 1);
  537. if (!lineAfter.startsWith(containedPairClose)) {
  538. isContainedPairPresent = false;
  539. }
  540. // Only consider auto closing the pair if an allowed character follows or if another autoclosed pair closing brace follows
  541. if (lineAfter.length > 0) {
  542. const characterAfter = lineAfter.charAt(0);
  543. const isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, lineAfter);
  544. if (!isBeforeCloseBrace && !shouldAutoCloseBefore(characterAfter)) {
  545. return null;
  546. }
  547. }
  548. // Do not auto-close ' or " after a word character
  549. if (pair.open.length === 1 && (ch === '\'' || ch === '"') && autoCloseConfig !== 'always') {
  550. const wordSeparators = getMapForWordSeparators(config.wordSeparators);
  551. if (lineBefore.length > 0) {
  552. const characterBefore = lineBefore.charCodeAt(lineBefore.length - 1);
  553. if (wordSeparators.get(characterBefore) === 0 /* WordCharacterClass.Regular */) {
  554. return null;
  555. }
  556. }
  557. }
  558. if (!model.tokenization.isCheapToTokenize(lineNumber)) {
  559. // Do not force tokenization
  560. return null;
  561. }
  562. model.tokenization.forceTokenization(lineNumber);
  563. const lineTokens = model.tokenization.getLineTokens(lineNumber);
  564. const scopedLineTokens = createScopedLineTokens(lineTokens, beforeColumn - 1);
  565. if (!pair.shouldAutoClose(scopedLineTokens, beforeColumn - scopedLineTokens.firstCharOffset)) {
  566. return null;
  567. }
  568. // Typing for example a quote could either start a new string, in which case auto-closing is desirable
  569. // or it could end a previously started string, in which case auto-closing is not desirable
  570. //
  571. // In certain cases, it is really not possible to look at the previous token to determine
  572. // what would happen. That's why we do something really unusual, we pretend to type a different
  573. // character and ask the tokenizer what the outcome of doing that is: after typing a neutral
  574. // character, are we in a string (i.e. the quote would most likely end a string) or not?
  575. //
  576. const neutralCharacter = pair.findNeutralCharacter();
  577. if (neutralCharacter) {
  578. const tokenType = model.tokenization.getTokenTypeIfInsertingCharacter(lineNumber, beforeColumn, neutralCharacter);
  579. if (!pair.isOK(tokenType)) {
  580. return null;
  581. }
  582. }
  583. }
  584. if (isContainedPairPresent) {
  585. return pair.close.substring(0, pair.close.length - containedPairClose.length);
  586. }
  587. else {
  588. return pair.close;
  589. }
  590. }
  591. static _runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, chIsAlreadyTyped, autoClosingPairClose) {
  592. const commands = [];
  593. for (let i = 0, len = selections.length; i < len; i++) {
  594. const selection = selections[i];
  595. commands[i] = new TypeWithAutoClosingCommand(selection, ch, !chIsAlreadyTyped, autoClosingPairClose);
  596. }
  597. return new EditOperationResult(4 /* EditOperationType.TypingOther */, commands, {
  598. shouldPushStackElementBefore: true,
  599. shouldPushStackElementAfter: false
  600. });
  601. }
  602. static _shouldSurroundChar(config, ch) {
  603. if (isQuote(ch)) {
  604. return (config.autoSurround === 'quotes' || config.autoSurround === 'languageDefined');
  605. }
  606. else {
  607. // Character is a bracket
  608. return (config.autoSurround === 'brackets' || config.autoSurround === 'languageDefined');
  609. }
  610. }
  611. static _isSurroundSelectionType(config, model, selections, ch) {
  612. if (!TypeOperations._shouldSurroundChar(config, ch) || !config.surroundingPairs.hasOwnProperty(ch)) {
  613. return false;
  614. }
  615. const isTypingAQuoteCharacter = isQuote(ch);
  616. for (const selection of selections) {
  617. if (selection.isEmpty()) {
  618. return false;
  619. }
  620. let selectionContainsOnlyWhitespace = true;
  621. for (let lineNumber = selection.startLineNumber; lineNumber <= selection.endLineNumber; lineNumber++) {
  622. const lineText = model.getLineContent(lineNumber);
  623. const startIndex = (lineNumber === selection.startLineNumber ? selection.startColumn - 1 : 0);
  624. const endIndex = (lineNumber === selection.endLineNumber ? selection.endColumn - 1 : lineText.length);
  625. const selectedText = lineText.substring(startIndex, endIndex);
  626. if (/[^ \t]/.test(selectedText)) {
  627. // this selected text contains something other than whitespace
  628. selectionContainsOnlyWhitespace = false;
  629. break;
  630. }
  631. }
  632. if (selectionContainsOnlyWhitespace) {
  633. return false;
  634. }
  635. if (isTypingAQuoteCharacter && selection.startLineNumber === selection.endLineNumber && selection.startColumn + 1 === selection.endColumn) {
  636. const selectionText = model.getValueInRange(selection);
  637. if (isQuote(selectionText)) {
  638. // Typing a quote character on top of another quote character
  639. // => disable surround selection type
  640. return false;
  641. }
  642. }
  643. }
  644. return true;
  645. }
  646. static _runSurroundSelectionType(prevEditOperationType, config, model, selections, ch) {
  647. const commands = [];
  648. for (let i = 0, len = selections.length; i < len; i++) {
  649. const selection = selections[i];
  650. const closeCharacter = config.surroundingPairs[ch];
  651. commands[i] = new SurroundSelectionCommand(selection, ch, closeCharacter);
  652. }
  653. return new EditOperationResult(0 /* EditOperationType.Other */, commands, {
  654. shouldPushStackElementBefore: true,
  655. shouldPushStackElementAfter: true
  656. });
  657. }
  658. static _isTypeInterceptorElectricChar(config, model, selections) {
  659. if (selections.length === 1 && model.tokenization.isCheapToTokenize(selections[0].getEndPosition().lineNumber)) {
  660. return true;
  661. }
  662. return false;
  663. }
  664. static _typeInterceptorElectricChar(prevEditOperationType, config, model, selection, ch) {
  665. if (!config.electricChars.hasOwnProperty(ch) || !selection.isEmpty()) {
  666. return null;
  667. }
  668. const position = selection.getPosition();
  669. model.tokenization.forceTokenization(position.lineNumber);
  670. const lineTokens = model.tokenization.getLineTokens(position.lineNumber);
  671. let electricAction;
  672. try {
  673. electricAction = config.onElectricCharacter(ch, lineTokens, position.column);
  674. }
  675. catch (e) {
  676. onUnexpectedError(e);
  677. return null;
  678. }
  679. if (!electricAction) {
  680. return null;
  681. }
  682. if (electricAction.matchOpenBracket) {
  683. const endColumn = (lineTokens.getLineContent() + ch).lastIndexOf(electricAction.matchOpenBracket) + 1;
  684. const match = model.bracketPairs.findMatchingBracketUp(electricAction.matchOpenBracket, {
  685. lineNumber: position.lineNumber,
  686. column: endColumn
  687. }, 500 /* give at most 500ms to compute */);
  688. if (match) {
  689. if (match.startLineNumber === position.lineNumber) {
  690. // matched something on the same line => no change in indentation
  691. return null;
  692. }
  693. const matchLine = model.getLineContent(match.startLineNumber);
  694. const matchLineIndentation = strings.getLeadingWhitespace(matchLine);
  695. const newIndentation = config.normalizeIndentation(matchLineIndentation);
  696. const lineText = model.getLineContent(position.lineNumber);
  697. const lineFirstNonBlankColumn = model.getLineFirstNonWhitespaceColumn(position.lineNumber) || position.column;
  698. const prefix = lineText.substring(lineFirstNonBlankColumn - 1, position.column - 1);
  699. const typeText = newIndentation + prefix + ch;
  700. const typeSelection = new Range(position.lineNumber, 1, position.lineNumber, position.column);
  701. const command = new ReplaceCommand(typeSelection, typeText);
  702. return new EditOperationResult(getTypingOperation(typeText, prevEditOperationType), [command], {
  703. shouldPushStackElementBefore: false,
  704. shouldPushStackElementAfter: true
  705. });
  706. }
  707. }
  708. return null;
  709. }
  710. /**
  711. * This is very similar with typing, but the character is already in the text buffer!
  712. */
  713. static compositionEndWithInterceptors(prevEditOperationType, config, model, compositions, selections, autoClosedCharacters) {
  714. if (!compositions) {
  715. // could not deduce what the composition did
  716. return null;
  717. }
  718. let insertedText = null;
  719. for (const composition of compositions) {
  720. if (insertedText === null) {
  721. insertedText = composition.insertedText;
  722. }
  723. else if (insertedText !== composition.insertedText) {
  724. // not all selections agree on what was typed
  725. return null;
  726. }
  727. }
  728. if (!insertedText || insertedText.length !== 1) {
  729. // we're only interested in the case where a single character was inserted
  730. return null;
  731. }
  732. const ch = insertedText;
  733. let hasDeletion = false;
  734. for (const composition of compositions) {
  735. if (composition.deletedText.length !== 0) {
  736. hasDeletion = true;
  737. break;
  738. }
  739. }
  740. if (hasDeletion) {
  741. // Check if this could have been a surround selection
  742. if (!TypeOperations._shouldSurroundChar(config, ch) || !config.surroundingPairs.hasOwnProperty(ch)) {
  743. return null;
  744. }
  745. const isTypingAQuoteCharacter = isQuote(ch);
  746. for (const composition of compositions) {
  747. if (composition.deletedSelectionStart !== 0 || composition.deletedSelectionEnd !== composition.deletedText.length) {
  748. // more text was deleted than was selected, so this could not have been a surround selection
  749. return null;
  750. }
  751. if (/^[ \t]+$/.test(composition.deletedText)) {
  752. // deleted text was only whitespace
  753. return null;
  754. }
  755. if (isTypingAQuoteCharacter && isQuote(composition.deletedText)) {
  756. // deleted text was a quote
  757. return null;
  758. }
  759. }
  760. const positions = [];
  761. for (const selection of selections) {
  762. if (!selection.isEmpty()) {
  763. return null;
  764. }
  765. positions.push(selection.getPosition());
  766. }
  767. if (positions.length !== compositions.length) {
  768. return null;
  769. }
  770. const commands = [];
  771. for (let i = 0, len = positions.length; i < len; i++) {
  772. commands.push(new CompositionSurroundSelectionCommand(positions[i], compositions[i].deletedText, ch));
  773. }
  774. return new EditOperationResult(4 /* EditOperationType.TypingOther */, commands, {
  775. shouldPushStackElementBefore: true,
  776. shouldPushStackElementAfter: false
  777. });
  778. }
  779. if (this._isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) {
  780. // Unfortunately, the close character is at this point "doubled", so we need to delete it...
  781. const commands = selections.map(s => new ReplaceCommand(new Range(s.positionLineNumber, s.positionColumn, s.positionLineNumber, s.positionColumn + 1), '', false));
  782. return new EditOperationResult(4 /* EditOperationType.TypingOther */, commands, {
  783. shouldPushStackElementBefore: true,
  784. shouldPushStackElementAfter: false
  785. });
  786. }
  787. const autoClosingPairClose = this._getAutoClosingPairClose(config, model, selections, ch, true);
  788. if (autoClosingPairClose !== null) {
  789. return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, true, autoClosingPairClose);
  790. }
  791. return null;
  792. }
  793. static typeWithInterceptors(isDoingComposition, prevEditOperationType, config, model, selections, autoClosedCharacters, ch) {
  794. if (!isDoingComposition && ch === '\n') {
  795. const commands = [];
  796. for (let i = 0, len = selections.length; i < len; i++) {
  797. commands[i] = TypeOperations._enter(config, model, false, selections[i]);
  798. }
  799. return new EditOperationResult(4 /* EditOperationType.TypingOther */, commands, {
  800. shouldPushStackElementBefore: true,
  801. shouldPushStackElementAfter: false,
  802. });
  803. }
  804. if (!isDoingComposition && this._isAutoIndentType(config, model, selections)) {
  805. const commands = [];
  806. let autoIndentFails = false;
  807. for (let i = 0, len = selections.length; i < len; i++) {
  808. commands[i] = this._runAutoIndentType(config, model, selections[i], ch);
  809. if (!commands[i]) {
  810. autoIndentFails = true;
  811. break;
  812. }
  813. }
  814. if (!autoIndentFails) {
  815. return new EditOperationResult(4 /* EditOperationType.TypingOther */, commands, {
  816. shouldPushStackElementBefore: true,
  817. shouldPushStackElementAfter: false,
  818. });
  819. }
  820. }
  821. if (this._isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) {
  822. return this._runAutoClosingOvertype(prevEditOperationType, config, model, selections, ch);
  823. }
  824. if (!isDoingComposition) {
  825. const autoClosingPairClose = this._getAutoClosingPairClose(config, model, selections, ch, false);
  826. if (autoClosingPairClose) {
  827. return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, false, autoClosingPairClose);
  828. }
  829. }
  830. if (!isDoingComposition && this._isSurroundSelectionType(config, model, selections, ch)) {
  831. return this._runSurroundSelectionType(prevEditOperationType, config, model, selections, ch);
  832. }
  833. // Electric characters make sense only when dealing with a single cursor,
  834. // as multiple cursors typing brackets for example would interfer with bracket matching
  835. if (!isDoingComposition && this._isTypeInterceptorElectricChar(config, model, selections)) {
  836. const r = this._typeInterceptorElectricChar(prevEditOperationType, config, model, selections[0], ch);
  837. if (r) {
  838. return r;
  839. }
  840. }
  841. // A simple character type
  842. const commands = [];
  843. for (let i = 0, len = selections.length; i < len; i++) {
  844. commands[i] = new ReplaceCommand(selections[i], ch);
  845. }
  846. const opType = getTypingOperation(ch, prevEditOperationType);
  847. return new EditOperationResult(opType, commands, {
  848. shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType),
  849. shouldPushStackElementAfter: false
  850. });
  851. }
  852. static typeWithoutInterceptors(prevEditOperationType, config, model, selections, str) {
  853. const commands = [];
  854. for (let i = 0, len = selections.length; i < len; i++) {
  855. commands[i] = new ReplaceCommand(selections[i], str);
  856. }
  857. const opType = getTypingOperation(str, prevEditOperationType);
  858. return new EditOperationResult(opType, commands, {
  859. shouldPushStackElementBefore: shouldPushStackElementBetween(prevEditOperationType, opType),
  860. shouldPushStackElementAfter: false
  861. });
  862. }
  863. static lineInsertBefore(config, model, selections) {
  864. if (model === null || selections === null) {
  865. return [];
  866. }
  867. const commands = [];
  868. for (let i = 0, len = selections.length; i < len; i++) {
  869. let lineNumber = selections[i].positionLineNumber;
  870. if (lineNumber === 1) {
  871. commands[i] = new ReplaceCommandWithoutChangingPosition(new Range(1, 1, 1, 1), '\n');
  872. }
  873. else {
  874. lineNumber--;
  875. const column = model.getLineMaxColumn(lineNumber);
  876. commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column));
  877. }
  878. }
  879. return commands;
  880. }
  881. static lineInsertAfter(config, model, selections) {
  882. if (model === null || selections === null) {
  883. return [];
  884. }
  885. const commands = [];
  886. for (let i = 0, len = selections.length; i < len; i++) {
  887. const lineNumber = selections[i].positionLineNumber;
  888. const column = model.getLineMaxColumn(lineNumber);
  889. commands[i] = this._enter(config, model, false, new Range(lineNumber, column, lineNumber, column));
  890. }
  891. return commands;
  892. }
  893. static lineBreakInsert(config, model, selections) {
  894. const commands = [];
  895. for (let i = 0, len = selections.length; i < len; i++) {
  896. commands[i] = this._enter(config, model, true, selections[i]);
  897. }
  898. return commands;
  899. }
  900. }
  901. export class TypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorState {
  902. constructor(selection, openCharacter, insertOpenCharacter, closeCharacter) {
  903. super(selection, (insertOpenCharacter ? openCharacter : '') + closeCharacter, 0, -closeCharacter.length);
  904. this._openCharacter = openCharacter;
  905. this._closeCharacter = closeCharacter;
  906. this.closeCharacterRange = null;
  907. this.enclosingRange = null;
  908. }
  909. computeCursorState(model, helper) {
  910. const inverseEditOperations = helper.getInverseEditOperations();
  911. const range = inverseEditOperations[0].range;
  912. this.closeCharacterRange = new Range(range.startLineNumber, range.endColumn - this._closeCharacter.length, range.endLineNumber, range.endColumn);
  913. this.enclosingRange = new Range(range.startLineNumber, range.endColumn - this._openCharacter.length - this._closeCharacter.length, range.endLineNumber, range.endColumn);
  914. return super.computeCursorState(model, helper);
  915. }
  916. }
  917. export class CompositionOutcome {
  918. constructor(deletedText, deletedSelectionStart, deletedSelectionEnd, insertedText, insertedSelectionStart, insertedSelectionEnd) {
  919. this.deletedText = deletedText;
  920. this.deletedSelectionStart = deletedSelectionStart;
  921. this.deletedSelectionEnd = deletedSelectionEnd;
  922. this.insertedText = insertedText;
  923. this.insertedSelectionStart = insertedSelectionStart;
  924. this.insertedSelectionEnd = insertedSelectionEnd;
  925. }
  926. }
  927. function getTypingOperation(typedText, previousTypingOperation) {
  928. if (typedText === ' ') {
  929. return previousTypingOperation === 5 /* EditOperationType.TypingFirstSpace */
  930. || previousTypingOperation === 6 /* EditOperationType.TypingConsecutiveSpace */
  931. ? 6 /* EditOperationType.TypingConsecutiveSpace */
  932. : 5 /* EditOperationType.TypingFirstSpace */;
  933. }
  934. return 4 /* EditOperationType.TypingOther */;
  935. }
  936. function shouldPushStackElementBetween(previousTypingOperation, typingOperation) {
  937. if (isTypingOperation(previousTypingOperation) && !isTypingOperation(typingOperation)) {
  938. // Always set an undo stop before non-type operations
  939. return true;
  940. }
  941. if (previousTypingOperation === 5 /* EditOperationType.TypingFirstSpace */) {
  942. // `abc |d`: No undo stop
  943. // `abc |d`: Undo stop
  944. return false;
  945. }
  946. // Insert undo stop between different operation types
  947. return normalizeOperationType(previousTypingOperation) !== normalizeOperationType(typingOperation);
  948. }
  949. function normalizeOperationType(type) {
  950. return (type === 6 /* EditOperationType.TypingConsecutiveSpace */ || type === 5 /* EditOperationType.TypingFirstSpace */)
  951. ? 'space'
  952. : type;
  953. }
  954. function isTypingOperation(type) {
  955. return type === 4 /* EditOperationType.TypingOther */
  956. || type === 5 /* EditOperationType.TypingFirstSpace */
  957. || type === 6 /* EditOperationType.TypingConsecutiveSpace */;
  958. }