snippets.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844
  1. /* ***** BEGIN LICENSE BLOCK *****
  2. * Distributed under the BSD license:
  3. *
  4. * Copyright (c) 2010, Ajax.org B.V.
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. * * Redistributions of source code must retain the above copyright
  10. * notice, this list of conditions and the following disclaimer.
  11. * * Redistributions in binary form must reproduce the above copyright
  12. * notice, this list of conditions and the following disclaimer in the
  13. * documentation and/or other materials provided with the distribution.
  14. * * Neither the name of Ajax.org B.V. nor the
  15. * names of its contributors may be used to endorse or promote products
  16. * derived from this software without specific prior written permission.
  17. *
  18. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  19. * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  24. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  27. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *
  29. * ***** END LICENSE BLOCK ***** */
  30. define(function(require, exports, module) {
  31. "use strict";
  32. var lang = require("./lib/lang")
  33. var Range = require("./range").Range
  34. var HashHandler = require("./keyboard/hash_handler").HashHandler;
  35. var Tokenizer = require("./tokenizer").Tokenizer;
  36. var comparePoints = Range.comparePoints;
  37. var SnippetManager = function() {
  38. this.snippetMap = {};
  39. this.snippetNameMap = {};
  40. };
  41. (function() {
  42. this.getTokenizer = function() {
  43. function TabstopToken(str, _, stack) {
  44. str = str.substr(1);
  45. if (/^\d+$/.test(str) && !stack.inFormatString)
  46. return [{tabstopId: parseInt(str, 10)}];
  47. return [{text: str}]
  48. }
  49. function escape(ch) {
  50. return "(?:[^\\\\" + ch + "]|\\\\.)";
  51. }
  52. SnippetManager.$tokenizer = new Tokenizer({
  53. start: [
  54. {regex: /:/, onMatch: function(val, state, stack) {
  55. if (stack.length && stack[0].expectIf) {
  56. stack[0].expectIf = false;
  57. stack[0].elseBranch = stack[0];
  58. return [stack[0]];
  59. }
  60. return ":";
  61. }},
  62. {regex: /\\./, onMatch: function(val, state, stack) {
  63. var ch = val[1];
  64. if (ch == "}" && stack.length) {
  65. val = ch;
  66. }else if ("`$\\".indexOf(ch) != -1) {
  67. val = ch;
  68. } else if (stack.inFormatString) {
  69. if (ch == "n")
  70. val = "\n";
  71. else if (ch == "t")
  72. val = "\n";
  73. else if ("ulULE".indexOf(ch) != -1) {
  74. val = {changeCase: ch, local: ch > "a"};
  75. }
  76. }
  77. return [val];
  78. }},
  79. {regex: /}/, onMatch: function(val, state, stack) {
  80. return [stack.length ? stack.shift() : val];
  81. }},
  82. {regex: /\$(?:\d+|\w+)/, onMatch: TabstopToken},
  83. {regex: /\$\{[\dA-Z_a-z]+/, onMatch: function(str, state, stack) {
  84. var t = TabstopToken(str.substr(1), state, stack);
  85. stack.unshift(t[0]);
  86. return t;
  87. }, next: "snippetVar"},
  88. {regex: /\n/, token: "newline", merge: false}
  89. ],
  90. snippetVar: [
  91. {regex: "\\|" + escape("\\|") + "*\\|", onMatch: function(val, state, stack) {
  92. stack[0].choices = val.slice(1, -1).split(",");
  93. }, next: "start"},
  94. {regex: "/(" + escape("/") + "+)/(?:(" + escape("/") + "*)/)(\\w*):?",
  95. onMatch: function(val, state, stack) {
  96. var ts = stack[0];
  97. ts.fmtString = val;
  98. val = this.splitRegex.exec(val);
  99. ts.guard = val[1];
  100. ts.fmt = val[2];
  101. ts.flag = val[3];
  102. return "";
  103. }, next: "start"},
  104. {regex: "`" + escape("`") + "*`", onMatch: function(val, state, stack) {
  105. stack[0].code = val.splice(1, -1);
  106. return "";
  107. }, next: "start"},
  108. {regex: "\\?", onMatch: function(val, state, stack) {
  109. if (stack[0])
  110. stack[0].expectIf = true;
  111. }, next: "start"},
  112. {regex: "([^:}\\\\]|\\\\.)*:?", token: "", next: "start"}
  113. ],
  114. formatString: [
  115. {regex: "/(" + escape("/") + "+)/", token: "regex"},
  116. {regex: "", onMatch: function(val, state, stack) {
  117. stack.inFormatString = true;
  118. }, next: "start"}
  119. ]
  120. });
  121. SnippetManager.prototype.getTokenizer = function() {
  122. return SnippetManager.$tokenizer;
  123. }
  124. return SnippetManager.$tokenizer;
  125. };
  126. this.tokenizeTmSnippet = function(str, startState) {
  127. return this.getTokenizer().getLineTokens(str, startState).tokens.map(function(x) {
  128. return x.value || x;
  129. });
  130. };
  131. this.$getDefaultValue = function(editor, name) {
  132. if (/^[A-Z]\d+$/.test(name)) {
  133. var i = name.substr(1);
  134. return (this.variables[name[0] + "__"] || {})[i];
  135. }
  136. if (/^\d+$/.test(name)) {
  137. return (this.variables.__ || {})[name];
  138. }
  139. name = name.replace(/^TM_/, "");
  140. if (!editor)
  141. return;
  142. var s = editor.session;
  143. switch(name) {
  144. case "CURRENT_WORD":
  145. var r = s.getWordRange();
  146. case "SELECTION":
  147. case "SELECTED_TEXT":
  148. return s.getTextRange(r);
  149. case "CURRENT_LINE":
  150. return s.getLine(editor.getCursorPosition().row);
  151. case "PREV_LINE": // not possible in textmate
  152. return s.getLine(editor.getCursorPosition().row - 1);
  153. case "LINE_INDEX":
  154. return editor.getCursorPosition().column;
  155. case "LINE_NUMBER":
  156. return editor.getCursorPosition().row + 1;
  157. case "SOFT_TABS":
  158. return s.getUseSoftTabs() ? "YES" : "NO";
  159. case "TAB_SIZE":
  160. return s.getTabSize();
  161. // defult but can't fill :(
  162. case "FILENAME":
  163. case "FILEPATH":
  164. return "ace.ajax.org";
  165. case "FULLNAME":
  166. return "Ace";
  167. }
  168. };
  169. this.variables = {};
  170. this.getVariableValue = function(editor, varName) {
  171. if (this.variables.hasOwnProperty(varName))
  172. return this.variables[varName](editor, varName) || "";
  173. return this.$getDefaultValue(editor, varName) || "";
  174. };
  175. // returns string formatted according to http://manual.macromates.com/en/regular_expressions#replacement_string_syntax_format_strings
  176. this.tmStrFormat = function(str, ch, editor) {
  177. var flag = ch.flag || "";
  178. var re = ch.guard;
  179. re = new RegExp(re, flag.replace(/[^gi]/, ""));
  180. var fmtTokens = this.tokenizeTmSnippet(ch.fmt, "formatString");
  181. var _self = this;
  182. var formatted = str.replace(re, function() {
  183. _self.variables.__ = arguments;
  184. var fmtParts = _self.resolveVariables(fmtTokens, editor);
  185. var gChangeCase = "E";
  186. for (var i = 0; i < fmtParts.length; i++) {
  187. var ch = fmtParts[i];
  188. if (typeof ch == "object") {
  189. fmtParts[i] = "";
  190. if (ch.changeCase && ch.local) {
  191. var next = fmtParts[i + 1];
  192. if (next && typeof next == "string") {
  193. if (ch.changeCase == "u")
  194. fmtParts[i] = next[0].toUpperCase();
  195. else
  196. fmtParts[i] = next[0].toLowerCase();
  197. fmtParts[i + 1] = next.substr(1);
  198. }
  199. } else if (ch.changeCase) {
  200. gChangeCase = ch.changeCase;
  201. }
  202. } else if (gChangeCase == "U") {
  203. fmtParts[i] = ch.toUpperCase();
  204. } else if (gChangeCase == "L") {
  205. fmtParts[i] = ch.toLowerCase();
  206. }
  207. }
  208. return fmtParts.join("");
  209. });
  210. this.variables.__ = null;
  211. return formatted;
  212. };
  213. this.resolveVariables = function(snippet, editor) {
  214. var result = [];
  215. for (var i = 0; i < snippet.length; i++) {
  216. var ch = snippet[i];
  217. if (typeof ch == "string") {
  218. result.push(ch);
  219. } else if (typeof ch != "object") {
  220. continue;
  221. } else if (ch.skip) {
  222. gotoNext(ch);
  223. } else if (ch.processed < i) {
  224. continue;
  225. } else if (ch.text) {
  226. var value = this.getVariableValue(editor, ch.text);
  227. if (value && ch.fmtString)
  228. value = this.tmStrFormat(value, ch);
  229. ch.processed = i;
  230. if (ch.expectIf == null) {
  231. if (value) {
  232. result.push(value);
  233. gotoNext(ch);
  234. }
  235. } else {
  236. if (value) {
  237. ch.skip = ch.elseBranch;
  238. } else
  239. gotoNext(ch);
  240. }
  241. } else if (ch.tabstopId != null) {
  242. result.push(ch);
  243. } else if (ch.changeCase != null) {
  244. result.push(ch);
  245. }
  246. }
  247. function gotoNext(ch) {
  248. var i1 = snippet.indexOf(ch, i + 1);
  249. if (i1 != -1)
  250. i = i1;
  251. }
  252. return result;
  253. };
  254. this.insertSnippet = function(editor, snippetText) {
  255. var cursor = editor.getCursorPosition();
  256. var line = editor.session.getLine(cursor.row);
  257. var indentString = line.match(/^\s*/)[0];
  258. var tabString = editor.session.getTabString();
  259. var tokens = this.tokenizeTmSnippet(snippetText);
  260. tokens = this.resolveVariables(tokens, editor);
  261. // indent
  262. tokens = tokens.map(function(x) {
  263. if (x == "\n")
  264. return x + indentString;
  265. if (typeof x == "string")
  266. return x.replace(/\t/g, tabString);
  267. return x;
  268. });
  269. // tabstop values
  270. var tabstops = [];
  271. tokens.forEach(function(p, i) {
  272. if (typeof p != "object")
  273. return;
  274. var id = p.tabstopId;
  275. var ts = tabstops[id];
  276. if (!ts) {
  277. ts = tabstops[id] = [];
  278. ts.index = id;
  279. ts.value = "";
  280. }
  281. if (ts.indexOf(p) !== -1)
  282. return;
  283. ts.push(p);
  284. var i1 = tokens.indexOf(p, i + 1);
  285. if (i1 === -1)
  286. return;
  287. var value = tokens.slice(i + 1, i1);
  288. var isNested = value.some(function(t) {return typeof t === "object"});
  289. if (isNested && !ts.value) {
  290. ts.value = value;
  291. } else if (value.length && (!ts.value || typeof ts.value !== "string")) {
  292. ts.value = value.join("");
  293. }
  294. });
  295. // expand tabstop values
  296. tabstops.forEach(function(ts) {ts.length = 0});
  297. var expanding = {};
  298. function copyValue(val) {
  299. var copy = []
  300. for (var i = 0; i < val.length; i++) {
  301. var p = val[i];
  302. if (typeof p == "object") {
  303. if (expanding[p.tabstopId])
  304. continue;
  305. var j = val.lastIndexOf(p, i - 1);
  306. p = copy[j] || {tabstopId: p.tabstopId};
  307. }
  308. copy[i] = p;
  309. }
  310. return copy;
  311. }
  312. for (var i = 0; i < tokens.length; i++) {
  313. var p = tokens[i];
  314. if (typeof p != "object")
  315. continue;
  316. var id = p.tabstopId;
  317. var i1 = tokens.indexOf(p, i + 1);
  318. if (expanding[id] == p) {
  319. expanding[id] = null;
  320. continue;
  321. }
  322. var ts = tabstops[id];
  323. var arg = typeof ts.value == "string" ? [ts.value] : copyValue(ts.value);
  324. arg.unshift(i + 1, Math.max(0, i1 - i));
  325. arg.push(p);
  326. expanding[id] = p;
  327. tokens.splice.apply(tokens, arg);
  328. if (ts.indexOf(p) === -1)
  329. ts.push(p);
  330. };
  331. // convert to plain text
  332. var row = 0, column = 0;
  333. var text = "";
  334. tokens.forEach(function(t) {
  335. if (typeof t === "string") {
  336. if (t[0] === "\n"){
  337. column = t.length - 1;
  338. row ++;
  339. } else
  340. column += t.length;
  341. text += t;
  342. } else {
  343. if (!t.start)
  344. t.start = {row: row, column: column};
  345. else
  346. t.end = {row: row, column: column};
  347. }
  348. });
  349. var range = editor.getSelectionRange();
  350. var end = editor.session.replace(range, text);
  351. var tabstopManager = new TabstopManager(editor);
  352. tabstopManager.addTabstops(tabstops, range.start, end);
  353. tabstopManager.tabNext();
  354. };
  355. this.$getScope = function(editor) {
  356. var scope = editor.session.$mode.$id || "";
  357. scope = scope.split("/").pop();
  358. if (scope === "html" || scope === "php") {
  359. // PHP is actually HTML
  360. if (scope === "php")
  361. scope = "html";
  362. var c = editor.getCursorPosition()
  363. var state = editor.session.getState(c.row);
  364. if (typeof state === "object") {
  365. state = state[0];
  366. }
  367. if (state.substring) {
  368. if (state.substring(0, 3) == "js-")
  369. scope = "javascript";
  370. else if (state.substring(0, 4) == "css-")
  371. scope = "css";
  372. else if (state.substring(0, 4) == "php-")
  373. scope = "php";
  374. }
  375. }
  376. return scope;
  377. };
  378. this.expandWithTab = function(editor) {
  379. var cursor = editor.getCursorPosition();
  380. var line = editor.session.getLine(cursor.row);
  381. var before = line.substring(0, cursor.column);
  382. var after = line.substr(cursor.column);
  383. var scope = this.$getScope(editor);
  384. var snippetMap = this.snippetMap;
  385. var snippet;
  386. [scope, "_"].some(function(scope) {
  387. var snippets = snippetMap[scope];
  388. if (snippets)
  389. snippet = this.findMatchingSnippet(snippets, before, after);
  390. return !!snippet;
  391. }, this);
  392. if (!snippet)
  393. return false;
  394. editor.session.doc.removeInLine(cursor.row,
  395. cursor.column - snippet.replaceBefore.length,
  396. cursor.column + snippet.replaceAfter.length
  397. );
  398. this.variables.M__ = snippet.matchBefore;
  399. this.variables.T__ = snippet.matchAfter;
  400. this.insertSnippet(editor, snippet.content);
  401. this.variables.M__ = this.variables.T__ = null;
  402. return true;
  403. };
  404. this.findMatchingSnippet = function(snippetList, before, after) {
  405. for (var i = snippetList.length; i--;) {
  406. var s = snippetList[i];
  407. if (s.startRe && !s.startRe.test(before))
  408. continue;
  409. if (s.endRe && !s.endRe.test(after))
  410. continue;
  411. if (!s.startRe && !s.endRe)
  412. continue;
  413. s.matchBefore = s.startRe ? s.startRe.exec(before) : [""];
  414. s.matchAfter = s.endRe ? s.endRe.exec(after) : [""];
  415. s.replaceBefore = s.triggerRe ? s.triggerRe.exec(before)[0] : "";
  416. s.replaceAfter = s.endTriggerRe ? s.endTriggerRe.exec(after)[0] : "";
  417. return s;
  418. }
  419. };
  420. this.snippetMap = {};
  421. this.snippetNameMap = {};
  422. this.register = function(snippets, scope) {
  423. var snippetMap = this.snippetMap;
  424. var snippetNameMap = this.snippetNameMap;
  425. var self = this;
  426. function wrapRegexp(src) {
  427. if (src && !/^\^?\(.*\)\$?$|^\\b$/.test(src))
  428. src = "(?:" + src + ")"
  429. return src || "";
  430. }
  431. function guardedRegexp(re, guard, opening) {
  432. re = wrapRegexp(re);
  433. guard = wrapRegexp(guard);
  434. if (opening) {
  435. re = guard + re;
  436. if (re && re[re.length - 1] != "$")
  437. re = re + "$";
  438. } else {
  439. re = re + guard;
  440. if (re && re[0] != "^")
  441. re = "^" + re;
  442. }
  443. return new RegExp(re);
  444. }
  445. function addSnippet(s) {
  446. if (!s.scope)
  447. s.scope = scope || "_";
  448. scope = s.scope
  449. if (!snippetMap[scope]) {
  450. snippetMap[scope] = [];
  451. snippetNameMap[scope] = {};
  452. }
  453. var map = snippetNameMap[scope];
  454. if (s.name) {
  455. var old = map[s.name];
  456. if (old)
  457. self.unregister(old);
  458. map[s.name] = s;
  459. }
  460. snippetMap[scope].push(s);
  461. if (s.tabTrigger && !s.trigger) {
  462. if (!s.guard && /^\w/.test(s.tabTrigger))
  463. s.guard = "\\b";
  464. s.trigger = lang.escapeRegExp(s.tabTrigger);
  465. }
  466. s.startRe = guardedRegexp(s.trigger, s.guard, true);
  467. s.triggerRe = new RegExp(s.trigger, "", true);
  468. s.endRe = guardedRegexp(s.endTrigger, s.endGuard, true);
  469. s.endTriggerRe = new RegExp(s.endTrigger, "", true);
  470. };
  471. if (snippets.content)
  472. addSnippet(snippets);
  473. else if (Array.isArray(snippets))
  474. snippets.forEach(addSnippet);
  475. };
  476. this.unregister = function(snippets, scope) {
  477. var snippetMap = this.snippetMap;
  478. var snippetNameMap = this.snippetNameMap;
  479. function removeSnippet(s) {
  480. var nameMap = snippetNameMap[s.scope||scope];
  481. if (nameMap && nameMap[s.name]) {
  482. delete nameMap[s.name];
  483. var map = snippetMap[s.scope||scope];
  484. var i = map && map.indexOf(s);
  485. if (i >= 0)
  486. map.splice(i, 1);
  487. }
  488. }
  489. if (snippets.content)
  490. removeSnippet(snippets);
  491. else if (Array.isArray(snippets))
  492. snippets.forEach(removeSnippet);
  493. };
  494. this.parseSnippetFile = function(str) {
  495. str = str.replace(/\r/g, "");
  496. var list = [], snippet = {};
  497. var re = /^#.*|^({[\s\S]*})\s*$|^(\S+) (.*)$|^((?:\n*\t.*)+)/gm;
  498. var m;
  499. while (m = re.exec(str)) {
  500. if (m[1]) {
  501. try {
  502. snippet = JSON.parse(m[1])
  503. list.push(snippet);
  504. } catch (e) {}
  505. } if (m[4]) {
  506. snippet.content = m[4].replace(/^\t/gm, "");
  507. list.push(snippet);
  508. snippet = {};
  509. } else {
  510. var key = m[2], val = m[3];
  511. if (key == "regex") {
  512. var guardRe = /\/((?:[^\/\\]|\\.)*)|$/g;
  513. snippet.guard = guardRe.exec(val)[1];
  514. snippet.trigger = guardRe.exec(val)[1];
  515. snippet.endTrigger = guardRe.exec(val)[1];
  516. snippet.endGuard = guardRe.exec(val)[1];
  517. } else if (key == "snippet") {
  518. snippet.tabTrigger = val.match(/^\S*/)[0];
  519. if (!snippet.name)
  520. snippet.name = val;
  521. } else {
  522. snippet[key] = val;
  523. }
  524. }
  525. }
  526. return list;
  527. };
  528. this.getSnippetByName = function(name, editor) {
  529. var scope = editor && this.$getScope(editor);
  530. var snippetMap = this.snippetNameMap;
  531. var snippet;
  532. [scope, "_"].some(function(scope) {
  533. var snippets = snippetMap[scope];
  534. if (snippets)
  535. snippet = snippets[name];
  536. return !!snippet;
  537. }, this);
  538. return snippet;
  539. };
  540. }).call(SnippetManager.prototype);
  541. var TabstopManager = function(editor) {
  542. if (editor.tabstopManager)
  543. return editor.tabstopManager;
  544. editor.tabstopManager = this;
  545. this.$onChange = this.onChange.bind(this);
  546. this.$onChangeSelection = lang.delayedCall(this.onChangeSelection.bind(this)).schedule;
  547. this.$onChangeSession = this.onChangeSession.bind(this);
  548. this.$onAfterExec = this.onAfterExec.bind(this);
  549. this.attach(editor);
  550. };
  551. (function() {
  552. this.attach = function(editor) {
  553. this.index = -1;
  554. this.ranges = [];
  555. this.tabstops = [];
  556. this.selectedTabstop = null;
  557. this.editor = editor;
  558. this.editor.on("change", this.$onChange);
  559. this.editor.on("changeSelection", this.$onChangeSelection);
  560. this.editor.on("changeSession", this.$onChangeSession);
  561. this.editor.commands.on("afterExec", this.$onAfterExec);
  562. this.editor.keyBinding.addKeyboardHandler(this.keyboardHandler);
  563. };
  564. this.detach = function() {
  565. this.tabstops.forEach(this.removeTabstopMarkers, this);
  566. this.ranges = null;
  567. this.tabstops = null;
  568. this.selectedTabstop = null;
  569. this.editor.removeListener("change", this.$onChange);
  570. this.editor.removeListener("changeSelection", this.$onChangeSelection);
  571. this.editor.removeListener("changeSession", this.$onChangeSession);
  572. this.editor.commands.removeListener("afterExec", this.$onAfterExec);
  573. this.editor.keyBinding.removeKeyboardHandler(this.keyboardHandler);
  574. this.editor.tabstopManager = null;
  575. this.editor = null;
  576. };
  577. this.onChange = function(e) {
  578. var changeRange = e.data.range;
  579. var isRemove = e.data.action[0] == "r";
  580. var start = changeRange.start;
  581. var end = changeRange.end;
  582. var startRow = start.row;
  583. var endRow = end.row;
  584. var lineDif = endRow - startRow;
  585. var colDiff = end.column - start.column;
  586. if (isRemove) {
  587. lineDif = -lineDif;
  588. colDiff = -colDiff;
  589. }
  590. if (!this.$inChange && isRemove) {
  591. var ts = this.selectedTabstop;
  592. var changedOutside = !ts.some(function(r) {
  593. return comparePoints(r.start, start) <= 0 && comparePoints(r.end, end) >= 0;
  594. });
  595. if (changedOutside)
  596. return this.detach();
  597. }
  598. var ranges = this.ranges;
  599. for (var i = 0; i < ranges.length; i++) {
  600. var r = ranges[i];
  601. if (r.end.row < start.row)
  602. continue;
  603. if (comparePoints(start, r.start) < 0 && comparePoints(end, r.end) > 0) {
  604. this.removeRange(r);
  605. i--;
  606. continue;
  607. }
  608. if (r.start.row == startRow && r.start.column > start.column)
  609. r.start.column += colDiff;
  610. if (r.end.row == startRow && r.end.column >= start.column)
  611. r.end.column += colDiff;
  612. if (r.start.row >= startRow)
  613. r.start.row += lineDif;
  614. if (r.end.row >= startRow)
  615. r.end.row += lineDif;
  616. if (comparePoints(r.start, r.end) > 0)
  617. this.removeRange(r);
  618. }
  619. if (!ranges.length)
  620. this.detach();
  621. };
  622. this.updateLinkedFields = function() {
  623. var ts = this.selectedTabstop;
  624. if (!ts.hasLinkedRanges)
  625. return;
  626. this.$inChange = true;
  627. var session = this.editor.session;
  628. var text = session.getTextRange(ts.firstNonLinked);
  629. for (var i = ts.length; i--;) {
  630. var range = ts[i];
  631. if (!range.linked)
  632. continue;
  633. var fmt = exports.snippetManager.tmStrFormat(text, range.original)
  634. session.replace(range, fmt);
  635. }
  636. this.$inChange = false;
  637. };
  638. this.onAfterExec = function(e) {
  639. if (e.command && !e.command.readOnly)
  640. this.updateLinkedFields();
  641. };
  642. this.onChangeSelection = function() {
  643. if (!this.editor)
  644. return
  645. var lead = this.editor.selection.lead;
  646. var anchor = this.editor.selection.anchor;
  647. var isEmpty = this.editor.selection.isEmpty();
  648. for (var i = this.ranges.length; i--;) {
  649. if (this.ranges[i].linked)
  650. continue;
  651. var containsLead = this.ranges[i].contains(lead.row, lead.column);
  652. var containsAnchor = isEmpty || this.ranges[i].contains(anchor.row, anchor.column);
  653. if (containsLead && containsAnchor)
  654. return;
  655. }
  656. this.detach();
  657. };
  658. this.onChangeSession = function() {
  659. this.detach();
  660. };
  661. this.tabNext = function(dir) {
  662. var max = this.tabstops.length - 1;
  663. var index = this.index + (dir || 1);
  664. index = Math.min(Math.max(index, 0), max);
  665. this.selectTabstop(index);
  666. if (index == max)
  667. this.detach();
  668. };
  669. this.selectTabstop = function(index) {
  670. var ts = this.tabstops[this.index];
  671. if (ts)
  672. this.addTabstopMarkers(ts);
  673. this.index = index;
  674. ts = this.tabstops[this.index];
  675. if (!ts || !ts.length)
  676. return;
  677. this.selectedTabstop = ts;
  678. if (!this.editor.inVirtualSelectionMode) {
  679. var sel = this.editor.multiSelect;
  680. sel.toSingleRange(ts.firstNonLinked.clone());
  681. for (var i = ts.length; i--;) {
  682. if (ts.hasLinkedRanges && ts[i].linked)
  683. continue;
  684. sel.addRange(ts[i].clone(), true);
  685. }
  686. } else {
  687. this.editor.selection.setRange(ts.firstNonLinked);
  688. }
  689. this.editor.keyBinding.addKeyboardHandler(this.keyboardHandler);
  690. };
  691. this.addTabstops = function(tabstops, start, end) {
  692. // add final tabstop if missing
  693. if (!tabstops[0]) {
  694. var p = Range.fromPoints(end, end);
  695. moveRelative(p.start, start);
  696. moveRelative(p.end, start);
  697. tabstops[0] = [p];
  698. tabstops[0].index = 0;
  699. }
  700. var i = this.index;
  701. var arg = [i, 0];
  702. var ranges = this.ranges;
  703. var editor = this.editor;
  704. tabstops.forEach(function(ts) {
  705. for (var i = ts.length; i--;) {
  706. var p = ts[i];
  707. var range = Range.fromPoints(p.start, p.end || p.start);
  708. movePoint(range.start, start);
  709. movePoint(range.end, start);
  710. range.original = p;
  711. range.tabstop = ts;
  712. ranges.push(range);
  713. ts[i] = range;
  714. if (p.fmtString) {
  715. range.linked = true;
  716. ts.hasLinkedRanges = true;
  717. } else if (!ts.firstNonLinked)
  718. ts.firstNonLinked = range;
  719. }
  720. if (!ts.firstNonLinked)
  721. ts.hasLinkedRanges = false;
  722. arg.push(ts);
  723. this.addTabstopMarkers(ts);
  724. }, this);
  725. // tabstop 0 is the last one
  726. arg.push(arg.splice(2, 1)[0]);
  727. this.tabstops.splice.apply(this.tabstops, arg);
  728. };
  729. this.addTabstopMarkers = function(ts) {
  730. var session = this.editor.session;
  731. ts.forEach(function(range) {
  732. if (!range.markerId)
  733. range.markerId = session.addMarker(range, "ace_snippet-marker", "text");
  734. });
  735. };
  736. this.removeTabstopMarkers = function(ts) {
  737. var session = this.editor.session;
  738. ts.forEach(function(range) {
  739. session.removeMarker(range.markerId);
  740. range.markerId = null;
  741. });
  742. };
  743. this.removeRange = function(range) {
  744. var i = range.tabstop.indexOf(range);
  745. range.tabstop.splice(i, 1);
  746. i = this.ranges.indexOf(range);
  747. this.ranges.splice(i, 1);
  748. this.editor.session.removeMarker(range.markerId);
  749. };
  750. this.keyboardHandler = new HashHandler();
  751. this.keyboardHandler.bindKeys({
  752. "Tab": function(ed) {
  753. ed.tabstopManager.tabNext(1);
  754. },
  755. "Shift-Tab": function(ed) {
  756. ed.tabstopManager.tabNext(-1);
  757. },
  758. "Esc": function(ed) {
  759. ed.tabstopManager.detach();
  760. },
  761. "Return": function(ed) {
  762. //ed.tabstopManager.tabNext(1);
  763. return false;
  764. }
  765. });
  766. }).call(TabstopManager.prototype);
  767. var movePoint = function(point, diff) {
  768. if (point.row == 0)
  769. point.column += diff.column;
  770. point.row += diff.row;
  771. };
  772. var moveRelative = function(point, start) {
  773. if (point.row == start.row)
  774. point.column -= start.column;
  775. point.row -= start.row;
  776. };
  777. require("./lib/dom").importCssString("\
  778. .ace_snippet-marker {\
  779. -moz-box-sizing: border-box;\
  780. box-sizing: border-box;\
  781. background: rgba(194, 193, 208, 0.09);\
  782. border: 1px dotted rgba(211, 208, 235, 0.62);\
  783. position: absolute;\
  784. }");
  785. exports.snippetManager = new SnippetManager();
  786. });