Skip to content

Commit 22687f0

Browse files
committed
Merge pull request #7 from smashwilson/navigation-panel
Navigate the frames of a stacktrace.
2 parents 71bb063 + e3c8b3e commit 22687f0

File tree

8 files changed

+383
-27
lines changed

8 files changed

+383
-27
lines changed

lib/main.coffee

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
EnterDialog = require './enter-dialog'
22
{Stacktrace} = require './stacktrace'
33
{StacktraceView} = require './stacktrace-view'
4+
{NavigationView} = require './navigation-view'
45
editorDecorator = require './editor-decorator'
56

67
module.exports =
@@ -15,18 +16,21 @@ module.exports =
1516
atom.emit 'stacktrace:accept-trace', trace: text
1617

1718
atom.workspace.eachEditor editorDecorator
18-
Stacktrace.on 'active-changed', ->
19+
@activeChanged = Stacktrace.on 'active-changed', ->
1920
editorDecorator(e) for e in atom.workspace.getEditors()
2021

22+
@navigationView = new NavigationView
23+
atom.workspaceView.appendToBottom @navigationView
24+
2125
StacktraceView.registerIn(atom.workspace)
2226

23-
atom.on 'stacktrace:accept-trace', ({trace}) =>
27+
@acceptTrace = atom.on 'stacktrace:accept-trace', ({trace}) =>
2428
for trace in Stacktrace.parse(trace)
2529
trace.register()
2630
atom.workspace.open trace.getUrl()
2731

2832
deactivate: ->
29-
Stacktrace.off 'active-changed'
30-
atom.off 'stacktrace:accept-trace'
33+
@navigationView.remove()
3134

32-
serialize: ->
35+
@activeChanged.off()
36+
@acceptTrace.off()

lib/navigation-view.coffee

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
{View} = require 'atom'
2+
{Subscriber} = require 'emissary'
3+
{Stacktrace} = require './stacktrace'
4+
5+
class NavigationView extends View
6+
7+
Subscriber.includeInto this
8+
9+
@content: ->
10+
activatedClass = if Stacktrace.getActivated()? then '' else 'inactive'
11+
12+
@div class: "tool-panel panel-bottom padded stacktrace navigation #{activatedClass}", =>
13+
@div class: 'inline-block trace-name', =>
14+
@h2 class: 'inline-block text-highlight message', outlet: 'message', click: 'backToTrace'
15+
@span class: 'inline-block icon icon-x', click: 'deactivateTrace'
16+
@div class: 'inline-block current-frame unfocused', outlet: 'frameContainer', =>
17+
@span class: 'inline-block icon icon-code'
18+
@span class: 'inline-block function', outlet: 'frameFunction', click: 'navigateToLastActive'
19+
@span class: 'inline-block index', outlet: 'frameIndex'
20+
@span class: 'inline-block divider', '/'
21+
@span class: 'inline-block total', outlet: 'frameTotal'
22+
@div class: 'pull-right controls', =>
23+
@button class: 'inline-block btn', click: 'navigateToCaller', =>
24+
@span class: 'icon icon-arrow-up'
25+
@text 'Caller'
26+
@button class: 'inline-block btn', click: 'navigateToCalled', =>
27+
@span class: 'icon icon-arrow-down'
28+
@text 'Follow Call'
29+
30+
initialize: ->
31+
@subscribe Stacktrace, 'active-changed', (e) =>
32+
if e.newTrace? then @useTrace(e.newTrace) else @noTrace()
33+
34+
# Subscribe to opening editors. Set the current frame when a cursor is moved over a frame's
35+
# line.
36+
atom.workspace.eachEditor (e) =>
37+
@subscribe e, 'cursors-moved', =>
38+
if @trace?
39+
pos =
40+
position: e.getCursorBufferPosition()
41+
path: e.getPath()
42+
43+
# Allow the already-set @frame a chance to see if it still applies.
44+
# This lets the caller and called navigation work properly, even if multiple frames are
45+
# on the same line.
46+
if @frame? and @frame.isOn(pos)
47+
@useFrame(@frame)
48+
else
49+
# Otherwise, scan the trace for a matching frame.
50+
frame = @trace.atEditorPosition(pos)
51+
if frame? then @useFrame(frame) else @unfocusFrame()
52+
53+
if Stacktrace.getActivated? then @hide()
54+
55+
beforeRemove: ->
56+
@unsubscribe Stacktrace
57+
58+
useTrace: (@trace) ->
59+
@removeClass 'inactive'
60+
@message.text(trace.message)
61+
@noFrame()
62+
@show()
63+
64+
noTrace: ->
65+
@addClass 'inactive'
66+
@message.text('')
67+
@noFrame()
68+
@hide()
69+
70+
useFrame: (@frame) ->
71+
@frameContainer.removeClass 'unfocused'
72+
@frameFunction.text @frame.functionName
73+
@frameFunction.addClass 'highlight-info'
74+
@frameIndex.text @frame.humanIndex().toString()
75+
@frameTotal.text @trace.frames.length.toString()
76+
77+
unfocusFrame: ->
78+
@frameContainer.addClass 'unfocused'
79+
@frameFunction.removeClass 'highlight-info'
80+
81+
noFrame: ->
82+
@unfocusFrame()
83+
@frameFunction.text ''
84+
@frameIndex.text ''
85+
@frameTotal.text ''
86+
87+
deactivateTrace: ->
88+
Stacktrace.getActivated().deactivate()
89+
90+
backToTrace: ->
91+
url = Stacktrace.getActivated()?.getUrl()
92+
atom.workspace.open(url) if url
93+
94+
navigateToCaller: ->
95+
return unless @trace? and @frame?
96+
97+
f = @trace.callerOf(@frame)
98+
if f?
99+
@frame = f
100+
@frame.navigateTo()
101+
102+
navigateToCalled: ->
103+
return unless @trace? and @frame?
104+
105+
f = @trace.calledFrom(@frame)
106+
if f?
107+
@frame = f
108+
@frame.navigateTo()
109+
110+
navigateToLastActive: ->
111+
return unless @frame?
112+
@frame.navigateTo()
113+
114+
module.exports = NavigationView: NavigationView

