Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a4f22c3
Stacktrace.parse() returns multiple results now.
smashwilson Jul 26, 2014
45b8506
Spec for rendering the error message.
smashwilson Jul 26, 2014
e250a0b
Rendering a subview for each Frame.
smashwilson Jul 26, 2014
3e1c1f7
Specs for FrameView.
smashwilson Jul 26, 2014
2387c86
Stub out controls for activation and deactivation.
smashwilson Jul 26, 2014
f62810c
Toggle each FrameView.
smashwilson Jul 26, 2014
2958639
Oops, broke that test when I renamed path.
smashwilson Jul 27, 2014
8aadb50
Test case for fetching line context.
smashwilson Jul 27, 2014
d6d4041
Use line-chomper to read files by lines.
smashwilson Jul 27, 2014
701598c
Implement Frame.getContext().
smashwilson Jul 27, 2014
b306d95
Use my line-chomper fork.
smashwilson Jul 27, 2014
840d863
Use a mini-EditorView to show code snippets.
smashwilson Jul 27, 2014
70d5e3b
Hey look an off-by-one error
smashwilson Jul 27, 2014
3bf2b3c
Navigate to a specific Frame.
smashwilson Jul 27, 2014
267140a
Navigate on a click anywhere in the frame.
smashwilson Jul 27, 2014
c7f9779
Mark the currently "active" Stacktrace.
smashwilson Jul 27, 2014
6f338d5
Wire up the activate and deactivate events.
smashwilson Jul 27, 2014
b7de299
Depend on Emissary for event stuff.
smashwilson Jul 27, 2014
28b8eae
Emit events when the active Stacktrace is changed.
smashwilson Jul 27, 2014
1850e30
Toggle the StacktraceView class on activation.
smashwilson Jul 27, 2014
1190bce
Activate the Stacktrace on navigation.
smashwilson Jul 27, 2014
12db069
Unsubscribe on view close.
smashwilson Jul 27, 2014
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/main.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ module.exports =
StacktraceView.registerIn(atom.workspace)

atom.on 'stacktrace:accept-trace', ({trace}) =>
t = Stacktrace.parse(trace)
t.register()
atom.workspace.open t.getUrl()
for trace in Stacktrace.parse(trace)
trace.register()
atom.workspace.open trace.getUrl()

deactivate: ->
atom.off 'stacktrace:accept-trace'
Expand Down
70 changes: 59 additions & 11 deletions lib/stacktrace-view.coffee
Original file line number Diff line number Diff line change
@@ -1,24 +1,53 @@
{View} = require 'atom'
{View, EditorView} = require 'atom'
{Subscriber} = require 'emissary'

{Stacktrace, PREFIX} = require './stacktrace'

class StacktraceView extends View

Subscriber.includeInto this

@content: (trace) ->
@div class: 'stacktrace tool-panel padded', =>
@div class: 'header panel', =>
@h2 trace.message
tclass = if trace.isActive() then 'activated' else ''
@div class: "stacktrace tool-panel padded #{tclass}", =>
@div class: 'panel padded', =>
@h2 class: 'error-message', trace.message
@p class: 'activate-control', =>
@button class: 'btn btn-primary selected inline-block', click: 'activate', 'Activate'
@span class: 'inline-block', 'to navigate around this stacktrace.'
@p class: 'deactivate-control', =>
@button class: 'btn btn-primary inline-block', click: 'deactivate', 'Deactivate'
@span class: 'inline-block', 'to close the stacktrace navigation panel.'
@div class: 'frames', =>
for frame in trace.frames
@subview 'frame', new FrameView(frame)
@subview 'frame', new FrameView frame, => trace.activate()

initialize: (@trace) ->
@subscribe Stacktrace, 'active-changed', (e) =>
if e.newTrace is @trace
@addClass 'activated'
else
@removeClass 'activated'

beforeRemove: ->
@unsubscribe Stacktrace

# Internal: Return the window title.
#
getTitle: ->
@trace.message

# Public: Activate the current {Stacktrace}.
#
activate: -> @trace.activate()

# Public: Deactivate the current {Stacktrace}.
#
deactivate: -> @trace.deactivate()

# Internal: Register an opener function in the workspace to handle URLs
# generated by a Stacktrace.
#
@registerIn: (workspace) ->
workspace.registerOpener (filePath) ->
trace = Stacktrace.forUrl(filePath)
Expand All @@ -27,16 +56,35 @@ class StacktraceView extends View

class FrameView extends View

