diff --git a/css/editor.css b/css/editor.css index 2cf1701a..5000cb73 100644 --- a/css/editor.css +++ b/css/editor.css @@ -92,4 +92,10 @@ .marker-layer .selection { position: absolute; background: rgba(77, 151, 255, 0.33); +} + +.marker-layer .bracket { + position: absolute; + margin: -1px 0 0 -1px; + border: 1px solid rgb(192, 192, 192); } \ No newline at end of file diff --git a/src/Editor.js b/src/Editor.js index eda02314..769546d3 100644 --- a/src/Editor.js +++ b/src/Editor.js @@ -46,8 +46,41 @@ ace.Editor.prototype.resize = function() { ace.Editor.prototype.updateCursor = function() { this.renderer.updateCursor(this.cursor); + this._highlightBrackets(); }; +ace.Editor.prototype._highlightBrackets = function() { + + if (this._bracketHighlight) { + this.renderer.removeMarker(this._bracketHighlight); + this._bracketHighlight = null; + } + + if (this._highlightPending) { + return; + } + + // perform highlight async to not block the browser during navigation + var self = this; + this._highlightPending = true; + setTimeout(function() { + self._highlightPending = false; + + var pos = self.doc.findMatchingBracket(self.cursor); + if (pos) { + range = { + start: pos, + end: { + row: pos.row, + column: pos.column+1 + } + }; + self._bracketHighlight = self.renderer.addMarker(range, "bracket"); + } + }, 10); +}; + + ace.Editor.prototype.onFocus = function() { this.renderer.showCursor(); this.renderer.visualizeFocus(); diff --git a/src/TextDocument.js b/src/TextDocument.js index d6b66e75..5c286081 100644 --- a/src/TextDocument.js +++ b/src/TextDocument.js @@ -90,6 +90,98 @@ ace.TextDocument.prototype.getTextRange = function(range) { } }; +ace.TextDocument.prototype.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); + } +}; + +ace.TextDocument.prototype._brackets = { + ")": "(", + "(": ")", + "]": "[", + "[": "]", + "{": "}", + "}": "{" +}; + +ace.TextDocument.prototype._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 char = line.charAt(column); + if (char == openBracket) { + depth -= 1; + if (depth == 0) { + return {row: row, column: column}; + } + } + else if (char == bracket) { + depth +=1; + } + column -= 1; + } + row -=1; + if (row < 0) break; + + var line = this.getLine(row); + var column = line.length-1; + } + return null; +}; + +ace.TextDocument.prototype._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 char = line.charAt(column); + if (char == closingBracket) { + depth -= 1; + if (depth == 0) { + return {row: row, column: column}; + } + } + else if (char == bracket) { + depth +=1; + } + column += 1; + } + row +=1; + if (row >= lineCount) break; + + var line = this.getLine(row); + var column = 0; + } + return null; +}; + ace.TextDocument.prototype.insert = function(position, text) { var end = this._insert(position, text); this.fireChangeEvent(position.row, position.row == end.row ? position.row diff --git a/test/TextDocumentTest.js b/test/TextDocumentTest.js new file mode 100644 index 00000000..39f6cbaa --- /dev/null +++ b/test/TextDocumentTest.js @@ -0,0 +1,35 @@ +var TextDocumentTest = new TestCase("TextDocumentTest", { + + "test: find matching opening bracket" : function() { + var doc = new ace.TextDocument(["(()(", "())))"].join("\n")); + + assertPosition(0, 1, doc.findMatchingBracket({row: 0, column: 3})); + assertPosition(1, 0, doc.findMatchingBracket({row: 1, column: 2})); + assertPosition(0, 3, doc.findMatchingBracket({row: 1, column: 3})); + assertPosition(0, 0, doc.findMatchingBracket({row: 1, column: 4})); + assertEquals(null, doc.findMatchingBracket({row: 1, column: 5})); + }, + + "test: find matching closing bracket" : function() { + var doc = new ace.TextDocument(["(()(", "())))"].join("\n")); + + assertPosition(1, 1, doc.findMatchingBracket({row: 1, column: 1})); + assertPosition(1, 1, doc.findMatchingBracket({row: 1, column: 1})); + assertPosition(1, 2, doc.findMatchingBracket({row: 0, column: 4})); + assertPosition(0, 2, doc.findMatchingBracket({row: 0, column: 2})); + assertPosition(1, 3, doc.findMatchingBracket({row: 0, column: 1})); + assertEquals(null, doc.findMatchingBracket({row: 0, column: 0})); + }, + + "test: match different bracket types" : function() { + var doc = new ace.TextDocument(["({[", ")]}"].join("\n")); + + assertPosition(1, 0, doc.findMatchingBracket({row: 0, column: 1})); + assertPosition(1, 2, doc.findMatchingBracket({row: 0, column: 2})); + assertPosition(1, 1, doc.findMatchingBracket({row: 0, column: 3})); + + assertPosition(0, 0, doc.findMatchingBracket({row: 1, column: 1})); + assertPosition(0, 2, doc.findMatchingBracket({row: 1, column: 2})); + assertPosition(0, 1, doc.findMatchingBracket({row: 1, column: 3})); + } +}); \ No newline at end of file