search rewrite

This commit is contained in:
nightwing 2012-05-25 21:15:38 +04:00
commit 3d774aa2b6
4 changed files with 300 additions and 269 deletions

View file

@ -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

View file

@ -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;

View file

@ -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<matches.length; 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 = 0; i < matches.length; i++)
if (callback(matches[i], row, startIndex))
return true;
}
}
}
return {forEach: function(_callback) {
callback = _callback;
self.$lineIterator(session).forEach(matchIterator);
}};
};
this.$rangeFromMatch = function(row, column, length) {
return new Range(row, column, row, column+length);
};
this.$assembleRegExp = function() {
if (typeof this.$options.needle != 'string')
return this.$options.needle;
var needle = this.$options.needle;
if (!this.$options.regExp) {
if (/[\n\r]/.test(needle)){
this.$options.$isMultiLine = true;
return needle.split(/\r\n|\r|\n/)
return {
forEach: function(_callback) {
callback = _callback;
self.$lineIterator(session, options).forEach(matchIterator);
}
};
};
this.$assembleRegExp = function(options) {
if (options.needle instanceof RegExp)
return options.re = options.needle;
var needle = options.needle;
if (!options.needle)
return options.re = false;
if (!options.regExp)
needle = lang.escapeRegExp(needle);
}
this.$options.$isMultiLine = false;
if (this.$options.wholeWord) {
if (options.wholeWord)
needle = "\\b" + needle + "\\b";
}
var modifier = "g";
if (!this.$options.caseSensitive) {
modifier += "i";
}
var modifier = options.caseSensitive ? "g" : "gi";
options.$isMultiLine = /[\n\r]/.test(needle);
if (options.$isMultiLine)
return options.re = this.$assembleMultilineRegExp(needle, modifier);
try {
var re = new RegExp(needle, modifier);
} catch(e) {
var re = false;
}
catch(e) {
return options.re = re;
};
this.$assembleMultilineRegExp = function(needle, modifier) {
var parts = needle.replace(/\r\n|\r|\n/g, "$\n^").split("\n");
var re = [];
for (var i = 0; i < parts.length; i++) try {
re.push(new RegExp(parts[i], modifier));
} catch(e) {
return false;
}
if (parts[0] == "") {
re.shift();
re.offset = 1;
} else {
re.offset = 0;
}
return re;
};
this.$lineIterator = function(session) {
var searchSelection = this.$options.scope == Search.SELECTION;
var backwards = this.$options.backwards;
this.$lineIterator = function(session, options) {
var range = options.range;
var backwards = options.backwards == true;
var skipCurrent = options.skipCurrent != false;
var range = this.$options.range || session.getSelection().getRange();
var start = this.$options.start || range[searchSelection != backwards ? "start" : "end"];
var range = options.range;
var start = options.start;
if (!start)
start = range ? range[backwards ? "end" : "start"] : session.selection.getRange();
if (start.start)
start = start[skipCurrent != backwards ? "end" : "start"];
var firstRow = searchSelection ? range.start.row : 0;
var firstColumn = searchSelection ? range.start.column : 0;
var lastRow = searchSelection ? range.end.row : session.getLength() - 1;
var wrap = this.$options.wrap;
var inWrap = false;
function getLine(row) {
var line = session.getLine(row);
if (searchSelection && row == range.end.row) {
line = line.substring(0, range.end.column);
}
if (inWrap && row == start.row) {
line = line.substring(0, start.column);
}
return line;
}
var firstRow = range ? range.start.row : 0;
var firstColumn = range ? range.start.column : 0;
var lastRow = range ? range.end.row : session.getLength() - 1;
if (!backwards) {
var forEach = function(callback) {
var row = start.row;
var line = getLine(row);
var startIndex = start.column;
var line = session.getLine(row).substr(start.column);
if (callback(line, row, start.column))
return;
var stop = false;
inWrap = false;
while (!callback(line, startIndex, row)) {
if (stop)
for (row = row+1; row <= lastRow; row++)
if (callback(session.getLine(row), row))
return;
row++;
startIndex = 0;
if (options.wrap == false)
return;
if (row > 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;
}
}

View file

@ -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);
},
};