text.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  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 Tokenizer = require("../tokenizer").Tokenizer;
  33. var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
  34. var Behaviour = require("./behaviour").Behaviour;
  35. var unicode = require("../unicode");
  36. var lang = require("../lib/lang");
  37. var TokenIterator = require("../token_iterator").TokenIterator;
  38. var Range = require("../range").Range;
  39. var Mode = function() {
  40. this.HighlightRules = TextHighlightRules;
  41. this.$behaviour = new Behaviour();
  42. };
  43. (function() {
  44. this.tokenRe = new RegExp("^["
  45. + unicode.packages.L
  46. + unicode.packages.Mn + unicode.packages.Mc
  47. + unicode.packages.Nd
  48. + unicode.packages.Pc + "\\$_]+", "g"
  49. );
  50. this.nonTokenRe = new RegExp("^(?:[^"
  51. + unicode.packages.L
  52. + unicode.packages.Mn + unicode.packages.Mc
  53. + unicode.packages.Nd
  54. + unicode.packages.Pc + "\\$_]|\\s])+", "g"
  55. );
  56. this.getTokenizer = function() {
  57. if (!this.$tokenizer) {
  58. this.$highlightRules = this.$highlightRules || new this.HighlightRules();
  59. this.$tokenizer = new Tokenizer(this.$highlightRules.getRules());
  60. }
  61. return this.$tokenizer;
  62. };
  63. this.lineCommentStart = "";
  64. this.blockComment = "";
  65. this.toggleCommentLines = function(state, session, startRow, endRow) {
  66. var doc = session.doc;
  67. var ignoreBlankLines = true;
  68. var shouldRemove = true;
  69. var minIndent = Infinity;
  70. var tabSize = session.getTabSize();
  71. var insertAtTabStop = false;
  72. if (!this.lineCommentStart) {
  73. if (!this.blockComment)
  74. return false;
  75. var lineCommentStart = this.blockComment.start;
  76. var lineCommentEnd = this.blockComment.end;
  77. var regexpStart = new RegExp("^(\\s*)(?:" + lang.escapeRegExp(lineCommentStart) + ")");
  78. var regexpEnd = new RegExp("(?:" + lang.escapeRegExp(lineCommentEnd) + ")\\s*$");
  79. var comment = function(line, i) {
  80. if (testRemove(line, i))
  81. return;
  82. if (!ignoreBlankLines || /\S/.test(line)) {
  83. doc.insertInLine({row: i, column: line.length}, lineCommentEnd);
  84. doc.insertInLine({row: i, column: minIndent}, lineCommentStart);
  85. }
  86. };
  87. var uncomment = function(line, i) {
  88. var m;
  89. if (m = line.match(regexpEnd))
  90. doc.removeInLine(i, line.length - m[0].length, line.length);
  91. if (m = line.match(regexpStart))
  92. doc.removeInLine(i, m[1].length, m[0].length);
  93. };
  94. var testRemove = function(line, row) {
  95. if (regexpStart.test(line))
  96. return true;
  97. var tokens = session.getTokens(row);
  98. for (var i = 0; i < tokens.length; i++) {
  99. if (tokens[i].type === 'comment')
  100. return true;
  101. }
  102. };
  103. } else {
  104. if (Array.isArray(this.lineCommentStart)) {
  105. var regexpStart = this.lineCommentStart.map(lang.escapeRegExp).join("|");
  106. var lineCommentStart = this.lineCommentStart[0];
  107. } else {
  108. var regexpStart = lang.escapeRegExp(this.lineCommentStart);
  109. var lineCommentStart = this.lineCommentStart;
  110. }
  111. regexpStart = new RegExp("^(\\s*)(?:" + regexpStart + ") ?");
  112. insertAtTabStop = session.getUseSoftTabs();
  113. var uncomment = function(line, i) {
  114. var m = line.match(regexpStart);
  115. if (!m) return;
  116. var start = m[1].length, end = m[0].length;
  117. if (!shouldInsertSpace(line, start, end) && m[0][end - 1] == " ")
  118. end--;
  119. doc.removeInLine(i, start, end);
  120. };
  121. var commentWithSpace = lineCommentStart + " ";
  122. var comment = function(line, i) {
  123. if (!ignoreBlankLines || /\S/.test(line)) {
  124. if (shouldInsertSpace(line, minIndent, minIndent))
  125. doc.insertInLine({row: i, column: minIndent}, commentWithSpace);
  126. else
  127. doc.insertInLine({row: i, column: minIndent}, lineCommentStart);
  128. }
  129. };
  130. var testRemove = function(line, i) {
  131. return regexpStart.test(line);
  132. };
  133. var shouldInsertSpace = function(line, before, after) {
  134. var spaces = 0;
  135. while (before-- && line.charAt(before) == " ")
  136. spaces++;
  137. if (spaces % tabSize != 0)
  138. return false;
  139. var spaces = 0;
  140. while (line.charAt(after++) == " ")
  141. spaces++;
  142. if (tabSize > 2)
  143. return spaces % tabSize != tabSize - 1;
  144. else
  145. return spaces % tabSize == 0;
  146. return true;
  147. };
  148. }
  149. function iter(fun) {
  150. for (var i = startRow; i <= endRow; i++)
  151. fun(doc.getLine(i), i);
  152. }
  153. var minEmptyLength = Infinity;
  154. iter(function(line, i) {
  155. var indent = line.search(/\S/);
  156. if (indent !== -1) {
  157. if (indent < minIndent)
  158. minIndent = indent;
  159. if (shouldRemove && !testRemove(line, i))
  160. shouldRemove = false;
  161. } else if (minEmptyLength > line.length) {
  162. minEmptyLength = line.length;
  163. }
  164. });
  165. if (minIndent == Infinity) {
  166. minIndent = minEmptyLength;
  167. ignoreBlankLines = false;
  168. shouldRemove = false;
  169. }
  170. if (insertAtTabStop && minIndent % tabSize != 0)
  171. minIndent = Math.floor(minIndent / tabSize) * tabSize;
  172. iter(shouldRemove ? uncomment : comment);
  173. };
  174. this.toggleBlockComment = function(state, session, range, cursor) {
  175. var comment = this.blockComment;
  176. if (!comment)
  177. return;
  178. if (!comment.start && comment[0])
  179. comment = comment[0];
  180. var iterator = new TokenIterator(session, cursor.row, cursor.column);
  181. var token = iterator.getCurrentToken();
  182. var sel = session.selection;
  183. var initialRange = session.selection.toOrientedRange();
  184. var startRow, colDiff;
  185. if (token && /comment/.test(token.type)) {
  186. var startRange, endRange;
  187. while (token && /comment/.test(token.type)) {
  188. var i = token.value.indexOf(comment.start);
  189. if (i != -1) {
  190. var row = iterator.getCurrentTokenRow();
  191. var column = iterator.getCurrentTokenColumn() + i;
  192. startRange = new Range(row, column, row, column + comment.start.length);
  193. break;
  194. }
  195. token = iterator.stepBackward();
  196. }
  197. var iterator = new TokenIterator(session, cursor.row, cursor.column);
  198. var token = iterator.getCurrentToken();
  199. while (token && /comment/.test(token.type)) {
  200. var i = token.value.indexOf(comment.end);
  201. if (i != -1) {
  202. var row = iterator.getCurrentTokenRow();
  203. var column = iterator.getCurrentTokenColumn() + i;
  204. endRange = new Range(row, column, row, column + comment.end.length);
  205. break;
  206. }
  207. token = iterator.stepForward();
  208. }
  209. if (endRange)
  210. session.remove(endRange);
  211. if (startRange) {
  212. session.remove(startRange);
  213. startRow = startRange.start.row;
  214. colDiff = -comment.start.length;
  215. }
  216. } else {
  217. colDiff = comment.start.length;
  218. startRow = range.start.row;
  219. session.insert(range.end, comment.end);
  220. session.insert(range.start, comment.start);
  221. }
  222. // todo: selection should have ended up in the right place automatically!
  223. if (initialRange.start.row == startRow)
  224. initialRange.start.column += colDiff;
  225. if (initialRange.end.row == startRow)
  226. initialRange.end.column += colDiff;
  227. session.selection.fromOrientedRange(initialRange);
  228. };
  229. this.getNextLineIndent = function(state, line, tab) {
  230. return this.$getIndent(line);
  231. };
  232. this.checkOutdent = function(state, line, input) {
  233. return false;
  234. };
  235. this.autoOutdent = function(state, doc, row) {
  236. };
  237. this.$getIndent = function(line) {
  238. return line.match(/^\s*/)[0];
  239. };
  240. this.createWorker = function(session) {
  241. return null;
  242. };
  243. this.createModeDelegates = function (mapping) {
  244. this.$embeds = [];
  245. this.$modes = {};
  246. for (var i in mapping) {
  247. if (mapping[i]) {
  248. this.$embeds.push(i);
  249. this.$modes[i] = new mapping[i]();
  250. }
  251. }
  252. var delegations = ['toggleBlockComment', 'toggleCommentLines', 'getNextLineIndent',
  253. 'checkOutdent', 'autoOutdent', 'transformAction', 'getCompletions'];
  254. for (var i = 0; i < delegations.length; i++) {
  255. (function(scope) {
  256. var functionName = delegations[i];
  257. var defaultHandler = scope[functionName];
  258. scope[delegations[i]] = function() {
  259. return this.$delegator(functionName, arguments, defaultHandler);
  260. };
  261. } (this));
  262. }
  263. };
  264. this.$delegator = function(method, args, defaultHandler) {
  265. var state = args[0];
  266. if (typeof state != "string")
  267. state = state[0];
  268. for (var i = 0; i < this.$embeds.length; i++) {
  269. if (!this.$modes[this.$embeds[i]]) continue;
  270. var split = state.split(this.$embeds[i]);
  271. if (!split[0] && split[1]) {
  272. args[0] = split[1];
  273. var mode = this.$modes[this.$embeds[i]];
  274. return mode[method].apply(mode, args);
  275. }
  276. }
  277. var ret = defaultHandler.apply(this, args);
  278. return defaultHandler ? ret : undefined;
  279. };
  280. this.transformAction = function(state, action, editor, session, param) {
  281. if (this.$behaviour) {
  282. var behaviours = this.$behaviour.getBehaviours();
  283. for (var key in behaviours) {
  284. if (behaviours[key][action]) {
  285. var ret = behaviours[key][action].apply(this, arguments);
  286. if (ret) {
  287. return ret;
  288. }
  289. }
  290. }
  291. }
  292. };
  293. this.getKeywords = function(append) {
  294. // this is for autocompletion to pick up regexp'ed keywords
  295. if (!this.completionKeywords) {
  296. var rules = this.$tokenizer.rules;
  297. var completionKeywords = [];
  298. for (var rule in rules) {
  299. var ruleItr = rules[rule];
  300. for (var r = 0, l = ruleItr.length; r < l; r++) {
  301. if (typeof ruleItr[r].token === "string") {
  302. if (/keyword|support|storage/.test(ruleItr[r].token))
  303. completionKeywords.push(ruleItr[r].regex);
  304. }
  305. else if (typeof ruleItr[r].token === "object") {
  306. for (var a = 0, aLength = ruleItr[r].token.length; a < aLength; a++) {
  307. if (/keyword|support|storage/.test(ruleItr[r].token[a])) {
  308. // drop surrounding parens
  309. var rule = ruleItr[r].regex.match(/\(.+?\)/g)[a];
  310. completionKeywords.push(rule.substr(1, rule.length - 2));
  311. }
  312. }
  313. }
  314. }
  315. }
  316. this.completionKeywords = completionKeywords;
  317. }
  318. // this is for highlighting embed rules, like HAML/Ruby or Obj-C/C
  319. if (!append)
  320. return this.$keywordList;
  321. return completionKeywords.concat(this.$keywordList || []);
  322. };
  323. this.$createKeywordList = function() {
  324. if (!this.$highlightRules)
  325. this.getTokenizer();
  326. return this.$keywordList = this.$highlightRules.$keywordList || [];
  327. };
  328. this.getCompletions = function(state, session, pos, prefix) {
  329. var keywords = this.$keywordList || this.$createKeywordList();
  330. return keywords.map(function(word) {
  331. return {
  332. name: word,
  333. value: word,
  334. score: 0,
  335. meta: "keyword"
  336. };
  337. });
  338. };
  339. this.$id = "ace/mode/text";
  340. }).call(Mode.prototype);
  341. exports.Mode = Mode;
  342. });