diff --git a/lib/breakpoint-store.coffee b/lib/breakpoint-store.coffee index ab3d8b6..63b579e 100644 --- a/lib/breakpoint-store.coffee +++ b/lib/breakpoint-store.coffee @@ -9,7 +9,7 @@ class BreakpointStore addDecoration = true if breakpointSearched - @breakpoints.splice(breakpointSearched, 1) + @breakpoints.splice(@breakpoints.indexOf(breakpointSearched), 1) addDecoration = false else @breakpoints.push(breakpoint) @@ -21,12 +21,14 @@ class BreakpointStore d = editor.decorateMarker(marker, type: "line-number", class: "line-number-red") d.setProperties(type: "line-number", class: "line-number-red") breakpoint.decoration = d + return "b" else editor = atom.workspace.getActiveTextEditor() ds = editor.getLineNumberDecorations(type: "line-number", class: "line-number-red") for d in ds marker = d.getMarker() marker.destroy() if marker.getBufferRange().start.row == breakpoint.lineNumber-1 + return "cl" containsBreakpoint: (bp) -> for breakpoint in @breakpoints diff --git a/lib/python-debugger-view.coffee b/lib/python-debugger-view.coffee index 81f8fde..328b34a 100644 --- a/lib/python-debugger-view.coffee +++ b/lib/python-debugger-view.coffee @@ -13,6 +13,13 @@ class PythonDebuggerView extends View debuggedFileArgs: [] backendDebuggerPath: null backendDebuggerName: "atom_pdb.py" + flagActionStarted: false + flagVarsNeedUpdate: false + flagCallstackNeedsUpdate: false + # 0 - normal output, 1 - print variables, 2 - print call stack + currentState: 0 + varScrollTop: 0 + callStackScrollTop: 0 getCurrentFilePath: -> editor = atom.workspace.getActivePaneItem() @@ -32,35 +39,151 @@ class PythonDebuggerView extends View @subview "commandEntryView", new TextEditorView mini: true, placeholderText: "> Enter debugger commands here" - @button outlet: "runBtn", click: "runApp", class: "btn", => - @span "run" - @button outlet: "stopBtn", click: "stopApp", class: "btn", => - @span "stop" - @button outlet: "clearBtn", click: "clearOutput", class: "btn", => - @span "clear" - @button outlet: "stepOverBtn", click: "stepOverBtnPressed", class: "btn", => - @span "next" - @button outlet: "stepInBtn", click: "stepInBtnPressed", class: "btn", => - @span "step" - @button outlet: "continueBtn", click: "continueBtnPressed", class: "btn", => - @span "continue" - @button outlet: "returnBtn", click: "returnBtnPressed", class: "btn", => - @span "return" - @div class: "panel-body", outlet: "outputContainer", => - @pre class: "command-output", outlet: "output" + @div class: "btn-toolbar", => + @div class: "btn-group", => + @button outlet: "breakpointBtn", click: "toggleBreak", class: "btn", => + @span "break point" + @div class: "btn-group", => + @button outlet: "runBtn", click: "runApp", class: "btn", => + @span "run" + @button outlet: "stopBtn", click: "stopApp", class: "btn", => + @span "stop" + @div class: "btn-group", => + @button outlet: "stepOverBtn", click: "stepOverBtnPressed", class: "btn", => + @span "next" + @button outlet: "stepInBtn", click: "stepInBtnPressed", class: "btn", => + @span "step" + @button outlet: "returnBtn", click: "returnBtnPressed", class: "btn", => + @span "return" + @button outlet: "continueBtn", click: "continueBtnPressed", class: "btn", => + @span "continue" + @div class: "btn-group", => + @button outlet: "upBtn", click: "upBtnPressed", class: "btn", => + @span "up" + @button outlet: "downBtn", click: "downBtnPressed", class: "btn", => + @span "down" + @div class: "btn-group", => + @button outlet: "clearBtn", click: "clearOutput", class: "btn", => + @span "clear" + @input class : "input-checkbox", type: "checkbox", id: "ck_input", outlet: "showInput", click: "toggleInput" + @label class : "label", for: "ck_input", => + @span "Input" + @input class : "input-checkbox", type: "checkbox", id: "ck_vars", outlet: "showVars", click: "toggleVars" + @label class : "label", for: "ck_vars", => + @span "Variables" + @input class : "input-checkbox", type: "checkbox", id: "ck_callstack", outlet: "showCallstack", click: "toggleCallstack" + @label class : "label", for: "ck_callstack", => + @span "Call stack" + @div class: "block", outlet: "bottomPane", => + @div class: "inline-block panel", id: "outputPane", outlet: "outputPane", => + @pre class: "command-output", outlet: "output" + @div class: "inline-block panel", id: "variablesPane", outlet: "variablesPane", => + @pre class: "command-output", outlet: "variables" + @div class: "inline-block panel", id: "callstackPane", outlet: "callstackPane", => + @pre class: "command-output", outlet: "callstack" + + toggleInput: -> + if @backendDebugger + @argsEntryView.hide() + if @showInput.prop('checked') + @commandEntryView.show() + else + @commandEntryView.hide() + else + if @showInput.prop('checked') + @argsEntryView.show() + else + @argsEntryView.hide() + @commandEntryView.hide() + + toggleVars: -> + @togglePanes() + + toggleCallstack: -> + @togglePanes() + + togglePanes: -> + n = 1 + if @showVars.prop('checked') + @variablesPane.show() + n = n+1 + else + @variablesPane.hide() + if @showCallstack.prop('checked') + @callstackPane.show() + n = n+1 + else + @callstackPane.hide() + width = ''+(100/n)+'%' + @outputPane.css('width', width) + if @showVars.prop('checked') + @variablesPane.css('width', width) + if @showCallstack.prop('checked') + @callstackPane.css('width', width) + # the following statements are used to update the information in the variables/callstack + @setFlags() + @backendDebugger?.stdin.write("print 'display option changed.'\n") + + toggleBreak: -> + editor = atom.workspace.getActiveTextEditor() + filename = editor.getTitle() + lineNumber = editor.getCursorBufferPosition().row + 1 + breakpoint = new Breakpoint(filename, lineNumber) + cmd = @breakpointStore.toggle(breakpoint) + if @backendDebugger + @backendDebugger.stdin.write(cmd + " " + @getCurrentFilePath() + ":" + lineNumber + "\n") + @output.empty() + for breakpoint in @breakpointStore.breakpoints + @output.append(breakpoint.toCommand() + "\n") stepOverBtnPressed: -> + @setFlags() @backendDebugger?.stdin.write("n\n") stepInBtnPressed: -> + @setFlags() @backendDebugger?.stdin.write("s\n") continueBtnPressed: -> + @setFlags() @backendDebugger?.stdin.write("c\n") returnBtnPressed: -> + @setFlags() @backendDebugger?.stdin.write("r\n") + upBtnPressed: -> + @setFlags() + @backendDebugger?.stdin.write("up\n") + + downBtnPressed: -> + @setFlags() + @backendDebugger?.stdin.write("down\n") + + printVars: -> + @variables.empty() + @backendDebugger?.stdin.write("print ('@{variables_start}')\n") + @backendDebugger?.stdin.write("for (__k, __v) in [(__k, __v) for __k, __v in globals().items() if not __k.startswith('__')]: print __k, '=', __v\n") + @backendDebugger?.stdin.write("print '-------------'\n") + @backendDebugger?.stdin.write("for (__k, __v) in [(__k, __v) for __k, __v in locals().items() if __k != 'self' and not __k.startswith('__')]: print __k, '=', __v\n") + @backendDebugger?.stdin.write("for (__k, __v) in [(__k, __v) for __k, __v in (self.__dict__ if 'self' in locals().keys() else {}).items()]: print 'self.{0}'.format(__k), '=', __v\n") + @backendDebugger?.stdin.write("print ('@{variables_end}')\n") + + printCallstack: -> + @callstack.empty() + @backendDebugger?.stdin.write("print ('@{callstack_start}')\n") + @backendDebugger?.stdin.write("bt\n") + @backendDebugger?.stdin.write("print ('@{callstack_end}')\n") + + setFlags: -> + @flagActionStarted = true + if @showVars.prop('checked') + @varScrollTop = @variables.prop('scrollTop') + @flagVarsNeedUpdate = true + if @showCallstack.prop('checked') + @callStackScrollTop = @callstack.prop('scrollTop') + @flagCallstackNeedsUpdate = true + workspacePath: -> editor = atom.workspace.getActiveTextEditor() activePath = editor.getPath() @@ -72,17 +195,56 @@ class PythonDebuggerView extends View @stopApp() if @backendDebugger @debuggedFileArgs = @getInputArguments() console.log @debuggedFileArgs + @debuggedFileName = @getCurrentFilePath() if @pathsNotSet() @askForPaths() return + @setFlags() @runBackendDebugger() + @toggleInput() + + highlightLineInEditor: (fileName, lineNumber) -> + if lineNumber && fileName + lineNumber = parseInt(lineNumber) + editor = atom.workspace.getActiveTextEditor() + if fileName.toLowerCase() == editor.getPath().toLowerCase() + position = Point(lineNumber-1, 0) + editor.setCursorBufferPosition(position) + editor.unfoldBufferRow(lineNumber) + editor.scrollToBufferPosition(position) + else + options = {initialLine: lineNumber-1, initialColumn:0} + atom.workspace.open(fileName, options) if fs.existsSync(fileName) + # TODO: add decoration to current line? + + processNormalOutput: (data_str) -> - # Extract the file name and line number output by the debugger. - processDebuggerOutput: (data) -> - data_str = data.toString().trim() lineNumber = null fileName = null + # print the action_end string + if @flagActionStarted + @backendDebugger?.stdin.write("print ('@{action_end}')\n") + @flagActionStarted = false + + # detect predefined flag strings + isActionEnd = data_str.includes('@{action_end}') + isVarsStart = data_str.includes('@{variables_start}') + isCallstackStart = data_str.includes('@{callstack_start}') + + # variables print started + if isVarsStart + @currentState = 1 + @processVariables(data_str) + return + + # call stack print started + if isCallstackStart + @currentState = 2 + @processCallstack(data_str) + return + + # handle normal output [data_str, tail] = data_str.split("line:: ") if tail [lineNumber, tail] = tail.split("\n") @@ -95,13 +257,72 @@ class PythonDebuggerView extends View fileName = fileName.trim() if fileName fileName = null if fileName == "" + # highlight the current line if lineNumber && fileName - lineNumber = parseInt(lineNumber) - options = {initialLine: lineNumber-1, initialColumn:0} - atom.workspace.open(fileName, options) if fs.existsSync(fileName) - # TODO: add decoration to current line? + @highlightLineInEditor(fileName, lineNumber) + + # print the output + @addOutput(data_str.trim().replace('@{action_end}', '')) + + # if action end, trigger the follow up actions + if isActionEnd + if @flagVarsNeedUpdate + @printVars() + @flagVarsNeedUpdate = false + else + if @flagCallstackNeedsUpdate + @printCallstack() + @flagCallstackNeedsUpdate = false + + processVariables: (data_str) -> + isVarsEnd = data_str.includes('@{variables_end}') + for line in data_str.split '\n' + if ! line.includes("@{variable") + @variables.append(@createOutputNode(line)) + @variables.append('\n') + if isVarsEnd + @variables.prop('scrollTop', @varScrollTop) + @currentState = 0 + if @flagCallstackNeedsUpdate + @printCallstack() + @flagCallstackNeedsUpdate = false + + processCallstack: (data_str) -> + lineNumber = null + fileName = null + isCallstackEnd = data_str.includes('@{callstack_end}') + m = /[^-]> (.*[.]py)[(]([0-9]*)[)].*/.exec(data_str) + if m + [fileName, lineNumber] = [m[1], m[2]] + callstack_pre = @callstack + ` + re = /[\n](>*)[ \t]*(.*[.]py)[(]([0-9]*)[)]([^\n]*)[\n]([^\n]*)/gi; + while ((match = re.exec(data_str))) + { + if (match[5].includes('exec cmd in globals, locals')) continue; + if (match[1].includes('>')) + item = ""+match[5].replace("->", "")+""; + else + item = match[5].replace("->", ""); + callstack_pre.append(item); + callstack_pre.append('\n'); + } + ` + if lineNumber && fileName + @highlightLineInEditor(fileName, lineNumber) + if isCallstackEnd + @currentState = 0 + @callstack.prop('scrollTop', @callStackScrollTop) - @addOutput(data_str.trim()) + # Extract the file name and line number output by the debugger. + processDebuggerOutput: (data) -> + data_str = data.toString().trim() + if @currentState == 1 + @processVariables(data_str) + else if @currentState == 2 + @processCallstack(data_str) + else + @processNormalOutput(data_str) runBackendDebugger: -> args = [path.join(@backendDebuggerPath, @backendDebuggerName)] @@ -129,6 +350,7 @@ class PythonDebuggerView extends View @backendDebugger?.stdin.write("\nexit()\n") @backendDebugger = null console.log "debugger stopped" + @toggleInput() clearOutput: -> @output.empty() @@ -155,6 +377,8 @@ class PythonDebuggerView extends View @breakpointStore = breakpointStore @debuggedFileName = @getCurrentFilePath() @backendDebuggerPath = @getDebuggerPath() + @toggleInput() + @togglePanes() @addOutput("Welcome to Python Debugger for Atom!") @addOutput("The file being debugged is: " + @debuggedFileName) @askForPaths() diff --git a/styles/python-debugger.less b/styles/python-debugger.less index c386515..17f72d9 100644 --- a/styles/python-debugger.less +++ b/styles/python-debugger.less @@ -2,8 +2,20 @@ @import "octicon-mixins"; .command-output { - background-color: transparent; - height: 100px; - max-height: 100px; + height: 100%; overflow-y: scroll; + padding: 5px; + margin: 0px; +} + +#outputPane, #variablesPane, #callstackPane { + width: 33%; + height: 200px; + max-height: 200px; + margin: 0px; +} + +#ck_input, #ck_vars, #ck_callstack { + margin-left: 10px; + margin-right: 5px; }