lib/stacktrace-view.coffee

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,12 @@ class StacktraceView extends View
1212
@div class: "stacktrace traceview tool-panel padded #{tclass}", =>
1313
@div class: 'panel padded', =>
1414
@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.'
2115
@div class: 'frames', =>
2216
for frame in trace.frames
2317
@subview 'frame', new FrameView frame, => trace.activate()
2418

2519
initialize: (@trace) ->
20+
@uri = @trace.getUrl()
2621
@subscribe Stacktrace, 'active-changed', (e) =>
2722
if e.newTrace is @trace
2823
@addClass 'activated'
@@ -37,14 +32,6 @@ class StacktraceView extends View
3732
getTitle: ->
3833
@trace.message
3934

40-
# Public: Activate the current {Stacktrace}.
41-
#
42-
activate: -> @trace.activate()
43-
44-
# Public: Deactivate the current {Stacktrace}.
45-
#
46-
deactivate: -> @trace.deactivate()
47-
4835
# Internal: Register an opener function in the workspace to handle URLs
4936
# generated by a Stacktrace.
5037
#

lib/stacktrace.coffee

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ class Stacktrace
1919
Emitter.extend this
2020

2121
constructor: (@frames = [], @message = '') ->
22+
i = 0
23+
for f in @frames
24+
f.index = i
25+
i += 1
2226

2327
# Internal: Compute the SHA256 checksum of the normalized stacktrace.
2428
#
@@ -64,6 +68,32 @@ class Stacktrace
6468
ACTIVE = null
6569
Stacktrace.emit 'active-changed', oldTrace: this, newTrace: null
6670

71+
# Public: Return the Frame corresponding to an Editor position, if any, along with its position
72+
# within the trace.
73+
#
74+
# object - "position" should be a Point corresponding to a cursor position, and "path" the full
75+
# path of an Editor.
76+
#
77+
atEditorPosition: (editorPosition) ->
78+
[index, total] = [1, @frames.length]
79+
for frame in @frames
80+
return frame if frame.isOn editorPosition
81+
index += 1
82+
return null
83+
84+
# Public: Return the Frame that called the given Frame, or undefined if given the top of the stack.
85+
#
86+
# frame - The current Frame to use as a reference point.
87+
#
88+
callerOf: (frame) -> @frames[frame.index + 1]
89+
90+
# Public: Return the Frame that a given Frame called into, or undefined if given the bottom of the
91+
# stack.
92+
#
93+
# frame - The current Frame to use as a reference point.
94+
#
95+
calledFrom: (frame) -> @frames[frame.index - 1]
96+
6797
# Public: Parse zero to many Stacktrace instances from a corpus of text.
6898
#
6999
# text - A raw blob of text.
@@ -74,10 +104,12 @@ class Stacktrace
74104

75105
# Internal: Return a registered trace, or null if none match the provided
76106
# URL.
107+
#
77108
@forUrl: (url) ->
78109
REGISTRY[url]
79110

80111
# Internal: Clear the global trace registry.
112+
#
81113
@clearRegistry: ->
82114
REGISTRY = {}
83115

@@ -90,12 +122,17 @@ class Stacktrace
90122
class Frame
91123