@content: (frame) ->
@content: (frame, navCallback) ->
@div class: 'frame inset-panel', =>
@div class: 'panel-heading', =>
@span class: 'icon icon-fold inline-block', click: 'minimize'
@span class: 'icon icon-unfold inline-block', click: 'restore'
@span class: 'function-name text-highlight inline-block', frame.functionName
@span class: 'source-location text-info inline-block pull-right', =>
@text "#{frame.path} @ #{frame.lineNumber}"
@div class: 'panel-body padded', =>
@pre output: 'source', 'Source goes here'
@span class: 'source-location text-info inline-block pull-right', click: 'navigate', =>
@text "#{frame.rawPath} @ #{frame.lineNumber}"
@div class: 'panel-body padded', outlet: 'body', click: 'navigate', =>
@subview 'source', new EditorView(mini: true)

initialize: (@frame, @navCallback) ->
@frame.getContext 3, (err, lines) =>
if err?
console.error err
else
@source.getEditor().setText lines.join("\n")

navigate: ->
@navCallback()
@frame.navigateTo()

minimize: ->
@addClass 'minimized'
@body.hide 'fast'

initialize: (@frame) ->
restore: ->
@removeClass 'minimized'
@body.show 'fast'

module.exports =
StacktraceView: StacktraceView
Expand Down
66 changes: 64 additions & 2 deletions lib/stacktrace.coffee
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
fs = require 'fs'

{Emitter} = require 'emissary'

jsSHA = require 'jssha'
{chomp} = require 'line-chomper'
traceParser = null

PREFIX = 'stacktrace://trace'

REGISTRY = {}
ACTIVE = null

# Internal: A heuristically parsed and interpreted stacktrace.
#
class Stacktrace

# Turn the Stacktrace class into an emitter.
Emitter.extend this

constructor: (@frames = [], @message = '') ->

# Internal: Compute the SHA256 checksum of the normalized stacktrace.
Expand All @@ -23,6 +32,11 @@ class Stacktrace
#
getUrl: -> @url ?= "#{PREFIX}/#{@getChecksum()}"

# Public: Determine whether or not this Stacktrace is the "active" one. The active Stacktrace is
# shown in a bottom navigation panel and highlighted in opened editors.
#
isActive: -> false

# Internal: Register this trace in a global map by its URL.
#
register: ->
Expand All @@ -34,6 +48,22 @@ class Stacktrace
unregister: ->
delete REGISTRY[@getUrl()]

# Public: Mark this trace as the "active" one. The active trace is shown in the navigation view
# and its frames are given a marker in an open {EditorView}.
#
activate: ->
former = ACTIVE
ACTIVE = this
if former isnt ACTIVE
Stacktrace.emit 'active-changed', oldTrace: former, newTrace: ACTIVE

# Public: Deactivate this trace if it's active.
#
deactivate: ->
if ACTIVE is this
ACTIVE = null
Stacktrace.emit 'active-changed', oldTrace: this, newTrace: null

# Public: Parse zero to many Stacktrace instances from a corpus of text.
#
# text - A raw blob of text.
Expand All @@ -51,11 +81,43 @@ class Stacktrace
@clearRegistry: ->
REGISTRY = {}

# Internal: A single stack frame within a {Stacktrace}.
# Public: Retrieve the currently activated {Stacktrace}, or null if no trace is active.
#
@getActivated: -> ACTIVE

# Public: A single stack frame within a {Stacktrace}.
#
class Frame

constructor: (@rawLine, @path, @lineNumber, @functionName) ->
constructor: (@rawLine, @rawPath, @lineNumber, @functionName) ->
@realPath = @rawPath

# Public: Asynchronously collect n lines of context around the specified line number in this
# frame's source file.
#
# n - The number of lines of context to collect on *each* side of the error line. The error
# line will always be `lines[n]` and `lines.length` will be `n * 2 + 1`.
# callback - Invoked with any errors or an Array containing the relevant lines.
#
getContext: (n, callback) ->
# Notice that @lineNumber is one-indexed, not zero-indexed.
range =
fromLine: @lineNumber - n - 1
toLine: @lineNumber + n
trim: false
keepLastEmptyLine: true
chomp fs.createReadStream(@realPath), range, callback

navigateTo: ->
position = [@lineNumber - 1, 0]
promise = atom.workspace.open @realPath, initialLine: position[0]
promise.then (editor) ->
editor.setCursorBufferPosition position
for ev in atom.workspaceView.getEditorViews()
editorView = ev if ev.getEditor() is editor
if editorView?
editorView.scrollToBufferPosition position, center: true


module.exports =
PREFIX: PREFIX
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"atom": ">0.50.0"
},
"dependencies": {
"jssha": "^1.5.0"
"emissary": "^1.2.1",
"jssha": "^1.5.0",
"line-chomper": "git+https://github.com/smashwilson/line-chomper.git#optional-trim"
}
}
10 changes: 10 additions & 0 deletions spec/fixtures/context.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
one
two
three
four
five
six

