Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
244995d
Spec-first.
smashwilson Jul 31, 2014
77d03df
More specs for the multiple-frame case.
smashwilson Jul 31, 2014
1e3e03c
Attach the navigation view to the workspace.
smashwilson Aug 9, 2014
0fd987c
Tests for displaying the active trace.
smashwilson Aug 9, 2014
c8ccf25
Toggle the "inactive" class on trace activation.
smashwilson Aug 9, 2014
0750ad3
Upgrade line-chomper.
smashwilson Aug 10, 2014
ed36087
Open the active Stacktrace when clicking the message.
smashwilson Aug 10, 2014
49161ce
Actually register the trace.
smashwilson Aug 10, 2014
6e6f669
Improve the CSS selectors to target new markup.
smashwilson Aug 18, 2014
ddc93be
Deactive a trace with the "x" button.
smashwilson Aug 23, 2014
e5525a1
Hide the navigation panel if there's no trace.
smashwilson Aug 23, 2014
726346a
Remove the activation controls from StacktraceView.
smashwilson Aug 23, 2014
426b28a
Tests for showing information about the frame.
smashwilson Aug 23, 2014
bed65f2
Stacktrace and Frame recognize Editor positions.
smashwilson Aug 23, 2014
668aec2
Comment touchups :lipstick:
smashwilson Aug 23, 2014
7188353
Show the current frame on the NavigationView.
smashwilson Aug 24, 2014
b02fa7f
Fuss with styles.
smashwilson Aug 24, 2014
f372a68
Give precedence to already-focused frames.
smashwilson Aug 24, 2014
6bd63c5
Assign indices to frames on Trace construction.
smashwilson Aug 24, 2014
d18f195
Access callers and callees of stack frames.
smashwilson Aug 24, 2014
c8a8f60
Just return a Frame from atEditorPosition.
smashwilson Aug 24, 2014
e0fa818
Navigate to caller and callee of a frame.
smashwilson Aug 24, 2014
8316fa7
Add buttons for stack navigation.
smashwilson Aug 24, 2014
e504089
Navigate back to the last active frame.
smashwilson Aug 24, 2014
e3c8b3e
Get those tests passing.
smashwilson Aug 24, 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
14 changes: 9 additions & 5 deletions lib/main.coffee
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
EnterDialog = require './enter-dialog'
{Stacktrace} = require './stacktrace'
{StacktraceView} = require './stacktrace-view'
{NavigationView} = require './navigation-view'
editorDecorator = require './editor-decorator'

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

atom.workspace.eachEditor editorDecorator
Stacktrace.on 'active-changed', ->
@activeChanged = Stacktrace.on 'active-changed', ->
editorDecorator(e) for e in atom.workspace.getEditors()

@navigationView = new NavigationView
atom.workspaceView.appendToBottom @navigationView

StacktraceView.registerIn(atom.workspace)

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

deactivate: ->
Stacktrace.off 'active-changed'
atom.off 'stacktrace:accept-trace'
@navigationView.remove()

serialize: ->
@activeChanged.off()
@acceptTrace.off()
114 changes: 114 additions & 0 deletions lib/navigation-view.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{View} = require 'atom'
{Subscriber} = require 'emissary'
{Stacktrace} = require './stacktrace'

class NavigationView extends View

Subscriber.includeInto this

@content: ->
activatedClass = if Stacktrace.getActivated()? then '' else 'inactive'

@div class: "tool-panel panel-bottom padded stacktrace navigation #{activatedClass}", =>
@div class: 'inline-block trace-name', =>
@h2 class: 'inline-block text-highlight message', outlet: 'message', click: 'backToTrace'
@span class: 'inline-block icon icon-x', click: 'deactivateTrace'
@div class: 'inline-block current-frame unfocused', outlet: 'frameContainer', =>
@span class: 'inline-block icon icon-code'
@span class: 'inline-block function', outlet: 'frameFunction', click: 'navigateToLastActive'
@span class: 'inline-block index', outlet: 'frameIndex'
@span class: 'inline-block divider', '/'
@span class: 'inline-block total', outlet: 'frameTotal'
@div class: 'pull-right controls', =>
@button class: 'inline-block btn', click: 'navigateToCaller', =>
@span class: 'icon icon-arrow-up'
@text 'Caller'
@button class: 'inline-block btn', click: 'navigateToCalled', =>
@span class: 'icon icon-arrow-down'
@text 'Follow Call'

