├── .gitignore ├── keymaps └── rspec.cson ├── package.json ├── menus └── rspec.cson ├── spec ├── rspec-view-spec.coffee ├── rspec-spec.coffee └── text-formatter-spec.coffee ├── README.md ├── LICENSE.md ├── lib ├── text-formatter.coffee ├── rspec.coffee └── rspec-view.coffee └── styles └── rspec.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | .travis.yml 5 | -------------------------------------------------------------------------------- /keymaps/rspec.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/advanced/keymaps 10 | 'atom-text-editor': 11 | 'ctrl-alt-t': 'rspec:run' 12 | 'ctrl-alt-x': 'rspec:run-for-line' 13 | 'ctrl-alt-e': 'rspec:run-last' 14 | 'ctrl-alt-r': 'rspec:run-all' 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rspec", 3 | "main": "./lib/rspec", 4 | "version": "0.4.0", 5 | "private": true, 6 | "description": "Atom RSpec runner package", 7 | "activationCommands": { 8 | "atom-workspace": [ 9 | "rspec:run", 10 | "rspec:run-for-line", 11 | "rspec:run-last", 12 | "rspec:run-all" 13 | ] 14 | }, 15 | "repository": "https://github.com/fcoury/atom-rspec", 16 | "license": "MIT", 17 | "engines": { 18 | "atom": ">=1.0.0 <2.0.0" 19 | }, 20 | "dependencies": { 21 | "atom-space-pen-views": "^2.0.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /menus/rspec.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/creating-a-package#menus for more details 2 | 'context-menu': 3 | '.overlayer': [ 4 | { 'label': 'Run All Specs', 'command': 'rspec:run' } 5 | { 'label': 'Run Current Spec', 'command': 'rspec:run-for-line' } 6 | { 'label': 'Re-Run Last Spec', 'command': 'rspec:run-last' } 7 | ] 8 | 9 | 'menu': [ 10 | { 11 | 'label': 'Packages' 12 | 'submenu': [ 13 | 'label': 'RSpec' 14 | 'submenu': [ 15 | { 'label': 'Run All Specs', 'command': 'rspec:run' } 16 | { 'label': 'Run Current Spec', 'command': 'rspec:run-for-line' } 17 | { 'label': 'Re-Run Last Spec', 'command': 'rspec:run-last' } 18 | ] 19 | ] 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /spec/rspec-view-spec.coffee: -------------------------------------------------------------------------------- 1 | RspecView = require '../lib/rspec-view' 2 | 3 | describe "RspecView", -> 4 | beforeEach -> 5 | @rspecView = new RspecView('example_spec.rb') 6 | 7 | describe 'addOutput', -> 8 | it 'adds output', -> 9 | @rspecView.addOutput('foo') 10 | expect(@rspecView.output.html()).toBe 'foo' 11 | 12 | it 'corectly formats complex output', -> 13 | output = '[31m# ./foo/bar_spec.rb:123:in `block (3 levels) in [0m' 14 | @rspecView.addOutput(output) 15 | expect(@rspecView.output.html()).toBe '

' + 16 | '# ' + 17 | './foo/bar_spec.rb:123' + 18 | '' + 19 | ':in `block (3 levels) in <top (required)>' + 20 | '

