├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── keymaps └── stacktrace.cson ├── lib ├── editor-decorator.coffee ├── enter-dialog.coffee ├── main.coffee ├── navigation-view.coffee ├── parsers │ ├── coffeescript-trace-parser.coffee │ └── ruby-trace-parser.coffee ├── stacktrace-view.coffee ├── stacktrace.coffee └── trace-parser.coffee ├── menus └── stacktrace.cson ├── package.json ├── spec ├── editor-decorator-spec.coffee ├── enter-dialog-spec.coffee ├── fixtures │ ├── bottom.rb │ ├── context.txt │ ├── middle.rb │ ├── top.rb │ └── withtrace.txt ├── main-spec.coffee ├── navigation-view-spec.coffee ├── parsers │ ├── coffeescript-trace-parser-spec.coffee │ └── ruby-trace-parser-spec.coffee ├── stacktrace-spec.coffee ├── stacktrace-view-spec.coffee ├── trace-fixtures.coffee └── trace-parser-spec.coffee └── styles ├── stacktrace.atom-text-editor.less ├── stacktrace.less └── variables.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change 7 | 8 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh' 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## The Next Release 2 | 3 | * Your contribution here! 4 | * [#14](https://github.com/smashwilson/stacktrace/pull/14) Update event subscriptions to use to the new-style event-kit API. 5 | * [#13](https://github.com/smashwilson/stacktrace/pull/13) Add an "install" section to the README. - [@fibric](https://github.com/fibric) 6 | 7 | 8 | ## 0.1.0 - First Release 9 | * Every feature added 10 | * Every bug fixed 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ash Wilson 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stacktrace for Atom 2 | 3 | *Navigate stacktraces within Atom!* 4 | 5 | [![Build Status](https://travis-ci.org/smashwilson/stacktrace.svg?branch=master)](https://travis-ci.org/smashwilson/stacktrace?branch=master) 6 | 7 | Given a stacktrace from a supported language, this package gives you: 8 | 9 | * A mile-high view of the full trace, with a few lines of context on each stack 10 | frame; 11 | * Highlighting and navigation tools to walk up and down the stack while you're 12 | looking at the full files. 13 | * *[planned]* Intelligent mappings from paths from other systems to yours. If it looks like 14 | a ruby gem path, it'll map into your `${GEM_HOME}`; if it looks like a 15 | virtualenv path, it'll map into your virtualenv. 16 | 17 | ## Installation 18 | 19 | ```apm install stacktrace``` 20 | 21 | ## Obligatory Animated Gif 22 | 23 | ![walkthrough](https://cloud.githubusercontent.com/assets/17565/4100060/aa761e90-307e-11e4-83c8-e4bf04c20d95.gif) 24 | 25 | ## Commands 26 | 27 | Stacktrace is a **Bring Your Own Keybinding** :tm: package. Rather than try to guess a set of bindings that won't collide with any other package, or that aren't six-key chords, I'm not providing any default keybindings. 28 | 29 | To set hotkeys for stacktrace commands, invoke `Application: Open Your Keymap` from the command palette, and add a section like this one: 30 | 31 | ```coffee 32 | '.workspace': 33 | 'alt-s enter': 'stacktrace:from-selection' 34 | 'alt-s p': 'stacktrace:paste' 35 | 'alt-s up': 'stacktrace:to-caller' 36 | 'alt-s down': 'stacktrace:follow-call' 37 | ``` 38 | 39 | ## Language Support 40 | 41 | Stacktraces are currently recognized in the following languages: 42 | 43 | * Ruby 44 | * (Java|Coffee)script 45 | 46 | ## Countdown to 1.0 47 | 48 | In the true spirit of README-driven development, these are the features that I'd 49 | like to see in place before I mark it 1.0. 50 | 51 | - [x] Accept stacktraces pasted into a dialog you call up from the command 52 | palette. 53 | - [x] Present a view that gives you bits of context around each frame of a 54 | specific stack. 55 | - [x] Pluggable stacktrace recognition and parsing code. 56 | - [ ] Map parsed frames to source files on the local filesystem. 57 | - [x] While a stacktrace is active, highlight individual lines from the trace 58 | in open editors. 59 | - [x] Provide commands for next-frame, previous-frame, and turning it off. 60 | - [x] Show a stacktrace navigation view as a bottom panel with next, previous 61 | and stop buttons. 62 | -------------------------------------------------------------------------------- /keymaps/stacktrace.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 | -------------------------------------------------------------------------------- /lib/editor-decorator.coffee: -------------------------------------------------------------------------------- 1 | # Decorate any lines within an {Editor} that correspond to an active {Stacktrace}. 2 | 3 | {Stacktrace} = require './stacktrace' 4 | 5 | markers = [] 6 | 7 | module.exports = 8 | decorate: (editor) -> 9 | active = Stacktrace.getActivated() 10 | return unless active? 11 | 12 | for frame in active.frames 13 | if frame.realPath is editor.getPath() 14 | range = editor.getBuffer().rangeForRow frame.bufferLineNumber() 15 | marker = editor.markBufferRange range, persistent: false 16 | editor.decorateMarker marker, type: 'line', class: 'line-stackframe' 17 | editor.decorateMarker marker, type: 'line-number', class: 'line-number-stackframe' 18 | markers.push marker 19 | 20 | cleanup: -> 21 | m.destroy() for m in markers 22 | -------------------------------------------------------------------------------- /lib/enter-dialog.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'event-kit' 2 | {View, TextEditorView} = require 'atom-space-pen-views' 3 | 4 | class EnterDialog extends View 5 | 6 | @content: -> 7 | @div class: 'stacktrace enter-dialog overlay from-top', => 8 | @h2 class: 'text-info block', 'Paste a stacktrace here:' 9 | @div class: 'block', => 10 | @subview 'traceEditor', new TextEditorView(mini: true) 11 | @div class: 'block padded', => 12 | @button({ 13 | class: 'btn btn-lg btn-primary inline-block', 14 | click: 'traceIt' 15 | }, 'Trace It!') 16 | @button({ 17 | class: 'btn btn-lg inline-block', 18 | click: 'cancel' 19 | }, 'Cancel') 20 | 21 | initialize: (@pkg) -> 22 | @subs = new CompositeDisposable() 23 | 24 | @traceEditor.focus() 25 | 26 | @subs.add atom.commands.add '.stacktrace.enter-dialog', 'core:cancel', => @cancel() 27 | 28 | traceIt: -> 29 | @pkg.traceHandlerV1().acceptTrace @traceEditor.getText() 30 | @remove() 31 | 32 | cancel: -> @remove() 33 | 34 | module.exports = EnterDialog 35 | -------------------------------------------------------------------------------- /lib/main.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable} = require 'event-kit' 2 | 3 | EnterDialog = require './enter-dialog' 4 | {Stacktrace} = require './stacktrace' 5 | {StacktraceView} = require './stacktrace-view' 6 | {NavigationView} = require './navigation-view' 7 | {decorate, cleanup} = require './editor-decorator' 8 | 9 | subs = new CompositeDisposable() 10 | 11 | module.exports = 12 | 13 | activate: (state) -> 14 | subs.add atom.commands.add 'atom-workspace', 15 | 'stacktrace:paste': => atom.workspace.addTopPanel item: new EnterDialog(this) 16 | 'stacktrace:from-selection': => 17 | selections = atom.workspace.getActiveTextEditor()?.getSelections() or [] 18 | text = (s.getText() for s in selections).join '' 19 | @traceHandlerV1().acceptTrace(text) 20 | 'stacktrace:to-caller': -> NavigationView.current()?.navigateToCaller() 21 | 'stacktrace:follow-call': -> NavigationView.current()?.navigateToCalled() 22 | 23 | subs.add atom.workspace.observeTextEditors decorate 24 | subs.add Stacktrace.onDidChangeActive (e) -> 25 | cleanup() 26 | if e.newTrace? 27 | decorate(e) for e in atom.workspace.getTextEditors() 28 | 29 | @navigationView = new NavigationView 30 | atom.workspace.addBottomPanel item: @navigationView 31 | 32 | subs.add StacktraceView.registerIn(atom.workspace) 33 | 34 | deactivate: -> 35 | @navigationView.remove() 36 | subs.dispose() 37 | 38 | # Public: Construct a service object that implements the stacktrace parsing service. 39 | # 40 | traceHandlerV1: -> 41 | 42 | # Public: Parse any and all stacktraces recognized from a sample of text. Open a new 43 | # StacktraceView for each. 44 | # 45 | # trace [String] - A sample of text that may contain zero to many stacktraces in recognized 46 | # languages. 47 | # 48 | acceptTrace: (trace) -> 49 | for trace in Stacktrace.parse(trace) 50 | trace.register() 51 | atom.workspace.open trace.getUrl() 52 | -------------------------------------------------------------------------------- /lib/navigation-view.coffee: -------------------------------------------------------------------------------- 1 | {View} = require 'atom-space-pen-views' 2 | _ = require 'underscore-plus' 3 | {Stacktrace} = require './stacktrace' 4 | {CompositeDisposable} = require 'event-kit' 5 | 6 | class NavigationView extends View 7 | 8 | @content: -> 9 | activatedClass = if Stacktrace.getActivated()? then '' else 'inactive' 10 | 11 | @div class: "tool-panel panel-bottom padded stacktrace navigation #{activatedClass}", => 12 | @div class: 'inline-block trace-name', => 13 | @h2 class: 'inline-block text-highlight message', outlet: 'message', click: 'backToTrace' 14 | @span class: 'inline-block icon icon-x', click: 'deactivateTrace' 15 | @div class: 'inline-block current-frame unfocused', outlet: 'frameContainer', => 16 | @span class: 'inline-block icon icon-code' 17 | @span class: 'inline-block function', outlet: 'frameFunction', click: 'navigateToLastActive' 18 | @span class: 'inline-block index', outlet: 'frameIndex' 19 | @span class: 'inline-block divider', '/' 20 | @span class: 'inline-block total', outlet: 'frameTotal' 21 | @div class: 'pull-right controls', => 22 | @button class: 'inline-block btn', click: 'navigateToCaller', => 23 | @span class: 'icon icon-arrow-up' 24 | @span 'Caller' 25 | @span class: 'text-info button-label-up', outlet: 'upButtonLabel' 26 | @button class: 'inline-block btn', click: 'navigateToCalled', => 27 | @span class: 'text-info button-label-down', outlet: 'downButtonLabel' 28 | @span 'Follow Call' 29 | @span class: 'icon icon-arrow-down' 30 | 31 | initialize: -> 32 | @subs = new CompositeDisposable 33 | 34 | @subs.add Stacktrace.onDidChangeActive (e) => 35 | if e.newTrace? then @useTrace(e.newTrace) else @noTrace() 36 | 37 | # Subscribe to opening editors. Set the current frame when a cursor is moved over a frame's 38 | # line. 39 | @subs.add atom.workspace.observeTextEditors (e) => 40 | @updateTraceState(e) 41 | @subs.add e.onDidChangeCursorPosition => @updateTraceState(e) 42 | 43 | if Stacktrace.getActivated? then @hide() 44 | 45 | # Prepend keystroke glyphs to the up and down buttons. 46 | upBindings = atom.keymaps.findKeyBindings command: 'stacktrace:to-caller' 47 | if upBindings.length > 0 48 | binding = upBindings[0] 49 | @upButtonLabel.text _.humanizeKeystroke binding.keystrokes 50 | 51 | downBindings = atom.keymaps.findKeyBindings command: 'stacktrace:follow-call' 52 | if downBindings.length > 0 53 | binding = downBindings[0] 54 | @downButtonLabel.text _.humanizeKeystroke binding.keystrokes 55 | 56 | detached: -> 57 | @subs.dispose() 58 | 59 | updateTraceState: (editor) -> 60 | if @trace? 61 | pos = 62 | position: editor.getCursorBufferPosition() 63 | path: editor.getPath() 64 | 65 | # Allow the already-set @frame a chance to see if it still applies. 66 | # This lets the caller and called navigation work properly, even if multiple frames are 67 | # on the same line. 68 | if @frame? and @frame.isOn(pos) 69 | @useFrame(@frame) 70 | else 71 | # Otherwise, scan the trace for a matching frame. 72 | frame = @trace.atEditorPosition(pos) 73 | if frame? then @useFrame(frame) else @unfocusFrame() 74 | 75 | useTrace: (@trace) -> 76 | @removeClass 'inactive' 77 | @message.text(trace.message) 78 | @noFrame() 79 | @show() 80 | 81 | noTrace: -> 82 | @addClass 'inactive' 83 | @message.text('') 84 | @noFrame() 85 | @hide() 86 | 87 | useFrame: (@frame) -> 88 | @frameContainer.removeClass 'unfocused' 89 | @frameFunction.text @frame.functionName 90 | @frameFunction.addClass 'highlight-info' 91 | @frameIndex.text @frame.humanIndex().toString() 92 | @frameTotal.text @trace.frames.length.toString() 93 | 94 | unfocusFrame: -> 95 | @frameContainer.addClass 'unfocused' 96 | @frameFunction.removeClass 'highlight-info' 97 | 98 | noFrame: -> 99 | @unfocusFrame() 100 | @frameFunction.text '' 101 | @frameIndex.text '' 102 | @frameTotal.text '' 103 | 104 | deactivateTrace: -> 105 | Stacktrace.getActivated().deactivate() 106 | 107 | backToTrace: -> 108 | url = Stacktrace.getActivated()?.getUrl() 109 | atom.workspace.open(url) if url 110 | 111 | navigateToCaller: -> 112 | return unless @trace? and @frame? 113 | 114 | f = @trace.callerOf(@frame) 115 | if f? 116 | @frame = f 117 | @frame.navigateTo() 118 | 119 | navigateToCalled: -> 120 | return unless @trace? and @frame? 121 | 122 | f = @trace.calledFrom(@frame) 123 | if f? 124 | @frame = f 125 | @frame.navigateTo() 126 | 127 | navigateToLastActive: -> 128 | return unless @frame? 129 | @frame.navigateTo() 130 | 131 | @current: -> 132 | atom.workspaceView.find('.stacktrace.navigation')?.view() 133 | 134 | module.exports = NavigationView: NavigationView 135 | -------------------------------------------------------------------------------- /lib/parsers/coffeescript-trace-parser.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = 3 | 4 | recognize: (line, f, {emitMessage, emitFrame, emitStack}) -> 5 | m = line.match /// ^ 6 | (.*Error) : # Error name 7 | (.+) # Message 8 | $ 9 | /// 10 | return unless m? 11 | 12 | emitMessage line 13 | 14 | consume: (line, f, {emitMessage, emitFrame, emitStack}) -> 15 | m = line.match /// ^ 16 | at \s+ 17 | ([^(]+) # Function name 18 | \( 19 | ([^:]+) : # Path 20 | (\d+) : # Line 21 | (\d+) # Column 22 | \) 23 | /// 24 | return emitStack() unless m? 25 | 26 | f.functionName m[1].trim() 27 | f.path m[2] 28 | f.lineNumber parseInt m[3] 29 | emitFrame() 30 | -------------------------------------------------------------------------------- /lib/parsers/ruby-trace-parser.coffee: -------------------------------------------------------------------------------- 1 | 2 | module.exports = 3 | 4 | recognize: (line, f, {emitMessage, emitFrame, emitStack}) -> 5 | m = line.match /// ^ 6 | ([^:]+) : # File path 7 | (\d+) : # Line number 8 | in \s* ` ([^']+) ' # Function name 9 | : \s (.+) # Error message 10 | $ 11 | /// 12 | return unless m? 13 | 14 | f.path m[1] 15 | f.lineNumber parseInt m[2] 16 | f.functionName m[3] 17 | 18 | emitMessage m[4] 19 | emitFrame() 20 | 21 | consume: (line, f, {emitMessage, emitFrame, emitStack}) -> 22 | m = line.match /// ^ 23 | from \s+ # from 24 | ([^:]+) : # File path 25 | (\d+) : # Line number 26 | in \s* ` ([^']+) ' # Function name 27 | $ 28 | /// 29 | return emitStack() unless m? 30 | 31 | f.path m[1] 32 | f.lineNumber parseInt m[2] 33 | f.functionName m[3] 34 | emitFrame() 35 | -------------------------------------------------------------------------------- /lib/stacktrace-view.coffee: -------------------------------------------------------------------------------- 1 | {View, TextEditorView} = require 'atom-space-pen-views' 2 | {chomp} = require 'line-chomper' 3 | {CompositeDisposable} = require 'event-kit' 4 | 5 | {Stacktrace, PREFIX} = require './stacktrace' 6 | 7 | existingViews = {} 8 | 9 | class StacktraceView extends View 10 | 11 | @content: (trace) -> 12 | tclass = if trace.isActive() then 'activated' else '' 13 | @div class: "stacktrace traceview tool-panel padded #{tclass}", => 14 | @div class: 'panel padded', => 15 | @h2 class: 'error-message', trace.message 16 | @div class: 'frames', => 17 | for frame in trace.frames 18 | @subview 'frame', new FrameView frame, => trace.activate() 19 | 20 | initialize: (@trace) -> 21 | @subs = new CompositeDisposable 22 | 23 | @uri = @trace.getUrl() 24 | @subs.add Stacktrace.onDidChangeActive (e) => 25 | if e.newTrace is @trace 26 | @addClass 'activated' 27 | else 28 | @removeClass 'activated' 29 | 30 | detached: -> 31 | @subs.dispose() 32 | delete existingViews[@trace] 33 | 34 | # Internal: Return the window title. 35 | # 36 | getTitle: -> 37 | @trace.message 38 | 39 | # Internal: Register a callback to be fired when the title is changed. 40 | # 41 | # The title can never change, so this is a no-op. 42 | # 43 | onDidChangeTitle: -> 44 | dispose: -> 45 | 46 | # Internal: Register a callback to be fired when the pane should be marked modified. 47 | # 48 | # StacktraceViews are read-only, so this will never happen. 49 | # 50 | onDidChangeModified: -> 51 | dispose: -> 52 | 53 | # Internal: Register an opener function in the workspace to handle URLs 54 | # generated by a Stacktrace. 55 | # 56 | @registerIn: (workspace) -> 57 | workspace.addOpener (filePath) -> 58 | trace = Stacktrace.forUrl(filePath) 59 | if trace? 60 | existingViews[trace] ?= new StacktraceView(trace) 61 | 62 | 63 | class FrameView extends View 64 | 65 | @content: (frame, navCallback) -> 66 | @div class: 'frame inset-panel', => 67 | @div class: 'panel-heading', => 68 | @span class: 'icon icon-fold inline-block', click: 'minimize' 69 | @span class: 'icon icon-unfold inline-block', click: 'restore' 70 | @span class: 'function-name text-highlight inline-block', frame.functionName 71 | @span class: 'source-location text-info inline-block pull-right', click: 'navigate', => 72 | @text "#{frame.rawPath} @ #{frame.lineNumber}" 73 | @div class: 'panel-body padded', outlet: 'body', click: 'navigate', => 74 | @subview 'source', new TextEditorView(mini: true) 75 | 76 | initialize: (@frame, @navCallback) -> 77 | chomp @frame.realPath, fromLine: 0, toLine: 100, (err, lines) => 78 | if err? 79 | console.error err 80 | else 81 | grammar = atom.grammars.selectGrammar @frame.realPath, lines.join("\n") 82 | @source.getModel().setGrammar grammar 83 | 84 | @frame.getContext 3, (err, lines) => 85 | if err? 86 | console.error err 87 | else 88 | @source.getModel().setText lines.join("\n") 89 | 90 | navigate: -> 91 | @navCallback() 92 | @frame.navigateTo() 93 | 94 | minimize: -> 95 | @addClass 'minimized' 96 | @body.hide 'fast' 97 | 98 | restore: -> 99 | @removeClass 'minimized' 100 | @body.show 'fast' 101 | 102 | module.exports = 103 | StacktraceView: StacktraceView 104 | FrameView: FrameView 105 | -------------------------------------------------------------------------------- /lib/stacktrace.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | 3 | {Emitter} = require 'event-kit' 4 | 5 | jsSHA = require 'jssha' 6 | {chomp} = require 'line-chomper' 7 | traceParser = null 8 | 9 | PREFIX = 'stacktrace://trace' 10 | 11 | REGISTRY = {} 12 | ACTIVE = null 13 | 14 | emitter = new Emitter 15 | 16 | # Internal: A heuristically parsed and interpreted stacktrace. 17 | # 18 | class Stacktrace 19 | 20 | constructor: (@frames = [], @message = '') -> 21 | i = 0 22 | for f in @frames 23 | f.index = i 24 | i += 1 25 | 26 | # Internal: Compute the SHA256 checksum of the normalized stacktrace. 27 | # 28 | getChecksum: -> 29 | body = (frame.rawLine for frame in @frames).join() 30 | sha = new jsSHA(body, 'TEXT') 31 | sha.getHash('SHA-256', 'HEX') 32 | 33 | # Internal: Generate a URL that can be used to launch or focus a 34 | # {StacktraceView}. 35 | # 36 | getUrl: -> @url ?= "#{PREFIX}/#{@getChecksum()}" 37 | 38 | # Public: Determine whether or not this Stacktrace is the "active" one. The active Stacktrace is 39 | # shown in a bottom navigation panel and highlighted in opened editors. 40 | # 41 | isActive: -> false 42 | 43 | # Internal: Register this trace in a global map by its URL. 44 | # 45 | register: -> 46 | REGISTRY[@getUrl()] = this 47 | 48 | # Internal: Remove this trace from the global map if it had previously been 49 | # registered. 50 | # 51 | unregister: -> 52 | delete REGISTRY[@getUrl()] 53 | 54 | # Public: Mark this trace as the "active" one. The active trace is shown in the navigation view 55 | # and its frames are given a marker in an open {EditorView}. 56 | # 57 | activate: -> 58 | former = ACTIVE 59 | ACTIVE = this 60 | if former isnt ACTIVE 61 | emitter.emit 'did-change-active', oldTrace: former, newTrace: ACTIVE 62 | 63 | # Public: Deactivate this trace if it's active. 64 | # 65 | deactivate: -> 66 | if ACTIVE is this 67 | ACTIVE = null 68 | emitter.emit 'did-change-active', oldTrace: this, newTrace: null 69 | 70 | # Public: Return the Frame corresponding to an Editor position, if any, along with its position 71 | # within the trace. 72 | # 73 | # object - "position" should be a Point corresponding to a cursor position, and "path" the full 74 | # path of an Editor. 75 | # 76 | atEditorPosition: (editorPosition) -> 77 | [index, total] = [1, @frames.length] 78 | for frame in @frames 79 | return frame if frame.isOn editorPosition 80 | index += 1 81 | return null 82 | 83 | # Public: Return the Frame that called the given Frame, or undefined if given the top of the stack. 84 | # 85 | # frame - The current Frame to use as a reference point. 86 | # 87 | callerOf: (frame) -> @frames[frame.index + 1] 88 | 89 | # Public: Return the Frame that a given Frame called into, or undefined if given the bottom of the 90 | # stack. 91 | # 92 | # frame - The current Frame to use as a reference point. 93 | # 94 | calledFrom: (frame) -> @frames[frame.index - 1] 95 | 96 | # Public: Subscribe to be notified when the active Stacktrace is set or cleared. 97 | # 98 | # callback - The callback to invoke with the oldTrace and newTrace. 99 | # 100 | # Returns a Disposable to cancel a subscription. 101 | # 102 | @onDidChangeActive: (callback) -> 103 | emitter.on 'did-change-active', callback 104 | 105 | # Public: Parse zero to many Stacktrace instances from a corpus of text. 106 | # 107 | # text - A raw blob of text. 108 | # 109 | @parse: (text) -> 110 | {traceParser} = require('./trace-parser') unless traceParser? 111 | traceParser(text) 112 | 113 | # Internal: Return a registered trace, or null if none match the provided 114 | # URL. 115 | # 116 | @forUrl: (url) -> 117 | REGISTRY[url] 118 | 119 | # Internal: Clear the global trace registry. 120 | # 121 | @clearRegistry: -> 122 | REGISTRY = {} 123 | 124 | # Public: Retrieve the currently activated {Stacktrace}, or null if no trace is active. 125 | # 126 | @getActivated: -> ACTIVE 127 | 128 | # Public: A single stack frame within a {Stacktrace}. 129 | # 130 | class Frame 131 | 132 | constructor: (@rawLine, @rawPath, @lineNumber, @functionName) -> 133 | @index = null 134 | @realPath = @rawPath 135 | 136 | # Public: Return the zero-indexed line number. 137 | # 138 | bufferLineNumber: -> @lineNumber - 1 139 | 140 | # Public: Return the one-based frame index. 141 | # 142 | humanIndex: -> @index + 1 143 | 144 | # Public: Asynchronously collect n lines of context around the specified line number in this 145 | # frame's source file. 146 | # 147 | # n - The number of lines of context to collect on *each* side of the error line. The error 148 | # line will always be `lines[n]` and `lines.length` will be `n * 2 + 1`. 149 | # callback - Invoked with any errors or an Array containing the relevant lines. 150 | # 151 | getContext: (n, callback) -> 152 | # Notice that @lineNumber is one-indexed, not zero-indexed. 153 | range = 154 | fromLine: @lineNumber - n - 1 155 | toLine: @lineNumber + n 156 | trim: false 157 | keepLastEmptyLine: true 158 | chomp fs.createReadStream(@realPath), range, callback 159 | 160 | navigateTo: -> 161 | position = [@lineNumber - 1, 0] 162 | promise = atom.workspace.open @realPath, initialLine: position[0] 163 | promise.then (editor) -> 164 | editor.setCursorBufferPosition position 165 | editor.scrollToBufferPosition position, center: true 166 | 167 | # Public: Return true if the buffer position and path correspond to this Frame's line. 168 | # 169 | isOn: ({position, path}) -> 170 | path is @realPath and position.row is @bufferLineNumber() 171 | 172 | 173 | module.exports = 174 | PREFIX: PREFIX 175 | Stacktrace: Stacktrace 176 | Frame: Frame 177 | -------------------------------------------------------------------------------- /lib/trace-parser.coffee: -------------------------------------------------------------------------------- 1 | {Stacktrace, Frame} = require './stacktrace' 2 | fs = require 'fs' 3 | path = require 'path' 4 | util = require 'util' 5 | 6 | # Internal: Build a Frame instance with a simple DSL. 7 | # 8 | class FrameBuilder 9 | 10 | constructor: (@_rawLine) -> 11 | [@_path, @_lineNumber, @_functionName] = [] 12 | 13 | path: (@_path) -> 14 | 15 | lineNumber: (@_lineNumber) -> 16 | 17 | functionName: (@_functionName) -> 18 | 19 | # Internal: Use the collected information from a FrameBuilder to instantiate a Frame. 20 | # 21 | asFrame = (fb) -> 22 | required = [ 23 | { name: 'rawLine', ok: fb._rawLine? } 24 | { name: 'path', ok: fb._path? } 25 | { name: 'lineNumber', ok: fb._lineNumber? } 26 | { name: 'functionName', ok: fb._functionName? } 27 | ] 28 | missing = (r.name for r in required when not r.ok) 29 | 30 | unless missing.length is 0 31 | e = new Error("Missing required frame attributes: #{missing.join ', '}") 32 | e.missing = missing 33 | e.rawLine = fb.rawLine 34 | throw e 35 | 36 | new Frame(fb._rawLine, fb._path, fb._lineNumber, fb._functionName) 37 | 38 | allTracers = null 39 | 40 | # Internal: Load stacktrace parsers from the parsers/ directory. 41 | # 42 | loadTracers = -> 43 | allTracers = [] 44 | parsersPath = path.resolve(__dirname, 'parsers') 45 | for parserFile in fs.readdirSync(parsersPath) 46 | allTracers.push require(path.join parsersPath, parserFile) 47 | 48 | # Internal: Parse zero or more stacktraces from a sample of text. 49 | # 50 | # text - String output sample that may contain one or more stacktraces from a 51 | # supported language. 52 | # tracers - If provided, use only the provided tracer objects. Otherwise, everything in parsers/ 53 | # will be loaded and used. 54 | # 55 | # Returns: An Array of Stacktrace objects, in the order in which they occurred 56 | # in the original sample. 57 | # 58 | traceParser = (text, tracers = null) -> 59 | unless tracers? 60 | loadTracers() unless allTracers? 61 | tracers = allTracers 62 | 63 | stacks = [] 64 | frames = [] 65 | message = null 66 | activeTracer = null 67 | 68 | finishStacktrace = -> 69 | s = new Stacktrace(frames, message) 70 | stacks.push s 71 | 72 | frames = [] 73 | message = null 74 | activeTracer = null 75 | 76 | for rawLine in text.split(/\r?\n/) 77 | trimmed = rawLine.trim() 78 | 79 | # Mid-stack frame. 80 | if activeTracer? 81 | fb = new FrameBuilder(trimmed) 82 | activeTracer.consume trimmed, fb, 83 | emitMessage: (m) -> message = m 84 | emitFrame: -> frames.push asFrame fb 85 | emitStack: finishStacktrace 86 | 87 | # Outside of a frame. Attempt to recognize the next trace by emitting at least one frame. 88 | unless activeTracer? 89 | for t in tracers 90 | fb = new FrameBuilder(trimmed) 91 | t.recognize trimmed, fb, 92 | emitMessage: (m) -> message = m 93 | emitFrame: -> frames = [asFrame(fb)] 94 | emitStack: finishStacktrace 95 | if message? or frames.length > 0 96 | activeTracer = t 97 | break 98 | 99 | # Finalize the last Stacktrace. 100 | finishStacktrace() if frames.length > 0 101 | 102 | stacks 103 | 104 | module.exports = 105 | traceParser: traceParser 106 | -------------------------------------------------------------------------------- /menus/stacktrace.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/creating-a-package#menus for more details 2 | 3 | 'menu': [ 4 | { 5 | 'label': 'Packages' 6 | 'submenu': [ 7 | 'label': 'Stacktrace' 8 | 'submenu': [ 9 | { 'label': 'Enter', 'command': 'stacktrace:enter' } 10 | ] 11 | ] 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stacktrace", 3 | "main": "./lib/main", 4 | "version": "0.0.2", 5 | "description": "Navigate stacktraces within Atom.", 6 | "activationCommands": { 7 | "atom-workspace": [ 8 | "stacktrace:paste", 9 | "stacktrace:from-selection" 10 | ] 11 | }, 12 | "repository": "https://github.com/smashwilson/stacktrace", 13 | "license": "MIT", 14 | "engines": { 15 | "atom": ">=0.185.0 <2.0.0" 16 | }, 17 | "dependencies": { 18 | "atom-space-pen-views": "^2.0.5", 19 | "emissary": "^1.2.1", 20 | "event-kit": "^0.7.2", 21 | "jssha": "^1.5.0", 22 | "line-chomper": "^0.4.5", 23 | "underscore-plus": "^1.5.1" 24 | }, 25 | "providedServices": { 26 | "stacktrace:trace-handler": { 27 | "description": "Parse all stack traces recognized in sample of text and open a StacktraceView on each.", 28 | "versions": { 29 | "1.0.0": "traceHandlerV1" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /spec/editor-decorator-spec.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | {$} = require 'atom-space-pen-views' 4 | {Stacktrace, Frame} = require '../lib/stacktrace' 5 | {decorate, cleanup} = require '../lib/editor-decorator' 6 | 7 | framePath = (fname) -> path.join __dirname, 'fixtures', fname 8 | 9 | frames = [ 10 | new Frame('raw0', framePath('bottom.rb'), 12, 'botfunc') 11 | new Frame('raw1', framePath('middle.rb'), 42, 'midfunc') 12 | new Frame('raw2', framePath('top.rb'), 37, 'topfunc') 13 | new Frame('raw3', framePath('middle.rb'), 5, 'otherfunc') 14 | ] 15 | trace = new Stacktrace(frames, 'Boom') 16 | 17 | describe 'editorDecorator', -> 18 | [editor, editorView] = [] 19 | 20 | beforeEach -> 21 | workspaceElement = atom.views.getView(atom.workspace) 22 | activationPromise = atom.packages.activatePackage('stacktrace') 23 | 24 | jasmine.attachToDOM(workspaceElement) 25 | 26 | afterEach -> 27 | Stacktrace.getActivated()?.deactivate() 28 | 29 | withEditorOn = (fname, callback) -> 30 | waitsForPromise -> 31 | atom.workspace.open(framePath fname) 32 | 33 | runs -> 34 | editor = atom.workspace.getActiveTextEditor() 35 | editorView = atom.views.getView(editor) 36 | 37 | callback() 38 | 39 | linesMatching = (selector) -> $(editorView.shadowRoot).find selector 40 | 41 | it 'does nothing if there is no active trace', -> 42 | expect(Stacktrace.getActivated()).toBeNull() 43 | 44 | withEditorOn 'bottom.rb', -> 45 | decorate(editor) 46 | expect(linesMatching '.line.line-stackframe').toHaveLength 0 47 | 48 | describe 'with an active trace', -> 49 | 50 | beforeEach -> trace.activate() 51 | 52 | it "does nothing if the file doesn't appear in the active trace", -> 53 | withEditorOn 'context.txt', -> 54 | decorate(editor) 55 | expect(linesMatching '.line.line-stackframe').toHaveLength 0 56 | 57 | it 'decorates stackframe lines in applicable editors', -> 58 | withEditorOn 'bottom.rb', -> 59 | decorate(editor) 60 | decorated = linesMatching '.line.line-stackframe' 61 | expect(decorated).toHaveLength 1 62 | expect(decorated.text()).toEqual(" puts 'this is the stack line'") 63 | 64 | it 'removes prior decorations when deactivated', -> 65 | withEditorOn 'bottom.rb', -> 66 | decorate(editor) 67 | trace.deactivate() 68 | cleanup() 69 | expect($(editorView).find '.line.line-stackframe').toHaveLength 0 70 | -------------------------------------------------------------------------------- /spec/enter-dialog-spec.coffee: -------------------------------------------------------------------------------- 1 | EnterDialog = require '../lib/enter-dialog' 2 | 3 | TRACE = """ 4 | /home/smash/tmp/tracer/dir/file1.rb:3:in `innerfunction': Oh shit (RuntimeError) 5 | from /home/smash/tmp/tracer/otherdir/file2.rb:5:in `outerfunction' 6 | from entry.rb:7:in `toplevel' 7 | from entry.rb:10:in `
' 8 | """ 9 | 10 | describe 'EnterDialog', -> 11 | 12 | it 'calls an acceptTrace function', -> 13 | txt = null 14 | pkg = traceHandlerV1: -> 15 | acceptTrace: (t) -> txt = t 16 | 17 | d = new EnterDialog(pkg) 18 | d.traceEditor.setText(TRACE) 19 | d.traceIt() 20 | 21 | expect(txt).toEqual TRACE 22 | -------------------------------------------------------------------------------- /spec/fixtures/bottom.rb: -------------------------------------------------------------------------------- 1 | # This isn't a real Ruby file. It's a test fixture I can reference in other places. 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | def botfunc 11 | before = true 12 | puts 'this is the stack line' 13 | after = false 14 | end 15 | -------------------------------------------------------------------------------- /spec/fixtures/context.txt: -------------------------------------------------------------------------------- 1 | one 2 | two 3 | three 4 | four 5 | five 6 | six 7 | 8 | eight 9 | nine 10 | ten 11 | -------------------------------------------------------------------------------- /spec/fixtures/middle.rb: -------------------------------------------------------------------------------- 1 | # This isn't a real Ruby file. It's a test fixture I can reference in other places. 2 | # This one has two lines in the "stack trace". 3 | 4 | def otherfunc 5 | puts 'this is the line' 6 | # and more stuff 7 | end 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | def botfunc 41 | # more things 42 | puts 'this is the line' 43 | end 44 | -------------------------------------------------------------------------------- /spec/fixtures/top.rb: -------------------------------------------------------------------------------- 1 | # This isn't a real Ruby file. It's a test fixture I can reference in other places. 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | def topfunc 36 | # things 37 | puts 'this is the line' 38 | end 39 | -------------------------------------------------------------------------------- /spec/fixtures/withtrace.txt: -------------------------------------------------------------------------------- 1 | this is a file with a stacktrace in it 2 | 3 | /home/smash/samples/tracer/otherdir/file2.rb:6:in `block in outerfunction': whoops (RuntimeError) 4 | from /home/smash/samples/tracer/dir/file1.rb:3:in `innerfunction' 5 | from /home/smash/samples/tracer/otherdir/file2.rb:5:in `outerfunction' 6 | from /home/smash/samples/tracer/entry.rb:7:in `toplevel' 7 | from /home/smash/samples/tracer/entry.rb:10:in `
' 8 | 9 | so i can test from-selection stuff 10 | -------------------------------------------------------------------------------- /spec/main-spec.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | {$} = require 'atom-space-pen-views' 4 | Stacktrace = require '../lib/main' 5 | 6 | describe "Main", -> 7 | activationPromise = null 8 | workspaceElement = null 9 | 10 | beforeEach -> 11 | workspaceElement = atom.views.getView(atom.workspace) 12 | activationPromise = atom.packages.activatePackage('stacktrace') 13 | 14 | jasmine.attachToDOM(workspaceElement) 15 | 16 | describe 'when the stacktrace:paste event is triggered', -> 17 | 18 | beforeEach -> 19 | atom.commands.dispatch workspaceElement, 'stacktrace:paste' 20 | waitsForPromise -> activationPromise 21 | 22 | it 'activates the package', -> 23 | expect(atom.packages.isPackageActive 'stacktrace').toBe(true) 24 | 25 | it 'displays the EnterDialog', -> 26 | expect($(workspaceElement).find '.enter-dialog').toExist() 27 | 28 | describe 'when the stacktrace:from-selection event is triggered', -> 29 | 30 | beforeEach -> 31 | p = path.join __dirname, 'fixtures', 'withtrace.txt' 32 | editorPromise = atom.workspace.open(p) 33 | 34 | waitsForPromise -> editorPromise 35 | 36 | runs -> 37 | editorPromise.then (editor) -> 38 | editor.setSelectedBufferRange [[1, 0], [7, 0]] 39 | atom.commands.dispatch workspaceElement, 'stacktrace:from-selection' 40 | 41 | waitsForPromise -> activationPromise 42 | 43 | it 'activates the package', -> 44 | expect(atom.packages.isPackageActive 'stacktrace').toBe(true) 45 | 46 | it 'displays a StacktraceView', -> 47 | expect($(workspaceElement).find '.traceview').toExist() 48 | -------------------------------------------------------------------------------- /spec/navigation-view-spec.coffee: -------------------------------------------------------------------------------- 1 | {$} = require 'atom-space-pen-views' 2 | {Stacktrace, Frame} = require '../lib/stacktrace' 3 | {NavigationView} = require '../lib/navigation-view' 4 | 5 | path = require 'path' 6 | 7 | fixturePath = (p) -> 8 | path.join __dirname, 'fixtures', p 9 | 10 | frames = [ 11 | new Frame('raw0', fixturePath('bottom.rb'), 12, 'botfunc') 12 | new Frame('raw1', fixturePath('middle.rb'), 42, 'midfunc') 13 | new Frame('raw2', fixturePath('top.rb'), 37, 'topfunc') 14 | new Frame('raw3', fixturePath('middle.rb'), 5, 'otherfunc') 15 | ] 16 | trace = new Stacktrace(frames, 'Boom') 17 | 18 | describe 'NavigationView', -> 19 | [view] = [] 20 | 21 | beforeEach -> 22 | workspaceElement = atom.views.getView(atom.workspace) 23 | activationPromise = atom.packages.activatePackage('stacktrace') 24 | 25 | jasmine.attachToDOM(workspaceElement) 26 | 27 | atom.commands.dispatch workspaceElement, 'stacktrace:paste' 28 | 29 | waitsForPromise -> activationPromise 30 | 31 | runs -> 32 | panels = atom.workspace.getBottomPanels() 33 | for panel in panels 34 | view = panel.item if panel.item.hasClass 'navigation' 35 | 36 | afterEach -> 37 | Stacktrace.getActivated()?.deactivate() 38 | Stacktrace.clearRegistry() 39 | 40 | it 'attaches itself to the workspace', -> 41 | expect(view).not.toBeNull() 42 | 43 | describe 'with an active stacktrace', -> 44 | 45 | beforeEach -> 46 | trace.register() 47 | trace.activate() 48 | 49 | it 'should be visible', -> 50 | expect(view.hasClass 'inactive').toBeFalsy() 51 | 52 | it 'shows the active trace name', -> 53 | text = view.find('.message').text() 54 | expect(text).toEqual('Boom') 55 | 56 | it 'navigates back to the trace on a click', -> 57 | waitsForPromise -> view.backToTrace() 58 | 59 | runs -> 60 | expect(atom.workspace.getActivePaneItem().hasClass 'traceview').toBeTruthy() 61 | 62 | it 'deactivates the trace', -> 63 | view.deactivateTrace() 64 | expect(trace.isActive()).toBeFalsy() 65 | 66 | describe 'on an editor corresponding to a single frame', -> 67 | [editor] = [] 68 | 69 | beforeEach -> 70 | waitsForPromise -> trace.frames[2].navigateTo() 71 | 72 | runs -> 73 | editor = atom.workspace.getActiveTextEditor() 74 | 75 | it 'shows the current frame and its index', -> 76 | expect(view.find('.current-frame .function').text()).toBe('topfunc') 77 | expect(view.find('.current-frame .index').text()).toBe('3') 78 | expect(view.find('.current-frame .total').text()).toBe('4') 79 | 80 | it "navigates to the caller's frame", -> 81 | waitsForPromise -> view.navigateToCaller() 82 | 83 | runs -> 84 | expect(view.frame).toBe(trace.frames[3]) 85 | 86 | it 'navigates to the called frame', -> 87 | waitsForPromise -> view.navigateToCalled() 88 | 89 | runs -> 90 | expect(view.frame).toBe(trace.frames[1]) 91 | 92 | it 'navigates back to the last active frame', -> 93 | editor.setCursorBufferPosition [5, 0] 94 | expect(view.find '.current-frame.unfocused').toHaveLength 1 95 | 96 | waitsForPromise -> view.navigateToLastActive() 97 | 98 | runs -> 99 | expect(view.find '.current-frame.unfocused').toHaveLength 0 100 | expect(editor.getCursorBufferPosition().row).toBe 36 101 | 102 | describe 'on an editor with multiple frames', -> 103 | [editor] = [] 104 | 105 | beforeEach -> 106 | waitsForPromise -> trace.frames[1].navigateTo() 107 | 108 | runs -> 109 | editor = atom.workspace.getActiveTextEditor() 110 | 111 | it 'notices if you manually navigate to a different frame', -> 112 | expect(view.find('.current-frame .function').text()).toEqual 'midfunc' 113 | 114 | editor.setCursorBufferPosition [4, 1] 115 | 116 | expect(view.frame).toBe(trace.frames[3]) 117 | expect(view.find('.current-frame .function').text()).toEqual 'otherfunc' 118 | -------------------------------------------------------------------------------- /spec/parsers/coffeescript-trace-parser-spec.coffee: -------------------------------------------------------------------------------- 1 | {traceParser} = require '../../lib/trace-parser' 2 | coffeeTracer = require '../../lib/parsers/coffeescript-trace-parser' 3 | ts = require '../trace-fixtures' 4 | 5 | describe 'coffeeTracer', -> 6 | describe 'recognition', -> 7 | 8 | it 'parses a trace from each CoffeeScript fixture', -> 9 | for f in Object.keys(ts.COFFEESCRIPT) 10 | result = traceParser(ts.COFFEESCRIPT[f], [coffeeTracer]) 11 | expect(result.length > 0).toBe(true) 12 | 13 | it "doesn't parse a trace from any non-CoffeeScript fixture", -> 14 | for k in Object.keys(ts) 15 | if k isnt 'COFFEESCRIPT' 16 | for f in Object.keys(ts[k]) 17 | result = traceParser(ts[k][f], [coffeeTracer]) 18 | expect(result.length).toBe(0) 19 | -------------------------------------------------------------------------------- /spec/parsers/ruby-trace-parser-spec.coffee: -------------------------------------------------------------------------------- 1 | {traceParser} = require '../../lib/trace-parser' 2 | rubyTracer = require '../../lib/parsers/ruby-trace-parser' 3 | ts = require '../trace-fixtures' 4 | 5 | describe 'rubyTracer', -> 6 | describe 'recognition', -> 7 | 8 | it 'parses a trace from each Ruby fixture', -> 9 | for f in Object.keys(ts.RUBY) 10 | result = traceParser(ts.RUBY[f], [rubyTracer]) 11 | expect(result.length > 0).toBe(true) 12 | 13 | it "doesn't parse a trace from any non-Ruby fixture", -> 14 | for k in Object.keys(ts) 15 | if k isnt 'RUBY' 16 | for f in Object.keys(ts[k]) 17 | result = traceParser(ts[k][f], [rubyTracer]) 18 | expect(result.length).toBe(0) 19 | -------------------------------------------------------------------------------- /spec/stacktrace-spec.coffee: -------------------------------------------------------------------------------- 1 | {Point} = require 'atom' 2 | path = require 'path' 3 | 4 | {Stacktrace, Frame} = require '../lib/stacktrace' 5 | {RUBY: {FUNCTION: TRACE}} = require './trace-fixtures' 6 | 7 | describe 'Stacktrace', -> 8 | describe 'with a Ruby trace', -> 9 | [trace, checksum] = [] 10 | 11 | beforeEach -> 12 | [trace] = Stacktrace.parse(TRACE) 13 | checksum = '9528763b5ab8ef052e2400e39d0f32dbe59ffcd06f039adc487f4f956511691f' 14 | 15 | describe 'preparation', -> 16 | it 'trims leading and trailing whitespace from each raw line', -> 17 | lines = (frame.rawLine for frame in trace.frames) 18 | expected = [ 19 | "/home/smash/samples/tracer/otherdir/file2.rb:6:in `block in outerfunction': whoops (RuntimeError)" 20 | "from /home/smash/samples/tracer/dir/file1.rb:3:in `innerfunction'" 21 | "from /home/smash/samples/tracer/otherdir/file2.rb:5:in `outerfunction'" 22 | "from /home/smash/samples/tracer/entry.rb:7:in `toplevel'" 23 | "from /home/smash/samples/tracer/entry.rb:10:in `
'" 24 | ] 25 | expect(lines).toEqual(expected) 26 | 27 | describe 'parsing a Ruby stack trace', -> 28 | it 'parses the error message', -> 29 | expect(trace.message).toBe('whoops (RuntimeError)') 30 | 31 | it 'parses file paths from each frame', -> 32 | filePaths = (frame.realPath for frame in trace.frames) 33 | expected = [ 34 | '/home/smash/samples/tracer/otherdir/file2.rb' 35 | '/home/smash/samples/tracer/dir/file1.rb' 36 | '/home/smash/samples/tracer/otherdir/file2.rb' 37 | '/home/smash/samples/tracer/entry.rb' 38 | '/home/smash/samples/tracer/entry.rb' 39 | ] 40 | expect(filePaths).toEqual(expected) 41 | 42 | it 'parses line numbers from each frame', -> 43 | lineNumbers = (frame.lineNumber for frame in trace.frames) 44 | expected = [6, 3, 5, 7, 10] 45 | expect(lineNumbers).toEqual(lineNumbers) 46 | 47 | it 'parses function names from each frame', -> 48 | functionNames = (frame.functionName for frame in trace.frames) 49 | expected = [ 50 | 'block in outerfunction' 51 | 'innerfunction' 52 | 'outerfunction' 53 | 'toplevel' 54 | '
' 55 | ] 56 | expect(functionNames).toEqual(expected) 57 | 58 | it 'assigns an index to each frame', -> 59 | positions = (frame.index for frame in trace.frames) 60 | expect(positions).toEqual([0..4]) 61 | 62 | describe 'registration', -> 63 | afterEach -> 64 | Stacktrace.clearRegistry() 65 | 66 | it 'computes the SHA256 checksum of the normalized trace', -> 67 | expect(trace.getChecksum()).toBe(checksum) 68 | 69 | it 'generates a unique URL', -> 70 | url = "stacktrace://trace/#{checksum}" 71 | expect(trace.getUrl()).toBe(url) 72 | 73 | it 'can be registered in a global map', -> 74 | trace.register() 75 | expect(Stacktrace.forUrl(trace.getUrl())).toBe(trace) 76 | 77 | it 'can be unregistered cleanly', -> 78 | trace.register() 79 | expect(Stacktrace.forUrl(trace.getUrl())).toBe(trace) 80 | trace.unregister() 81 | expect(Stacktrace.forUrl(trace.getUrl())).toBeUndefined() 82 | 83 | describe 'activation', -> 84 | afterEach -> 85 | activated = Stacktrace.getActivated() 86 | activated.deactivate() if activated? 87 | 88 | it 'can be activated', -> 89 | trace.activate() 90 | expect(Stacktrace.getActivated()).toBe(trace) 91 | 92 | it 'can be deactivated if activated', -> 93 | trace.activate() 94 | trace.deactivate() 95 | expect(Stacktrace.getActivated()).toBeNull() 96 | 97 | it 'can be deactivated even if not activated', -> 98 | trace.deactivate() 99 | expect(Stacktrace.getActivated()).toBeNull() 100 | 101 | it 'broadcasts an onDidChangeActive event', -> 102 | event = null 103 | Stacktrace.onDidChangeActive (e) -> event = e 104 | 105 | trace.activate() 106 | expect(event.oldTrace).toBeNull() 107 | expect(event.newTrace).toBe(trace) 108 | 109 | describe 'walking up and down the stack', -> 110 | 111 | it 'links to the callee of each frame', -> 112 | callees = (trace.calledFrom(f) for f in trace.frames) 113 | expected = [ 114 | undefined 115 | trace.frames[0] 116 | trace.frames[1] 117 | trace.frames[2] 118 | trace.frames[3] 119 | ] 120 | expect(callees).toEqual(expected) 121 | 122 | it 'links to the caller of each frame', -> 123 | callers = (trace.callerOf(f) for f in trace.frames) 124 | expected = [ 125 | trace.frames[1] 126 | trace.frames[2] 127 | trace.frames[3] 128 | trace.frames[4] 129 | undefined 130 | ] 131 | expect(callers).toEqual(expected) 132 | 133 | describe 'active frame location', -> 134 | 135 | it 'locates the frame corresponding to an Editor position', -> 136 | frame = trace.atEditorPosition 137 | position: Point.fromObject([4, 0]) 138 | path: '/home/smash/samples/tracer/otherdir/file2.rb' 139 | 140 | expect(frame).toBe(trace.frames[2]) 141 | expect(frame.humanIndex()).toBe(3) 142 | 143 | it 'returns null if none are found', -> 144 | frame = trace.atEditorPosition 145 | position: Point.fromObject([2, 1]) 146 | path: '/home/smash/samples/tracer/otherdir/file2.rb' 147 | 148 | expect(frame).toBeNull() 149 | 150 | describe 'Frame', -> 151 | [frame, fixturePath] = [] 152 | 153 | beforeEach -> 154 | fixturePath = path.join __dirname, 'fixtures', 'context.txt' 155 | frame = new Frame('five', fixturePath, 5, 'something') 156 | 157 | it 'acquires n lines of context asynchronously', -> 158 | lines = null 159 | 160 | frame.getContext 2, (err, ls) -> 161 | throw err if err? 162 | lines = ls 163 | 164 | waitsFor -> lines? 165 | 166 | runs -> 167 | expect(lines.length).toBe(5) 168 | expect(lines[0]).toEqual('three') 169 | expect(lines[1]).toEqual(' four') 170 | expect(lines[2]).toEqual('five') 171 | expect(lines[3]).toEqual('six') 172 | expect(lines[4]).toEqual('') 173 | 174 | describe 'recognizes itself in an Editor', -> 175 | it 'is on a cursor', -> 176 | expect(frame.isOn(position: Point.fromObject([4, 0]), path: fixturePath)).toBeTruthy() 177 | 178 | it 'is not on a cursor', -> 179 | expect(frame.isOn(position: Point.fromObject([2, 0]), path: fixturePath)).toBeFalsy() 180 | 181 | it 'is on a different file', -> 182 | expect(frame.isOn(position: Point.fromObject([4, 0]), path: 'some/other/path.rb')).toBeFalsy() 183 | -------------------------------------------------------------------------------- /spec/stacktrace-view-spec.coffee: -------------------------------------------------------------------------------- 1 | {StacktraceView, FrameView} = require '../lib/stacktrace-view' 2 | {Stacktrace, Frame} = require '../lib/stacktrace' 3 | 4 | frames = [ 5 | new Frame('raw0', 'bottom.rb', 12, 'botfunc') 6 | new Frame('raw1', 'middle.rb', 42, 'midfunc') 7 | new Frame('raw2', 'top.rb', 37, 'topfunc') 8 | ] 9 | trace = new Stacktrace(frames, 'Boom') 10 | 11 | describe 'StacktraceView', -> 12 | [view] = [] 13 | 14 | beforeEach -> 15 | view = new StacktraceView(trace) 16 | 17 | afterEach -> 18 | Stacktrace.clearRegistry() 19 | 20 | it 'registers an opener', -> 21 | opener = null 22 | mock = 23 | addOpener: (callback) -> opener = callback 24 | StacktraceView.registerIn(mock) 25 | 26 | expect(opener).not.toBeNull() 27 | expect(opener '/some/other/path').toBeUndefined() 28 | 29 | trace.register() 30 | stv = opener(trace.getUrl()) 31 | expect(stv.trace).toBe(trace) 32 | expect(opener(trace.getUrl())).toBe(stv) 33 | 34 | it 'shows the error message', -> 35 | text = view.find('.error-message').text() 36 | expect(text).toEqual('Boom') 37 | 38 | it 'renders a subview for each frame', -> 39 | vs = view.find('.frame') 40 | expect(vs.length).toBe(3) 41 | 42 | it 'changes its class when its trace is activated or deactivated', -> 43 | Stacktrace.getActivated()?.deactivate() 44 | expect(view.hasClass 'activated').toBe(false) 45 | trace.activate() 46 | expect(view.hasClass 'activated').toBe(true) 47 | 48 | describe 'FrameView', -> 49 | [view] = [] 50 | 51 | beforeEach -> 52 | view = new FrameView frames[1], -> 53 | 54 | it 'shows the filename and line number', -> 55 | text = view.find('.source-location').text() 56 | expect(text).toMatch(/middle\.rb/) 57 | expect(text).toMatch(/42/) 58 | 59 | it 'shows the function name', -> 60 | text = view.find('.function-name').text() 61 | expect(text).toEqual('midfunc') 62 | -------------------------------------------------------------------------------- /spec/trace-fixtures.coffee: -------------------------------------------------------------------------------- 1 | # Stack traces shared among several specs. 2 | 3 | module.exports = 4 | RUBY: 5 | FUNCTION: """ 6 | /home/smash/samples/tracer/otherdir/file2.rb:6:in `block in outerfunction': whoops (RuntimeError) 7 | from /home/smash/samples/tracer/dir/file1.rb:3:in `innerfunction' 8 | from /home/smash/samples/tracer/otherdir/file2.rb:5:in `outerfunction' 9 | from /home/smash/samples/tracer/entry.rb:7:in `toplevel' 10 | from /home/smash/samples/tracer/entry.rb:10:in `
' 11 | """ 12 | COFFEESCRIPT: 13 | ERROR: """ 14 | Error: yep 15 | at asFrame (/home/smash/code/stacktrace/lib/trace-parser.coffee:36:13) 16 | at t.recognize.emitFrame (/home/smash/code/stacktrace/lib/trace-parser.coffee:95:35) 17 | at Object.module.exports.recognize (/home/smash/code/stacktrace/lib/parsers/ruby-trace-parser.coffee:19:5) 18 | at traceParser (/home/smash/code/stacktrace/lib/trace-parser.coffee:93:11) 19 | at Function.Stacktrace.parse (/home/smash/code/stacktrace/lib/stacktrace.coffee:43:5) 20 | at [object Object]. (/home/smash/code/stacktrace/spec/stacktrace-spec.coffee:9:28) 21 | """ 22 | -------------------------------------------------------------------------------- /spec/trace-parser-spec.coffee: -------------------------------------------------------------------------------- 1 | {traceParser} = require '../lib/trace-parser' 2 | 3 | describe 'traceParser', -> 4 | describe 'with no traces', -> 5 | it 'returns an empty array', -> 6 | expect(traceParser('')).toEqual([]) 7 | -------------------------------------------------------------------------------- /styles/stacktrace.atom-text-editor.less: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .line.line-stackframe, .line.line-stackframe.cursor-line { 4 | background: @stackframe-background; 5 | } 6 | 7 | // Work around some selector precedence issues. 8 | &.is-focused .line.line-stackframe, &.is-focused .line.line-stackframe.cursor-line { 9 | background: @stackframe-background; 10 | } 11 | 12 | .gutter .line-number-stackframe { 13 | background: @stackframe-gutter-background; 14 | } 15 | -------------------------------------------------------------------------------- /styles/stacktrace.less: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .stacktrace { 4 | &.enter-dialog atom-text-editor { 5 | height: 300px; 6 | max-height: 300px; 7 | } 8 | 9 | .frame .panel-heading { 10 | font-size: 130%; 11 | 12 | .source-location:hover { 13 | cursor: pointer; 14 | text-decoration: underline; 15 | } 16 | } 17 | 18 | .frame { 19 | margin-bottom: 10px; 20 | 21 | .icon-fold, .icon-unfold { 22 | cursor: pointer; 23 | } 24 | 25 | .icon-unfold { display: none; } 26 | 27 | &.minimized { 28 | .icon-unfold { display: inline-block; } 29 | .icon-fold { display: none; } 30 | } 31 | 32 | atom-text-editor { 33 | height: 175px; 34 | max-height: 175px; 35 | cursor: pointer; 36 | } 37 | 38 | atom-text-editor::shadow .scroll-view { 39 | cursor: pointer; 40 | } 41 | } 42 | 43 | &.navigation { 44 | .trace-name { 45 | margin-right: 30px; 46 | 47 | h2 { 48 | margin: 2px 15px 0 5px; 49 | font-weight: bold; 50 | cursor: pointer; 51 | } 52 | span.icon-x { 53 | font-size: 1.5em; 54 | cursor: pointer; 55 | } 56 | } 57 | 58 | .current-frame { 59 | span.icon-code { 60 | margin: 0; 61 | } 62 | 63 | &.unfocused { 64 | color: @text-color-subtle; 65 | 66 | span.function { 67 | padding: 1px 3px; 68 | font-weight: bold; 69 | 70 | &:hover { 71 | cursor: pointer; 72 | text-decoration: underline; 73 | } 74 | } 75 | } 76 | } 77 | 78 | .btn span { 79 | margin-left: 5px; 80 | 81 | &:nth-child(1) { 82 | margin-left: 0; 83 | } 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /styles/variables.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 | 6 | @import "ui-variables"; 7 | @import "syntax-variables"; 8 | 9 | @stackframe-background: mix(@background-color-info, @syntax-background-color, 40%); 10 | @stackframe-gutter-background: mix(@background-color-info, @syntax-gutter-background-color, 40%); 11 | --------------------------------------------------------------------------------