From 0270ba3e1c7900e5fc0a620f09ccce655a7d27d6 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 4 Feb 2013 15:23:14 -0800 Subject: [PATCH] Add bracket matcher that highlights pair (), {}, and [] pairs are now highlighted when after or before the cursor --- src/app/edit-session.coffee | 3 + src/app/editor.coffee | 1 + src/packages/bracket-matcher/index.coffee | 94 +++++++++++++++++++ .../spec/bracket-matcher-spec.coffee | 60 ++++++++++++ .../stylesheets/bracket-matcher.css | 3 + themes/atom-dark-ui/bracket-matcher.css | 5 + themes/atom-dark-ui/package.json | 3 +- themes/atom-light-ui/bracket-matcher.css | 5 + themes/atom-light-ui/package.json | 3 +- 9 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 src/packages/bracket-matcher/index.coffee create mode 100644 src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee create mode 100644 src/packages/bracket-matcher/stylesheets/bracket-matcher.css create mode 100644 themes/atom-dark-ui/bracket-matcher.css create mode 100644 themes/atom-light-ui/bracket-matcher.css diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index f6248c173..838a13b76 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -307,6 +307,9 @@ class EditSession fold.destroy() @setCursorBufferPosition([fold.startRow, 0]) + isFoldedAtCursorRow: -> + @isFoldedAtScreenRow(@getCursorScreenRow()) + isFoldedAtBufferRow: (bufferRow) -> screenRow = @screenPositionForBufferPosition([bufferRow]).row @isFoldedAtScreenRow(screenRow) diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 161e7505e..738f8269c 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -277,6 +277,7 @@ class Editor extends View destroyFoldsContainingBufferRow: (bufferRow) -> @activeEditSession.destroyFoldsContainingBufferRow(bufferRow) isFoldedAtScreenRow: (screenRow) -> @activeEditSession.isFoldedAtScreenRow(screenRow) isFoldedAtBufferRow: (bufferRow) -> @activeEditSession.isFoldedAtBufferRow(bufferRow) + isFoldedAtCursorRow: -> @activeEditSession.isFoldedAtCursorRow() lineForScreenRow: (screenRow) -> @activeEditSession.lineForScreenRow(screenRow) linesForScreenRows: (start, end) -> @activeEditSession.linesForScreenRows(start, end) diff --git a/src/packages/bracket-matcher/index.coffee b/src/packages/bracket-matcher/index.coffee new file mode 100644 index 000000000..aeb18f60c --- /dev/null +++ b/src/packages/bracket-matcher/index.coffee @@ -0,0 +1,94 @@ +AtomPackage = require 'atom-package' +_ = require 'underscore' +{$$} = require 'space-pen' +Range = require 'range' + +module.exports = +class BracketMatcher extends AtomPackage + startPairMatches: + '(': ')' + '[': ']' + '{': '}' + + endPairMatches: + ')': '(' + ']': '[' + '}': '{' + + pairHighlighted: false + + activate: (rootView) -> + rootView.eachEditor (editor) => @subscribeToEditor(editor) if editor.attached + + subscribeToEditor: (editor) -> + editor.on 'cursor:moved', => @updateMatch(editor) + + createView: (editor, bufferPosition) -> + pixelPosition = editor.pixelPositionForBufferPosition(bufferPosition) + view = $$ -> @div class: 'bracket-matcher' + view.css('top', pixelPosition.top).css('left', pixelPosition.left) + view.width(editor.charWidth).height(editor.charHeight) + + findCurrentPair: (editor, buffer, matches) -> + position = editor.getCursorBufferPosition() + currentPair = buffer.getTextInRange(Range.fromPointWithDelta(position, 0, 1)) + unless matches[currentPair] + position = position.translate([0, -1]) + currentPair = buffer.getTextInRange(Range.fromPointWithDelta(position, 0, 1)) + matchingPair = matches[currentPair] + if matchingPair + {position, currentPair, matchingPair} + else + {} + + findMatchingEndPair: (buffer, startPairPosition, startPair, endPair) -> + scanRange = new Range(startPairPosition.translate([0, 1]), buffer.getEofPosition()) + regex = new RegExp("[#{_.escapeRegExp(startPair + endPair)}]", 'g') + endPairPosition = null + unpairedCount = 0 + buffer.scanInRange regex, scanRange, (match, range, {stop}) => + if match[0] is startPair + unpairedCount++ + else if match[0] is endPair + unpairedCount-- + endPairPosition = range.start + stop() if unpairedCount < 0 + endPairPosition + + findMatchingStartPair: (buffer, endPairPosition, startPair, endPair) -> + scanRange = new Range([0, 0], endPairPosition) + regex = new RegExp("[#{_.escapeRegExp(startPair + endPair)}]", 'g') + startPairPosition = null + unpairedCount = 0 + scanner = (match, range, {stop}) => + if match[0] is endPair + unpairedCount++ + else if match[0] is startPair + unpairedCount-- + startPairPosition = range.start + stop() if unpairedCount < 0 + buffer.scanInRange(regex, scanRange, scanner, true) + startPairPosition + + updateMatch: (editor) -> + return unless underlayer = editor.pane()?.find('.underlayer') + + underlayer.find('.bracket-matcher').remove() if @pairHighlighted + @pairHighlighted = false + + return unless editor.getSelection().isEmpty() + return if editor.isFoldedAtCursorRow() + + buffer = editor.getBuffer() + {position, currentPair, matchingPair} = @findCurrentPair(editor, buffer, @startPairMatches) + if position + matchPosition = @findMatchingEndPair(buffer, position, currentPair, matchingPair) + else + {position, currentPair, matchingPair} = @findCurrentPair(editor, buffer, @endPairMatches) + if position + matchPosition = @findMatchingStartPair(buffer, position, matchingPair, currentPair) + + if position? and matchPosition? + underlayer.append(@createView(editor, position)) + underlayer.append(@createView(editor, matchPosition)) + @pairHighlighted = true diff --git a/src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee b/src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee new file mode 100644 index 000000000..8b1e1708d --- /dev/null +++ b/src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee @@ -0,0 +1,60 @@ +RootView = require 'root-view' + +describe "bracket matching", -> + [rootView, editor] = [] + + beforeEach -> + rootView = new RootView(require.resolve('fixtures/sample.js')) + atom.loadPackage('bracket-matcher') + rootView.attachToDom() + editor = rootView.getActiveEditor() + + afterEach -> + rootView.deactivate() + + describe "when the cursor is before a starting pair", -> + it "highlights the starting pair and ending pair", -> + editor.moveCursorToEndOfLine() + editor.moveCursorLeft() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) + expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) + + describe "when the cursor is after a starting pair", -> + it "highlights the starting pair and ending pair", -> + editor.moveCursorToEndOfLine() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) + expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) + + describe "when the cursor is before an ending pair", -> + it "highlights the starting pair and ending pair", -> + editor.moveCursorToBottom() + editor.moveCursorLeft() + editor.moveCursorLeft() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) + expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) + + describe "when the cursor is after an ending pair", -> + it "highlights the starting pair and ending pair", -> + editor.moveCursorToBottom() + editor.moveCursorLeft() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) + expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([0,28]) + + describe "when the cursor is moved off a pair", -> + it "removes the starting pair and ending pair highlights", -> + editor.moveCursorToEndOfLine() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + editor.moveCursorToBeginningOfLine() + expect(editor.underlayer.find('.bracket-matcher').length).toBe 0 + + describe "pair balancing", -> + describe "when a second starting pair preceeds the first ending pair", -> + it "advances to the second ending pair", -> + editor.setCursorBufferPosition([8,42]) + expect(editor.underlayer.find('.bracket-matcher').length).toBe 2 + expect(editor.underlayer.find('.bracket-matcher:first').position()).toEqual editor.pixelPositionForBufferPosition([8,42]) + expect(editor.underlayer.find('.bracket-matcher:last').position()).toEqual editor.pixelPositionForBufferPosition([8,54]) diff --git a/src/packages/bracket-matcher/stylesheets/bracket-matcher.css b/src/packages/bracket-matcher/stylesheets/bracket-matcher.css new file mode 100644 index 000000000..8d77b10de --- /dev/null +++ b/src/packages/bracket-matcher/stylesheets/bracket-matcher.css @@ -0,0 +1,3 @@ +.bracket-matcher { + position: absolute; +} diff --git a/themes/atom-dark-ui/bracket-matcher.css b/themes/atom-dark-ui/bracket-matcher.css new file mode 100644 index 000000000..d579fdc35 --- /dev/null +++ b/themes/atom-dark-ui/bracket-matcher.css @@ -0,0 +1,5 @@ +.bracket-matcher { + border-bottom: 1px solid #f8de7e; + margin-top: -1px; + opacity: .7; +} diff --git a/themes/atom-dark-ui/package.json b/themes/atom-dark-ui/package.json index d0a007f7f..44dd4534d 100644 --- a/themes/atom-dark-ui/package.json +++ b/themes/atom-dark-ui/package.json @@ -8,6 +8,7 @@ "status-bar.css", "markdown-preview.css", "command-panel.css", - "command-logger.css" + "command-logger.css", + "bracket-matcher.css" ] } diff --git a/themes/atom-light-ui/bracket-matcher.css b/themes/atom-light-ui/bracket-matcher.css new file mode 100644 index 000000000..d579fdc35 --- /dev/null +++ b/themes/atom-light-ui/bracket-matcher.css @@ -0,0 +1,5 @@ +.bracket-matcher { + border-bottom: 1px solid #f8de7e; + margin-top: -1px; + opacity: .7; +} diff --git a/themes/atom-light-ui/package.json b/themes/atom-light-ui/package.json index d0a007f7f..44dd4534d 100644 --- a/themes/atom-light-ui/package.json +++ b/themes/atom-light-ui/package.json @@ -8,6 +8,7 @@ "status-bar.css", "markdown-preview.css", "command-panel.css", - "command-logger.css" + "command-logger.css", + "bracket-matcher.css" ] }