diff --git a/demo/editor.html b/demo/editor.html
index bb947174..adeb605c 100644
--- a/demo/editor.html
+++ b/demo/editor.html
@@ -54,6 +54,7 @@
+
diff --git a/src/ace/Editor.js b/src/ace/Editor.js
index 6e6ef4d9..abe0ea57 100644
--- a/src/ace/Editor.js
+++ b/src/ace/Editor.js
@@ -23,6 +23,10 @@ ace.Editor = function(renderer, doc) {
this.$highlightLineMarker = null;
this.$blockScrolling = false;
+ this.$search = new ace.Search().set({
+ wrap: true
+ });
+
this.setDocument(doc || new ace.Document(""));
};
@@ -687,4 +691,33 @@ ace.Editor = function(renderer, doc) {
this.clearSelection();
this.selection.moveCursorWordLeft();
};
+
+ this.find = function(needle) {
+ this.clearSelection();
+ this.$search.set({needle: needle});
+ this.findNext();
+ },
+
+ this.findNext = function() {
+ this.$find(false);
+ };
+
+ this.findPrevious = function() {
+ this.$find(true);
+ };
+
+ this.$find = function(backwards) {
+ if (!this.selection.isEmpty()) {
+ this.$search.set({needle: this.doc.getTextRange(this.getSelectionRange())});
+ }
+
+ this.$search.set({
+ backwards: backwards
+ });
+
+ var range = this.$search.find(this.doc);
+ if (range)
+ this.selection.setSelectionRange(range);
+ };
+
}).call(ace.Editor.prototype);
\ No newline at end of file
diff --git a/src/ace/KeyBinding.js b/src/ace/KeyBinding.js
index 482d0541..de1ff40f 100644
--- a/src/ace/KeyBinding.js
+++ b/src/ace/KeyBinding.js
@@ -30,21 +30,21 @@ ace.KeyBinding = function(element, editor) {
(function() {
this.keys = {
- 8: "Backspace",
- 9: "Tab",
- 16: "Shift",
- 17: "Control",
- 18: "Alt",
- 33: "PageUp",
- 34: "PageDown",
- 35: "End",
- 36: "Home",
- 37: "Left",
- 38: "Up",
- 39: "Right",
- 40: "Down",
- 46: "Delete",
- 91: "Meta"
+ 8 : "Backspace",
+ 9 : "Tab",
+ 16 : "Shift",
+ 17 : "Control",
+ 18 : "Alt",
+ 33 : "PageUp",
+ 34 : "PageDown",
+ 35 : "End",
+ 36 : "Home",
+ 37 : "Left",
+ 38 : "Up",
+ 39 : "Right",
+ 40 : "Down",
+ 46 : "Delete",
+ 91 : "Meta"
};
this["Control-A"] = function() {
@@ -66,6 +66,19 @@ ace.KeyBinding = function(element, editor) {
this.editor.toggleCommentLines();
};
+ this["Control-K"] = function() {
+ this.editor.findNext();
+ };
+
+ this["Control-Shift-K"] = function() {
+ this.editor.findPrevious();
+ };
+
+ this["Control-F"] = function() {
+ var needle = prompt("Find:");
+ this.editor.find(needle);
+ };
+
this["Control-Alt-Up"] = function() {
this.editor.copyLinesUp();
};
@@ -209,7 +222,8 @@ ace.KeyBinding = function(element, editor) {
this["Tab"] = function() {
if (this.selection.isMultiLine()) {
this.editor.blockIndent();
- } else {
+ }
+ else {
this.editor.onTextInput("\t");
}
};
diff --git a/src/ace/Search.js b/src/ace/Search.js
new file mode 100644
index 00000000..f753a47e
--- /dev/null
+++ b/src/ace/Search.js
@@ -0,0 +1,156 @@
+ace.provide("ace.Search");
+
+ace.Search = function() {
+ this.$options = {
+ needle: "",
+ backwards: false,
+ wrap: false,
+ caseSensitive: false,
+ wholeWord: false
+ };
+};
+
+ace.Search.ALL = 1;
+ace.Search.SELECTION = 2;
+
+(function() {
+
+ this.set = function(options) {
+ ace.mixin(this.$options, options);
+ return this;
+ };
+
+ this.find = function(doc) {
+ var needle = this.$options.needle;
+ if (!this.$options.needle)
+ return null;
+
+ if (this.$options.backwards) {
+ return this.$findBackward(doc);
+ } else {
+ return this.$findForward(doc);
+ }
+ };
+
+ this.$assembleRegExp = function() {
+ var needle = ace.escapeRegExp(this.$options.needle);
+ if (this.$options.wholeWord) {
+ needle = "\\b" + needle + "\\b";
+ }
+
+ var modifier = "g";
+ if (this.$options.caseSensitive) {
+ modifier += "i";
+ }
+
+ var re = new RegExp(needle, modifier);
+ return re;
+ };
+
+ this.$findForward = function(doc) {
+ var start = doc.getSelection().getCursor();
+ var row = start.row;
+ var column = start.column;
+
+ var startRow = row;
+
+ var line = doc.getLine(row);
+ var wrapped = false;
+
+ var re = this.$assembleRegExp();
+ re.lastIndex = column;
+
+ do {
+ var match = re.exec(line);
+ if (!match) {
+ if (row == startRow && wrapped) {
+ return null;
+ }
+
+ row++;
+
+ if (row >= doc.getLength()) {
+ if (this.$options.wrap) {
+ row = 0;
+ wrapped = true;
+ } else {
+ return null;
+ }
+ }
+
+ line = doc.getLine(row);
+ re.lastIndex = 0;
+ }
+ } while(!match);
+
+ var range = {
+ start: {
+ row: row,
+ column: match.index
+ },
+ end: {
+ row: row,
+ column: match.index + match[0].length
+ }
+ };
+ return range;
+ };
+
+ this.$findBackward = function(doc) {
+ var start = doc.getSelection().getRange().start;
+ var row = start.row;
+ var column = start.column;
+
+ var startRow = row;
+
+ var line = doc.getLine(row).substring(0, column);
+ var wrapped = false;
+
+ var re = this.$assembleRegExp();
+
+ var found = false;
+ var lastOffset = 0;
+ var match = "";
+
+ do {
+ line.replace(re, function(str, offset) {
+ match = str;
+ found = true;
+ lastOffset = offset;
+ return str;
+ });
+
+ if (!found) {
+ if (row == startRow && wrapped) {
+ return null;
+ }
+
+ row--;
+
+ if (row < 0) {
+ if (this.$options.wrap) {
+ row = doc.getLength() - 1;
+ wrapped = true;
+ } else {
+ return null;
+ }
+ }
+
+ line = doc.getLine(row);
+ }
+ } while(!found);
+
+ var range = {
+ start: {
+ row: row,
+ column: lastOffset
+ },
+ end: {
+ row: row,
+ column: lastOffset + match.length
+ }
+ };
+ return range;
+ };
+
+}).call(ace.Search.prototype);
\ No newline at end of file
diff --git a/src/ace/Selection.js b/src/ace/Selection.js
index e209748f..c6dec599 100644
--- a/src/ace/Selection.js
+++ b/src/ace/Selection.js
@@ -102,6 +102,11 @@ ace.Selection = function(doc) {
});
};
+ this.setSelectionRange = function(range) {
+ this.setSelectionAnchor(range.start.row, range.start.column);
+ this.selectTo(range.end.row, range.end.column);
+ };
+
this.$moveSelection = function(mover) {
var changed = false;
diff --git a/src/ace/ace.js b/src/ace/ace.js
index 214ea027..8574d653 100644
--- a/src/ace/ace.js
+++ b/src/ace/ace.js
@@ -21,6 +21,12 @@ ace.inherits = function(ctor, superCtor) {
ctor.prototype.constructor = ctor;
};
+ace.mixin = function(obj, mixin) {
+ for (var key in mixin) {
+ obj[key] = mixin[key];
+ }
+};
+
ace.implement = function(proto, mixin) {
mixin.call(proto);
};
@@ -175,6 +181,10 @@ ace.isArray = function(value) {
return Object.prototype.toString.call(value) == "[object Array]";
};
+ace.escapeRegExp = function(str) {
+ return str.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1');
+};
+
ace.bind = function(fcn, context) {
return function() {
return fcn.apply(context, arguments);
diff --git a/src/test/SearchTest.js b/src/test/SearchTest.js
new file mode 100644
index 00000000..9ccd4b81
--- /dev/null
+++ b/src/test/SearchTest.js
@@ -0,0 +1,130 @@
+var SearchTest = new TestCase("SearchTest", {
+
+ "test: configure the search object" : function() {
+ var search = new ace.Search();
+ search.set({
+ needle: "juhu",
+ scope: ace.Search.ALL
+ });
+ },
+
+ "test: find simple text in document" : function() {
+ var doc = new ace.Document(["juhu kinners 123", "456"]);
+ var search = new ace.Search().set({
+ needle: "kinners"
+ });
+
+ var range = search.find(doc);
+ assertPosition(0, 5, range.start);
+ assertPosition(0, 12, range.end);
+ },
+
+ "test: find simple text in next line" : function() {
+ var doc = new ace.Document(["abc", "juhu kinners 123", "456"]);
+ var search = new ace.Search().set({
+ needle: "kinners"
+ });
+
+ var range = search.find(doc);
+ assertPosition(1, 5, range.start);
+ assertPosition(1, 12, range.end);
+ },
+
+ "test: find text starting at cursor position" : function() {
+ var doc = new ace.Document(["juhu kinners", "juhu kinners 123"]);
+ doc.getSelection().moveCursorTo(0, 6);
+ var search = new ace.Search().set({
+ needle: "kinners"
+ });
+
+ var range = search.find(doc);
+ assertPosition(1, 5, range.start);
+ assertPosition(1, 12, range.end);
+ },
+
+ "test: wrap search is off by default" : function() {
+ var doc = new ace.Document(["abc", "juhu kinners 123", "456"]);
+ doc.getSelection().moveCursorTo(2, 1);
+
+ var search = new ace.Search().set({
+ needle: "kinners"
+ });
+
+ assertEquals(null, search.find(doc));
+ },
+
+ "test: wrap search should wrap at file end" : function() {
+ var doc = new ace.Document(["abc", "juhu kinners 123", "456"]);
+ doc.getSelection().moveCursorTo(2, 1);
+
+ var search = new ace.Search().set({
+ needle: "kinners",
+ wrap: true
+ });
+
+ var range = search.find(doc);
+ assertPosition(1, 5, range.start);
+ assertPosition(1, 12, range.end);
+ },
+
+ "test: wrap search with no match should return 'null'": function() {
+ var doc = new ace.Document(["abc", "juhu kinners 123", "456"]);
+ doc.getSelection().moveCursorTo(2, 1);
+
+ var search = new ace.Search().set({
+ needle: "xyz",
+ wrap: true
+ });
+
+ assertEquals(null, search.find(doc));
+ },
+
+ "test: case sensitive is by default off": function() {
+ var doc = new ace.Document(["abc", "juhu kinners 123", "456"]);
+
+ var search = new ace.Search().set({
+ needle: "JUHU"
+ });
+
+ assertEquals(null, search.find(doc));
+ },
+
+ "test: case sensitive search": function() {
+ var doc = new ace.Document(["abc", "juhu kinners 123", "456"]);
+
+ var search = new ace.Search().set({
+ needle: "KINNERS",
+ caseSensitive: true
+ });
+
+ var range = search.find(doc);
+ assertPosition(1, 5, range.start);
+ assertPosition(1, 12, range.end);
+ },
+
+ "test: whole word search should not match inside of words": function() {
+ var doc = new ace.Document(["juhukinners", "juhu kinners 123", "456"]);
+
+ var search = new ace.Search().set({
+ needle: "kinners",
+ wholeWord: true
+ });
+
+ var range = search.find(doc);
+ assertPosition(1, 5, range.start);
+ assertPosition(1, 12, range.end);
+ },
+
+ "test: find backwards": function() {
+ var doc = new ace.Document(["juhu juhu juhu juhu"]);
+ doc.getSelection().moveCursorTo(0, 10);
+ var search = new ace.Search().set({
+ needle: "juhu",
+ backwards: true
+ });
+
+ var range = search.find(doc);
+ assertPosition(0, 5, range.start);
+ assertPosition(0, 9, range.end);
+ }
+});
\ No newline at end of file