initialize: ->
@subscribe Stacktrace, 'active-changed', (e) =>
if e.newTrace? then @useTrace(e.newTrace) else @noTrace()

# Subscribe to opening editors. Set the current frame when a cursor is moved over a frame's
# line.
atom.workspace.eachEditor (e) =>
@subscribe e, 'cursors-moved', =>
if @trace?
pos =
position: e.getCursorBufferPosition()
path: e.getPath()

# Allow the already-set @frame a chance to see if it still applies.
# This lets the caller and called navigation work properly, even if multiple frames are
# on the same line.
if @frame? and @frame.isOn(pos)
@useFrame(@frame)
else
# Otherwise, scan the trace for a matching frame.
frame = @trace.atEditorPosition(pos)
if frame? then @useFrame(frame) else @unfocusFrame()

if Stacktrace.getActivated? then @hide()

beforeRemove: ->
@unsubscribe Stacktrace

useTrace: (@trace) ->
@removeClass 'inactive'
@message.text(trace.message)
@noFrame()
@show()

noTrace: ->
@addClass 'inactive'
@message.text('')
@noFrame()
@hide()

useFrame: (@frame) ->
@frameContainer.removeClass 'unfocused'
@frameFunction.text @frame.functionName
@frameFunction.addClass 'highlight-info'
@frameIndex.text @frame.humanIndex().toString()
@frameTotal.text @trace.frames.length.toString()

unfocusFrame: ->
@frameContainer.addClass 'unfocused'
@frameFunction.removeClass 'highlight-info'

noFrame: ->
@unfocusFrame()
@frameFunction.text ''
@frameIndex.text ''
@frameTotal.text ''

deactivateTrace: ->
Stacktrace.getActivated().deactivate()

backToTrace: ->
url = Stacktrace.getActivated()?.getUrl()
atom.workspace.open(url) if url

navigateToCaller: ->
return unless @trace? and @frame?

f = @trace.callerOf(@frame)
if f?
@frame = f
@frame.navigateTo()

navigateToCalled: ->
return unless @trace? and @frame?

f = @trace.calledFrom(@frame)
if f?
@frame = f
@frame.navigateTo()

navigateToLastActive: ->
return unless @frame?
@frame.navigateTo()

module.exports = NavigationView: NavigationView
15 changes: 1 addition & 14 deletions lib/stacktrace-view.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,12 @@ class StacktraceView extends View
@div class: "stacktrace traceview 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, => trace.activate()

initialize: (@trace) ->
@uri = @trace.getUrl()
@subscribe Stacktrace, 'active-changed', (e) =>
if e.newTrace is @trace
@addClass 'activated'
Expand All @@ -37,14 +32,6 @@ class StacktraceView extends View
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.
#
Expand Down
42 changes: 42 additions & 0 deletions lib/stacktrace.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ class Stacktrace
Emitter.extend this

constructor: (@frames = [], @message = '') ->
i = 0
for f in @frames
f.index = i
i += 1

# Internal: Compute the SHA256 checksum of the normalized stacktrace.
#
Expand Down Expand Up @@ -64,6 +68,32 @@ class Stacktrace
ACTIVE = null
Stacktrace.emit 'active-changed', oldTrace: this, newTrace: null

# Public: Return the Frame corresponding to an Editor position, if any, along with its position
# within the trace.
#
# object - "position" should be a Point corresponding to a cursor position, and "path" the full
# path of an Editor.
#
atEditorPosition: (editorPosition) ->
[index, total] = [1, @frames.length]
for frame in @frames
return frame if frame.isOn editorPosition
index += 1
return null

# Public: Return the Frame that called the given Frame, or undefined if given the top of the stack.
#
# frame - The current Frame to use as a reference point.
#
callerOf: (frame) -> @frames[frame.index + 1]

# Public: Return the Frame that a given Frame called into, or undefined if given the bottom of the
# stack.
#
# frame - The current Frame to use as a reference point.
#
calledFrom: (frame) -> @frames[frame.index - 1]

# Public: Parse zero to many Stacktrace instances from a corpus of text.
#
# text - A raw blob of text.
Expand All @@ -74,10 +104,12 @@ class Stacktrace

# Internal: Return a registered trace, or null if none match the provided
# URL.
#
@forUrl: (url) ->
REGISTRY[url]

