Skip to content

Commit c849b82

Browse files
committed
Merge pull request #2 from smashwilson/trace-view
Continue work on the Stacktrace View.
2 parents 251c8a5 + 12db069 commit c849b82

File tree

8 files changed

+253
-26
lines changed

8 files changed

+253
-26
lines changed

lib/main.coffee

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ module.exports =
1111
StacktraceView.registerIn(atom.workspace)
1212

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

1818
deactivate: ->
1919
atom.off 'stacktrace:accept-trace'

lib/stacktrace-view.coffee

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,53 @@
1-
{View} = require 'atom'
1+
{View, EditorView} = require 'atom'
2+
{Subscriber} = require 'emissary'
3+
24
{Stacktrace, PREFIX} = require './stacktrace'
35

46
class StacktraceView extends View
57

8+
Subscriber.includeInto this
9+
610
@content: (trace) ->
7-
@div class: 'stacktrace tool-panel padded', =>
8-
@div class: 'header panel', =>
9-
@h2 trace.message
11+
tclass = if trace.isActive() then 'activated' else ''
12+
@div class: "stacktrace tool-panel padded #{tclass}", =>
13+
@div class: 'panel padded', =>
14+
@h2 class: 'error-message', trace.message
15+
@p class: 'activate-control', =>
16+
@button class: 'btn btn-primary selected inline-block', click: 'activate', 'Activate'
17+
@span class: 'inline-block', 'to navigate around this stacktrace.'
18+
@p class: 'deactivate-control', =>
19+
@button class: 'btn btn-primary inline-block', click: 'deactivate', 'Deactivate'
20+
@span class: 'inline-block', 'to close the stacktrace navigation panel.'
1021
@div class: 'frames', =>
1122
for frame in trace.frames
12-
@subview 'frame', new FrameView(frame)
23+
@subview 'frame', new FrameView frame, => trace.activate()
1324

1425
initialize: (@trace) ->
26+
@subscribe Stacktrace, 'active-changed', (e) =>
27+
if e.newTrace is @trace
28+
@addClass 'activated'
29+
else
30+
@removeClass 'activated'
31+
32+
beforeRemove: ->
33+
@unsubscribe Stacktrace
1534

1635
# Internal: Return the window title.
36+
#
1737
getTitle: ->
1838
@trace.message
1939

40+
# Public: Activate the current {Stacktrace}.
41+
#
42+
activate: -> @trace.activate()
43+
44+
# Public: Deactivate the current {Stacktrace}.
45+
#
46+
deactivate: -> @trace.deactivate()
47+
2048
# Internal: Register an opener function in the workspace to handle URLs
2149
# generated by a Stacktrace.
50+
#
2251
@registerIn: (workspace) ->
2352
workspace.registerOpener (filePath) ->
2453
trace = Stacktrace.forUrl(filePath)
@@ -27,16 +56,35 @@ class StacktraceView extends View
2756

2857
class FrameView extends View
2958

30-
@content: (frame) ->
59+
@content: (frame, navCallback) ->
3160
@div class: 'frame inset-panel', =>
3261
@div class: 'panel-heading', =>
62+
@span class: 'icon icon-fold inline-block', click: 'minimize'
63+
@span class: 'icon icon-unfold inline-block', click: 'restore'
3364
@span class: 'function-name text-highlight inline-block', frame.functionName
34-
@span class: 'source-location text-info inline-block pull-right', =>
35-
@text "#{frame.path} @ #{frame.lineNumber}"
36-
@div class: 'panel-body padded', =>
37-
@pre output: 'source', 'Source goes here'
65+
@span class: 'source-location text-info inline-block pull-right', click: 'navigate', =>
66+
@text "#{frame.rawPath} @ #{frame.lineNumber}"
67+
@div class: 'panel-body padded', outlet: 'body', click: 'navigate', =>
68+
@subview 'source', new EditorView(mini: true)
69+
70+
initialize: (@frame, @navCallback) ->
71+
@frame.getContext 3, (err, lines) =>
72+
if err?
73+
console.error err
74+
else
75+
@source.getEditor().setText lines.join("\n")
76+
77+
navigate: ->
78+
@navCallback()
79+
@frame.navigateTo()
80+
81+
minimize: ->
82+
@addClass 'minimized'
83+
@body.hide 'fast'
3884

39-
initialize: (@frame) ->
85+
restore: ->
86+
@removeClass 'minimized'
87+
@body.show 'fast'
4088

4189
module.exports =
4290
StacktraceView: StacktraceView

lib/stacktrace.coffee

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1+
fs = require 'fs'
2+
3+
{Emitter} = require 'emissary'
4+
15
jsSHA = require 'jssha'
6+
{chomp} = require 'line-chomper'
27
traceParser = null
38

