db7166e6bf42d04e80c1f716f68df7b2697360538a0fa045525e2bb1cd397e714949605e268426d4e72b62ae05767ef5edafdecc14f879e683f0b2e0ddc6ad 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  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 * as stringBuilder from '../../core/stringBuilder.js';
  7. import { Range } from '../../core/range.js';
  8. /**
  9. * Represents a grouping of colliding bracket pairs.
  10. *
  11. * Most of the times this contains a single bracket pair,
  12. * but sometimes this contains multiple bracket pairs in cases
  13. * where the same string appears as a closing bracket for multiple
  14. * bracket pairs, or the same string appears an opening bracket for
  15. * multiple bracket pairs.
  16. *
  17. * e.g. of a group containing a single pair:
  18. * open: ['{'], close: ['}']
  19. *
  20. * e.g. of a group containing multiple pairs:
  21. * open: ['if', 'for'], close: ['end', 'end']
  22. */
  23. export class RichEditBracket {
  24. constructor(languageId, index, open, close, forwardRegex, reversedRegex) {
  25. this._richEditBracketBrand = undefined;
  26. this.languageId = languageId;
  27. this.index = index;
  28. this.open = open;
  29. this.close = close;
  30. this.forwardRegex = forwardRegex;
  31. this.reversedRegex = reversedRegex;
  32. this._openSet = RichEditBracket._toSet(this.open);
  33. this._closeSet = RichEditBracket._toSet(this.close);
  34. }
  35. /**
  36. * Check if the provided `text` is an open bracket in this group.
  37. */
  38. isOpen(text) {
  39. return this._openSet.has(text);
  40. }
  41. /**
  42. * Check if the provided `text` is a close bracket in this group.
  43. */
  44. isClose(text) {
  45. return this._closeSet.has(text);
  46. }
  47. static _toSet(arr) {
  48. const result = new Set();
  49. for (const element of arr) {
  50. result.add(element);
  51. }
  52. return result;
  53. }
  54. }
  55. /**
  56. * Groups together brackets that have equal open or close sequences.
  57. *
  58. * For example, if the following brackets are defined:
  59. * ['IF','END']
  60. * ['for','end']
  61. * ['{','}']
  62. *
  63. * Then the grouped brackets would be:
  64. * { open: ['if', 'for'], close: ['end', 'end'] }
  65. * { open: ['{'], close: ['}'] }
  66. *
  67. */
  68. function groupFuzzyBrackets(brackets) {
  69. const N = brackets.length;
  70. brackets = brackets.map(b => [b[0].toLowerCase(), b[1].toLowerCase()]);
  71. const group = [];
  72. for (let i = 0; i < N; i++) {
  73. group[i] = i;
  74. }
  75. const areOverlapping = (a, b) => {
  76. const [aOpen, aClose] = a;
  77. const [bOpen, bClose] = b;
  78. return (aOpen === bOpen || aOpen === bClose || aClose === bOpen || aClose === bClose);
  79. };
  80. const mergeGroups = (g1, g2) => {
  81. const newG = Math.min(g1, g2);
  82. const oldG = Math.max(g1, g2);
  83. for (let i = 0; i < N; i++) {
  84. if (group[i] === oldG) {
  85. group[i] = newG;
  86. }
  87. }
  88. };
  89. // group together brackets that have the same open or the same close sequence
  90. for (let i = 0; i < N; i++) {
  91. const a = brackets[i];
  92. for (let j = i + 1; j < N; j++) {
  93. const b = brackets[j];
  94. if (areOverlapping(a, b)) {
  95. mergeGroups(group[i], group[j]);
  96. }
  97. }
  98. }
  99. const result = [];
  100. for (let g = 0; g < N; g++) {
  101. const currentOpen = [];
  102. const currentClose = [];
  103. for (let i = 0; i < N; i++) {
  104. if (group[i] === g) {
  105. const [open, close] = brackets[i];
  106. currentOpen.push(open);
  107. currentClose.push(close);
  108. }
  109. }
  110. if (currentOpen.length > 0) {
  111. result.push({
  112. open: currentOpen,
  113. close: currentClose
  114. });
  115. }
  116. }
  117. return result;
  118. }
  119. export class RichEditBrackets {
  120. constructor(languageId, _brackets) {
  121. this._richEditBracketsBrand = undefined;
  122. const brackets = groupFuzzyBrackets(_brackets);
  123. this.brackets = brackets.map((b, index) => {
  124. return new RichEditBracket(languageId, index, b.open, b.close, getRegexForBracketPair(b.open, b.close, brackets, index), getReversedRegexForBracketPair(b.open, b.close, brackets, index));
  125. });
  126. this.forwardRegex = getRegexForBrackets(this.brackets);
  127. this.reversedRegex = getReversedRegexForBrackets(this.brackets);
  128. this.textIsBracket = {};
  129. this.textIsOpenBracket = {};
  130. this.maxBracketLength = 0;
  131. for (const bracket of this.brackets) {
  132. for (const open of bracket.open) {
  133. this.textIsBracket[open] = bracket;
  134. this.textIsOpenBracket[open] = true;
  135. this.maxBracketLength = Math.max(this.maxBracketLength, open.length);
  136. }
  137. for (const close of bracket.close) {
  138. this.textIsBracket[close] = bracket;
  139. this.textIsOpenBracket[close] = false;
  140. this.maxBracketLength = Math.max(this.maxBracketLength, close.length);
  141. }
  142. }
  143. }
  144. }
  145. function collectSuperstrings(str, brackets, currentIndex, dest) {
  146. for (let i = 0, len = brackets.length; i < len; i++) {
  147. if (i === currentIndex) {
  148. continue;
  149. }
  150. const bracket = brackets[i];
  151. for (const open of bracket.open) {
  152. if (open.indexOf(str) >= 0) {
  153. dest.push(open);
  154. }
  155. }
  156. for (const close of bracket.close) {
  157. if (close.indexOf(str) >= 0) {
  158. dest.push(close);
  159. }
  160. }
  161. }
  162. }
  163. function lengthcmp(a, b) {
  164. return a.length - b.length;
  165. }
  166. function unique(arr) {
  167. if (arr.length <= 1) {
  168. return arr;
  169. }
  170. const result = [];
  171. const seen = new Set();
  172. for (const element of arr) {
  173. if (seen.has(element)) {
  174. continue;
  175. }
  176. result.push(element);
  177. seen.add(element);
  178. }
  179. return result;
  180. }
  181. /**
  182. * Create a regular expression that can be used to search forward in a piece of text
  183. * for a group of bracket pairs. But this regex must be built in a way in which
  184. * it is aware of the other bracket pairs defined for the language.
  185. *
  186. * For example, if a language contains the following bracket pairs:
  187. * ['begin', 'end']
  188. * ['if', 'end if']
  189. * The two bracket pairs do not collide because no open or close brackets are equal.
  190. * So the function getRegexForBracketPair is called twice, once with
  191. * the ['begin'], ['end'] group consisting of one bracket pair, and once with
  192. * the ['if'], ['end if'] group consiting of the other bracket pair.
  193. *
  194. * But there could be a situation where an occurrence of 'end if' is mistaken
  195. * for an occurrence of 'end'.
  196. *
  197. * Therefore, for the bracket pair ['begin', 'end'], the regex will also
  198. * target 'end if'. The regex will be something like:
  199. * /(\bend if\b)|(\bend\b)|(\bif\b)/
  200. *
  201. * The regex also searches for "superstrings" (other brackets that might be mistaken with the current bracket).
  202. *
  203. */
  204. function getRegexForBracketPair(open, close, brackets, currentIndex) {
  205. // search in all brackets for other brackets that are a superstring of these brackets
  206. let pieces = [];
  207. pieces = pieces.concat(open);
  208. pieces = pieces.concat(close);
  209. for (let i = 0, len = pieces.length; i < len; i++) {
  210. collectSuperstrings(pieces[i], brackets, currentIndex, pieces);
  211. }
  212. pieces = unique(pieces);
  213. pieces.sort(lengthcmp);
  214. pieces.reverse();
  215. return createBracketOrRegExp(pieces);
  216. }
  217. /**
  218. * Matching a regular expression in JS can only be done "forwards". So JS offers natively only
  219. * methods to find the first match of a regex in a string. But sometimes, it is useful to
  220. * find the last match of a regex in a string. For such a situation, a nice solution is to
  221. * simply reverse the string and then search for a reversed regex.
  222. *
  223. * This function also has the fine details of `getRegexForBracketPair`. For the same example
  224. * given above, the regex produced here would look like:
  225. * /(\bfi dne\b)|(\bdne\b)|(\bfi\b)/
  226. */
  227. function getReversedRegexForBracketPair(open, close, brackets, currentIndex) {
  228. // search in all brackets for other brackets that are a superstring of these brackets
  229. let pieces = [];
  230. pieces = pieces.concat(open);
  231. pieces = pieces.concat(close);
  232. for (let i = 0, len = pieces.length; i < len; i++) {
  233. collectSuperstrings(pieces[i], brackets, currentIndex, pieces);
  234. }
  235. pieces = unique(pieces);
  236. pieces.sort(lengthcmp);
  237. pieces.reverse();
  238. return createBracketOrRegExp(pieces.map(toReversedString));
  239. }
  240. /**
  241. * Creates a regular expression that targets all bracket pairs.
  242. *
  243. * e.g. for the bracket pairs:
  244. * ['{','}']
  245. * ['begin,'end']
  246. * ['for','end']
  247. * the regex would look like:
  248. * /(\{)|(\})|(\bbegin\b)|(\bend\b)|(\bfor\b)/
  249. */
  250. function getRegexForBrackets(brackets) {
  251. let pieces = [];
  252. for (const bracket of brackets) {
  253. for (const open of bracket.open) {
  254. pieces.push(open);
  255. }
  256. for (const close of bracket.close) {
  257. pieces.push(close);
  258. }
  259. }
  260. pieces = unique(pieces);
  261. return createBracketOrRegExp(pieces);
  262. }
  263. /**
  264. * Matching a regular expression in JS can only be done "forwards". So JS offers natively only
  265. * methods to find the first match of a regex in a string. But sometimes, it is useful to
  266. * find the last match of a regex in a string. For such a situation, a nice solution is to
  267. * simply reverse the string and then search for a reversed regex.
  268. *
  269. * e.g. for the bracket pairs:
  270. * ['{','}']
  271. * ['begin,'end']
  272. * ['for','end']
  273. * the regex would look like:
  274. * /(\{)|(\})|(\bnigeb\b)|(\bdne\b)|(\brof\b)/
  275. */
  276. function getReversedRegexForBrackets(brackets) {
  277. let pieces = [];
  278. for (const bracket of brackets) {
  279. for (const open of bracket.open) {
  280. pieces.push(open);
  281. }
  282. for (const close of bracket.close) {
  283. pieces.push(close);
  284. }
  285. }
  286. pieces = unique(pieces);
  287. return createBracketOrRegExp(pieces.map(toReversedString));
  288. }
  289. function prepareBracketForRegExp(str) {
  290. // This bracket pair uses letters like e.g. "begin" - "end"
  291. const insertWordBoundaries = (/^[\w ]+$/.test(str));
  292. str = strings.escapeRegExpCharacters(str);
  293. return (insertWordBoundaries ? `\\b${str}\\b` : str);
  294. }
  295. function createBracketOrRegExp(pieces) {
  296. const regexStr = `(${pieces.map(prepareBracketForRegExp).join(')|(')})`;
  297. return strings.createRegExp(regexStr, true);
  298. }
  299. const toReversedString = (function () {
  300. function reverse(str) {
  301. if (stringBuilder.hasTextDecoder) {
  302. // create a Uint16Array and then use a TextDecoder to create a string
  303. const arr = new Uint16Array(str.length);
  304. let offset = 0;
  305. for (let i = str.length - 1; i >= 0; i--) {
  306. arr[offset++] = str.charCodeAt(i);
  307. }
  308. return stringBuilder.getPlatformTextDecoder().decode(arr);
  309. }
  310. else {
  311. const result = [];
  312. let resultLen = 0;
  313. for (let i = str.length - 1; i >= 0; i--) {
  314. result[resultLen++] = str.charAt(i);
  315. }
  316. return result.join('');
  317. }
  318. }
  319. let lastInput = null;
  320. let lastOutput = null;
  321. return function toReversedString(str) {
  322. if (lastInput !== str) {
  323. lastInput = str;
  324. lastOutput = reverse(lastInput);
  325. }
  326. return lastOutput;
  327. };
  328. })();
  329. export class BracketsUtils {
  330. static _findPrevBracketInText(reversedBracketRegex, lineNumber, reversedText, offset) {
  331. const m = reversedText.match(reversedBracketRegex);
  332. if (!m) {
  333. return null;
  334. }
  335. const matchOffset = reversedText.length - (m.index || 0);
  336. const matchLength = m[0].length;
  337. const absoluteMatchOffset = offset + matchOffset;
  338. return new Range(lineNumber, absoluteMatchOffset - matchLength + 1, lineNumber, absoluteMatchOffset + 1);
  339. }
  340. static findPrevBracketInRange(reversedBracketRegex, lineNumber, lineText, startOffset, endOffset) {
  341. // Because JS does not support backwards regex search, we search forwards in a reversed string with a reversed regex ;)
  342. const reversedLineText = toReversedString(lineText);
  343. const reversedSubstr = reversedLineText.substring(lineText.length - endOffset, lineText.length - startOffset);
  344. return this._findPrevBracketInText(reversedBracketRegex, lineNumber, reversedSubstr, startOffset);
  345. }
  346. static findNextBracketInText(bracketRegex, lineNumber, text, offset) {
  347. const m = text.match(bracketRegex);
  348. if (!m) {
  349. return null;
  350. }
  351. const matchOffset = m.index || 0;
  352. const matchLength = m[0].length;
  353. if (matchLength === 0) {
  354. return null;
  355. }
  356. const absoluteMatchOffset = offset + matchOffset;
  357. return new Range(lineNumber, absoluteMatchOffset + 1, lineNumber, absoluteMatchOffset + 1 + matchLength);
  358. }
  359. static findNextBracketInRange(bracketRegex, lineNumber, lineText, startOffset, endOffset) {
  360. const substr = lineText.substring(startOffset, endOffset);
  361. return this.findNextBracketInText(bracketRegex, lineNumber, substr, startOffset);
  362. }
  363. }