9702953aa548dd46d44c5ba1569b2789d69b7df13c642ae867a8e7c2b6fc2bf78bb53566998959429de9415477e8eb7fccc1e937e6c0639b4602fd5aef79b6 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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 { IndentAction } from './languageConfiguration.js';
  7. import { createScopedLineTokens } from './supports.js';
  8. import { getScopedLineTokens } from './languageConfigurationRegistry.js';
  9. /**
  10. * Get nearest preceding line which doesn't match unIndentPattern or contains all whitespace.
  11. * Result:
  12. * -1: run into the boundary of embedded languages
  13. * 0: every line above are invalid
  14. * else: nearest preceding line of the same language
  15. */
  16. function getPrecedingValidLine(model, lineNumber, indentRulesSupport) {
  17. const languageId = model.tokenization.getLanguageIdAtPosition(lineNumber, 0);
  18. if (lineNumber > 1) {
  19. let lastLineNumber;
  20. let resultLineNumber = -1;
  21. for (lastLineNumber = lineNumber - 1; lastLineNumber >= 1; lastLineNumber--) {
  22. if (model.tokenization.getLanguageIdAtPosition(lastLineNumber, 0) !== languageId) {
  23. return resultLineNumber;
  24. }
  25. const text = model.getLineContent(lastLineNumber);
  26. if (indentRulesSupport.shouldIgnore(text) || /^\s+$/.test(text) || text === '') {
  27. resultLineNumber = lastLineNumber;
  28. continue;
  29. }
  30. return lastLineNumber;
  31. }
  32. }
  33. return -1;
  34. }
  35. /**
  36. * Get inherited indentation from above lines.
  37. * 1. Find the nearest preceding line which doesn't match unIndentedLinePattern.
  38. * 2. If this line matches indentNextLinePattern or increaseIndentPattern, it means that the indent level of `lineNumber` should be 1 greater than this line.
  39. * 3. If this line doesn't match any indent rules
  40. * a. check whether the line above it matches indentNextLinePattern
  41. * b. If not, the indent level of this line is the result
  42. * c. If so, it means the indent of this line is *temporary*, go upward utill we find a line whose indent is not temporary (the same workflow a -> b -> c).
  43. * 4. Otherwise, we fail to get an inherited indent from aboves. Return null and we should not touch the indent of `lineNumber`
  44. *
  45. * This function only return the inherited indent based on above lines, it doesn't check whether current line should decrease or not.
  46. */
  47. export function getInheritIndentForLine(autoIndent, model, lineNumber, honorIntentialIndent = true, languageConfigurationService) {
  48. if (autoIndent < 4 /* EditorAutoIndentStrategy.Full */) {
  49. return null;
  50. }
  51. const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.tokenization.getLanguageId()).indentRulesSupport;
  52. if (!indentRulesSupport) {
  53. return null;
  54. }
  55. if (lineNumber <= 1) {
  56. return {
  57. indentation: '',
  58. action: null
  59. };
  60. }
  61. const precedingUnIgnoredLine = getPrecedingValidLine(model, lineNumber, indentRulesSupport);
  62. if (precedingUnIgnoredLine < 0) {
  63. return null;
  64. }
  65. else if (precedingUnIgnoredLine < 1) {
  66. return {
  67. indentation: '',
  68. action: null
  69. };
  70. }
  71. const precedingUnIgnoredLineContent = model.getLineContent(precedingUnIgnoredLine);
  72. if (indentRulesSupport.shouldIncrease(precedingUnIgnoredLineContent) || indentRulesSupport.shouldIndentNextLine(precedingUnIgnoredLineContent)) {
  73. return {
  74. indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent),
  75. action: IndentAction.Indent,
  76. line: precedingUnIgnoredLine
  77. };
  78. }
  79. else if (indentRulesSupport.shouldDecrease(precedingUnIgnoredLineContent)) {
  80. return {
  81. indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent),
  82. action: null,
  83. line: precedingUnIgnoredLine
  84. };
  85. }
  86. else {
  87. // precedingUnIgnoredLine can not be ignored.
  88. // it doesn't increase indent of following lines
  89. // it doesn't increase just next line
  90. // so current line is not affect by precedingUnIgnoredLine
  91. // and then we should get a correct inheritted indentation from above lines
  92. if (precedingUnIgnoredLine === 1) {
  93. return {
  94. indentation: strings.getLeadingWhitespace(model.getLineContent(precedingUnIgnoredLine)),
  95. action: null,
  96. line: precedingUnIgnoredLine
  97. };
  98. }
  99. const previousLine = precedingUnIgnoredLine - 1;
  100. const previousLineIndentMetadata = indentRulesSupport.getIndentMetadata(model.getLineContent(previousLine));
  101. if (!(previousLineIndentMetadata & (1 /* IndentConsts.INCREASE_MASK */ | 2 /* IndentConsts.DECREASE_MASK */)) &&
  102. (previousLineIndentMetadata & 4 /* IndentConsts.INDENT_NEXTLINE_MASK */)) {
  103. let stopLine = 0;
  104. for (let i = previousLine - 1; i > 0; i--) {
  105. if (indentRulesSupport.shouldIndentNextLine(model.getLineContent(i))) {
  106. continue;
  107. }
  108. stopLine = i;
  109. break;
  110. }
  111. return {
  112. indentation: strings.getLeadingWhitespace(model.getLineContent(stopLine + 1)),
  113. action: null,
  114. line: stopLine + 1
  115. };
  116. }
  117. if (honorIntentialIndent) {
  118. return {
  119. indentation: strings.getLeadingWhitespace(model.getLineContent(precedingUnIgnoredLine)),
  120. action: null,
  121. line: precedingUnIgnoredLine
  122. };
  123. }
  124. else {
  125. // search from precedingUnIgnoredLine until we find one whose indent is not temporary
  126. for (let i = precedingUnIgnoredLine; i > 0; i--) {
  127. const lineContent = model.getLineContent(i);
  128. if (indentRulesSupport.shouldIncrease(lineContent)) {
  129. return {
  130. indentation: strings.getLeadingWhitespace(lineContent),
  131. action: IndentAction.Indent,
  132. line: i
  133. };
  134. }
  135. else if (indentRulesSupport.shouldIndentNextLine(lineContent)) {
  136. let stopLine = 0;
  137. for (let j = i - 1; j > 0; j--) {
  138. if (indentRulesSupport.shouldIndentNextLine(model.getLineContent(i))) {
  139. continue;
  140. }
  141. stopLine = j;
  142. break;
  143. }
  144. return {
  145. indentation: strings.getLeadingWhitespace(model.getLineContent(stopLine + 1)),
  146. action: null,
  147. line: stopLine + 1
  148. };
  149. }
  150. else if (indentRulesSupport.shouldDecrease(lineContent)) {
  151. return {
  152. indentation: strings.getLeadingWhitespace(lineContent),
  153. action: null,
  154. line: i
  155. };
  156. }
  157. }
  158. return {
  159. indentation: strings.getLeadingWhitespace(model.getLineContent(1)),
  160. action: null,
  161. line: 1
  162. };
  163. }
  164. }
  165. }
  166. export function getGoodIndentForLine(autoIndent, virtualModel, languageId, lineNumber, indentConverter, languageConfigurationService) {
  167. if (autoIndent < 4 /* EditorAutoIndentStrategy.Full */) {
  168. return null;
  169. }
  170. const richEditSupport = languageConfigurationService.getLanguageConfiguration(languageId);
  171. if (!richEditSupport) {
  172. return null;
  173. }
  174. const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport;
  175. if (!indentRulesSupport) {
  176. return null;
  177. }
  178. const indent = getInheritIndentForLine(autoIndent, virtualModel, lineNumber, undefined, languageConfigurationService);
  179. const lineContent = virtualModel.getLineContent(lineNumber);
  180. if (indent) {
  181. const inheritLine = indent.line;
  182. if (inheritLine !== undefined) {
  183. const enterResult = richEditSupport.onEnter(autoIndent, '', virtualModel.getLineContent(inheritLine), '');
  184. if (enterResult) {
  185. let indentation = strings.getLeadingWhitespace(virtualModel.getLineContent(inheritLine));
  186. if (enterResult.removeText) {
  187. indentation = indentation.substring(0, indentation.length - enterResult.removeText);
  188. }
  189. if ((enterResult.indentAction === IndentAction.Indent) ||
  190. (enterResult.indentAction === IndentAction.IndentOutdent)) {
  191. indentation = indentConverter.shiftIndent(indentation);
  192. }
  193. else if (enterResult.indentAction === IndentAction.Outdent) {
  194. indentation = indentConverter.unshiftIndent(indentation);
  195. }
  196. if (indentRulesSupport.shouldDecrease(lineContent)) {
  197. indentation = indentConverter.unshiftIndent(indentation);
  198. }
  199. if (enterResult.appendText) {
  200. indentation += enterResult.appendText;
  201. }
  202. return strings.getLeadingWhitespace(indentation);
  203. }
  204. }
  205. if (indentRulesSupport.shouldDecrease(lineContent)) {
  206. if (indent.action === IndentAction.Indent) {
  207. return indent.indentation;
  208. }
  209. else {
  210. return indentConverter.unshiftIndent(indent.indentation);
  211. }
  212. }
  213. else {
  214. if (indent.action === IndentAction.Indent) {
  215. return indentConverter.shiftIndent(indent.indentation);
  216. }
  217. else {
  218. return indent.indentation;
  219. }
  220. }
  221. }
  222. return null;
  223. }
  224. export function getIndentForEnter(autoIndent, model, range, indentConverter, languageConfigurationService) {
  225. if (autoIndent < 4 /* EditorAutoIndentStrategy.Full */) {
  226. return null;
  227. }
  228. model.tokenization.forceTokenization(range.startLineNumber);
  229. const lineTokens = model.tokenization.getLineTokens(range.startLineNumber);
  230. const scopedLineTokens = createScopedLineTokens(lineTokens, range.startColumn - 1);
  231. const scopedLineText = scopedLineTokens.getLineContent();
  232. let embeddedLanguage = false;
  233. let beforeEnterText;
  234. if (scopedLineTokens.firstCharOffset > 0 && lineTokens.getLanguageId(0) !== scopedLineTokens.languageId) {
  235. // we are in the embeded language content
  236. embeddedLanguage = true; // if embeddedLanguage is true, then we don't touch the indentation of current line
  237. beforeEnterText = scopedLineText.substr(0, range.startColumn - 1 - scopedLineTokens.firstCharOffset);
  238. }
  239. else {
  240. beforeEnterText = lineTokens.getLineContent().substring(0, range.startColumn - 1);
  241. }
  242. let afterEnterText;
  243. if (range.isEmpty()) {
  244. afterEnterText = scopedLineText.substr(range.startColumn - 1 - scopedLineTokens.firstCharOffset);
  245. }
  246. else {
  247. const endScopedLineTokens = getScopedLineTokens(model, range.endLineNumber, range.endColumn);
  248. afterEnterText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset);
  249. }
  250. const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(scopedLineTokens.languageId).indentRulesSupport;
  251. if (!indentRulesSupport) {
  252. return null;
  253. }
  254. const beforeEnterResult = beforeEnterText;
  255. const beforeEnterIndent = strings.getLeadingWhitespace(beforeEnterText);
  256. const virtualModel = {
  257. tokenization: {
  258. getLineTokens: (lineNumber) => {
  259. return model.tokenization.getLineTokens(lineNumber);
  260. },
  261. getLanguageId: () => {
  262. return model.getLanguageId();
  263. },
  264. getLanguageIdAtPosition: (lineNumber, column) => {
  265. return model.getLanguageIdAtPosition(lineNumber, column);
  266. },
  267. },
  268. getLineContent: (lineNumber) => {
  269. if (lineNumber === range.startLineNumber) {
  270. return beforeEnterResult;
  271. }
  272. else {
  273. return model.getLineContent(lineNumber);
  274. }
  275. }
  276. };
  277. const currentLineIndent = strings.getLeadingWhitespace(lineTokens.getLineContent());
  278. const afterEnterAction = getInheritIndentForLine(autoIndent, virtualModel, range.startLineNumber + 1, undefined, languageConfigurationService);
  279. if (!afterEnterAction) {
  280. const beforeEnter = embeddedLanguage ? currentLineIndent : beforeEnterIndent;
  281. return {
  282. beforeEnter: beforeEnter,
  283. afterEnter: beforeEnter
  284. };
  285. }
  286. let afterEnterIndent = embeddedLanguage ? currentLineIndent : afterEnterAction.indentation;
  287. if (afterEnterAction.action === IndentAction.Indent) {
  288. afterEnterIndent = indentConverter.shiftIndent(afterEnterIndent);
  289. }
  290. if (indentRulesSupport.shouldDecrease(afterEnterText)) {
  291. afterEnterIndent = indentConverter.unshiftIndent(afterEnterIndent);
  292. }
  293. return {
  294. beforeEnter: embeddedLanguage ? currentLineIndent : beforeEnterIndent,
  295. afterEnter: afterEnterIndent
  296. };
  297. }
  298. /**
  299. * We should always allow intentional indentation. It means, if users change the indentation of `lineNumber` and the content of
  300. * this line doesn't match decreaseIndentPattern, we should not adjust the indentation.
  301. */
  302. export function getIndentActionForType(autoIndent, model, range, ch, indentConverter, languageConfigurationService) {
  303. if (autoIndent < 4 /* EditorAutoIndentStrategy.Full */) {
  304. return null;
  305. }
  306. const scopedLineTokens = getScopedLineTokens(model, range.startLineNumber, range.startColumn);
  307. if (scopedLineTokens.firstCharOffset) {
  308. // this line has mixed languages and indentation rules will not work
  309. return null;
  310. }
  311. const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(scopedLineTokens.languageId).indentRulesSupport;
  312. if (!indentRulesSupport) {
  313. return null;
  314. }
  315. const scopedLineText = scopedLineTokens.getLineContent();
  316. const beforeTypeText = scopedLineText.substr(0, range.startColumn - 1 - scopedLineTokens.firstCharOffset);
  317. // selection support
  318. let afterTypeText;
  319. if (range.isEmpty()) {
  320. afterTypeText = scopedLineText.substr(range.startColumn - 1 - scopedLineTokens.firstCharOffset);
  321. }
  322. else {
  323. const endScopedLineTokens = getScopedLineTokens(model, range.endLineNumber, range.endColumn);
  324. afterTypeText = endScopedLineTokens.getLineContent().substr(range.endColumn - 1 - scopedLineTokens.firstCharOffset);
  325. }
  326. // If previous content already matches decreaseIndentPattern, it means indentation of this line should already be adjusted
  327. // Users might change the indentation by purpose and we should honor that instead of readjusting.
  328. if (!indentRulesSupport.shouldDecrease(beforeTypeText + afterTypeText) && indentRulesSupport.shouldDecrease(beforeTypeText + ch + afterTypeText)) {
  329. // after typing `ch`, the content matches decreaseIndentPattern, we should adjust the indent to a good manner.
  330. // 1. Get inherited indent action
  331. const r = getInheritIndentForLine(autoIndent, model, range.startLineNumber, false, languageConfigurationService);
  332. if (!r) {
  333. return null;
  334. }
  335. let indentation = r.indentation;
  336. if (r.action !== IndentAction.Indent) {
  337. indentation = indentConverter.unshiftIndent(indentation);
  338. }
  339. return indentation;
  340. }
  341. return null;
  342. }
  343. export function getIndentMetadata(model, lineNumber, languageConfigurationService) {
  344. const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentRulesSupport;
  345. if (!indentRulesSupport) {
  346. return null;
  347. }
  348. if (lineNumber < 1 || lineNumber > model.getLineCount()) {
  349. return null;
  350. }
  351. return indentRulesSupport.getIndentMetadata(model.getLineContent(lineNumber));
  352. }