49
PREFIX = 'stacktrace://trace'
510

611
REGISTRY = {}
12+
ACTIVE = null
713

814
# Internal: A heuristically parsed and interpreted stacktrace.
915
#
1016
class Stacktrace
1117

18+
# Turn the Stacktrace class into an emitter.
19+
Emitter.extend this
20+
1221
constructor: (@frames = [], @message = '') ->
1322

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

35+
# Public: Determine whether or not this Stacktrace is the "active" one. The active Stacktrace is
36+
# shown in a bottom navigation panel and highlighted in opened editors.
37+
#
38+
isActive: -> false
39+
2640
# Internal: Register this trace in a global map by its URL.
2741
#
2842
register: ->
@@ -34,6 +48,22 @@ class Stacktrace
3448
unregister: ->
3549
delete REGISTRY[@getUrl()]
3650

51+
# Public: Mark this trace as the "active" one. The active trace is shown in the navigation view
52+
# and its frames are given a marker in an open {EditorView}.
53+
#
54+
activate: ->
55+
former = ACTIVE
56+
ACTIVE = this
57+
if former isnt ACTIVE
58+
Stacktrace.emit 'active-changed', oldTrace: former, newTrace: ACTIVE
59+
60+
# Public: Deactivate this trace if it's active.
61+
#
62+
deactivate: ->
63+
if ACTIVE is this
64+
ACTIVE = null
65+
Stacktrace.emit 'active-changed', oldTrace: this, newTrace: null
66+
3767
# Public: Parse zero to many Stacktrace instances from a corpus of text.
3868
#
3969
# text - A raw blob of text.
@@ -51,11 +81,43 @@ class Stacktrace
5181
@clearRegistry: ->
5282
REGISTRY = {}
5383

54-
# Internal: A single stack frame within a {Stacktrace}.
84+
# Public: Retrieve the currently activated {Stacktrace}, or null if no trace is active.
85+
#
86+
@getActivated: -> ACTIVE
87+
88+
# Public: A single stack frame within a {Stacktrace}.
5589
#
5690
class Frame
5791

58-
constructor: (@rawLine, @path, @lineNumber, @functionName) ->
92+
constructor: (@rawLine, @rawPath, @lineNumber, @functionName) ->
93+
@realPath = @rawPath
94+
95+
# Public: Asynchronously collect n lines of context around the specified line number in this
96+
# frame's source file.
97+
#
98+
# n - The number of lines of context to collect on *each* side of the error line. The error
99+
# line will always be `lines[n]` and `lines.length` will be `n * 2 + 1`.
100+
# callback - Invoked with any errors or an Array containing the relevant lines.
101+
#
102+
getContext: (n, callback) ->
103+
# Notice that @lineNumber is one-indexed, not zero-indexed.
104+
range =
105+
fromLine: @lineNumber - n - 1
106+
toLine: @lineNumber + n
107+
trim: false
108+
keepLastEmptyLine: true
109+
chomp fs.createReadStream(@realPath), range, callback
110+
111+
navigateTo: ->
112+
position = [@lineNumber - 1, 0]
113+
promise = atom.workspace.open @realPath, initialLine: position[0]
114+
promise.then (editor) ->
115+
editor.setCursorBufferPosition position
116+
for ev in atom.workspaceView.getEditorViews()
117+
editorView = ev if ev.getEditor() is editor
118+
if editorView?
119+
editorView.scrollToBufferPosition position, center: true
120+
59121

60122
module.exports =
61123
PREFIX: PREFIX

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"atom": ">0.50.0"
1313
},
1414
"dependencies": {
15-
"jssha": "^1.5.0"
15+
"emissary": "^1.2.1",
16+
"jssha": "^1.5.0",
17+
"line-chomper": "git+https://github.com/smashwilson/line-chomper.git#optional-trim"
1618
}
1719
}

spec/fixtures/context.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
one
2+
two
3+
three
4+
four
5+
five
6+
six
7+
8+
eight
9+
nine
10+
ten

spec/stacktrace-spec.coffee

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
{Stacktrace} = require '../lib/stacktrace'
1+
path = require 'path'
2+
3+
{Stacktrace, Frame} = require '../lib/stacktrace'
24
{RUBY: {FUNCTION: TRACE}} = require './trace-fixtures'
35

46
describe 'Stacktrace', ->
@@ -26,7 +28,7 @@ describe 'Stacktrace', ->
2628
expect(trace.message).toBe('whoops (RuntimeError)')
2729

