PageRenderTime 61ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/com.hangum.db.browser.rap.core/orion/orion/editor/textMateStyler.js

https://github.com/tadpolefordbtools/TadpoleForDBTools
JavaScript | 1322 lines | 968 code | 50 blank | 304 comment | 243 complexity | 6ecd6f6fc5bd4c98b6b84e622e99dd6e MD5 | raw file
  1. /*******************************************************************************
  2. * Copyright (c) 2011 IBM Corporation and others.
  3. * All rights reserved. This program and the accompanying materials are made
  4. * available under the terms of the Eclipse Public License v1.0
  5. * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
  6. * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
  7. *
  8. * Contributors: IBM Corporation - initial API and implementation
  9. ******************************************************************************/
  10. /*jslint regexp:false laxbreak:true*/
  11. /*global define dojo window*/
  12. var orion = orion || {};
  13. orion.editor = orion.editor || {};
  14. /**
  15. * A styler that does nothing, but can be extended by concrete stylers. Extenders can call
  16. * {@link orion.editor.AbstractStyler.extend} and provide their own {@link #_onSelection},
  17. * {@link #_onModelChanged}, {@link #_onDestroy} and {@link #_onLineStyle} methods.
  18. * @class orion.editor.AbstractStyler
  19. */
  20. orion.editor.AbstractStyler = (function() {
  21. /** @inner */
  22. function AbstractStyler() {
  23. }
  24. AbstractStyler.prototype = /** @lends orion.editor.AbstractStyler.prototype */ {
  25. /**
  26. * Initializes this styler with a textView. Extenders <b>must</b> call this from their constructor.
  27. * @param {orion.textview.TextView} textView
  28. */
  29. initialize: function(textView) {
  30. this.textView = textView;
  31. textView.addEventListener("Selection", this, this._onSelection);
  32. textView.addEventListener("ModelChanged", this, this._onModelChanged);
  33. textView.addEventListener("Destroy", this, this._onDestroy);
  34. textView.addEventListener("LineStyle", this, this._onLineStyle);
  35. textView.redrawLines();
  36. },
  37. /**
  38. * Destroys this styler and removes all listeners. Called by the editor.
  39. */
  40. destroy: function() {
  41. if (this.textView) {
  42. this.textView.removeEventListener("Selection", this, this._onSelection);
  43. this.textView.removeEventListener("ModelChanged", this, this._onModelChanged);
  44. this.textView.removeEventListener("Destroy", this, this._onDestroy);
  45. this.textView.removeEventListener("LineStyle", this, this._onLineStyle);
  46. this.textView = null;
  47. }
  48. },
  49. /** To be overridden by subclass.
  50. * @public
  51. */
  52. _onSelection: function(/**eclipse.SelectionEvent*/ e) {},
  53. /** To be overridden by subclass.
  54. * @public
  55. */
  56. _onModelChanged: function(/**eclipse.ModelChangedEvent*/ e) {},
  57. /** To be overridden by subclass.
  58. * @public
  59. */
  60. _onDestroy: function(/**eclipse.DestroyEvent*/ e) {},
  61. /** To be overridden by subclass.
  62. * @public
  63. */
  64. _onLineStyle: function(/**eclipse.LineStyleEvent*/ e) {}
  65. };
  66. return AbstractStyler;
  67. }());
  68. /**
  69. * Helper for extending AbstractStyler.
  70. * @methodOf orion.editor.AbstractStyler
  71. * @static
  72. * @param {Function} subCtor The constructor function for the subclass.
  73. * @param {Object} [proto] Object to be mixed into the subclass's prototype. This object can contain your
  74. * implementation of _onSelection, _onModelChanged, etc.
  75. * @see orion.editor.TextMateStyler for example usage.
  76. */
  77. orion.editor.AbstractStyler.extend = function(subCtor, proto) {
  78. if (typeof(subCtor) !== "function") { throw new Error("Function expected"); }
  79. subCtor.prototype = new orion.editor.AbstractStyler();
  80. subCtor.constructor = subCtor;
  81. for (var p in proto) {
  82. if (proto.hasOwnProperty(p)) { subCtor.prototype[p] = proto[p]; }
  83. }
  84. };
  85. orion.editor.RegexUtil = {
  86. // Rules to detect some unsupported Oniguruma features
  87. unsupported: [
  88. {regex: /\(\?[ims\-]:/, func: function(match) { return "option on/off for subexp"; }},
  89. {regex: /\(\?<([=!])/, func: function(match) { return (match[1] === "=") ? "lookbehind" : "negative lookbehind"; }},
  90. {regex: /\(\?>/, func: function(match) { return "atomic group"; }}
  91. ],
  92. /**
  93. * @param {String} str String giving a regular expression pattern from a TextMate grammar.
  94. * @param {String} [flags] [ismg]+
  95. * @returns {RegExp}
  96. */
  97. toRegExp: function(str) {
  98. function fail(feature, match) {
  99. throw new Error("Unsupported regex feature \"" + feature + "\": \"" + match[0] + "\" at index: "
  100. + match.index + " in " + match.input);
  101. }
  102. function getMatchingCloseParen(str, start) {
  103. var depth = 0,
  104. len = str.length,
  105. xStop = -1;
  106. for (var i=start; i < len && xStop === -1; i++) {
  107. switch (str[i]) {
  108. case "\\":
  109. i += 1; // skip next char
  110. break;
  111. case "(":
  112. depth++;
  113. break;
  114. case ")":
  115. depth--;
  116. if (depth === 0) {
  117. xStop = i;
  118. }
  119. break;
  120. }
  121. }
  122. return xStop;
  123. }
  124. // Turns an extended regex into a normal one
  125. function normalize(/**String*/ str) {
  126. var result = "";
  127. var insideCharacterClass = false;
  128. var len = str.length;
  129. for (var i=0; i < len; ) {
  130. var chr = str[i];
  131. if (!insideCharacterClass && chr === "#") {
  132. // skip to eol
  133. while (i < len && chr !== "\r" && chr !== "\n") {
  134. chr = str[++i];
  135. }
  136. } else if (!insideCharacterClass && /\s/.test(chr)) {
  137. // skip whitespace
  138. while (i < len && /\s/.test(chr)) {
  139. chr = str[++i];
  140. }
  141. } else if (chr === "\\") {
  142. result += chr;
  143. if (!/\s/.test(str[i+1])) {
  144. result += str[i+1];
  145. i += 1;
  146. }
  147. i += 1;
  148. } else if (chr === "[") {
  149. insideCharacterClass = true;
  150. result += chr;
  151. i += 1;
  152. } else if (chr === "]") {
  153. insideCharacterClass = false;
  154. result += chr;
  155. i += 1;
  156. } else {
  157. result += chr;
  158. i += 1;
  159. }
  160. }
  161. return result;
  162. }
  163. var flags = "";
  164. var i;
  165. // Check for unsupported syntax
  166. for (i=0; i < this.unsupported.length; i++) {
  167. var match;
  168. if ((match = this.unsupported[i].regex.exec(str))) {
  169. fail(this.unsupported[i].func(match));
  170. }
  171. }
  172. // Deal with "x" flag (whitespace/comments)
  173. if (str.substring(0, 4) === "(?x)") {
  174. // Leading (?x) term (means "x" flag applies to entire regex)
  175. str = normalize(str.substring(4));
  176. } else if (str.substring(0, 4) === "(?x:") {
  177. // Regex wrapped in a (?x: ...) -- again "x" applies to entire regex
  178. var xStop = getMatchingCloseParen(str, 0);
  179. if (xStop < str.length-1) {
  180. throw new Error("Only a (?x:) group that encloses the entire regex is supported: " + str);
  181. }
  182. str = normalize(str.substring(4, xStop));
  183. }
  184. // TODO: tolerate /(?iSubExp)/ -- eg. in PHP grammar (trickier)
  185. return new RegExp(str, flags);
  186. },
  187. hasBackReference: function(/**RegExp*/ regex) {
  188. return (/\\\d+/).test(regex.source);
  189. },
  190. /** @returns {RegExp} A regex made by substituting any backreferences in <tt>regex</tt> for the value of the property
  191. * in <tt>sub</tt> with the same name as the backreferenced group number. */
  192. getSubstitutedRegex: function(/**RegExp*/ regex, /**Object*/ sub, /**Boolean*/ escape) {
  193. escape = (typeof escape === "undefined") ? true : false;
  194. var exploded = regex.source.split(/(\\\d+)/g);
  195. var array = [];
  196. for (var i=0; i < exploded.length; i++) {
  197. var term = exploded[i];
  198. var backrefMatch = /\\(\d+)/.exec(term);
  199. if (backrefMatch) {
  200. var text = sub[backrefMatch[1]] || "";
  201. array.push(escape ? orion.editor.RegexUtil.escapeRegex(text) : text);
  202. } else {
  203. array.push(term);
  204. }
  205. }
  206. return new RegExp(array.join(""));
  207. },
  208. /** @returns {String} The input string with regex special characters escaped. */
  209. escapeRegex: function(/**String*/ str) {
  210. return str.replace(/([\\$\^*\/+?\.\(\)|{}\[\]])/g, "\\$&");
  211. },
  212. /**
  213. * Builds a version of <tt>regex</tt> with every non-capturing term converted into a capturing group. This is a workaround
  214. * for JavaScript's lack of API to get the index at which a matched group begins in the input string.<p>
  215. * Using the "groupified" regex, we can sum the lengths of matches from <i>consuming groups</i> 1..n-1 to obtain the
  216. * starting index of group n. (A consuming group is a capturing group that is not inside a lookahead assertion).</p>
  217. * Example: groupify(/(a+)x+(b+)/) === /(a+)(x+)(b+)/<br />
  218. * Example: groupify(/(?:x+(a+))b+/) === /(?:(x+)(a+))(b+)/
  219. * @param {RegExp} regex The regex to groupify.
  220. * @param {Object} [backRefOld2NewMap] Optional. If provided, the backreference numbers in regex will be updated using the
  221. * properties of this object rather than the new group numbers of regex itself.
  222. * <ul><li>[0] {RegExp} The groupified version of the input regex.</li>
  223. * <li>[1] {Object} A map containing old-group to new-group info. Each property is a capturing group number of <tt>regex</tt>
  224. * and its value is the corresponding capturing group number of [0].</li>
  225. * <li>[2] {Object} A map indicating which capturing groups of [0] are also consuming groups. If a group number is found
  226. * as a property in this object, then it's a consuming group.</li></ul>
  227. */
  228. groupify: function(regex, backRefOld2NewMap) {
  229. var NON_CAPTURING = 1,
  230. CAPTURING = 2,
  231. LOOKAHEAD = 3,
  232. NEW_CAPTURING = 4;
  233. var src = regex.source,
  234. len = src.length;
  235. var groups = [],
  236. lookaheadDepth = 0,
  237. newGroups = [],
  238. oldGroupNumber = 1,
  239. newGroupNumber = 1;
  240. var result = [],
  241. old2New = {},
  242. consuming = {};
  243. for (var i=0; i < len; i++) {
  244. var curGroup = groups[groups.length-1];
  245. var chr = src[i];
  246. switch (chr) {
  247. case "(":
  248. // If we're in new capturing group, close it since ( signals end-of-term
  249. if (curGroup === NEW_CAPTURING) {
  250. groups.pop();
  251. result.push(")");
  252. newGroups[newGroups.length-1].end = i;
  253. }
  254. var peek2 = (i + 2 < len) ? (src[i+1] + "" + src[i+2]) : null;
  255. if (peek2 === "?:" || peek2 === "?=" || peek2 === "?!") {
  256. // Found non-capturing group or lookahead assertion. Note that we preserve non-capturing groups
  257. // as such, but any term inside them will become a new capturing group (unless it happens to
  258. // also be inside a lookahead).
  259. var groupType;
  260. if (peek2 === "?:") {
  261. groupType = NON_CAPTURING;
  262. } else {
  263. groupType = LOOKAHEAD;
  264. lookaheadDepth++;
  265. }
  266. groups.push(groupType);
  267. newGroups.push({ start: i, end: -1, type: groupType /*non capturing*/ });
  268. result.push(chr);
  269. result.push(peek2);
  270. i += peek2.length;
  271. } else {
  272. groups.push(CAPTURING);
  273. newGroups.push({ start: i, end: -1, type: CAPTURING, oldNum: oldGroupNumber, num: newGroupNumber });
  274. result.push(chr);
  275. if (lookaheadDepth === 0) {
  276. consuming[newGroupNumber] = null;
  277. }
  278. old2New[oldGroupNumber] = newGroupNumber;
  279. oldGroupNumber++;
  280. newGroupNumber++;
  281. }
  282. break;
  283. case ")":
  284. var group = groups.pop();
  285. if (group === LOOKAHEAD) { lookaheadDepth--; }
  286. newGroups[newGroups.length-1].end = i;
  287. result.push(chr);
  288. break;
  289. case "*":
  290. case "+":
  291. case "?":
  292. case "}":
  293. // Unary operator. If it's being applied to a capturing group, we need to add a new capturing group
  294. // enclosing the pair
  295. var op = chr;
  296. var prev = src[i-1],
  297. prevIndex = i-1;
  298. if (chr === "}") {
  299. for (var j=i-1; src[j] !== "{" && j >= 0; j--) {}
  300. prev = src[j-1];
  301. prevIndex = j-1;
  302. op = src.substring(j, i+1);
  303. }
  304. var lastGroup = newGroups[newGroups.length-1];
  305. if (prev === ")" && (lastGroup.type === CAPTURING || lastGroup.type === NEW_CAPTURING)) {
  306. // Shove in the new group's (, increment num/start in from [lastGroup.start .. end]
  307. result.splice(lastGroup.start, 0, "(");
  308. result.push(op);
  309. result.push(")");
  310. var newGroup = { start: lastGroup.start, end: result.length-1, type: NEW_CAPTURING, num: lastGroup.num };
  311. for (var k=0; k < newGroups.length; k++) {
  312. group = newGroups[k];
  313. if (group.type === CAPTURING || group.type === NEW_CAPTURING) {
  314. if (group.start >= lastGroup.start && group.end <= prevIndex) {
  315. group.start += 1;
  316. group.end += 1;
  317. group.num = group.num + 1;
  318. if (group.type === CAPTURING) {
  319. old2New[group.oldNum] = group.num;
  320. }
  321. }
  322. }
  323. }
  324. newGroups.push(newGroup);
  325. newGroupNumber++;
  326. break;
  327. } else {
  328. // Fallthrough to default
  329. }
  330. default:
  331. if (chr !== "|" && curGroup !== CAPTURING && curGroup !== NEW_CAPTURING) {
  332. // Not in a capturing group, so make a new one to hold this term.
  333. // Perf improvement: don't create the new group if we're inside a lookahead, since we don't
  334. // care about them (nothing inside a lookahead actually consumes input so we don't need it)
  335. if (lookaheadDepth === 0) {
  336. groups.push(NEW_CAPTURING);
  337. newGroups.push({ start: i, end: -1, type: NEW_CAPTURING, num: newGroupNumber });
  338. result.push("(");
  339. consuming[newGroupNumber] = null;
  340. newGroupNumber++;
  341. }
  342. }
  343. result.push(chr);
  344. if (chr === "\\") {
  345. var peek = src[i+1];
  346. // Eat next so following iteration doesn't think it's a real special character
  347. result.push(peek);
  348. i += 1;
  349. }
  350. break;
  351. }
  352. }
  353. while (groups.length) {
  354. // Close any remaining new capturing groups
  355. groups.pop();
  356. result.push(")");
  357. }
  358. var newRegex = new RegExp(result.join(""));
  359. // Update backreferences so they refer to the new group numbers. Use backRefOld2NewMap if provided
  360. var subst = {};
  361. backRefOld2NewMap = backRefOld2NewMap || old2New;
  362. for (var prop in backRefOld2NewMap) {
  363. if (backRefOld2NewMap.hasOwnProperty(prop)) {
  364. subst[prop] = "\\" + backRefOld2NewMap[prop];
  365. }
  366. }
  367. newRegex = this.getSubstitutedRegex(newRegex, subst, false);
  368. return [newRegex, old2New, consuming];
  369. },
  370. /** @returns {Boolean} True if the captures object assigns scope to a matching group other than "0". */
  371. complexCaptures: function(capturesObj) {
  372. if (!capturesObj) { return false; }
  373. for (var prop in capturesObj) {
  374. if (capturesObj.hasOwnProperty(prop)) {
  375. if (prop !== "0") {
  376. return true;
  377. }
  378. }
  379. }
  380. return false;
  381. }
  382. };
  383. /**
  384. * A styler that knows how to apply a limited subset of the TextMate grammar format to style a line.<p>
  385. *
  386. * <h4>Styling from a grammar:</h4>
  387. * Each scope name given in the grammar is converted to an array of CSS class names. For example
  388. * a region of text with scope <tt>keyword.control.php</tt> will be assigned the CSS classes
  389. * <pre>keyword, keyword-control, keyword-control-php</pre>
  390. *
  391. * A CSS file can give rules matching any of these class names to provide generic or more specific styling.
  392. * For example, <pre>.keyword { font-color: blue; }</pre> colors all keywords blue, while
  393. * <pre>.keyword-control-php { font-weight: bold; }</pre> bolds only PHP control keywords.
  394. *
  395. * This is useful when using grammars that adhere to TextMate's
  396. * <a href="http://manual.macromates.com/en/language_grammars.html#naming_conventions">scope name conventions</a>,
  397. * as a single CSS rule can provide consistent styling to similar constructs across different languages.<p>
  398. *
  399. * <h4>Supported top-level grammar features:</h4>
  400. * <ul><li><tt>fileTypes, patterns, repository</tt> (but see below) are supported.</li>
  401. * <li><tt>scopeName, firstLineMatch, foldingStartMarker, foldingStopMarker</tt> are <b>not</b> supported.</li>
  402. * </ul>
  403. *
  404. * <p>TODO update this section</p>
  405. * <del>
  406. * <h4>Supported grammar rule features:</h4>
  407. * <ul><li><tt>match</tt> patterns are supported.</li>
  408. * <li><tt>name</tt> scope is supported.</li>
  409. * <li><tt>captures</tt> is <b>not</b> supported. Any scopes given inside a <tt>captures</tt> object are not applied.</li>
  410. * <li><tt>begin/end</tt> patterns are <b>not</b> supported and are ignored, along with their subrules. Consequently,
  411. * matched constructs may <b>not</b> span multiple lines.</li>
  412. * <li><tt>contentName, beginCaptures, endCaptures, applyEndPatternLast</tt> are <b>not</b> supported.</li>
  413. * <li><tt>include</tt> is supported, but only when it references a rule in the current grammar's <tt>repository</tt>.
  414. * Including <tt>$self</tt>, <tt>$base</tt>, or <tt>rule.from.another.grammar</tt> is <b>not</b> supported.</li>
  415. * <li>The <tt>(?x)</tt> option ("extended" regex format) is supported, but only when it appears at the beginning of a regex pattern.</li>
  416. * <li>Matching is done using native JavaScript {@link RegExp}s. As a result, many Oniguruma features are <b>not</b> supported.
  417. * Unsupported features include:
  418. * <ul><li>Named captures</li>
  419. * <li>Setting flags inside groups (eg. <tt>(?i:a)b</tt>)</li>
  420. * <li>Lookbehind and negative lookbehind</li>
  421. * <li>Subexpression call</li>
  422. * <li>etc.</li>
  423. * </ul>
  424. * </li>
  425. * </ul>
  426. * </del>
  427. *
  428. * @class orion.editor.TextMateStyler
  429. * @extends orion.editor.AbstractStyler
  430. * @param {orion.textview.TextView} textView The textView.
  431. * @param {Object} grammar The TextMate grammar as a JavaScript object. You can use a plist-to-JavaScript conversion tool
  432. * to produce this object. Note that some features of TextMate grammars are not supported.
  433. */
  434. orion.editor.TextMateStyler = (function() {
  435. /** @inner */
  436. function TextMateStyler(textView, grammar) {
  437. this.initialize(textView);
  438. // Copy the grammar since we'll mutate it
  439. this.grammar = this.copy(grammar);
  440. this._styles = {}; /* key: {String} scopeName, value: {String[]} cssClassNames */
  441. this._tree = null;
  442. this.preprocess();
  443. }
  444. orion.editor.AbstractStyler.extend(TextMateStyler, /** @lends orion.editor.TextMateStyler.prototype */ {
  445. copy: function(obj) {
  446. return JSON.parse(JSON.stringify(obj));
  447. },
  448. preprocess: function() {
  449. var stack = [this.grammar];
  450. for (; stack.length !== 0; ) {
  451. var rule = stack.pop();
  452. if (rule._resolvedRule && rule._typedRule) {
  453. continue;
  454. }
  455. // console.debug("Process " + (rule.include || rule.name));
  456. // Look up include'd rule, create typed *Rule instance
  457. rule._resolvedRule = this._resolve(rule);
  458. rule._typedRule = this._createTypedRule(rule);
  459. // Convert the scope names to styles and cache them for later
  460. this.addStyles(rule.name);
  461. this.addStyles(rule.contentName);
  462. this.addStylesForCaptures(rule.captures);
  463. this.addStylesForCaptures(rule.beginCaptures);
  464. this.addStylesForCaptures(rule.endCaptures);
  465. if (rule._resolvedRule !== rule) {
  466. // Add include target
  467. stack.push(rule._resolvedRule);
  468. }
  469. if (rule.patterns) {
  470. // Add subrules
  471. for (var i=0; i < rule.patterns.length; i++) {
  472. stack.push(rule.patterns[i]);
  473. }
  474. }
  475. }
  476. },
  477. /**
  478. * Adds eclipse.Style objects for scope to our _styles cache.
  479. * @param {String} scope A scope name, like "constant.character.php".
  480. */
  481. addStyles: function(scope) {
  482. if (scope && !this._styles[scope]) {
  483. this._styles[scope] = dojo.map(scope.split("."),
  484. function(segment, i, segments) {
  485. return segments.slice(0, i+1).join("-");
  486. });
  487. // console.debug("add style for " + scope + " = [" + this._styles[scope].join(", ") + "]");
  488. }
  489. },
  490. addStylesForCaptures: function(/**Object*/ captures) {
  491. for (var prop in captures) {
  492. if (captures.hasOwnProperty(prop)) {
  493. var scope = captures[prop].name;
  494. this.addStyles(scope);
  495. }
  496. }
  497. },
  498. /**
  499. * A rule that contains subrules ("patterns" in TextMate parlance) but has no "begin" or "end".
  500. * Also handles top level of grammar.
  501. * @private
  502. */
  503. ContainerRule: (function() {
  504. function ContainerRule(/**Object*/ rule) {
  505. this.rule = rule;
  506. this.subrules = rule.patterns;
  507. }
  508. ContainerRule.prototype.valueOf = function() { return "aa"; };
  509. return ContainerRule;
  510. }()),
  511. /**
  512. * A rule that is delimited by "begin" and "end" matches, which may be separated by any number of
  513. * lines. This type of rule may contain subrules, which apply only inside the begin .. end region.
  514. * @private
  515. */
  516. BeginEndRule: (function() {
  517. function BeginEndRule(/**Object*/ rule) {
  518. this.rule = rule;
  519. // TODO: the TextMate blog claims that "end" is optional.
  520. this.beginRegex = orion.editor.RegexUtil.toRegExp(rule.begin);
  521. this.endRegex = orion.editor.RegexUtil.toRegExp(rule.end);
  522. this.subrules = rule.patterns || [];
  523. this.endRegexHasBackRef = orion.editor.RegexUtil.hasBackReference(this.endRegex);
  524. // Deal with non-0 captures
  525. var complexCaptures = orion.editor.RegexUtil.complexCaptures(rule.captures);
  526. var complexBeginEnd = orion.editor.RegexUtil.complexCaptures(rule.beginCaptures) || orion.editor.RegexUtil.complexCaptures(rule.endCaptures);
  527. this.isComplex = complexCaptures || complexBeginEnd;
  528. if (this.isComplex) {
  529. var bg = orion.editor.RegexUtil.groupify(this.beginRegex);
  530. this.beginRegex = bg[0];
  531. this.beginOld2New = bg[1];
  532. this.beginConsuming = bg[2];
  533. var eg = orion.editor.RegexUtil.groupify(this.endRegex, this.beginOld2New /*Update end's backrefs to begin's new group #s*/);
  534. this.endRegex = eg[0];
  535. this.endOld2New = eg[1];
  536. this.endConsuming = eg[2];
  537. }
  538. }
  539. BeginEndRule.prototype.valueOf = function() { return this.beginRegex; };
  540. return BeginEndRule;
  541. }()),
  542. /**
  543. * A rule with a "match" pattern.
  544. * @private
  545. */
  546. MatchRule: (function() {
  547. function MatchRule(/**Object*/ rule) {
  548. this.rule = rule;
  549. this.matchRegex = orion.editor.RegexUtil.toRegExp(rule.match);
  550. this.isComplex = orion.editor.RegexUtil.complexCaptures(rule.captures);
  551. if (this.isComplex) {
  552. var mg = orion.editor.RegexUtil.groupify(this.matchRegex);
  553. this.matchRegex = mg[0];
  554. this.matchOld2New = mg[1];
  555. this.matchConsuming = mg[2];
  556. }
  557. }
  558. MatchRule.prototype.valueOf = function() { return this.matchRegex; };
  559. return MatchRule;
  560. }()),
  561. /**
  562. * @param {Object} rule A rule from the grammar.
  563. * @returns {MatchRule|BeginEndRule|ContainerRule}
  564. */
  565. _createTypedRule: function(rule) {
  566. if (rule.match) {
  567. return new this.MatchRule(rule);
  568. } else if (rule.begin) {
  569. return new this.BeginEndRule(rule);
  570. } else {
  571. return new this.ContainerRule(rule);
  572. }
  573. },
  574. /**
  575. * Resolves a rule from the grammar (which may be an include) into the real rule that it points to.
  576. */
  577. _resolve: function(rule) {
  578. var resolved = rule;
  579. if (rule.include) {
  580. if (rule.begin || rule.end || rule.match) {
  581. throw new Error("Unexpected regex pattern in \"include\" rule " + rule.include);
  582. }
  583. var name = rule.include;
  584. if (name.charAt(0) === "#") {
  585. resolved = this.grammar.repository && this.grammar.repository[name.substring(1)];
  586. if (!resolved) { throw new Error("Couldn't find included rule " + name + " in grammar repository"); }
  587. } else if (name === "$self") {
  588. resolved = this.grammar;
  589. } else if (name === "$base") {
  590. // $base is only relevant when including rules from foreign grammars
  591. throw new Error("Include \"$base\" is not supported");
  592. } else {
  593. throw new Error("Include external rule \"" + name + "\" is not supported");
  594. }
  595. }
  596. return resolved;
  597. },
  598. ContainerNode: (function() {
  599. function ContainerNode(parent, rule) {
  600. this.parent = parent;
  601. this.rule = rule;
  602. this.children = [];
  603. this.start = null;
  604. this.end = null;
  605. }
  606. ContainerNode.prototype.addChild = function(child) {
  607. this.children.push(child);
  608. };
  609. ContainerNode.prototype.valueOf = function() {
  610. var r = this.rule;
  611. return "ContainerNode { " + (r.include || "") + " " + (r.name || "") + (r.comment || "") + "}";
  612. };
  613. return ContainerNode;
  614. }()),
  615. BeginEndNode: (function() {
  616. function BeginEndNode(parent, rule, beginMatch) {
  617. this.parent = parent;
  618. this.rule = rule;
  619. this.children = [];
  620. this.setStart(beginMatch);
  621. this.end = null; // will be set eventually during parsing (may be EOF)
  622. this.endMatch = null; // may remain null if we never match our "end" pattern
  623. // Build a new regex if the "end" regex has backrefs since they refer to matched groups of beginMatch
  624. if (rule.endRegexHasBackRef) {
  625. this.endRegexSubstituted = orion.editor.RegexUtil.getSubstitutedRegex(rule.endRegex, beginMatch);
  626. } else {
  627. this.endRegexSubstituted = null;
  628. }
  629. }
  630. BeginEndNode.prototype.addChild = function(child) {
  631. this.children.push(child);
  632. };
  633. /** @return {Number} This node's index in its parent's "children" list */
  634. BeginEndNode.prototype.getIndexInParent = function(node) {
  635. return this.parent ? this.parent.children.indexOf(this) : -1;
  636. };
  637. /** @param {RegExp.match} beginMatch */
  638. BeginEndNode.prototype.setStart = function(beginMatch) {
  639. this.start = beginMatch.index;
  640. this.beginMatch = beginMatch;
  641. };
  642. /** @param {RegExp.match|Number} endMatchOrLastChar */
  643. BeginEndNode.prototype.setEnd = function(endMatchOrLastChar) {
  644. if (endMatchOrLastChar && typeof(endMatchOrLastChar) === "object") {
  645. var endMatch = endMatchOrLastChar;
  646. this.endMatch = endMatch;
  647. this.end = endMatch.index + endMatch[0].length;
  648. } else {
  649. var lastChar = endMatchOrLastChar;
  650. this.endMatch = null;
  651. this.end = lastChar;
  652. }
  653. };
  654. BeginEndNode.prototype.shiftStart = function(amount) {
  655. this.start += amount;
  656. this.beginMatch.index += amount;
  657. };
  658. BeginEndNode.prototype.shiftEnd = function(amount) {
  659. this.end += amount;
  660. if (this.endMatch) { this.endMatch.index += amount; }
  661. };
  662. BeginEndNode.prototype.valueOf = function() {
  663. return "{" + this.rule.beginRegex + " range=" + this.start + ".." + this.end + "}";
  664. };
  665. return BeginEndNode;
  666. }()),
  667. /** Pushes rules onto stack so that rules[startFrom] is on top */
  668. push: function(/**Array*/ stack, /**Array*/ rules) {
  669. if (!rules) { return; }
  670. for (var i = rules.length; i > 0; ) {
  671. stack.push(rules[--i]);
  672. }
  673. },
  674. /** Execs regex on text, and returns the match object with its index offset by the given amount. */
  675. exec: function(/**RegExp*/ regex, /**String*/ text, /**Number*/ offset) {
  676. var match = regex.exec(text);
  677. if (match) { match.index += offset; }
  678. regex.lastIndex = 0; // Just in case
  679. return match;
  680. },
  681. /** @returns {Number} The position immediately following the match. */
  682. afterMatch: function(/**RegExp.match*/ match) {
  683. return match.index + match[0].length;
  684. },
  685. /** @returns {RegExp.match} If node is a BeginEndNode and its rule's "end" pattern matches the text. */
  686. getEndMatch: function(/**Node*/ node, /**String*/ text, /**Number*/ offset) {
  687. if (node instanceof this.BeginEndNode) {
  688. var rule = node.rule;
  689. var endRegex = node.endRegexSubstituted || rule.endRegex;
  690. if (!endRegex) { return null; }
  691. return this.exec(endRegex, text, offset);
  692. }
  693. return null;
  694. },
  695. /** Called once when file is first loaded to build the parse tree. Tree is updated incrementally thereafter as buffer is modified */
  696. initialParse: function() {
  697. var last = this.textView.getModel().getCharCount();
  698. // First time; make parse tree for whole buffer
  699. var root = new this.ContainerNode(null, this.grammar._typedRule);
  700. this._tree = root;
  701. this.parse(this._tree, false, 0);
  702. },
  703. _onModelChanged: function(/**eclipse.ModelChangedEvent*/ e) {
  704. var addedCharCount = e.addedCharCount,
  705. addedLineCount = e.addedLineCount,
  706. removedCharCount = e.removedCharCount,
  707. removedLineCount = e.removedLineCount,
  708. start = e.start;
  709. if (!this._tree) {
  710. this.initialParse();
  711. } else {
  712. var model = this.textView.getModel();
  713. var charCount = model.getCharCount();
  714. // For rs, we must rewind to the line preceding the line 'start' is on. We can't rely on start's
  715. // line since it may've been changed in a way that would cause a new beginMatch at its lineStart.
  716. var rs = model.getLineEnd(model.getLineAtOffset(start) - 1); // may be < 0
  717. var fd = this.getFirstDamaged(rs, rs);
  718. rs = rs === -1 ? 0 : rs;
  719. var stoppedAt;
  720. if (fd) {
  721. // [rs, re] is the region we need to verify. If we find the structure of the tree
  722. // has changed in that area, then we may need to reparse the rest of the file.
  723. stoppedAt = this.parse(fd, true, rs, start, addedCharCount, removedCharCount);
  724. } else {
  725. // FIXME: fd == null ?
  726. stoppedAt = charCount;
  727. }
  728. this.textView.redrawRange(rs, stoppedAt);
  729. }
  730. },
  731. /** @returns {BeginEndNode|ContainerNode} The result of taking the first (smallest "start" value)
  732. * node overlapping [start,end] and drilling down to get its deepest damaged descendant (if any).
  733. */
  734. getFirstDamaged: function(start, end) {
  735. // If start === 0 we actually have to start from the root because there is no position
  736. // we can rely on. (First index is damaged)
  737. if (start < 0) {
  738. return this._tree;
  739. }
  740. var nodes = [this._tree];
  741. var result = null;
  742. while (nodes.length) {
  743. var n = nodes.pop();
  744. if (!n.parent /*n is root*/ || this.isDamaged(n, start, end)) {
  745. // n is damaged by the edit, so go into its children
  746. // Note: If a node is damaged, then some of its descendents MAY be damaged
  747. // If a node is undamaged, then ALL of its descendents are undamaged
  748. if (n instanceof this.BeginEndNode) {
  749. result = n;
  750. }
  751. // Examine children[0] last
  752. for (var i=0; i < n.children.length; i++) {
  753. nodes.push(n.children[i]);
  754. }
  755. }
  756. }
  757. return result || this._tree;
  758. },
  759. /** @returns true If n overlaps the interval [start,end] */
  760. isDamaged: function(/**BeginEndNode*/ n, start, end) {
  761. // Note strict > since [2,5] doesn't overlap [5,7]
  762. return (n.start <= end && n.end > start);
  763. },
  764. /**
  765. * Builds tree from some of the buffer content
  766. *
  767. * TODO cleanup params
  768. * @param {BeginEndNode|ContainerNode} origNode The deepest node that overlaps [rs,rs], or the root.
  769. * @param {Boolean} repairing
  770. * @param {Number} rs See _onModelChanged()
  771. * @param {Number} [editStart] Only used for repairing === true
  772. * @param {Number} [addedCharCount] Only used for repairing === true
  773. * @param {Number} [removedCharCount] Only used for repairing === true
  774. */
  775. parse: function(origNode, repairing, rs, editStart, addedCharCount, removedCharCount) {
  776. var model = this.textView.getModel();
  777. var lastLineStart = model.getLineStart(model.getLineCount() - 1);
  778. var eof = model.getCharCount();
  779. var initialExpected = this.getInitialExpected(origNode, rs);
  780. // re is best-case stopping point; if we detect change to tree, we must continue past it
  781. var re = -1;
  782. if (repairing) {
  783. origNode.repaired = true;
  784. origNode.endNeedsUpdate = true;
  785. var lastChild = origNode.children[origNode.children.length-1];
  786. var delta = addedCharCount - removedCharCount;
  787. var lastChildLineEnd = lastChild ? model.getLineEnd(model.getLineAtOffset(lastChild.end + delta)) : -1;
  788. var editLineEnd = model.getLineEnd(model.getLineAtOffset(editStart + removedCharCount));
  789. re = Math.max(lastChildLineEnd, editLineEnd);
  790. }
  791. re = (re === -1) ? eof : re;
  792. var expected = initialExpected;
  793. var node = origNode;
  794. var matchedChildOrEnd = false;
  795. var pos = rs;
  796. while (node && (!repairing || (pos < re))) {
  797. var matchInfo = this.getNextMatch(model, node, pos);
  798. if (!matchInfo) {
  799. // Go to next line, if any
  800. pos = (pos >= lastLineStart) ? eof : model.getLineStart(model.getLineAtOffset(pos) + 1);
  801. }
  802. var match = matchInfo && matchInfo.match,
  803. rule = matchInfo && matchInfo.rule,
  804. isSub = matchInfo && matchInfo.isSub,
  805. isEnd = matchInfo && matchInfo.isEnd;
  806. if (isSub) {
  807. pos = this.afterMatch(match);
  808. if (rule instanceof this.BeginEndRule) {
  809. matchedChildOrEnd = true;
  810. // Matched a child. Did we expect that?
  811. if (repairing && rule === expected.rule && node === expected.parent) {
  812. // Yes: matched expected child
  813. var foundChild = expected;
  814. foundChild.setStart(match);
  815. // Note: the 'end' position for this node will either be matched, or fixed up by us post-loop
  816. foundChild.repaired = true;
  817. foundChild.endNeedsUpdate = true;
  818. node = foundChild; // descend
  819. expected = this.getNextExpected(expected, "begin");
  820. } else {
  821. if (repairing) {
  822. // No: matched unexpected child.
  823. this.prune(node, expected);
  824. repairing = false;
  825. }
  826. // Add the new child (will replace 'expected' in node's children list)
  827. var subNode = new this.BeginEndNode(node, rule, match);
  828. node.addChild(subNode);
  829. node = subNode; // descend
  830. }
  831. } else {
  832. // Matched a MatchRule; no changes to tree required
  833. }
  834. } else if (isEnd || pos === eof) {
  835. if (node instanceof this.BeginEndNode) {
  836. if (match) {
  837. matchedChildOrEnd = true;
  838. node.setEnd(match);
  839. pos = this.afterMatch(match);
  840. // Matched node's end. Did we expect that?
  841. if (repairing && node === expected && node.parent === expected.parent) {
  842. // Yes: found the expected end of node
  843. node.repaired = true;
  844. delete node.endNeedsUpdate;
  845. expected = this.getNextExpected(expected, "end");
  846. } else {
  847. if (repairing) {
  848. // No: found an unexpected end
  849. this.prune(node, expected);
  850. repairing = false;
  851. }
  852. }
  853. } else {
  854. // Force-ending a BeginEndNode that runs until eof
  855. node.setEnd(eof);
  856. delete node.endNeedsUpdate;
  857. }
  858. }
  859. node = node.parent; // ascend
  860. }
  861. // if (repairing && pos >= re && !matchedChildOrEnd) {
  862. // // Reached re without matching any begin/end => initialExpected itself was removed => repair fail
  863. // this.prune(origNode, initialExpected);
  864. // repairing = false;
  865. // }
  866. } // end loop
  867. // TODO: do this for every node we end?
  868. this.removeUnrepairedChildren(origNode, repairing, rs);
  869. //console.debug("parsed " + (pos - rs) + " of " + model.getCharCount + "buf");
  870. this.cleanup(repairing, origNode, rs, re, eof, addedCharCount, removedCharCount);
  871. return pos; // where we stopped repairing/reparsing
  872. },
  873. /** Helper for parse() in the repair case. To be called when ending a node, as any children that
  874. * lie in [rs,node.end] and were not repaired must've been deleted.
  875. */
  876. removeUnrepairedChildren: function(node, repairing, start) {
  877. if (repairing) {
  878. var children = node.children;
  879. var removeFrom = -1;
  880. for (var i=0; i < children.length; i++) {
  881. var child = children[i];
  882. if (!child.repaired && this.isDamaged(child, start, Number.MAX_VALUE /*end doesn't matter*/)) {
  883. removeFrom = i;
  884. break;
  885. }
  886. }
  887. if (removeFrom !== -1) {
  888. node.children.length = removeFrom;
  889. }
  890. }
  891. },
  892. /** Helper for parse() in the repair case */
  893. cleanup: function(repairing, origNode, rs, re, eof, addedCharCount, removedCharCount) {
  894. var i, node, maybeRepairedNodes;
  895. if (repairing) {
  896. // The repair succeeded, so update stale begin/end indices by simple translation.
  897. var delta = addedCharCount - removedCharCount;
  898. // A repaired node's end can't exceed re, but it may exceed re-delta+1.
  899. // TODO: find a way to guarantee disjoint intervals for repaired vs unrepaired, then stop using flag
  900. var maybeUnrepairedNodes = this.getIntersecting(re-delta+1, eof);
  901. maybeRepairedNodes = this.getIntersecting(rs, re);
  902. // Handle unrepaired nodes. They are those intersecting [re-delta+1, eof] that don't have the flag
  903. for (i=0; i < maybeUnrepairedNodes.length; i++) {
  904. node = maybeUnrepairedNodes[i];
  905. if (!node.repaired && node instanceof this.BeginEndNode) {
  906. node.shiftEnd(delta);
  907. node.shiftStart(delta);
  908. }
  909. }
  910. // Translate 'end' index of repaired node whose 'end' was not matched in loop (>= re)
  911. for (i=0; i < maybeRepairedNodes.length; i++) {
  912. node = maybeRepairedNodes[i];
  913. if (node.repaired && node.endNeedsUpdate) {
  914. node.shiftEnd(delta);
  915. }
  916. delete node.endNeedsUpdate;
  917. delete node.repaired;
  918. }
  919. } else {
  920. // Clean up after ourself
  921. maybeRepairedNodes = this.getIntersecting(rs, re);
  922. for (i=0; i < maybeRepairedNodes.length; i++) {
  923. delete maybeRepairedNodes[i].repaired;
  924. }
  925. }
  926. },
  927. /**
  928. * @param model {orion.textview.TextModel}
  929. * @param node {Node}
  930. * @param pos {Number}
  931. * @param [matchRulesOnly] {Boolean} Optional, if true only "match" subrules will be considered.
  932. * @returns {Object} A match info object with properties:
  933. * {Boolean} isEnd
  934. * {Boolean} isSub
  935. * {RegExp.match} match
  936. * {(Match|BeginEnd)Rule} rule
  937. */
  938. getNextMatch: function(model, node, pos, matchRulesOnly) {
  939. var lineIndex = model.getLineAtOffset(pos);
  940. var lineEnd = model.getLineEnd(lineIndex);
  941. var line = model.getText(pos, lineEnd);
  942. var stack = [],
  943. expandedContainers = [],
  944. subMatches = [],
  945. subrules = [];
  946. this.push(stack, node.rule.subrules);
  947. while (stack.length) {
  948. var next = stack.length ? stack.pop() : null;
  949. var subrule = next && next._resolvedRule._typedRule;
  950. if (subrule instanceof this.ContainerRule && expandedContainers.indexOf(subrule) === -1) {
  951. // Expand ContainerRule by pushing its subrules on
  952. expandedContainers.push(subrule);
  953. this.push(stack, subrule.subrules);
  954. continue;
  955. }
  956. if (subrule && matchRulesOnly && !(subrule.matchRegex)) {
  957. continue;
  958. }
  959. var subMatch = subrule && this.exec(subrule.matchRegex || subrule.beginRegex, line, pos);
  960. if (subMatch) {
  961. subMatches.push(subMatch);
  962. subrules.push(subrule);
  963. }
  964. }
  965. var bestSub = Number.MAX_VALUE,
  966. bestSubIndex = -1;
  967. for (var i=0; i < subMatches.length; i++) {
  968. var match = subMatches[i];
  969. if (match.index < bestSub) {
  970. bestSub = match.index;
  971. bestSubIndex = i;
  972. }
  973. }
  974. if (!matchRulesOnly) {
  975. // See if the "end" pattern of the active begin/end node matches.
  976. // TODO: The active begin/end node may not be the same as the node that holds the subrules
  977. var activeBENode = node;
  978. var endMatch = this.getEndMatch(node, line, pos);
  979. if (endMatch) {
  980. var doEndLast = activeBENode.rule.applyEndPatternLast;
  981. var endWins = bestSubIndex === -1 || (endMatch.index < bestSub) || (!doEndLast && endMatch.index === bestSub);
  982. if (endWins) {
  983. return {isEnd: true, rule: activeBENode.rule, match: endMatch};
  984. }
  985. }
  986. }
  987. return bestSubIndex === -1 ? null : {isSub: true, rule: subrules[bestSubIndex], match: subMatches[bestSubIndex]};
  988. },
  989. /**
  990. * Gets the node corresponding to the first match we expect to see in the repair.
  991. * @param {BeginEndNode|ContainerNode} node The node returned via getFirstDamaged(rs,rs) -- may be the root.
  992. * @param {Number} rs See _onModelChanged()
  993. * Note that because rs is a line end (or 0, a line start), it will intersect a beginMatch or
  994. * endMatch either at their 0th character, or not at all. (begin/endMatches can't cross lines).
  995. * This is the only time we rely on the start/end values from the pre-change tree. After this
  996. * we only look at node ordering, never use the old indices.
  997. * @returns {Node}
  998. */
  999. getInitialExpected: function(node, rs) {
  1000. // TODO: Kind of weird.. maybe ContainerNodes should have start & end set, like BeginEndNodes
  1001. var i, child;
  1002. if (node === this._tree) {
  1003. // get whichever of our children comes after rs
  1004. for (i=0; i < node.children.length; i++) {
  1005. child = node.children[i]; // BeginEndNode
  1006. if (child.start >= rs) {
  1007. return child;
  1008. }
  1009. }
  1010. } else if (node instanceof this.BeginEndNode) {
  1011. if (node.endMatch) {
  1012. // Which comes next after rs: our nodeEnd or one of our children?
  1013. var nodeEnd = node.endMatch.index;
  1014. for (i=0; i < node.children.length; i++) {
  1015. child = node.children[i]; // BeginEndNode
  1016. if (child.start >= rs) {
  1017. break;
  1018. }
  1019. }
  1020. if (child && child.start < nodeEnd) {
  1021. return child; // Expect child as the next match
  1022. }
  1023. } else {
  1024. // No endMatch => node goes until eof => it end should be the next match
  1025. }
  1026. }
  1027. return node; // We expect node to end, so it should be the next match
  1028. },
  1029. /**
  1030. * Helper for repair() to tell us what kind of event we expect next.
  1031. * @param {Node} expected Last value returned by this method.
  1032. * @param {String} event "begin" if the last value of expected was matched as "begin",
  1033. * or "end" if it was matched as an end.
  1034. * @returns {Node} The next expected node to match, or null.
  1035. */
  1036. getNextExpected: function(/**Node*/ expected, event) {
  1037. var node = expected;
  1038. if (event === "begin") {
  1039. var child = node.children[0];
  1040. if (child) {
  1041. return child;
  1042. } else {
  1043. return node;
  1044. }
  1045. } else if (event === "end") {
  1046. var parent = node.parent;
  1047. if (parent) {
  1048. var nextSibling = parent.children[parent.children.indexOf(node) + 1];
  1049. if (nextSibling) {
  1050. return nextSibling;
  1051. } else {
  1052. return parent;
  1053. }
  1054. }
  1055. }
  1056. return null;
  1057. },
  1058. /** Helper for parse() when repairing. Prunes out the unmatched nodes from the tree so we can continue parsing. */
  1059. prune: function(/**BeginEndNode|ContainerNode*/ node, /**Node*/ expected) {
  1060. var expectedAChild = expected.parent === node;
  1061. if (expectedAChild) {
  1062. // Expected child wasn't matched; prune it and all siblings after it
  1063. node.children.length = expected.getIndexInParent();
  1064. } else if (node instanceof this.BeginEndNode) {
  1065. // Expected node to end but it didn't; set its end unknown and we'll match it eventually
  1066. node.endMatch = null;
  1067. node.end = null;
  1068. }
  1069. // Reparsing from node, so prune the successors outside of node's subtree
  1070. if (node.parent) {
  1071. node.parent.children.length = node.getIndexInParent() + 1;
  1072. }
  1073. },
  1074. _onLineStyle: function(/**eclipse.LineStyleEvent*/ e) {
  1075. function byStart(r1, r2) {
  1076. return r1.start - r2.start;
  1077. }
  1078. if (!this._tree) {
  1079. // In some cases it seems onLineStyle is called before onModelChanged, so we need to parse here
  1080. this.initialParse();
  1081. }
  1082. var lineStart = e.lineStart,
  1083. model = this.textView.getModel(),
  1084. lineEnd = model.getLineEnd(e.lineIndex);
  1085. var rs = model.getLineEnd(model.getLineAtOffset(lineStart) - 1); // may be < 0
  1086. var node = this.getFirstDamaged(rs, rs);
  1087. var scopes = this.getLineScope(model, node, lineStart, lineEnd);
  1088. e.ranges = this.toStyleRanges(scopes);
  1089. // // Editor requires StyleRanges must be in ascending order by 'start', or else some will be ignored
  1090. e.ranges.sort(byStart);
  1091. },
  1092. // Run parse algorithm on [start, end] in the context of node, assigning scope as we find matches
  1093. getLineScope: function(model, node, start, end) {
  1094. var pos = start;
  1095. var expected = this.getInitialExpected(node, start);
  1096. var scopes = [],
  1097. gaps = [];
  1098. while (node && (pos < end)) {
  1099. var matchInfo = this.getNextMatch(model, node, pos);
  1100. if (!matchInfo) {
  1101. break; // line is over
  1102. }
  1103. var match = matchInfo && matchInfo.match,
  1104. rule = matchInfo && matchInfo.rule,
  1105. isSub = matchInfo && matchInfo.isSub,
  1106. isEnd = matchInfo && matchInfo.isEnd;
  1107. if (match.index !== pos) {
  1108. // gap [pos..match.index]
  1109. gaps.push({ start: pos, end: match.index, node: node});
  1110. }
  1111. if (isSub) {
  1112. pos = this.afterMatch(match);
  1113. if (rule instanceof this.BeginEndRule) {
  1114. // Matched a "begin", assign its scope and descend into it
  1115. this.addBeginScope(scopes, match, rule);
  1116. node = expected; // descend
  1117. expected = this.getNextExpected(expected, "begin");
  1118. } else {
  1119. // Matched a child MatchRule;
  1120. this.addMatchScope(scopes, match, rule);
  1121. }
  1122. } else if (isEnd) {
  1123. pos = this.afterMatch(match);
  1124. // Matched and "end", assign its end scope and go up
  1125. this.addEndScope(scopes, match, rule);
  1126. expected = this.getNextExpected(expected, "end");
  1127. node = node.parent; // ascend
  1128. }
  1129. }
  1130. if (pos < end) {
  1131. gaps.push({ start: pos, end: end, node: node });
  1132. }
  1133. var inherited = this.getInheritedLineScope(gaps, start, end);
  1134. return scopes.concat(inherited);
  1135. },
  1136. getInheritedLineScope: function(gaps, start, end) {
  1137. var scopes = [];
  1138. for (var i=0; i < gaps.length; i++) {
  1139. var gap = gaps[i];
  1140. var node = gap.node;
  1141. while (node) {
  1142. // if node defines a contentName or name, apply it
  1143. var rule = node.rule.rule;
  1144. var name = rule.name,
  1145. contentName = rule.contentName;
  1146. // TODO: if both are given, we don't resolve the conflict. contentName always wins
  1147. var scope = contentName || name;
  1148. if (scope) {
  1149. this.addScopeRange(scopes, gap.start, gap.end, scope);
  1150. break;
  1151. }
  1152. node = node.parent;
  1153. }
  1154. }
  1155. return scopes;
  1156. },
  1157. addBeginScope: function(scopes, match, typedRule) {
  1158. var rule = typedRule.rule;
  1159. this.addCapturesScope(scopes, match, (rule.beginCaptures || rule.captures), typedRule.isComplex, typedRule.beginOld2New, typedRule.beginConsuming);
  1160. },
  1161. addEndScope: function(scopes, match, typedRule) {
  1162. var rule = typedRule.rule;
  1163. this.addCapturesScope(scopes, match, (rule.endCaptures || rule.captures), typedRule.isComplex, typedRule.endOld2New, typedRule.endConsuming);
  1164. },
  1165. addMatchScope: function(scopes, match, typedRule) {
  1166. var rule = typedRule.rule,
  1167. name = rule.name,
  1168. captures = rule.captures;
  1169. if (captures) {
  1170. // captures takes priority over name
  1171. this.addCapturesScope(scopes, match, captures, typedRule.isComplex, typedRule.matchOld2New, typedRule.matchConsuming);
  1172. } else {
  1173. this.addScope(scopes, match, name);
  1174. }
  1175. },
  1176. addScope: function(scopes, match, name) {
  1177. if (!name) { return; }
  1178. scopes.push({start: match.index, end: this.afterMatch(match), scope: name });
  1179. },
  1180. addScopeRange: function(scopes, start, end, name) {
  1181. if (!name) { return; }
  1182. scopes.push({start: start, end: end, scope: name });
  1183. },
  1184. addCapturesScope: function(/**Array*/scopes, /*RegExp.match*/ match, /**Object*/captures, /**Boolean*/isComplex, /**Object*/old2New, /**Object*/consuming) {
  1185. if (!captures) { return; }
  1186. if (!isComplex) {
  1187. this.addScope(scopes, match, captures[0] && captures[0].name);
  1188. } else {
  1189. // apply scopes captures[1..n] to matching groups [1]..[n] of match
  1190. // Sum up the lengths of preceding consuming groups to get the start offset for each matched group.
  1191. var newGroupStarts = {1: 0};
  1192. var sum = 0;
  1193. for (var num = 1; match[num] !== undefined; num++) {
  1194. if (consuming[num] !== undefined) {
  1195. sum += match[num].length;
  1196. }
  1197. if (match[num+1] !== undefined) {
  1198. newGroupStarts[num + 1] = sum;
  1199. }
  1200. }
  1201. // Map the group numbers referred to in captures object to the new group numbers, and get the actual matched range.
  1202. var start = match.index;
  1203. for (var oldGroupNum = 1; captures[oldGroupNum]; oldGroupNum++) {
  1204. var scope = captures[oldGroupNum].name;
  1205. var newGroupNum = old2New[oldGroupNum];
  1206. var groupStart = start + newGroupStarts[newGroupNum];
  1207. // Not every capturing group defined in regex need match every time the regex is run.
  1208. // eg. (a)|b matches "b" but group 1 is undefined
  1209. if (typeof match[newGroupNum] !== "undefined") {
  1210. var groupEnd = groupStart + match[newGroupNum].length;
  1211. this.addScopeRange(scopes, groupStart, groupEnd, scope);
  1212. }
  1213. }
  1214. }
  1215. },
  1216. /** @returns {Node[]} In depth-first order */
  1217. getIntersecting: function(start, end) {
  1218. var result = [];
  1219. var nodes = this._tree ? [this._tree] : [];
  1220. while (nodes.length) {
  1221. var n = nodes.pop();
  1222. var visitChildren = false;
  1223. if (n instanceof this.ContainerNode) {
  1224. visitChildren = true;
  1225. } else if (this.isDamaged(n, start, end)) {
  1226. visitChildren = true;
  1227. result.push(n);
  1228. }
  1229. if (visitChildren) {
  1230. var len = n.children.length;
  1231. // for (var i=len-1; i >= 0; i--) {
  1232. // nodes.push(n.children[i]);
  1233. // }
  1234. for (var i=0; i < len; i++) {
  1235. nodes.push(n.children[i]);
  1236. }
  1237. }
  1238. }
  1239. return result.reverse();
  1240. },
  1241. _onSelection: function(e) {
  1242. },
  1243. _onDestroy: function(/**eclipse.DestroyEvent*/ e) {
  1244. this.grammar = null;
  1245. this._styles = null;
  1246. this._tree = null;
  1247. },
  1248. /**
  1249. * Applies the grammar to obtain the {@link eclipse.StyleRange[]} for the given line.
  1250. * @returns eclipse.StyleRange[]
  1251. */
  1252. toStyleRanges: function(/**ScopeRange[]*/ scopeRanges) {
  1253. var styleRanges = [];
  1254. for (var i=0; i < scopeRanges.length; i++) {
  1255. var scopeRange = scopeRanges[i];
  1256. var classNames = this._styles[scopeRange.scope];
  1257. if (!classNames) { throw new Error("styles not found for " + scopeRange.scope); }
  1258. var classNamesString = classNames.join(" ");
  1259. styleRanges.push({start: scopeRange.start, end: scopeRange.end, style: {styleClass: classNamesString}});
  1260. // console.debug("{start " + styleRanges[i].start + ", end " + styleRanges[i].end + ", style: " + styleRanges[i].style.styleClass + "}");
  1261. }
  1262. return styleRanges;
  1263. }
  1264. });
  1265. return TextMateStyler;
  1266. }());
  1267. if (typeof window !== "undefined" && typeof window.define !== "undefined") {
  1268. define(['dojo'], function() {
  1269. return orion.editor;
  1270. });
  1271. }