diff --git a/lib/ace/editor.js b/lib/ace/editor.js index 834e603a..6576452d 100644 --- a/lib/ace/editor.js +++ b/lib/ace/editor.js @@ -1954,65 +1954,84 @@ var Editor = function(renderer, session) { }; /** related to: Search.find - * Editor.find(needle, options) - * - needle (String): The text to search for + * Editor.find(needle, options) + * - needle (String): The text to search for (optional) * - options (Object): An object defining various search properties * - animate (Boolean): If `true` animate scrolling * * Attempts to find `needle` within the document. For more information on `options`, see [[Search `Search`]]. **/ this.find = function(needle, options, animate) { - this.clearSelection(); - options = options || {}; - options.needle = needle; + if (!options) + options = {}; + + if (typeof needle == "string" || needle instanceof RegExp) + options.needle = needle; + else if (typeof needle == "object") + oop.mixin(options, needle); + + var range = this.selection.getRange(); + if (options.needle == null) { + needle = this.session.getTextRange(range) + || this.$search.$options.needle; + if (!needle) { + range = this.session.getWordRange(range.start.row, range.start.column); + needle = this.session.getTextRange(range); + } + this.$search.set({needle: needle}); + } + this.$search.set(options); - this.$find(options.backwards, animate); + if (!options.start) + this.$search.set({start: range}); + + var newRange = this.$search.find(this.session); + if (options.preventScroll) + return newRange; + if (newRange) { + this.revealRange(newRange, animate); + return newRange; + } + // clear selection if nothing is found + if (options.backwards) + range.start = range.end; + else + range.end = range.start; + this.selection.setRange(range); }; /** related to: Editor.find - * Editor.findNext(options) + * Editor.findNext(options) * - options (Object): search options * - animate (Boolean): If `true` animate scrolling - * + * * Performs another search for `needle` in the document. For more information on `options`, see [[Search `Search`]]. **/ this.findNext = function(options, animate) { - options = options || {}; - this.$search.set(options); - this.$find(false, animate); + this.find({skipCurrent: true, backwards: false}, options, animate); }; /** related to: Editor.find - * Editor.findPrevious(options) + * Editor.findPrevious(options) * - options (Object): search options * - animate (Boolean): If `true` animate scrolling - * + * * Performs a search for `needle` backwards. For more information on `options`, see [[Search `Search`]]. **/ this.findPrevious = function(options, animate) { - options = options || {}; - this.$search.set(options); - this.$find(true, animate); + this.find(options, {skipCurrent: true, backwards: true}, animate); }; - this.$find = function(backwards, animate) { - if (!this.selection.isEmpty()) - this.$search.set({needle: this.session.getTextRange(this.getSelectionRange())}); + this.revealRange = function(range, animate) { + this.$blockScrolling += 1; + this.session.unfold(range); + this.selection.setSelectionRange(range); + this.$blockScrolling -= 1; - if (typeof backwards != "undefined") - this.$search.set({backwards: backwards}); - - var range = this.$search.find(this.session); - if (range) { - this.$blockScrolling += 1; - this.session.unfold(range); - this.selection.setSelectionRange(range); - this.$blockScrolling -= 1; - - var scrollTop = this.renderer.scrollTop; - this.renderer.scrollSelectionIntoView(range.start, range.end, 0.5); + var scrollTop = this.renderer.scrollTop; + this.renderer.scrollSelectionIntoView(range.start, range.end, 0.5); + if (animate != false) this.renderer.animateScrolling(scrollTop); - } }; /** related to: UndoManager.undo diff --git a/lib/ace/lib/lang.js b/lib/ace/lib/lang.js index c8b7afa3..3b862bdb 100644 --- a/lib/ace/lib/lang.js +++ b/lib/ace/lib/lang.js @@ -116,6 +116,20 @@ exports.escapeRegExp = function(str) { return str.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1'); }; +exports.getMatchOffsets = function(string, regExp) { + var matches = []; + + string.replace(regExp, function(str) { + matches.push({ + offset: arguments[arguments.length-2], + length: str.length + }); + }); + + return matches; +}; + + exports.deferredCall = function(fcn) { var timer = null; diff --git a/lib/ace/search.js b/lib/ace/search.js index dedcadea..38e96ea6 100644 --- a/lib/ace/search.js +++ b/lib/ace/search.js @@ -54,35 +54,25 @@ var Range = require("./range").Range; /** * new Search() * - * Creates a new `Search` object. The search options contain the following defaults: + * Creates a new `Search` object. The following search options ae avaliable: * - * * `needle`: `""` - * * `backwards`: `false` - * * `wrap`: `false` - * * `caseSensitive`: `false` - * * `wholeWord`: `false` - * * `scope`: `ALL` - * * `regExp`: `false` + * * needle: string or regular expression + * * backwards: false + * * wrap: false + * * caseSensitive: false + * * wholeWord: false + * * range: Range or null for whole document + * * regExp: false + * * start: Range or position + * * skipCurrent: false * **/ var Search = function() { - this.$options = { - needle: "", - backwards: false, - wrap: false, - caseSensitive: false, - wholeWord: false, - scope: Search.ALL, - regExp: false - }; + this.$options = {}; }; -Search.ALL = 1; -Search.SELECTION = 2; - (function() { - /** * Search.set(options) -> Search * - options (Object): An object containing all the new search properties @@ -105,6 +95,9 @@ Search.SELECTION = 2; return lang.copyObject(this.$options); }; + this.setOptions = function(options) { + this.$options = options; + }; /** * Search.find(session) -> Range * - session (EditSession): The session to search with @@ -113,17 +106,18 @@ Search.SELECTION = 2; * **/ this.find = function(session) { - if (!this.$options.needle) - return null; - - var iterator = this.$matchIterator(session); + var iterator = this.$matchIterator(session, this.$options); if (!iterator) return false; var firstRange = null; - iterator.forEach(function(range) { - firstRange = range; + iterator.forEach(function(range, row, offset) { + if (!range.start) { + var column = range.offset + (offset || 0); + firstRange = new Range(row, column, row, column+range.length); + } else + firstRange = range; return true; }); @@ -141,23 +135,53 @@ Search.SELECTION = 2; var options = this.$options; if (!options.needle) return []; + this.$assembleRegExp(options); - var iterator = this.$matchIterator(session); - - if (!iterator) - return false; - - var ignoreCursor = !options.start && options.wrap && options.scope == Search.ALL; - if (ignoreCursor) - options.start = {row: 0, column: 0}; + if (options.range) { + var range = options.range; + var lines = session.getLines(range.start.row, range.end.row); + } else + var lines = session.doc.getAllLines(); var ranges = []; - iterator.forEach(function(range) { - ranges.push(range); - }); + var re = options.re; + if (options.$isMultiLine) { + var len = re.length; + var maxRow = lines.length - len; + for (var row = re.offset || 0; row < maxRow; row++) { + for (var j = 0; j < re.length; j++) + if (lines[row + j].search(re[j]) == -1) + break; - if (ignoreCursor) - options.start = null; + var startIndex = lines[row + j].match(re[0])[0].length; + var endIndex = line.match(re[len - 1])[0].length; + + ranges.push(new Range( + row, startLine.length - startIndex, + row + len - 1, endIndex + )); + } + } else { + for (var i = 0; i < lines.length; i++) { + var matches = lang.getMatchOffsets(lines[i], re); + for (var j = 0; j < matches.length; j++) { + var match = matches[j]; + ranges.push(new Range(i, match.offset, i, match.offset + match.length)); + }; + } + } + + if (options.range) { + var startColumn = range.start.column; + var endColumn = range.start.column; + var i = 0, j = ranges.length - 1; + while (i < j && ranges[i].start.column < startColumn && ranges[i].start.row == range.start.row) + i++; + + while (i < j && ranges[j].end.column > endColumn && ranges[j].end.row == range.end.row) + j--; + return ranges.slice(i, j + 1); + } return ranges; }; @@ -173,252 +197,195 @@ Search.SELECTION = 2; * **/ this.replace = function(input, replacement) { - if (!this.$options.regExp) - return input == this.$options.needle ? replacement : null; - - var re = this.$assembleRegExp(); + var options = this.$options; + + var re = this.$assembleRegExp(options); + if (options.$isMultiLine) + return replacement; + if (!re) return; var match = re.exec(input); - if (match && match[0].length == input.length) { - return input.replace(re, replacement); - } - else { + if (!match || match[0].length != input.length) return null; + + replacement = input.replace(re, replacement) + if (options.preserveCase) { + replacement = replacement.split(""); + for (var i = Math.min(input.length, input.length); i--; ) { + var ch = input[i]; + if (ch && ch.toLowerCase() != ch) + replacement[i] = replacement[i].toUpperCase(); + else + replacement[i] = replacement[i].toLowerCase(); + } + replacement = replacement.join(""); } + + return replacement; }; /** internal, hide * Search.$matchIterator(session) -> String | Boolean * - session (EditSession): The session to search with * - * - * **/ - this.$matchIterator = function(session) { - var re = this.$assembleRegExp(); + this.$matchIterator = function(session, options) { + var re = this.$assembleRegExp(options); if (!re) return false; - var self = this, callback, backwards = this.$options.backwards; + var self = this, callback, backwards = options.backwards; - if (this.$options.$isMultiLine) { - var matchIterator = function(line, startIndex, row) { - var startLine = line; - if (startIndex) - line = line.substring(startIndex); - - var len = re.length; - var part = re[0]; - if (line.slice(-part.length) != part) + if (options.$isMultiLine) { + var len = re.length; + var matchIterator = function(line, row, offset) { + var startIndex = line.search(re[0]); + if (startIndex == -1) return; - - for (var i = 1; i < len - 1; i++) - if (re[i] != session.getLine(row + i)) + for (var i = 1; i < len; i++) { + line = session.getLine(row + i); + if (line.search(re[i]) == -1) return; + } - part = re[len - 1]; - if (session.getLine(row + len - 1).slice(0, part.length) != part) - return; + var endIndex = line.match(re[len - 1])[0].length; + + var range = new Range(row, startIndex, row + len - 1, endIndex); + if (re.offset == 1) { + range.start.row--; + range.start.column = Number.MAX_VALUE; + } else if (offset) + range.start.column += offset; - var range = new Range( - row, startLine.length - re[0].length, - row + len - 1, re[len - 1].length - ); if (callback(range)) return true; } } else if (backwards) { - var matchIterator = function(line, startIndex, row) { - if (startIndex) - line = line.substring(startIndex); - - var matches = []; - - line.replace(re, function(str) { - var offset = arguments[arguments.length-2]; - matches.push({ - str: str, - offset: startIndex + offset - }); - return str; - }); - - for (var i=matches.length-1; i>= 0; i--) { - var match = matches[i]; - var range = self.$rangeFromMatch(row, match.offset, match.str.length); - if (callback(range)) + var matchIterator = function(line, row, startIndex) { + var matches = lang.getMatchOffsets(line, re); + for (var i = matches.length-1; i >= 0; i--) + if (callback(matches[i], row, startIndex)) return true; - } } } else { - var matchIterator = function(line, startIndex, row) { - if (startIndex) - line = line.substring(startIndex); - - var matches = []; - - line.replace(re, function(str) { - var offset = arguments[arguments.length-2]; - matches.push({ - str: str, - offset: startIndex + offset - }); - return str; - }); - - for (var i=0; i lastRow) { - if (wrap) { - row = firstRow; - startIndex = firstColumn; - inWrap = true; - } else { - return; - } - } - - if (row == start.row) - stop = true; - - line = getLine(row); - } + for (row = firstRow, lastRow = start.row; row <= lastRow; row++) + if (callback(session.getLine(row), row)) + return; } } else { var forEach = function(callback) { var row = start.row; var line = session.getLine(row).substring(0, start.column); - var startIndex = 0; - var stop = false; - var inWrap = false; + if (callback(line, row)) + return; - while (!callback(line, startIndex, row)) { - if (stop) + for (row--; row >= firstRow; row--) + if (callback(session.getLine(row), row)) return; - row--; - startIndex = 0; + if (options.wrap == false) + return; - if (row < firstRow) { - if (wrap) { - row = lastRow; - inWrap = true; - } else { - return; - } - } - - if (row == start.row) - stop = true; - - line = session.getLine(row); - if (searchSelection) { - if (row == firstRow) - startIndex = firstColumn; - else if (row == lastRow) - line = line.substring(0, range.end.column); - } - - if (inWrap && row == start.row) - startIndex = start.column; - } + for (row = lastRow, firstRow = start.row; row >= firstRow; row--) + if (callback(session.getLine(row), row)) + return; } } diff --git a/lib/ace/search_test.js b/lib/ace/search_test.js index e1f02c46..7a28d89c 100644 --- a/lib/ace/search_test.js +++ b/lib/ace/search_test.js @@ -52,7 +52,6 @@ module.exports = { var search = new Search(); search.set({ needle: "juhu", - scope: Search.ALL }); }, @@ -90,7 +89,7 @@ module.exports = { assert.position(range.end, 1, 12); }, - "test: wrap search is off by default" : function() { + "test: wrap search is on by default" : function() { var session = new EditSession(["abc", "juhu kinners 123", "456"]); session.getSelection().moveCursorTo(2, 1); @@ -98,7 +97,7 @@ module.exports = { needle: "kinners" }); - assert.equal(search.find(session), null); + assert.notEqual(search.find(session), null); }, "test: wrap search should wrap at file end" : function() { @@ -115,6 +114,20 @@ module.exports = { assert.position(range.end, 1, 12); }, + "test: wrap search should find needle even if it starts inside it" : function() { + var session = new EditSession(["abc", "juhu kinners 123", "456"]); + session.getSelection().moveCursorTo(6, 1); + + var search = new Search().set({ + needle: "kinners", + wrap: true + }); + + var range = search.find(session); + assert.position(range.start, 1, 5); + assert.position(range.end, 1, 12); + }, + "test: wrap search with no match should return 'null'": function() { var session = new EditSession(["abc", "juhu kinners 123", "456"]); session.getSelection().moveCursorTo(2, 1); @@ -183,13 +196,19 @@ module.exports = { var search = new Search().set({ needle: "juhu", wrap: true, - scope: Search.SELECTION + range: session.getSelection().getRange() }); var range = search.find(session); assert.position(range.start, 1, 0); assert.position(range.end, 1, 4); + search = new Search().set({ + needle: "juhu", + wrap: true, + range: session.getSelection().getRange() + }); + session.getSelection().setSelectionAnchor(0, 2); session.getSelection().selectTo(3, 2); @@ -201,24 +220,32 @@ module.exports = { "test: find backwards in selection": function() { var session = new EditSession(["juhu", "juhu", "juhu", "juhu"]); + session.getSelection().setSelectionAnchor(0, 2); + session.getSelection().selectTo(3, 2); + var search = new Search().set({ needle: "juhu", wrap: true, backwards: true, - scope: Search.SELECTION + range: session.getSelection().getRange() }); - session.getSelection().setSelectionAnchor(0, 2); - session.getSelection().selectTo(3, 2); - var range = search.find(session); assert.position(range.start, 2, 0); assert.position(range.end, 2, 4); + search = new Search().set({ + needle: "juhu", + wrap: true, + range: session.getSelection().getRange() + }); + session.getSelection().setSelectionAnchor(0, 2); session.getSelection().selectTo(1, 2); - assert.equal(search.find(session), null); + var range = search.find(session); + assert.position(range.start, 1, 0); + assert.position(range.end, 1, 4); }, "test: edge case - match directly before the cursor" : function() { @@ -295,15 +322,15 @@ module.exports = { "test: find all matches in selection" : function() { var session = new EditSession(["juhu", "juhu", "juhu", "juhu"]); + session.getSelection().setSelectionAnchor(0, 2); + session.getSelection().selectTo(3, 2); + var search = new Search().set({ needle: "uh", wrap: true, - scope: Search.SELECTION + range: session.getSelection().getRange() }); - session.getSelection().setSelectionAnchor(0, 2); - session.getSelection().selectTo(3, 2); - var ranges = search.findAll(session); assert.equal(ranges.length, 2); @@ -322,6 +349,11 @@ module.exports = { assert.equal(search.replace("", "kinners"), null); assert.equal(search.replace(" juhu", "kinners"), null); + // case sensitivity + assert.equal(search.replace("Juhu", "kinners"), "kinners"); + search.set({caseSensitive: true}); + assert.equal(search.replace("Juhu", "kinners"), null); + // regexp replacement }, @@ -356,7 +388,6 @@ module.exports = { needle: "[ ]+$", regExp: true, wrap: true, - scope: Search.ALL }); session.getSelection().moveCursorTo(1, 2); @@ -404,12 +435,12 @@ module.exports = { var ranges = search.findAll(session); assert.equal(ranges.length, 3); - assert.position(ranges[0].start, 0, 23); - assert.position(ranges[0].end, 0, 26); + assert.position(ranges[2].start, 0, 23); + assert.position(ranges[2].end, 0, 26); assert.position(ranges[1].start, 0, 8); assert.position(ranges[1].end, 0, 11); - assert.position(ranges[2].start, 0, 0); - assert.position(ranges[2].end, 0, 3); + assert.position(ranges[0].start, 0, 0); + assert.position(ranges[0].end, 0, 3); }, };