# Internal: Clear the global trace registry.
#
@clearRegistry: ->
REGISTRY = {}

Expand All @@ -90,12 +122,17 @@ class Stacktrace
class Frame

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

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

# Public: Return the one-based frame index.
#
humanIndex: -> @index + 1

# Public: Asynchronously collect n lines of context around the specified line number in this
# frame's source file.
#
Expand All @@ -122,6 +159,11 @@ class Frame
if editorView?
editorView.scrollToBufferPosition position, center: true

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


module.exports =
PREFIX: PREFIX
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
"dependencies": {
"emissary": "^1.2.1",
"jssha": "^1.5.0",
"line-chomper": "git+https://github.com/smashwilson/line-chomper.git#optional-trim"
"line-chomper": "^0.4.5"
}
}
114 changes: 114 additions & 0 deletions spec/navigation-view-spec.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{WorkspaceView} = require 'atom'
{Stacktrace, Frame} = require '../lib/stacktrace'
{NavigationView} = require '../lib/navigation-view'

path = require 'path'

fixturePath = (p) ->
path.join __dirname, 'fixtures', p

frames = [
new Frame('raw0', fixturePath('bottom.rb'), 12, 'botfunc')
new Frame('raw1', fixturePath('middle.rb'), 42, 'midfunc')
new Frame('raw2', fixturePath('top.rb'), 37, 'topfunc')
new Frame('raw3', fixturePath('middle.rb'), 5, 'otherfunc')
]
trace = new Stacktrace(frames, 'Boom')

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

beforeEach ->
atom.workspaceView = new WorkspaceView
atom.workspaceView.attachToDom()
activationPromise = atom.packages.activatePackage('stacktrace')

atom.workspaceView.trigger 'stacktrace:paste'

waitsForPromise -> activationPromise

runs ->
view = atom.workspaceView.find('.stacktrace.navigation').view()

afterEach ->
Stacktrace.getActivated()?.deactivate()
Stacktrace.clearRegistry()

it 'attaches itself to the workspace', ->
expect(view).not.toBeNull()

describe 'with an active stacktrace', ->

beforeEach ->
trace.register()
trace.activate()

it 'should be visible', ->
expect(view.hasClass 'inactive').toBeFalsy()

it 'shows the active trace name', ->
text = view.find('.message').text()
expect(text).toEqual('Boom')

it 'navigates back to the trace on a click', ->
waitsForPromise -> view.backToTrace()

runs ->
expect(atom.workspaceView.getActiveView().hasClass 'traceview').toBeTruthy()

it 'deactivates the trace', ->
view.deactivateTrace()
expect(trace.isActive()).toBeFalsy()

describe 'on an editor corresponding to a single frame', ->
[editor] = []

beforeEach ->
waitsForPromise -> trace.frames[2].navigateTo()

runs ->
editor = atom.workspace.getActiveEditor()

it 'shows the current frame and its index', ->
expect(view.find('.current-frame .function').text()).toBe('topfunc')
expect(view.find('.current-frame .index').text()).toBe('3')
expect(view.find('.current-frame .total').text()).toBe('4')

it "navigates to the caller's frame", ->
waitsForPromise -> view.navigateToCaller()

runs ->
expect(view.frame).toBe(trace.frames[3])

it 'navigates to the called frame', ->
waitsForPromise -> view.navigateToCalled()

runs ->
expect(view.frame).toBe(trace.frames[1])

it 'navigates back to the last active frame', ->
editor.setCursorBufferPosition [5, 0]
expect(view.find '.current-frame.unfocused').toHaveLength 1

waitsForPromise -> view.navigateToLastActive()

runs ->
expect(view.find '.current-frame.unfocused').toHaveLength 0
expect(editor.getCursorBufferPosition().row).toBe 36

describe 'on an editor with multiple frames', ->
[editor] = []

beforeEach ->
waitsForPromise -> trace.frames[1].navigateTo()

runs ->
editor = atom.workspace.getActiveEditor()

it 'notices if you manually navigate to a different frame', ->
expect(view.find('.current-frame .function').text()).toEqual 'midfunc'

editor.setCursorBufferPosition [4, 1]

expect(view.frame).toBe(trace.frames[3])
expect(view.find('.current-frame .function').text()).toEqual 'otherfunc'
Loading