PageRenderTime 61ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/ace/search.js

https://github.com/bnowel/ace
JavaScript | 384 lines | 234 code | 57 blank | 93 comment | 79 complexity | 9f4c2b3fece5c42423c84a256040eb7f MD5 | raw file
  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 oop = require("./lib/oop");
  34. var Range = require("./range").Range;
  35. /**
  36. * class Search
  37. *
  38. * A class designed to handle all sorts of text searches within a [[Document `Document`]].
  39. *
  40. **/
  41. /**
  42. * new Search()
  43. *
  44. * Creates a new `Search` object. The following search options are avaliable:
  45. *
  46. * * `needle`: The string or regular expression you're looking for
  47. * * `backwards`: Whether to search backwards from where cursor currently is. Defaults to `false`.
  48. * * `wrap`: Whether to wrap the search back to the beginning when it hits the end. Defaults to `false`.
  49. * * `caseSensitive`: Whether the search ought to be case-sensitive. Defaults to `false`.
  50. * * `wholeWord`: Whether the search matches only on whole words. Defaults to `false`.
  51. * * `range`: The [[Range]] to search within. Set this to `null` for the whole document
  52. * * `regExp`: Whether the search is a regular expression or not. Defaults to `false`.
  53. * * `start`: The starting [[Range]] or cursor position to begin the search
  54. * * `skipCurrent`: Whether or not to include the current line in the search. Default to `false`.
  55. *
  56. **/
  57. var Search = function() {
  58. this.$options = {};
  59. };
  60. (function() {
  61. /**
  62. * Search.set(options) -> Search
  63. * - options (Object): An object containing all the new search properties
  64. *
  65. * Sets the search options via the `options` parameter.
  66. *
  67. **/
  68. this.set = function(options) {
  69. oop.mixin(this.$options, options);
  70. return this;
  71. };
  72. /**
  73. * Search.getOptions() -> Object
  74. *
  75. * [Returns an object containing all the search options.]{: #Search.getOptions}
  76. *
  77. **/
  78. this.getOptions = function() {
  79. return lang.copyObject(this.$options);
  80. };
  81. this.setOptions = function(options) {
  82. this.$options = options;
  83. };
  84. /**
  85. * Search.find(session) -> Range
  86. * - session (EditSession): The session to search with
  87. *
  88. * Searches for `options.needle`. If found, this method returns the [[Range `Range`]] where the text first occurs. If `options.backwards` is `true`, the search goes backwards in the session.
  89. *
  90. **/
  91. this.find = function(session) {
  92. var iterator = this.$matchIterator(session, this.$options);
  93. if (!iterator)
  94. return false;
  95. var firstRange = null;
  96. iterator.forEach(function(range, row, offset) {
  97. if (!range.start) {
  98. var column = range.offset + (offset || 0);
  99. firstRange = new Range(row, column, row, column+range.length);
  100. } else
  101. firstRange = range;
  102. return true;
  103. });
  104. return firstRange;
  105. };
  106. /**
  107. * Search.findAll(session) -> [Range]
  108. * - session (EditSession): The session to search with
  109. *
  110. * Searches for all occurances `options.needle`. If found, this method returns an array of [[Range `Range`s]] where the text first occurs. If `options.backwards` is `true`, the search goes backwards in the session.
  111. *
  112. **/
  113. this.findAll = function(session) {
  114. var options = this.$options;
  115. if (!options.needle)
  116. return [];
  117. this.$assembleRegExp(options);
  118. var range = options.range;
  119. var lines = range
  120. ? session.getLines(range.start.row, range.end.row)
  121. : session.doc.getAllLines();
  122. var ranges = [];
  123. var re = options.re;
  124. if (options.$isMultiLine) {
  125. var len = re.length;
  126. var maxRow = lines.length - len;
  127. for (var row = re.offset || 0; row <= maxRow; row++) {
  128. for (var j = 0; j < len; j++)
  129. if (lines[row + j].search(re[j]) == -1)
  130. break;
  131. var startLine = lines[row];
  132. var line = lines[row + len - 1];
  133. var startIndex = startLine.match(re[0])[0].length;
  134. var endIndex = line.match(re[len - 1])[0].length;
  135. ranges.push(new Range(
  136. row, startLine.length - startIndex,
  137. row + len - 1, endIndex
  138. ));
  139. }
  140. } else {
  141. for (var i = 0; i < lines.length; i++) {
  142. var matches = lang.getMatchOffsets(lines[i], re);
  143. for (var j = 0; j < matches.length; j++) {
  144. var match = matches[j];
  145. ranges.push(new Range(i, match.offset, i, match.offset + match.length));
  146. }
  147. }
  148. }
  149. if (range) {
  150. var startColumn = range.start.column;
  151. var endColumn = range.start.column;
  152. var i = 0, j = ranges.length - 1;
  153. while (i < j && ranges[i].start.column < startColumn && ranges[i].start.row == range.start.row)
  154. i++;
  155. while (i < j && ranges[j].end.column > endColumn && ranges[j].end.row == range.end.row)
  156. j--;
  157. return ranges.slice(i, j + 1);
  158. }
  159. return ranges;
  160. };
  161. /**
  162. * Search.replace(input, replacement) -> String
  163. * - input (String): The text to search in
  164. * - replacement (String): The replacing text
  165. * + (String): If `options.regExp` is `true`, this function returns `input` with the replacement already made. Otherwise, this function just returns `replacement`.<br/>
  166. * If `options.needle` was not found, this function returns `null`.
  167. *
  168. * Searches for `options.needle` in `input`, and, if found, replaces it with `replacement`.
  169. *
  170. **/
  171. this.replace = function(input, replacement) {
  172. var options = this.$options;
  173. var re = this.$assembleRegExp(options);
  174. if (options.$isMultiLine)
  175. return replacement;
  176. if (!re)
  177. return;
  178. var match = re.exec(input);
  179. if (!match || match[0].length != input.length)
  180. return null;
  181. replacement = input.replace(re, replacement);
  182. if (options.preserveCase) {
  183. replacement = replacement.split("");
  184. for (var i = Math.min(input.length, input.length); i--; ) {
  185. var ch = input[i];
  186. if (ch && ch.toLowerCase() != ch)
  187. replacement[i] = replacement[i].toUpperCase();
  188. else
  189. replacement[i] = replacement[i].toLowerCase();
  190. }
  191. replacement = replacement.join("");
  192. }
  193. return replacement;
  194. };
  195. /** internal, hide
  196. * Search.$matchIterator(session) -> String | Boolean
  197. * - session (EditSession): The session to search with
  198. *
  199. **/
  200. this.$matchIterator = function(session, options) {
  201. var re = this.$assembleRegExp(options);
  202. if (!re)
  203. return false;
  204. var self = this, callback, backwards = options.backwards;
  205. if (options.$isMultiLine) {
  206. var len = re.length;
  207. var matchIterator = function(line, row, offset) {
  208. var startIndex = line.search(re[0]);
  209. if (startIndex == -1)
  210. return;
  211. for (var i = 1; i < len; i++) {
  212. line = session.getLine(row + i);
  213. if (line.search(re[i]) == -1)
  214. return;
  215. }
  216. var endIndex = line.match(re[len - 1])[0].length;
  217. var range = new Range(row, startIndex, row + len - 1, endIndex);
  218. if (re.offset == 1) {
  219. range.start.row--;
  220. range.start.column = Number.MAX_VALUE;
  221. } else if (offset)
  222. range.start.column += offset;
  223. if (callback(range))
  224. return true;
  225. };
  226. } else if (backwards) {
  227. var matchIterator = function(line, row, startIndex) {
  228. var matches = lang.getMatchOffsets(line, re);
  229. for (var i = matches.length-1; i >= 0; i--)
  230. if (callback(matches[i], row, startIndex))
  231. return true;
  232. };
  233. } else {
  234. var matchIterator = function(line, row, startIndex) {
  235. var matches = lang.getMatchOffsets(line, re);
  236. for (var i = 0; i < matches.length; i++)
  237. if (callback(matches[i], row, startIndex))
  238. return true;
  239. };
  240. }
  241. return {
  242. forEach: function(_callback) {
  243. callback = _callback;
  244. self.$lineIterator(session, options).forEach(matchIterator);
  245. }
  246. };
  247. };
  248. this.$assembleRegExp = function(options) {
  249. if (options.needle instanceof RegExp)
  250. return options.re = options.needle;
  251. var needle = options.needle;
  252. if (!options.needle)
  253. return options.re = false;
  254. if (!options.regExp)
  255. needle = lang.escapeRegExp(needle);
  256. if (options.wholeWord)
  257. needle = "\\b" + needle + "\\b";
  258. var modifier = options.caseSensitive ? "g" : "gi";
  259. options.$isMultiLine = /[\n\r]/.test(needle);
  260. if (options.$isMultiLine)
  261. return options.re = this.$assembleMultilineRegExp(needle, modifier);
  262. try {
  263. var re = new RegExp(needle, modifier);
  264. } catch(e) {
  265. re = false;
  266. }
  267. return options.re = re;
  268. };
  269. this.$assembleMultilineRegExp = function(needle, modifier) {
  270. var parts = needle.replace(/\r\n|\r|\n/g, "$\n^").split("\n");
  271. var re = [];
  272. for (var i = 0; i < parts.length; i++) try {
  273. re.push(new RegExp(parts[i], modifier));
  274. } catch(e) {
  275. return false;
  276. }
  277. if (parts[0] == "") {
  278. re.shift();
  279. re.offset = 1;
  280. } else {
  281. re.offset = 0;
  282. }
  283. return re;
  284. };
  285. this.$lineIterator = function(session, options) {
  286. var backwards = options.backwards == true;
  287. var skipCurrent = options.skipCurrent != false;
  288. var range = options.range;
  289. var start = options.start;
  290. if (!start)
  291. start = range ? range[backwards ? "end" : "start"] : session.selection.getRange();
  292. if (start.start)
  293. start = start[skipCurrent != backwards ? "end" : "start"];
  294. var firstRow = range ? range.start.row : 0;
  295. var lastRow = range ? range.end.row : session.getLength() - 1;
  296. var forEach = backwards ? function(callback) {
  297. var row = start.row;
  298. var line = session.getLine(row).substring(0, start.column);
  299. if (callback(line, row))
  300. return;
  301. for (row--; row >= firstRow; row--)
  302. if (callback(session.getLine(row), row))
  303. return;
  304. if (options.wrap == false)
  305. return;
  306. for (row = lastRow, firstRow = start.row; row >= firstRow; row--)
  307. if (callback(session.getLine(row), row))
  308. return;
  309. } : function(callback) {
  310. var row = start.row;
  311. var line = session.getLine(row).substr(start.column);
  312. if (callback(line, row, start.column))
  313. return;
  314. for (row = row+1; row <= lastRow; row++)
  315. if (callback(session.getLine(row), row))
  316. return;
  317. if (options.wrap == false)
  318. return;
  319. for (row = firstRow, lastRow = start.row; row <= lastRow; row++)
  320. if (callback(session.getLine(row), row))
  321. return;
  322. };
  323. return {forEach: forEach};
  324. };
  325. }).call(Search.prototype);
  326. exports.Search = Search;
  327. });