Skip to content

Commit 1b64dcf

Browse files
committed
Merge branch 'master' into decorate-lines
Conflicts: lib/main.coffee spec/stacktrace-spec.coffee
2 parents 824240f + e9d6b33 commit 1b64dcf

14 files changed

+587
-27
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ like to see in place before I mark it 1.0.
2121

2222
- [x] Accept stacktraces pasted into a dialog you call up from the command
2323
palette.
24-
- [ ] Present a view that gives you bits of context around each frame of a
25-
specific stack. *(...)*
24+
- [x] Present a view that gives you bits of context around each frame of a
25+
specific stack.
2626
- [x] Pluggable stacktrace recognition and parsing code.
2727
- [ ] Map parsed frames to source files on the local filesystem.
28-
- [ ] While a stacktrace is active, highlight individual lines from the trace
28+
- [x] While a stacktrace is active, highlight individual lines from the trace
2929
in open editors.
3030
- [ ] Provide commands for next-frame, previous-frame, and turning it off.
31-
- [ ] Show a stacktrace navigation view as a bottom panel with next, previous
31+
- [x] Show a stacktrace navigation view as a bottom panel with next, previous
3232
and stop buttons.

lib/editor-decorator.coffee

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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 = (editor) ->
8+
m.destroy() for m in markers
9+
markers = []
10+
11+
active = Stacktrace.getActivated()
12+
return unless active?
13+
14+
for frame in active.frames
15+
if frame.realPath is editor.getPath()
16+
range = editor.getBuffer().rangeForRow frame.bufferLineNumber()
17+
marker = editor.markBufferRange range
18+
editor.decorateMarker marker, type: 'line', class: 'line-stackframe'
19+
editor.decorateMarker marker, type: 'gutter', class: 'gutter-stackframe'
20+
markers.push marker

lib/main.coffee

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

57
module.exports =
68

@@ -13,12 +15,22 @@ module.exports =
1315
text = (s.getText() for s in (selections or [])).join ''
1416
atom.emit 'stacktrace:accept-trace', trace: text
1517

18+
atom.workspace.eachEditor editorDecorator
19+
@activeChanged = Stacktrace.on 'active-changed', ->
20+
editorDecorator(e) for e in atom.workspace.getEditors()
21+
22+
@navigationView = new NavigationView
23+
atom.workspaceView.appendToBottom @navigationView
24+
1625
StacktraceView.registerIn(atom.workspace)
1726

18-
atom.on 'stacktrace:accept-trace', ({trace}) =>
27+
@acceptTrace = atom.on 'stacktrace:accept-trace', ({trace}) =>
1928
for trace in Stacktrace.parse(trace)
2029
trace.register()
2130
atom.workspace.open trace.getUrl()
2231

2332
deactivate: ->
24-
atom.off 'stacktrace:accept-trace'
33+
@navigationView.remove()
34+
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: 46 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,8 +122,17 @@ class Stacktrace
90122
class Frame
91123

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

128+
# Public: Return the zero-indexed line number.
129+
#
130+
bufferLineNumber: -> @lineNumber - 1
131+
132+
# Public: Return the one-based frame index.
133+
#
134+
humanIndex: -> @index + 1
135+
95136
# Public: Asynchronously collect n lines of context around the specified line number in this
96137
# frame's source file.
97138
#
@@ -129,6 +170,11 @@ class Frame
129170
if editorView?
130171
editorView.scrollToBufferPosition position, center: true
131172

173+
# Public: Return true if the buffer position and path correspond to this Frame's line.
174+
#
175+
isOn: ({position, path}) ->
176+
path is @realPath and position.row is @bufferLineNumber()
177+
132178

133179
module.exports =
134180
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/editor-marker-spec.coffee

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
path = require 'path'
2+
{Editor, WorkspaceView} = require 'atom'
3+
4+
{Stacktrace, Frame} = require '../lib/stacktrace'
5+
editorDecorator = 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+
atom.workspaceView = new WorkspaceView
22+
23+
afterEach ->
24+
Stacktrace.getActivated()?.deactivate()
25+
26+
withEditorOn = (fname, callback) ->
27+
waitsForPromise ->
28+
atom.workspace.open(framePath fname)
29+
30+
runs ->
31+
atom.workspaceView.attachToDom()
32+
editorView = atom.workspaceView.getActiveView()
33+
editor = editorView.getEditor()
34+
callback()
35+
36+
it 'does nothing if there is no active trace', ->
37+
expect(Stacktrace.getActivated()).toBeNull()
38+
39+
withEditorOn 'bottom.rb', ->
40+
editorDecorator(editor)
41+
expect(editorView.find '.line.line-stackframe').toHaveLength 0
42+
43+
describe 'with an active trace', ->
44+
45+
beforeEach -> trace.activate()
46+
47+
it "does nothing if the file doesn't appear in the active trace", ->
48+
withEditorOn 'context.txt', ->
49+
editorDecorator(editor)
50+
expect(editorView.find '.line.line-stackframe').toHaveLength 0
51+
52+
it 'decorates stackframe lines in applicable editors', ->
53+
withEditorOn 'bottom.rb', ->
54+
editorDecorator(editor)
55+
decorated = editorView.find '.line.line-stackframe'
56+
expect(decorated).toHaveLength 1
57+
expect(decorated.text()).toEqual(" puts 'this is the stack line'")
58+
59+
it 'removes prior decorations when deactivated', ->
60+
withEditorOn 'bottom.rb', ->
61+
editorDecorator(editor)
62+
trace.deactivate()
63+
editorDecorator(editor)
64+
expect(editorView.find '.line.line-stackframe').toHaveLength 0

spec/fixtures/bottom.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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

0 commit comments

Comments
 (0)