From e0048b4967c9d32f2036b84761f785631f5ba3d4 Mon Sep 17 00:00:00 2001 From: dgeorge Date: Fri, 28 Oct 2011 14:23:55 -0700 Subject: [PATCH] EditSession.findMatchingBracket now uses tokens instead of a simple text search. Moved the bracket-matching code to a separate file edit_session/bracket_match.js. Added a TokenIterator helper class, which walks forward or backward through all tokens in the document. --- lib/ace/edit_session.js | 93 +------------- lib/ace/edit_session/bracket_match.js | 174 ++++++++++++++++++++++++++ lib/ace/edit_session_test.js | 13 +- lib/ace/mode/javascript_test.js | 2 +- lib/ace/token_iterator.js | 112 +++++++++++++++++ 5 files changed, 300 insertions(+), 94 deletions(-) create mode 100644 lib/ace/edit_session/bracket_match.js create mode 100644 lib/ace/token_iterator.js diff --git a/lib/ace/edit_session.js b/lib/ace/edit_session.js index 9f22e284..c2fbbf5b 100644 --- a/lib/ace/edit_session.js +++ b/lib/ace/edit_session.js @@ -612,98 +612,6 @@ var EditSession = function(text, mode) { return this.doc.getTextRange(range); }; - this.findMatchingBracket = function(position) { - if (position.column == 0) return null; - - var charBeforeCursor = this.getLine(position.row).charAt(position.column-1); - if (charBeforeCursor == "") return null; - - var match = charBeforeCursor.match(/([\(\[\{])|([\)\]\}])/); - if (!match) { - return null; - } - - if (match[1]) { - return this.$findClosingBracket(match[1], position); - } else { - return this.$findOpeningBracket(match[2], position); - } - }; - - this.$brackets = { - ")": "(", - "(": ")", - "]": "[", - "[": "]", - "{": "}", - "}": "{" - }; - - this.$findOpeningBracket = function(bracket, position) { - var openBracket = this.$brackets[bracket]; - - var column = position.column - 2; - var row = position.row; - var depth = 1; - - var line = this.getLine(row); - - while (true) { - while(column >= 0) { - var ch = line.charAt(column); - if (ch == openBracket) { - depth -= 1; - if (depth == 0) { - return {row: row, column: column}; - } - } - else if (ch == bracket) { - depth +=1; - } - column -= 1; - } - row -=1; - if (row < 0) break; - - var line = this.getLine(row); - var column = line.length-1; - } - return null; - }; - - this.$findClosingBracket = function(bracket, position) { - var closingBracket = this.$brackets[bracket]; - - var column = position.column; - var row = position.row; - var depth = 1; - - var line = this.getLine(row); - var lineCount = this.getLength(); - - while (true) { - while(column < line.length) { - var ch = line.charAt(column); - if (ch == closingBracket) { - depth -= 1; - if (depth == 0) { - return {row: row, column: column}; - } - } - else if (ch == bracket) { - depth +=1; - } - column += 1; - } - row +=1; - if (row >= lineCount) break; - - var line = this.getLine(row); - var column = 0; - } - return null; - }; - this.insert = function(position, text) { return this.doc.insert(position, text); }; @@ -1732,6 +1640,7 @@ var EditSession = function(text, mode) { }).call(EditSession.prototype); require("ace/edit_session/folding").Folding.call(EditSession.prototype); +require("ace/edit_session/bracket_match").BracketMatch.call(EditSession.prototype); exports.EditSession = EditSession; }); \ No newline at end of file diff --git a/lib/ace/edit_session/bracket_match.js b/lib/ace/edit_session/bracket_match.js new file mode 100644 index 00000000..05fe0307 --- /dev/null +++ b/lib/ace/edit_session/bracket_match.js @@ -0,0 +1,174 @@ +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +define(function(require, exports, module) { + +var TokenIterator = require("ace/token_iterator").TokenIterator; + +function BracketMatch() { + + this.findMatchingBracket = function(position) { + if (position.column == 0) return null; + + var charBeforeCursor = this.getLine(position.row).charAt(position.column-1); + if (charBeforeCursor == "") return null; + + var match = charBeforeCursor.match(/([\(\[\{])|([\)\]\}])/); + if (!match) { + return null; + } + + if (match[1]) { + return this.$findClosingBracket(match[1], position); + } else { + return this.$findOpeningBracket(match[2], position); + } + }; + + this.$brackets = { + ")": "(", + "(": ")", + "]": "[", + "[": "]", + "{": "}", + "}": "{" + }; + + this.$findOpeningBracket = function(bracket, position) { + var openBracket = this.$brackets[bracket]; + var depth = 1; + + var iterator = new TokenIterator(this, position.row, position.column); + var token = iterator.getCurrentToken(); + if (!token) return null; + + // Create a pattern that matches any token with the same type as token.type. + // Exception: if token.type includes "rparen", then also match "lparen". + var typeRe = new RegExp("(\\.?[" + + token.type.replace(".", "|").replace("rparen", "lparen|rparen") + "])+"); + + // Start searching in token, just before the character at position.column + var vIndex = position.column - iterator.getCurrentTokenColumn() - 2; + var value = token.value; + + while (true) { + + while (vIndex >= 0) { + var char = value.charAt(vIndex); + if (char == openBracket) { + depth -= 1; + if (depth == 0) { + return {row: iterator.getCurrentTokenRow(), + column: vIndex + iterator.getCurrentTokenColumn()}; + } + } + else if (char == bracket) { + depth += 1; + } + vIndex -= 1; + } + + // Scan backward through the document, looking for the next token + // whose type matches typeRe + do { + token = iterator.stepBackward(); + } while (token && !typeRe.test(token.type)) + + if (token == null) + break; + + value = token.value; + vIndex = token.value.length - 1; + } + + return null; + }; + + this.$findClosingBracket = function(bracket, position) { + var closingBracket = this.$brackets[bracket]; + var depth = 1; + + var iterator = new TokenIterator(this, position.row, position.column); + var token = iterator.getCurrentToken(); + if (!token) return null; + + // Create a pattern that matches any token with the same type as token.type. + // Exception: if token.type includes "lparen", then also match "rparen". + var typeRe = new RegExp("(\\.?[" + + token.type.replace(".", "|").replace("lparen", "lparen|rparen") + "])+"); + + // Start searching in token, after after the character at position.column + var vIndex = position.column - iterator.getCurrentTokenColumn(); + + while (true) { + + var value = token.value; + var valueLength = value.length; + while (vIndex < valueLength) { + var char = value.charAt(vIndex); + if (char == closingBracket) { + depth -= 1; + if (depth == 0) { + return {row: iterator.getCurrentTokenRow(), + column: vIndex + iterator.getCurrentTokenColumn()}; + } + } + else if (char == bracket) { + depth += 1; + } + vIndex += 1; + } + + // Scan forward through the document, looking for the next token + // whose type matches typeRe + do { + token = iterator.stepForward(); + } while (token && !typeRe.test(token.type)) + + if (token == null) + break; + + vIndex = 0; + } + + return null; + }; +} +exports.BracketMatch = BracketMatch; + +}); diff --git a/lib/ace/edit_session_test.js b/lib/ace/edit_session_test.js index cb3ce7bb..aa7d796c 100644 --- a/lib/ace/edit_session_test.js +++ b/lib/ace/edit_session_test.js @@ -50,6 +50,7 @@ var UndoManager = require("ace/undomanager").UndoManager; var MockRenderer = require("ace/test/mockrenderer").MockRenderer; var Range = require("ace/range").Range; var assert = require("ace/test/assertions"); +var JavaScriptMode = require("ace/mode/javascript").Mode; function createFoldTestSession() { var lines = [ @@ -69,7 +70,7 @@ function createFoldTestSession() { module.exports = { - "test: find matching opening bracket" : function() { + "test: find matching opening bracket in Text mode" : function() { var session = new EditSession(["(()(", "())))"]); assert.position(session.findMatchingBracket({row: 0, column: 3}), 0, 1); @@ -79,6 +80,16 @@ module.exports = { assert.equal(session.findMatchingBracket({row: 1, column: 5}), null); }, + "test: find matching opening bracket in JavaScript mode" : function() { + var session = new EditSession(["(()(", "", "())))"], new JavaScriptMode()); + + assert.position(session.findMatchingBracket({row: 0, column: 3}), 0, 1); + assert.position(session.findMatchingBracket({row: 2, column: 2}), 2, 0); + assert.position(session.findMatchingBracket({row: 2, column: 3}), 0, 3); + assert.position(session.findMatchingBracket({row: 2, column: 4}), 0, 0); + assert.equal(session.findMatchingBracket({row: 2, column: 5}), null); + }, + "test: find matching closing bracket" : function() { var session = new EditSession(["(()(", "())))"]); diff --git a/lib/ace/mode/javascript_test.js b/lib/ace/mode/javascript_test.js index e2a89be5..f2880f29 100644 --- a/lib/ace/mode/javascript_test.js +++ b/lib/ace/mode/javascript_test.js @@ -141,7 +141,7 @@ module.exports = { }, "test: auto outdent should indent the line with the same indent as the line with the matching opening brace" : function() { - var session = new EditSession([" function foo() {", " bla", " }"]); + var session = new EditSession([" function foo() {", " bla", " }"], new JavaScriptMode()); this.mode.autoOutdent("start", session, 2); assert.equal(" }", session.getLine(2)); }, diff --git a/lib/ace/token_iterator.js b/lib/ace/token_iterator.js new file mode 100644 index 00000000..7924a435 --- /dev/null +++ b/lib/ace/token_iterator.js @@ -0,0 +1,112 @@ +/* vim:ts=4:sts=4:sw=4: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Ajax.org Code Editor (ACE). + * + * The Initial Developer of the Original Code is + * Ajax.org B.V. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Fabian Jakobs + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +define(function(require, exports, module) { + +var TokenIterator = function(session, initialRow, initialColumn) { + this.$session = session; + this.$row = initialRow; + this.$rowTokens = session.getTokens(initialRow, initialRow)[0].tokens; + + var token = session.getTokenAt(initialRow, initialColumn); + this.$tokenIndex = token ? token.index : -1; +}; + +(function() { + + this.stepBackward = function() { + this.$tokenIndex -= 1; + + while (this.$tokenIndex < 0) { + this.$row -= 1; + if (this.$row < 0) + return null; + + this.$rowTokens = this.$session.getTokens(this.$row, this.$row)[0].tokens; + this.$tokenIndex = this.$rowTokens.length - 1; + } + + return this.$rowTokens[this.$tokenIndex]; + } + + this.stepForward = function() { + var rowCount = this.$session.getLength(); + this.$tokenIndex += 1; + + while (this.$tokenIndex >= this.$rowTokens.length) { + this.$row += 1; + if (this.$row >= rowCount) + return null; + + this.$rowTokens = this.$session.getTokens(this.$row, this.$row)[0].tokens; + this.$tokenIndex = 0; + } + + return this.$rowTokens[this.$tokenIndex]; + } + + this.getCurrentToken = function () { + return this.$rowTokens[this.$tokenIndex]; + } + + this.getCurrentTokenRow = function () { + return this.$row; + } + + this.getCurrentTokenColumn = function() { + var rowTokens = this.$rowTokens; + var tokenIndex = this.$tokenIndex; + + // If a column was cached by EditSession.getTokenAt, then use it + var column = rowTokens[tokenIndex].start; + if (column !== undefined) + return column; + + column = 0; + while (tokenIndex > 0) { + tokenIndex -= 1; + column += rowTokens[tokenIndex].value.length; + } + + return column; + } + +}).call(TokenIterator.prototype); + +exports.TokenIterator = TokenIterator; +});