92124
constructor: (@rawLine, @rawPath, @lineNumber, @functionName) ->
125+
@index = null
93126
@realPath = @rawPath
94127

95128
# Public: Return the zero-indexed line number.
96129
#
97130
bufferLineNumber: -> @lineNumber - 1
98131

132+
# Public: Return the one-based frame index.
133+
#
134+
humanIndex: -> @index + 1
135+
99136
# Public: Asynchronously collect n lines of context around the specified line number in this
100137
# frame's source file.
101138
#
@@ -122,6 +159,11 @@ class Frame
122159
if editorView?
123160
editorView.scrollToBufferPosition position, center: true
124161

162+
# Public: Return true if the buffer position and path correspond to this Frame's line.
163+
#
164+
isOn: ({position, path}) ->
165+
path is @realPath and position.row is @bufferLineNumber()
166+
125167

126168
module.exports =
127169
PREFIX: PREFIX

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@
1515
"dependencies": {
1616
"emissary": "^1.2.1",
1717
"jssha": "^1.5.0",
18-
"line-chomper": "git+https://github.com/smashwilson/line-chomper.git#optional-trim"
18+
"line-chomper": "^0.4.5"
1919
}
2020
}

spec/navigation-view-spec.coffee

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
{WorkspaceView} = require 'atom'
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+
atom.workspaceView = new WorkspaceView
23+
atom.workspaceView.attachToDom()
24+
activationPromise = atom.packages.activatePackage('stacktrace')
25+
26+
atom.workspaceView.trigger 'stacktrace:paste'
27+
28+
waitsForPromise -> activationPromise
29+
30+
runs ->
31+
view = atom.workspaceView.find('.stacktrace.navigation').view()
32+
33+
afterEach ->
34+
Stacktrace.getActivated()?.deactivate()
35+
Stacktrace.clearRegistry()
36+
37+
it 'attaches itself to the workspace', ->
38+
expect(view).not.toBeNull()
39+
40+
describe 'with an active stacktrace', ->
41+
42+
beforeEach ->
43+
trace.register()
44+
trace.activate()
45+
46+
it 'should be visible', ->
47+
expect(view.hasClass 'inactive').toBeFalsy()
48+
49+
it 'shows the active trace name', ->
50+
text = view.find('.message').text()
51+
expect(text).toEqual('Boom')
52+
53+
it 'navigates back to the trace on a click', ->
54+
waitsForPromise -> view.backToTrace()
55+
56+
runs ->
57+
expect(atom.workspaceView.getActiveView().hasClass 'traceview').toBeTruthy()
58+
59+
it 'deactivates the trace', ->
60+
view.deactivateTrace()
61+
expect(trace.isActive()).toBeFalsy()
62+
63+
describe 'on an editor corresponding to a single frame', ->
64+
[editor] = []
65+
66+
beforeEach ->
67+
waitsForPromise -> trace.frames[2].navigateTo()
68+
69+
runs ->
70+
editor = atom.workspace.getActiveEditor()
71+
72+
it 'shows the current frame and its index', ->
73+
expect(view.find('.current-frame .function').text()).toBe('topfunc')
74+
expect(view.find('.current-frame .index').text()).toBe('3')
75+
expect(view.find('.current-frame .total').text()).toBe('4')
76+
77+
it "navigates to the caller's frame", ->
78+
waitsForPromise -> view.navigateToCaller()
79+
80+
runs ->
81+
expect(view.frame).toBe(trace.frames[3])
82+
83+
it 'navigates to the called frame', ->
84+
waitsForPromise -> view.navigateToCalled()
85+
86+
runs ->
87+
expect(view.frame).toBe(trace.frames[1])
88+
89+
it 'navigates back to the last active frame', ->
90+
editor.setCursorBufferPosition [5, 0]
91+
expect(view.find '.current-frame.unfocused').toHaveLength 1
92+
93+
waitsForPromise -> view.navigateToLastActive()
94+
95+
runs ->
96+
expect(view.find '.current-frame.unfocused').toHaveLength 0
97+
expect(editor.getCursorBufferPosition().row).toBe 36
98+
99+
describe 'on an editor with multiple frames', ->
100+
[editor] = []
101+
102+
beforeEach ->
103+
waitsForPromise -> trace.frames[1].navigateTo()
104+
105+
runs ->
106+
editor = atom.workspace.getActiveEditor()
107+
108+
it 'notices if you manually navigate to a different frame', ->
109+
expect(view.find('.current-frame .function').text()).toEqual 'midfunc'
110+
111+
editor.setCursorBufferPosition [4, 1]
112+
113+
expect(view.frame).toBe(trace.frames[3])
114+
expect(view.find('.current-frame .function').text()).toEqual 'otherfunc'

0 commit comments

Comments
 (0)