search.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  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. *
  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. * @constructor
  57. **/
  58. var Search = function() {
  59. this.$options = {};
  60. };
  61. (function() {
  62. /**
  63. * Sets the search options via the `options` parameter.
  64. * @param {Object} options An object containing all the new search properties
  65. *
  66. *
  67. * @returns {Search}
  68. * @chainable
  69. **/
  70. this.set = function(options) {
  71. oop.mixin(this.$options, options);
  72. return this;
  73. };
  74. /**
  75. * [Returns an object containing all the search options.]{: #Search.getOptions}
  76. * @returns {Object}
  77. **/
  78. this.getOptions = function() {
  79. return lang.copyObject(this.$options);
  80. };
  81. /**
  82. * Sets the search options via the `options` parameter.
  83. * @param {Object} An object containing all the search propertie
  84. * @related Search.set
  85. **/
  86. this.setOptions = function(options) {
  87. this.$options = options;
  88. };
  89. /**
  90. * 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.
  91. * @param {EditSession} session The session to search with
  92. *
  93. *
  94. * @returns {Range}
  95. **/
  96. this.find = function(session) {
  97. var options = this.$options;
  98. var iterator = this.$matchIterator(session, options);
  99. if (!iterator)
  100. return false;
  101. var firstRange = null;
  102. iterator.forEach(function(range, row, offset) {
  103. if (!range.start) {
  104. var column = range.offset + (offset || 0);
  105. firstRange = new Range(row, column, row, column + range.length);
  106. if (!range.length && options.start && options.start.start
  107. && options.skipCurrent != false && firstRange.isEqual(options.start)
  108. ) {
  109. firstRange = null;
  110. return false;
  111. }
  112. } else
  113. firstRange = range;
  114. return true;
  115. });
  116. return firstRange;
  117. };
  118. /**
  119. * 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.
  120. * @param {EditSession} session The session to search with
  121. *
  122. *
  123. * @returns {[Range]}
  124. **/
  125. this.findAll = function(session) {
  126. var options = this.$options;
  127. if (!options.needle)
  128. return [];
  129. this.$assembleRegExp(options);
  130. var range = options.range;
  131. var lines = range
  132. ? session.getLines(range.start.row, range.end.row)
  133. : session.doc.getAllLines();
  134. var ranges = [];
  135. var re = options.re;
  136. if (options.$isMultiLine) {
  137. var len = re.length;
  138. var maxRow = lines.length - len;
  139. var prevRange;
  140. outer: for (var row = re.offset || 0; row <= maxRow; row++) {
  141. for (var j = 0; j < len; j++)
  142. if (lines[row + j].search(re[j]) == -1)
  143. continue outer;
  144. var startLine = lines[row];
  145. var line = lines[row + len - 1];
  146. var startIndex = startLine.length - startLine.match(re[0])[0].length;
  147. var endIndex = line.match(re[len - 1])[0].length;
  148. if (prevRange && prevRange.end.row === row &&
  149. prevRange.end.column > startIndex
  150. ) {
  151. continue;
  152. }
  153. ranges.push(prevRange = new Range(
  154. row, startIndex, row + len - 1, endIndex
  155. ));
  156. if (len > 2)
  157. row = row + len - 2;
  158. }
  159. } else {
  160. for (var i = 0; i < lines.length; i++) {
  161. var matches = lang.getMatchOffsets(lines[i], re);
  162. for (var j = 0; j < matches.length; j++) {
  163. var match = matches[j];
  164. ranges.push(new Range(i, match.offset, i, match.offset + match.length));
  165. }
  166. }
  167. }
  168. if (range) {
  169. var startColumn = range.start.column;
  170. var endColumn = range.start.column;
  171. var i = 0, j = ranges.length - 1;
  172. while (i < j && ranges[i].start.column < startColumn && ranges[i].start.row == range.start.row)
  173. i++;
  174. while (i < j && ranges[j].end.column > endColumn && ranges[j].end.row == range.end.row)
  175. j--;
  176. ranges = ranges.slice(i, j + 1);
  177. for (i = 0, j = ranges.length; i < j; i++) {
  178. ranges[i].start.row += range.start.row;
  179. ranges[i].end.row += range.start.row;
  180. }
  181. }
  182. return ranges;
  183. };
  184. /**
  185. * Searches for `options.needle` in `input`, and, if found, replaces it with `replacement`.
  186. * @param {String} input The text to search in
  187. * @param {String} replacement The replacing text
  188. * + (String): If `options.regExp` is `true`, this function returns `input` with the replacement already made. Otherwise, this function just returns `replacement`.<br/>
  189. * If `options.needle` was not found, this function returns `null`.
  190. *
  191. *
  192. * @returns {String}
  193. **/
  194. this.replace = function(input, replacement) {
  195. var options = this.$options;
  196. var re = this.$assembleRegExp(options);
  197. if (options.$isMultiLine)
  198. return replacement;
  199. if (!re)
  200. return;
  201. var match = re.exec(input);
  202. if (!match || match[0].length != input.length)
  203. return null;
  204. replacement = input.replace(re, replacement);
  205. if (options.preserveCase) {
  206. replacement = replacement.split("");
  207. for (var i = Math.min(input.length, input.length); i--; ) {
  208. var ch = input[i];
  209. if (ch && ch.toLowerCase() != ch)
  210. replacement[i] = replacement[i].toUpperCase();
  211. else
  212. replacement[i] = replacement[i].toLowerCase();
  213. }
  214. replacement = replacement.join("");
  215. }
  216. return replacement;
  217. };
  218. this.$matchIterator = function(session, options) {
  219. var re = this.$assembleRegExp(options);
  220. if (!re)
  221. return false;
  222. var callback;
  223. if (options.$isMultiLine) {
  224. var len = re.length;
  225. var matchIterator = function(line, row, offset) {
  226. var startIndex = line.search(re[0]);
  227. if (startIndex == -1)
  228. return;
  229. for (var i = 1; i < len; i++) {
  230. line = session.getLine(row + i);
  231. if (line.search(re[i]) == -1)
  232. return;
  233. }
  234. var endIndex = line.match(re[len - 1])[0].length;
  235. var range = new Range(row, startIndex, row + len - 1, endIndex);
  236. if (re.offset == 1) {
  237. range.start.row--;
  238. range.start.column = Number.MAX_VALUE;
  239. } else if (offset)
  240. range.start.column += offset;
  241. if (callback(range))
  242. return true;
  243. };
  244. } else if (options.backwards) {
  245. var matchIterator = function(line, row, startIndex) {
  246. var matches = lang.getMatchOffsets(line, re);
  247. for (var i = matches.length-1; i >= 0; i--)
  248. if (callback(matches[i], row, startIndex))
  249. return true;
  250. };
  251. } else {
  252. var matchIterator = function(line, row, startIndex) {
  253. var matches = lang.getMatchOffsets(line, re);
  254. for (var i = 0; i < matches.length; i++)
  255. if (callback(matches[i], row, startIndex))
  256. return true;
  257. };
  258. }
  259. var lineIterator = this.$lineIterator(session, options);
  260. return {
  261. forEach: function(_callback) {
  262. callback = _callback;
  263. lineIterator.forEach(matchIterator);
  264. }
  265. };
  266. };
  267. this.$assembleRegExp = function(options, $disableFakeMultiline) {
  268. if (options.needle instanceof RegExp)
  269. return options.re = options.needle;
  270. var needle = options.needle;
  271. if (!options.needle)
  272. return options.re = false;
  273. if (!options.regExp)
  274. needle = lang.escapeRegExp(needle);
  275. if (options.wholeWord)
  276. needle = "\\b" + needle + "\\b";
  277. var modifier = options.caseSensitive ? "gm" : "gmi";
  278. options.$isMultiLine = !$disableFakeMultiline && /[\n\r]/.test(needle);
  279. if (options.$isMultiLine)
  280. return options.re = this.$assembleMultilineRegExp(needle, modifier);
  281. try {
  282. var re = new RegExp(needle, modifier);
  283. } catch(e) {
  284. re = false;
  285. }
  286. return options.re = re;
  287. };
  288. this.$assembleMultilineRegExp = function(needle, modifier) {
  289. var parts = needle.replace(/\r\n|\r|\n/g, "$\n^").split("\n");
  290. var re = [];
  291. for (var i = 0; i < parts.length; i++) try {
  292. re.push(new RegExp(parts[i], modifier));
  293. } catch(e) {
  294. return false;
  295. }
  296. if (parts[0] == "") {
  297. re.shift();
  298. re.offset = 1;
  299. } else {
  300. re.offset = 0;
  301. }
  302. return re;
  303. };
  304. this.$lineIterator = function(session, options) {
  305. var backwards = options.backwards == true;
  306. var skipCurrent = options.skipCurrent != false;
  307. var range = options.range;
  308. var start = options.start;
  309. if (!start)
  310. start = range ? range[backwards ? "end" : "start"] : session.selection.getRange();
  311. if (start.start)
  312. start = start[skipCurrent != backwards ? "end" : "start"];
  313. var firstRow = range ? range.start.row : 0;
  314. var lastRow = range ? range.end.row : session.getLength() - 1;
  315. var forEach = backwards ? function(callback) {
  316. var row = start.row;
  317. var line = session.getLine(row).substring(0, start.column);
  318. if (callback(line, row))
  319. return;
  320. for (row--; row >= firstRow; row--)
  321. if (callback(session.getLine(row), row))
  322. return;
  323. if (options.wrap == false)
  324. return;
  325. for (row = lastRow, firstRow = start.row; row >= firstRow; row--)
  326. if (callback(session.getLine(row), row))
  327. return;
  328. } : function(callback) {
  329. var row = start.row;
  330. var line = session.getLine(row).substr(start.column);
  331. if (callback(line, row, start.column))
  332. return;
  333. for (row = row+1; row <= lastRow; row++)
  334. if (callback(session.getLine(row), row))
  335. return;
  336. if (options.wrap == false)
  337. return;
  338. for (row = firstRow, lastRow = start.row; row <= lastRow; row++)
  339. if (callback(session.getLine(row), row))
  340. return;
  341. };
  342. return {forEach: forEach};
  343. };
  344. }).call(Search.prototype);
  345. exports.Search = Search;
  346. });