diff --git a/.atom/config.cson b/.atom/config.cson new file mode 100644 index 000000000..a93b83e66 --- /dev/null +++ b/.atom/config.cson @@ -0,0 +1,7 @@ +'editor': + 'fontSize': 16 +'core': + 'themes': [ + 'atom-dark-ui' + 'atom-dark-syntax' + ] diff --git a/.atom/config.json b/.atom/config.json deleted file mode 100644 index 50770e810..000000000 --- a/.atom/config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "editor": { - "fontSize": 16 - }, - "core": { - "themes": [ - "atom-dark-ui", - "atom-dark-syntax" - ] - } -} diff --git a/native/atom_window_controller.mm b/native/atom_window_controller.mm index d61e46063..4a26e9601 100644 --- a/native/atom_window_controller.mm +++ b/native/atom_window_controller.mm @@ -69,6 +69,7 @@ [self initWithBootstrapScript:@"window-bootstrap" background:YES alwaysUseBundleResourcePath:stable]; [self.window setFrame:NSMakeRect(0, 0, 0, 0) display:NO]; [self.window setExcludedFromWindowsMenu:YES]; + [self.window setCollectionBehavior:NSWindowCollectionBehaviorStationary]; return self; } 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 2e994dc44..8d41771c2 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..081ca9923 --- /dev/null +++ b/src/packages/bracket-matcher/index.coffee @@ -0,0 +1,120 @@ +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.bracket-matcher', => @updateMatch(editor) + editor.command 'editor:go-to-matching-bracket.bracket-matcher', => + @goToMatchingPair(editor) + editor.on 'editor:will-be-removed', => editor.off('.bracket-matcher') + + goToMatchingPair: (editor) -> + return unless @pairHighlighted + return unless underlayer = editor.pane()?.find('.underlayer') + + position = editor.getCursorBufferPosition() + previousPosition = position.translate([0, -1]) + startPosition = underlayer.find('.bracket-matcher:first').data('bufferPosition') + endPosition = underlayer.find('.bracket-matcher:last').data('bufferPosition') + + if position.isEqual(startPosition) + editor.setCursorBufferPosition(endPosition.translate([0, 1])) + else if previousPosition.isEqual(startPosition) + editor.setCursorBufferPosition(endPosition) + else if position.isEqual(endPosition) + editor.setCursorBufferPosition(startPosition.translate([0, 1])) + else if previousPosition.isEqual(endPosition) + editor.setCursorBufferPosition(startPosition) + + createView: (editor, bufferPosition) -> + pixelPosition = editor.pixelPositionForBufferPosition(bufferPosition) + view = $$ -> @div class: 'bracket-matcher' + view.data('bufferPosition', bufferPosition) + 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? + if position.isLessThan(matchPosition) + underlayer.append(@createView(editor, position)) + underlayer.append(@createView(editor, matchPosition)) + else + underlayer.append(@createView(editor, matchPosition)) + underlayer.append(@createView(editor, position)) + @pairHighlighted = true diff --git a/src/packages/bracket-matcher/keymaps/bracket-matcher.cson b/src/packages/bracket-matcher/keymaps/bracket-matcher.cson new file mode 100644 index 000000000..672df9be6 --- /dev/null +++ b/src/packages/bracket-matcher/keymaps/bracket-matcher.cson @@ -0,0 +1,2 @@ +'.editor': + 'ctrl-j': 'editor:go-to-matching-bracket' 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..69e882502 --- /dev/null +++ b/src/packages/bracket-matcher/spec/bracket-matcher-spec.coffee @@ -0,0 +1,86 @@ +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:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) + expect(editor.underlayer.find('.bracket-matcher:first').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:last').position()).toEqual editor.pixelPositionForBufferPosition([12,0]) + expect(editor.underlayer.find('.bracket-matcher:first').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]) + + describe "when editor:go-to-matching-bracket is triggered", -> + describe "when the cursor is before the starting pair", -> + it "moves the cursor to after the ending pair", -> + editor.moveCursorToEndOfLine() + editor.moveCursorLeft() + editor.trigger "editor:go-to-matching-bracket" + expect(editor.getCursorBufferPosition()).toEqual [12, 1] + + describe "when the cursor is after the starting pair", -> + it "moves the cursor to before the ending pair", -> + editor.moveCursorToEndOfLine() + editor.trigger "editor:go-to-matching-bracket" + expect(editor.getCursorBufferPosition()).toEqual [12, 0] + + describe "when the cursor is before the ending pair", -> + it "moves the cursor to after the starting pair", -> + editor.setCursorBufferPosition([12, 0]) + editor.trigger "editor:go-to-matching-bracket" + expect(editor.getCursorBufferPosition()).toEqual [0, 29] + + describe "when the cursor is after the ending pair", -> + it "moves the cursor to before the starting pair", -> + editor.setCursorBufferPosition([12, 1]) + editor.trigger "editor:go-to-matching-bracket" + expect(editor.getCursorBufferPosition()).toEqual [0, 28] 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 e4c21621f..177ed813e 100644 --- a/themes/atom-dark-ui/package.json +++ b/themes/atom-dark-ui/package.json @@ -10,5 +10,6 @@ "command-panel.css", "command-logger.css", "blurred.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" ] }