2830
it 'parses file paths from each frame', ->
29-
filePaths = (frame.path for frame in trace.frames)
31+
filePaths = (frame.realPath for frame in trace.frames)
3032
expected = [
3133
'/home/smash/samples/tracer/otherdir/file2.rb'
3234
'/home/smash/samples/tracer/dir/file1.rb'
@@ -72,3 +74,54 @@ describe 'Stacktrace', ->
7274
expect(Stacktrace.forUrl(trace.getUrl())).toBe(trace)
7375
trace.unregister()
7476
expect(Stacktrace.forUrl(trace.getUrl())).toBeUndefined()
77+
78+
describe 'activation', ->
79+
afterEach ->
80+
activated = Stacktrace.getActivated()
81+
activated.deactivate() if activated?
82+
Stacktrace.off 'active-changed'
83+
84+
it 'can be activated', ->
85+
trace.activate()
86+
expect(Stacktrace.getActivated()).toBe(trace)
87+
88+
it 'can be deactivated if activated', ->
89+
trace.activate()
90+
trace.deactivate()
91+
expect(Stacktrace.getActivated()).toBeNull()
92+
93+
it 'can be deactivated even if not activated', ->
94+
trace.deactivate()
95+
expect(Stacktrace.getActivated()).toBeNull()
96+
97+
it 'broadcasts a "active-changed" event', ->
98+
event = null
99+
Stacktrace.on 'active-changed', (e) -> event = e
100+
101+
trace.activate()
102+
expect(event.oldTrace).toBeNull()
103+
expect(event.newTrace).toBe(trace)
104+
105+
describe 'Frame', ->
106+
[frame] = []
107+
108+
beforeEach ->
109+
fixturePath = path.join __dirname, 'fixtures', 'context.txt'
110+
frame = new Frame('five', fixturePath, 5, 'something')
111+
112+
it 'acquires n lines of context asynchronously', ->
113+
lines = null
114+
115+
frame.getContext 2, (err, ls) ->
116+
throw err if err?
117+
lines = ls
118+
119+
waitsFor -> lines?
120+
121+
runs ->
122+
expect(lines.length).toBe(5)
123+
expect(lines[0]).toEqual('three')
124+
expect(lines[1]).toEqual(' four')
125+
expect(lines[2]).toEqual('five')
126+
expect(lines[3]).toEqual('six')
127+
expect(lines[4]).toEqual('')

spec/stacktrace-view-spec.coffee

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{Stacktrace, Frame} = require '../lib/stacktrace'
33

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

32-
it 'shows the error message'
33-
it 'renders a subview for each frame'
32+
it 'shows the error message', ->
33+
text = view.find('.error-message').text()
34+
expect(text).toEqual('Boom')
35+
36+
it 'renders a subview for each frame', ->
37+
vs = view.find('.frame')
38+
expect(vs.length).toBe(3)
39+
40+
it 'changes its class when its trace is activated or deactivated', ->
41+
Stacktrace.getActivated()?.deactivate()
42+
expect(view.hasClass 'activated').toBe(false)
43+
trace.activate()
44+
expect(view.hasClass 'activated').toBe(true)
3445

3546
describe 'FrameView', ->
3647
[view] = []
3748

3849
beforeEach ->
39-
view = new FrameView(frames[1])
50+
view = new FrameView frames[1], ->
51+
52+
it 'shows the filename and line number', ->
53+
text = view.find('.source-location').text()
54+
expect(text).toMatch(/middle\.rb/)
55+
expect(text).toMatch(/42/)
4056

41-
it 'shows the filename'
42-
it 'shows the line number'
43-
it 'shows the function name'
57+
it 'shows the function name', ->
58+
text = view.find('.function-name').text()
59+
expect(text).toEqual('midfunc')

stylesheets/stacktrace.less

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,40 @@
88
&.enter-dialog .editor {
99
height: 300px;
1010
}
11+
12+
.frame .panel-heading {
13+
font-size: 130%;
14+
15+
.source-location:hover {
16+
cursor: pointer;
17+
text-decoration: underline;
18+
}
19+
}
20+
21+
.frame {
22+
margin-bottom: 10px;
23+
24+
.icon-fold, .icon-unfold {
25+
cursor: pointer;
26+
}
27+
28+
.icon-unfold { display: none; }
29+
30+
&.minimized {
31+
.icon-unfold { display: inline-block; }
32+
.icon-fold { display: none; }
33+
}
34+
35+
.editor {
36+
cursor: pointer;
37+
}
38+
}
39+
40+
.deactivate-control { display: none; }
41+
42+
&.activated {
43+
.deactivate-control { display: block; }
44+
.activate-control { display: none; }
45+
}
46+
1147
}

0 commit comments

Comments
 (0)