eight
nine
ten
57 changes: 55 additions & 2 deletions spec/stacktrace-spec.coffee
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{Stacktrace} = require '../lib/stacktrace'
path = require 'path'

{Stacktrace, Frame} = require '../lib/stacktrace'
{RUBY: {FUNCTION: TRACE}} = require './trace-fixtures'

describe 'Stacktrace', ->
Expand Down Expand Up @@ -26,7 +28,7 @@ describe 'Stacktrace', ->
expect(trace.message).toBe('whoops (RuntimeError)')

it 'parses file paths from each frame', ->
filePaths = (frame.path for frame in trace.frames)
filePaths = (frame.realPath for frame in trace.frames)
expected = [
'/home/smash/samples/tracer/otherdir/file2.rb'
'/home/smash/samples/tracer/dir/file1.rb'
Expand Down Expand Up @@ -72,3 +74,54 @@ describe 'Stacktrace', ->
expect(Stacktrace.forUrl(trace.getUrl())).toBe(trace)
trace.unregister()
expect(Stacktrace.forUrl(trace.getUrl())).toBeUndefined()

describe 'activation', ->
afterEach ->
activated = Stacktrace.getActivated()
activated.deactivate() if activated?
Stacktrace.off 'active-changed'

it 'can be activated', ->
trace.activate()
expect(Stacktrace.getActivated()).toBe(trace)

it 'can be deactivated if activated', ->
trace.activate()
trace.deactivate()
expect(Stacktrace.getActivated()).toBeNull()

it 'can be deactivated even if not activated', ->
trace.deactivate()
expect(Stacktrace.getActivated()).toBeNull()

it 'broadcasts a "active-changed" event', ->
event = null
Stacktrace.on 'active-changed', (e) -> event = e

trace.activate()
expect(event.oldTrace).toBeNull()
expect(event.newTrace).toBe(trace)

describe 'Frame', ->
[frame] = []

beforeEach ->
fixturePath = path.join __dirname, 'fixtures', 'context.txt'
frame = new Frame('five', fixturePath, 5, 'something')

it 'acquires n lines of context asynchronously', ->
lines = null

frame.getContext 2, (err, ls) ->
throw err if err?
lines = ls

waitsFor -> lines?

runs ->
expect(lines.length).toBe(5)
expect(lines[0]).toEqual('three')
expect(lines[1]).toEqual(' four')
expect(lines[2]).toEqual('five')
expect(lines[3]).toEqual('six')
expect(lines[4]).toEqual('')
30 changes: 23 additions & 7 deletions spec/stacktrace-view-spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{Stacktrace, Frame} = require '../lib/stacktrace'

frames = [
new Frame('raw0', 'bottom.rb', 12, 'botfunc', 'Boom')
new Frame('raw0', 'bottom.rb', 12, 'botfunc')
new Frame('raw1', 'middle.rb', 42, 'midfunc')
new Frame('raw2', 'top.rb', 37, 'topfunc')
]
Expand All @@ -29,15 +29,31 @@ describe 'StacktraceView', ->
trace.register()
expect(opener(trace.getUrl()).trace).toBe(trace)

it 'shows the error message'
it 'renders a subview for each frame'
it 'shows the error message', ->
text = view.find('.error-message').text()
expect(text).toEqual('Boom')

it 'renders a subview for each frame', ->
vs = view.find('.frame')
expect(vs.length).toBe(3)

it 'changes its class when its trace is activated or deactivated', ->
Stacktrace.getActivated()?.deactivate()
expect(view.hasClass 'activated').toBe(false)
trace.activate()
expect(view.hasClass 'activated').toBe(true)

describe 'FrameView', ->
[view] = []

beforeEach ->
view = new FrameView(frames[1])
view = new FrameView frames[1], ->

it 'shows the filename and line number', ->
text = view.find('.source-location').text()
expect(text).toMatch(/middle\.rb/)
expect(text).toMatch(/42/)

it 'shows the filename'
it 'shows the line number'
it 'shows the function name'
it 'shows the function name', ->
text = view.find('.function-name').text()
expect(text).toEqual('midfunc')
36 changes: 36 additions & 0 deletions stylesheets/stacktrace.less
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,40 @@
&.enter-dialog .editor {
height: 300px;
}

.frame .panel-heading {
font-size: 130%;

.source-location:hover {
cursor: pointer;
text-decoration: underline;
}
}

.frame {
margin-bottom: 10px;

.icon-fold, .icon-unfold {
cursor: pointer;
}

.icon-unfold { display: none; }

&.minimized {
.icon-unfold { display: inline-block; }
.icon-fold { display: none; }
}

.editor {
cursor: pointer;
}
}

.deactivate-control { display: none; }

&.activated {
.deactivate-control { display: block; }
.activate-control { display: none; }
}

}