' 21 | -------------------------------------------------------------------------------- /spec/rspec-spec.coffee: -------------------------------------------------------------------------------- 1 | RSpec = require '../lib/rspec' 2 | 3 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 4 | # 5 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 6 | # or `fdescribe`). Remove the `f` to unfocus the block. 7 | 8 | describe "Rspec", -> 9 | activationPromise = null 10 | 11 | beforeEach -> 12 | atom.workspaceView = new WorkspaceView 13 | activationPromise = atom.packages.activatePackage('rspec') 14 | 15 | xdescribe "when the rspec:toggle event is triggered", -> 16 | it "attaches and then detaches the view", -> 17 | expect(atom.workspaceView.find('.rspec')).not.toExist() 18 | 19 | # This is an activation event, triggering it will cause the package to be 20 | # activated. 21 | atom.workspaceView.trigger 'rspec:toggle' 22 | 23 | waitsForPromise -> 24 | activationPromise 25 | 26 | runs -> 27 | expect(atom.workspaceView.find('.rspec')).toExist() 28 | atom.workspaceView.trigger 'rspec:toggle' 29 | expect(atom.workspaceView.find('.rspec')).not.toExist() 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Atom RSpec Runner Package 2 | 3 | [![ghit.me](https://ghit.me/badge.svg?repo=fcoury/atom-rspec)](https://ghit.me/repo/fcoury/atom-rspec) 4 | 5 | Add ability to run RSpec and see the output without leaving Atom. 6 | 7 | HotKeys: 8 | 9 | - __Ctrl+Alt+T__ - executes all specs the current file 10 | - __Ctrl+Alt+X__ - executes only the spec on the line the cursor's at 11 | - __Ctrl+Alt+E__ - re-executes the last executed spec 12 | 13 | ![Screenshot](http://cl.ly/image/2G2B3M2g3l3k/stats_collector_spec.rb%20-%20-Users-fcoury-Projects-crm_bliss.png) 14 | 15 | ## Configuration 16 | 17 | By default this package will run `rspec` as the command. 18 | 19 | You can set the default command by either accessing the Settings page (Cmd+,) 20 | and changing the command option like below: 21 | 22 | ![Configuration Screenshot](http://f.cl.ly/items/2k1C0E0e1l2Z3m1l3e1R/Settings%20-%20-Users-fcoury-Projects-crm_bliss.jpg) 23 | 24 | Or by opening your configuration file (clicking __Atom__ > __Open Your Config__) 25 | and adding or changing the following snippet: 26 | 27 | 'rspec': 28 | 'command': 'bundle exec rspec' 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/text-formatter.coffee: -------------------------------------------------------------------------------- 1 | {$, $$$, EditorView, ScrollView} = require 'atom-space-pen-views' 2 | 3 | class TextFormatter 4 | constructor: (@text)-> 5 | 6 | htmlEscaped: -> 7 | new TextFormatter( $('
').text(@text).html() ) 8 | 9 | fileLinked: -> 10 | text = @text.replace /([\\\/.][^\s]*:[0-9]+)([^\d]|$)/g, (match) => 11 | file = match.split(":")[0] 12 | line = match.split(":")[1].replace(/[^\d]*$/, '') 13 | 14 | fileLineEnd = file.length + line.length 15 | fileAndLine = "#{file}:#{line}" 16 | matchWithoutFileAndLine = match.substr(fileLineEnd + 1) 17 | 18 | ""+ 19 | "#{fileAndLine}#{matchWithoutFileAndLine}" 20 | new TextFormatter(text) 21 | 22 | colorized: -> 23 | text = @text 24 | 25 | colorStartCount = text.match(/\[3[0-7]m/g)?.length || 0 26 | colorEndCount = text.match(/\[0m/g)?.length || 0 27 | 28 | # to avoid unclosed tags we always use smaller number of color starts / ends 29 | replaceCount = colorStartCount 30 | replaceCount = colorEndCount if colorEndCount < colorStartCount 31 | 32 | for i in [0..replaceCount] 33 | text = text.replace /\[(3[0-7])m/, (match, colorCode) => 34 | "

" 35 | text = text.replace /\[0m/g, '

' 36 | 37 | new TextFormatter(text) 38 | 39 | module.exports = TextFormatter 40 | -------------------------------------------------------------------------------- /styles/rspec.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/stylesheets/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | .rspec-spinner { 8 | margin: auto; 9 | margin-top: 20px; 10 | background-image: url(images/octocat-spinner-128.gif); 11 | background-repeat: no-repeat; 12 | background-size: 64px; 13 | background-position: top center; 14 | padding-top: 70px; 15 | text-align: center; 16 | } 17 | 18 | 19 | .rspec-console.rspec { 20 | background-color: black; 21 | color: white; 22 | overflow: scroll; 23 | } 24 | 25 | .rspec-console.rspec { 26 | pre, 27 | pre div atom-text-editor, 28 | code, 29 | tt { 30 | font-size: 12px; 31 | font-family: Consolas, "Liberation Mono", Courier, monospace; 32 | } 33 | 34 | pre a { 35 | color: #0099cc; 36 | } 37 | 38 | .rspec-output { 39 | background: #000; 40 | color: #fff; 41 | 42 | .rspec-color { 43 | display: inline-block; 44 | padding: 0; 45 | margin: 0; 46 | } 47 | .tty-30 { color: black; } 48 | .tty-31 { color: red; } 49 | .tty-32 { color: green; } 50 | .tty-33 { color: yellow; } 51 | .tty-34 { color: blue; } 52 | .tty-35 { color: magenta; } 53 | .tty-36 { color: cyan; } 54 | .tty-37 { color: white; } 55 | } 56 | 57 | .stderr { color: red; } 58 | .stdout { color: white; } 59 | } 60 | -------------------------------------------------------------------------------- /spec/text-formatter-spec.coffee: -------------------------------------------------------------------------------- 1 | TextFormatter = require '../lib/text-formatter' 2 | 3 | describe 'htmlEscaped', -> 4 | it 'escapes html tags', -> 5 | formatter = new TextFormatter('bold text') 6 | expect(formatter.htmlEscaped().text).toBe '<b>bold</b> text' 7 | 8 | describe 'fileLinked', -> 9 | it 'adds atom hyperlinks on files with line numbers', -> 10 | text = '# ./foo/bar_spec.rb:123:in `block (3 levels) in ' 11 | formatter = new TextFormatter(text) 12 | expect(formatter.fileLinked().text).toBe('# ./foo/bar_spec.rb:123' + 14 | ':in `block (3 levels) in ' 15 | ) 16 | 17 | it 'adds links when line number is at the end of line', -> 18 | text = './foo/bar_spec.rb:123\n' 19 | formatter = new TextFormatter(text) 20 | expect(formatter.fileLinked().text).toBe './foo/bar_spec.rb:123\n' 22 | 23 | it 'adds links when file paths is wrapped with color marks', -> 24 | text = '[31m./foo/bar_spec.rb:123[0m' 25 | formatter = new TextFormatter(text) 26 | expect(formatter.fileLinked().text).toBe '[31m./foo/bar_spec.rb:123[0m' 28 | 29 | it 'adds links when file path is absolute', -> 30 | text = '/foo/bar_spec.rb:123' 31 | formatter = new TextFormatter(text) 32 | expect(formatter.fileLinked().text).toBe '/foo/bar_spec.rb:123' 34 | 35 | describe 'colorized', -> 36 | it 'corretly sets colors to fail/pass marks', -> 37 | formatter = new TextFormatter("[31mF[0m[31mF[0m[31mF[0m[33m*[0m[33m*[0m[31mF[0m") 38 | expect(formatter.colorized().text).toBe( 39 | '

F

' + 40 | '

F

' + 41 | '

F

' + 42 | '

*

' + 43 | '

*

' + 44 | '

F

' 45 | ) 46 | -------------------------------------------------------------------------------- /lib/rspec.coffee: -------------------------------------------------------------------------------- 1 | RSpecView = require './rspec-view' 2 | {CompositeDisposable} = require 'atom' 3 | url = require 'url' 4 | 5 | module.exports = 6 | config: 7 | command: 8 | type: 'string' 9 | default: 'rspec' 10 | spec_directory: 11 | type: 'string' 12 | default: 'spec' 13 | save_before_run: 14 | type: 'boolean' 15 | default: false 16 | force_colored_results: 17 | type: 'boolean' 18 | default: true 19 | split: 20 | type: 'string' 21 | default: 'right' 22 | description: 'The direction in which to split the pane when launching rspec' 23 | enum: [ 24 | {value: 'right', description: 'Right'} 25 | {value: 'left', description: 'Left'} 26 | {value: 'up', description: 'Up'} 27 | {value: 'down', description: 'Down'} 28 | ] 29 | 30 | 31 | rspecView: null 32 | subscriptions: null 33 | 34 | activate: (state) -> 35 | if state? 36 | @lastFile = state.lastFile 37 | @lastLine = state.lastLine 38 | 39 | @subscriptions = new CompositeDisposable 40 | 41 | @subscriptions.add atom.commands.add 'atom-workspace', 42 | 'rspec:run': => 43 | @run() 44 | 45 | 'rspec:run-for-line': => 46 | @runForLine() 47 | 48 | 'rspec:run-last': => 49 | @runLast() 50 | 51 | 'rspec:run-all': => 52 | @runAll() 53 | 54 | atom.workspace.addOpener (uriToOpen) -> 55 | {protocol, pathname} = url.parse(uriToOpen) 56 | return unless protocol is 'rspec-output:' 57 | new RSpecView(pathname) 58 | 59 | deactivate: -> 60 | @rspecView.destroy() 61 | @subscriptions.dispose() 62 | 63 | serialize: -> 64 | if @rspecView 65 | rspecViewState: @rspecView.serialize() 66 | lastFile: @lastFile 67 | lastLine: @lastLine 68 | 69 | openUriFor: (file, lineNumber) -> 70 | @lastFile = file 71 | @lastLine = lineNumber 72 | 73 | previousActivePane = atom.workspace.getActivePane() 74 | uri = "rspec-output://#{file}" 75 | atom.workspace.open(uri, split: atom.config.get("rspec.split"), activatePane: false, searchAllPanes: true).then (rspecView) -> 76 | if rspecView instanceof RSpecView 77 | rspecView.run(lineNumber) 78 | previousActivePane.activate() 79 | 80 | runForLine: -> 81 | console.log "Starting runForLine..." 82 | editor = atom.workspace.getActiveTextEditor() 83 | console.log "Editor", editor 84 | return unless editor? 85 | 86 | cursor = editor.getLastCursor() 87 | console.log "Cursor", cursor 88 | line = cursor.getBufferRow() + 1 89 | console.log "Line", line 90 | 91 | @openUriFor(editor.getPath(), line) 92 | 93 | runLast: -> 94 | return unless @lastFile? 95 | @openUriFor(@lastFile, @lastLine) 96 | 97 | run: -> 98 | console.log "RUN" 99 | editor = atom.workspace.getActiveTextEditor() 100 | return unless editor? 101 | 102 | @openUriFor(editor.getPath()) 103 | 104 | runAll: -> 105 | project = atom.project 106 | return unless project? 107 | 108 | @openUriFor(project.getPaths()[0] + 109 | "/" + atom.config.get("rspec.spec_directory"), @lastLine) 110 | -------------------------------------------------------------------------------- /lib/rspec-view.coffee: -------------------------------------------------------------------------------- 1 | {$, $$$, EditorView, ScrollView} = require 'atom-space-pen-views' 2 | path = require 'path' 3 | ChildProcess = require 'child_process' 4 | TextFormatter = require './text-formatter' 5 | 6 | class RSpecView extends ScrollView 7 | atom.deserializers.add(this) 8 | 9 | @deserialize: ({filePath}) -> 10 | new RSpecView(filePath) 11 | 12 | @content: -> 13 | @div class: 'rspec rspec-console', tabindex: -1, => 14 | @div class: 'rspec-spinner', 'Starting RSpec...' 15 | @pre class: 'rspec-output' 16 | 17 | initialize: -> 18 | super 19 | rspec = this 20 | atom.commands.add 'atom-workspace','core:copy': (event) -> 21 | rspec.copySelectedText() 22 | 23 | constructor: (filePath) -> 24 | super 25 | console.log "File path:", filePath 26 | @filePath = filePath 27 | 28 | @output = @find(".rspec-output") 29 | @spinner = @find(".rspec-spinner") 30 | @output.on("click", @terminalClicked) 31 | 32 | serialize: -> 33 | deserializer: 'RSpecView' 34 | filePath: @getPath() 35 | 36 | copySelectedText: -> 37 | text = window.getSelection().toString() 38 | return if text == '' 39 | atom.clipboard.write(text) 40 | 41 | getTitle: -> 42 | "RSpec - #{path.basename(@getPath())}" 43 | 44 | getURI: -> 45 | "rspec-output://#{@getPath()}" 46 | 47 | getPath: -> 48 | @filePath 49 | 50 | showError: (result) -> 51 | failureMessage = "The error message" 52 | 53 | @html $$$ -> 54 | @h2 'Running RSpec Failed' 55 | @h3 failureMessage if failureMessage? 56 | 57 | terminalClicked: (e) => 58 | if e.target?.href 59 | line = $(e.target).data('line') 60 | file = $(e.target).data('file') 61 | console.log(file) 62 | file = "#{atom.project.getPaths()[0]}/#{file}" 63 | 64 | promise = atom.workspace.open(file, { searchAllPanes: true, initialLine: line }) 65 | promise.then (editor) -> 66 | editor.setCursorBufferPosition([line-1, 0]) 67 | 68 | run: (lineNumber) -> 69 | atom.workspace.saveAll() if atom.config.get("rspec.save_before_run") 70 | @spinner.show() 71 | @output.empty() 72 | projectPath = atom.project.getPaths()[0] 73 | 74 | spawn = ChildProcess.spawn 75 | 76 | # Atom saves config based on package name, so we need to use rspec here. 77 | specCommand = atom.config.get("rspec.command") 78 | options = " --tty" 79 | options += " --color" if atom.config.get("rspec.force_colored_results") 80 | command = "#{specCommand} #{options} #{@filePath}" 81 | command = "#{command}:#{lineNumber}" if lineNumber 82 | 83 | console.log "[RSpec] running: #{command}" 84 | 85 | terminal = spawn("bash", ["-l"]) 86 | 87 | terminal.on 'close', @onClose 88 | 89 | terminal.stdout.on 'data', @onStdOut 90 | terminal.stderr.on 'data', @onStdErr 91 | 92 | terminal.stdin.write("cd #{projectPath} && #{command}\n") 93 | terminal.stdin.write("exit\n") 94 | 95 | addOutput: (output) => 96 | formatter = new TextFormatter(output) 97 | output = formatter.htmlEscaped().colorized().fileLinked().text 98 | 99 | @spinner.hide() 100 | @output.append("#{output}") 101 | @scrollTop(@[0].scrollHeight) 102 | 103 | onStdOut: (data) => 104 | @addOutput data 105 | 106 | onStdErr: (data) => 107 | @addOutput data 108 | 109 | onClose: (code) => 110 | console.log "[RSpec] exit with code: #{code}" 111 | 112 | module.exports = RSpecView 113 | --------------------------------------------------------------------------------