From 737d2ed9d01efb956bdffa0cae698aa90a04f019 Mon Sep 17 00:00:00 2001 From: Fabian Jakobs Date: Tue, 27 Apr 2010 18:16:52 +0200 Subject: [PATCH] add first search support --- demo/editor.html | 1 + src/ace/Editor.js | 33 +++++++++ src/ace/KeyBinding.js | 46 +++++++----- src/ace/Search.js | 156 +++++++++++++++++++++++++++++++++++++++++ src/ace/Selection.js | 5 ++ src/ace/ace.js | 10 +++ src/test/SearchTest.js | 130 ++++++++++++++++++++++++++++++++++ 7 files changed, 365 insertions(+), 16 deletions(-) create mode 100644 src/ace/Search.js create mode 100644 src/test/SearchTest.js 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