├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── keymaps └── debugger.cson ├── lib ├── asm-viewer.coffee ├── backend │ └── gdb │ │ ├── gdb-mi-parser.js │ │ ├── gdb-mi-parser.json │ │ └── gdb.coffee ├── debugger-view.coffee ├── debugger.coffee └── open-dialog-view.coffee ├── menus └── debugger.cson ├── package.json ├── screenshot.png ├── spec └── debugger-spec.coffee └── styles ├── debugger.atom-text-editor.less └── debugger.less /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 - First Release 2 | * Every feature added 3 | * Every bug fixed 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atom-debugger package 2 | 3 | This is a debugger for atom.io, which now only support `gdb`. 4 | 5 | This package is still in very early stage. In fact, I'm not sure whether there are people using atom.io to debug. 6 | 7 | Please feel free to create issues if you have any problem or good ideas, thank you! 8 | 9 | Before debugging, please add your source root directory into the tree-view. 10 | 11 | ![screenshot](https://raw.githubusercontent.com/xndcn/atom-debugger/master/screenshot.png?raw=true) 12 | 13 | 14 | ## TO DO 15 | 16 | * Add `watch` view to display variable value. 17 | -------------------------------------------------------------------------------- /keymaps/debugger.cson: -------------------------------------------------------------------------------- 1 | # Keybindings require three things to be fully defined: A selector that is 2 | # matched against the focused element, the keystroke and the command to 3 | # execute. 4 | # 5 | # Below is a basic keybinding which registers on all platforms by applying to 6 | # the root workspace element. 7 | 8 | # For more detailed documentation see 9 | # https://atom.io/docs/latest/advanced/keymaps 10 | 'atom-workspace': 11 | 'ctrl-alt-b': 'debugger:toggle' 12 | -------------------------------------------------------------------------------- /lib/asm-viewer.coffee: -------------------------------------------------------------------------------- 1 | {TextEditor} = require 'atom' 2 | 3 | module.exports = 4 | class AsmViewer extends TextEditor 5 | lines: null 6 | constructor: (params) -> 7 | line = params.startline 8 | file = params.fullname 9 | 10 | super params 11 | 12 | @GDB = params.gdb 13 | 14 | @lines = {} 15 | @setText("#{line+1}:") 16 | @GDB.disassembleData {file: {name: file, linenum: line+1, lines: -1}, mode: 1}, (instructions) => 17 | maxOffset = -1 18 | for src in instructions 19 | for asm in src.line_asm_insn 20 | maxOffset = Number(asm.offset) if Number(asm.offset) > maxOffset 21 | maxOffsetLength = maxOffset.toString().length 22 | 23 | text = [] 24 | linenum = 0 25 | for src in instructions 26 | text.push("#{src.line}:") 27 | @lines[Number(src.line)-1] = linenum 28 | linenum += 1 29 | for asm in src.line_asm_insn 30 | asm.offset = '0' unless asm.offset 31 | alignSpace = ' '.slice(0, maxOffsetLength-asm.offset.length) 32 | text.push(" #{asm['func-name']}+#{alignSpace}#{asm.offset}: #{asm.inst}") 33 | linenum += 1 34 | 35 | @setText(text.join('\n')) 36 | console.log(@lines) 37 | 38 | fileLineToBufferLine: (line) -> 39 | return Number(0) unless line of @lines 40 | return @lines[line] 41 | 42 | bufferLineToFileLine: (line) -> 43 | lines = Object.keys(@lines) 44 | left = 0 45 | right = lines.length-1 46 | while left <= right 47 | mid = (left+right) // 2 48 | midLine = @lines[lines[mid]] 49 | if line < midLine 50 | right = mid - 1 51 | else 52 | left = mid + 1 53 | return Number(lines[left-1]) 54 | 55 | shouldPromptToSave: ({windowCloseRequested}={}) -> 56 | return false 57 | -------------------------------------------------------------------------------- /lib/backend/gdb/gdb-mi-parser.js: -------------------------------------------------------------------------------- 1 | /* parser generated by jison 0.4.15 */ 2 | /* 3 | Returns a Parser object of the following structure: 4 | 5 | Parser: { 6 | yy: {} 7 | } 8 | 9 | Parser.prototype: { 10 | yy: {}, 11 | trace: function(), 12 | symbols_: {associative list: name ==> number}, 13 | terminals_: {associative list: number ==> name}, 14 | productions_: [...], 15 | performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), 16 | table: [...], 17 | defaultActions: {...}, 18 | parseError: function(str, hash), 19 | parse: function(input), 20 | 21 | lexer: { 22 | EOF: 1, 23 | parseError: function(str, hash), 24 | setInput: function(input), 25 | input: function(), 26 | unput: function(str), 27 | more: function(), 28 | less: function(n), 29 | pastInput: function(), 30 | upcomingInput: function(), 31 | showPosition: function(), 32 | test_match: function(regex_match_array, rule_index), 33 | next: function(), 34 | lex: function(), 35 | begin: function(condition), 36 | popState: function(), 37 | _currentRules: function(), 38 | topState: function(), 39 | pushState: function(condition), 40 | 41 | options: { 42 | ranges: boolean (optional: true ==> token location info will include a .range[] member) 43 | flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) 44 | backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) 45 | }, 46 | 47 | performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), 48 | rules: [...], 49 | conditions: {associative list: name ==> set}, 50 | } 51 | } 52 | 53 | 54 | token location info (@$, _$, etc.): { 55 | first_line: n, 56 | last_line: n, 57 | first_column: n, 58 | last_column: n, 59 | range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) 60 | } 61 | 62 | 63 | the parseError function receives a 'hash' object with these members for lexer and parser errors: { 64 | text: (matched text) 65 | token: (the produced terminal token, if any) 66 | line: (yylineno) 67 | } 68 | while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { 69 | loc: (yylloc) 70 | expected: (string describing the set of expected tokens) 71 | recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) 72 | } 73 | */ 74 | var parser = (function(){ 75 | var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,8],$V1=[1,16],$V2=[1,18],$V3=[9,12,24],$V4=[1,25],$V5=[1,26],$V6=[1,27],$V7=[9,12,24,26],$V8=[12,26]; 76 | var parser = {trace: function trace() { }, 77 | yy: {}, 78 | symbols_: {"error":2,"START":3,"result_record":4,"async_output":5,"token":6,"^":7,"result_with_class":8,"EOF":9,"NUMBER":10,"result_class":11,",":12,"result_one_more":13,"STRING":14,"result":15,"variable":16,"=":17,"value":18,"const":19,"tuple":20,"list":21,"CSTRING":22,"{":23,"}":24,"[":25,"]":26,"value_one_more":27,"list_result_one_more":28,"$accept":0,"$end":1}, 79 | terminals_: {2:"error",7:"^",9:"EOF",10:"NUMBER",12:",",14:"STRING",17:"=",22:"CSTRING",23:"{",24:"}",25:"[",26:"]"}, 80 | productions_: [0,[3,1],[3,1],[4,4],[6,1],[8,1],[8,3],[11,1],[13,1],[13,3],[15,3],[16,1],[18,1],[18,1],[18,1],[19,1],[20,2],[20,3],[21,2],[21,3],[21,3],[27,1],[27,3],[28,1],[28,3],[5,2]], 81 | performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { 82 | /* this == yyval */ 83 | 84 | var $0 = $$.length - 1; 85 | switch (yystate) { 86 | case 1: case 2: 87 | return $$[$0] 88 | break; 89 | case 3: 90 | this.$ = {token: $$[$0-3], clazz: $$[$0-1].clazz, result: $$[$0-1].result}; 91 | break; 92 | case 4: 93 | this.$ = Number(yytext); 94 | break; 95 | case 5: 96 | this.$ = {clazz: $$[$0], result: null}; 97 | break; 98 | case 6: 99 | this.$ = {clazz: $$[$0-2], result: $$[$0]}; 100 | break; 101 | case 7: 102 | this.$ = RESULT[$$[$0].toUpperCase()]; 103 | break; 104 | case 8: case 11: case 12: case 13: case 14: 105 | this.$ = $$[$0]; 106 | break; 107 | case 9: 108 | this.$ = $$[$0-2]; var key = Object.keys($$[$0])[0]; this.$[key] = $$[$0][key]; 109 | break; 110 | case 10: 111 | this.$ = {}; this.$[$$[$0-2]] = $$[$0]; 112 | break; 113 | case 15: 114 | this.$ = $$[$0].substr(1, $$[$0].length-2); 115 | break; 116 | case 16: 117 | this.$ = {}; 118 | break; 119 | case 17: case 19: case 20: case 25: 120 | this.$ = $$[$0-1]; 121 | break; 122 | case 18: 123 | this.$ = []; 124 | break; 125 | case 21: 126 | this.$ = [$$[$0]]; 127 | break; 128 | case 22: 129 | this.$ = $$[$0-2].concat($$[$0]); 130 | break; 131 | case 23: 132 | var key = Object.keys($$[$0])[0]; this.$ = {}; this.$[key] = [$$[$0][key]]; 133 | break; 134 | case 24: 135 | var key = Object.keys($$[$0])[0]; this.$ = {}; this.$[key] = $$[$0-2][key].concat($$[$0][key]); 136 | break; 137 | } 138 | }, 139 | table: [{3:1,4:2,5:3,6:4,8:5,10:[1,6],11:7,14:$V0},{1:[3]},{1:[2,1]},{1:[2,2]},{7:[1,9]},{9:[1,10]},{7:[2,4]},{9:[2,5],12:[1,11]},o([9,12],[2,7]),{8:12,11:7,14:$V0},{1:[2,25]},{13:13,14:$V1,15:14,16:15},{9:[1,17]},{9:[2,6],12:$V2},o($V3,[2,8]),{17:[1,19]},{17:[2,11]},{1:[2,3]},{14:$V1,15:20,16:15},{18:21,19:22,20:23,21:24,22:$V4,23:$V5,25:$V6},o($V3,[2,9]),o($V7,[2,10]),o($V7,[2,12]),o($V7,[2,13]),o($V7,[2,14]),o($V7,[2,15]),{13:29,14:$V1,15:14,16:15,24:[1,28]},{14:$V1,15:34,16:15,18:33,19:22,20:23,21:24,22:$V4,23:$V5,25:$V6,26:[1,30],27:31,28:32},o($V7,[2,16]),{12:$V2,24:[1,35]},o($V7,[2,18]),{12:[1,37],26:[1,36]},{12:[1,39],26:[1,38]},o($V8,[2,21]),o($V8,[2,23]),o($V7,[2,17]),o($V7,[2,19]),{18:40,19:22,20:23,21:24,22:$V4,23:$V5,25:$V6},o($V7,[2,20]),{14:$V1,15:41,16:15},o($V8,[2,22]),o($V8,[2,24])], 140 | defaultActions: {2:[2,1],3:[2,2],6:[2,4],10:[2,25],16:[2,11],17:[2,3]}, 141 | parseError: function parseError(str, hash) { 142 | if (hash.recoverable) { 143 | this.trace(str); 144 | } else { 145 | throw new Error(str); 146 | } 147 | }, 148 | parse: function parse(input) { 149 | var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; 150 | var args = lstack.slice.call(arguments, 1); 151 | var lexer = Object.create(this.lexer); 152 | var sharedState = { yy: {} }; 153 | for (var k in this.yy) { 154 | if (Object.prototype.hasOwnProperty.call(this.yy, k)) { 155 | sharedState.yy[k] = this.yy[k]; 156 | } 157 | } 158 | lexer.setInput(input, sharedState.yy); 159 | sharedState.yy.lexer = lexer; 160 | sharedState.yy.parser = this; 161 | if (typeof lexer.yylloc == 'undefined') { 162 | lexer.yylloc = {}; 163 | } 164 | var yyloc = lexer.yylloc; 165 | lstack.push(yyloc); 166 | var ranges = lexer.options && lexer.options.ranges; 167 | if (typeof sharedState.yy.parseError === 'function') { 168 | this.parseError = sharedState.yy.parseError; 169 | } else { 170 | this.parseError = Object.getPrototypeOf(this).parseError; 171 | } 172 | function popStack(n) { 173 | stack.length = stack.length - 2 * n; 174 | vstack.length = vstack.length - n; 175 | lstack.length = lstack.length - n; 176 | } 177 | _token_stack: 178 | function lex() { 179 | var token; 180 | token = lexer.lex() || EOF; 181 | if (typeof token !== 'number') { 182 | token = self.symbols_[token] || token; 183 | } 184 | return token; 185 | } 186 | var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; 187 | while (true) { 188 | state = stack[stack.length - 1]; 189 | if (this.defaultActions[state]) { 190 | action = this.defaultActions[state]; 191 | } else { 192 | if (symbol === null || typeof symbol == 'undefined') { 193 | symbol = lex(); 194 | } 195 | action = table[state] && table[state][symbol]; 196 | } 197 | if (typeof action === 'undefined' || !action.length || !action[0]) { 198 | var errStr = ''; 199 | expected = []; 200 | for (p in table[state]) { 201 | if (this.terminals_[p] && p > TERROR) { 202 | expected.push('\'' + this.terminals_[p] + '\''); 203 | } 204 | } 205 | if (lexer.showPosition) { 206 | errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; 207 | } else { 208 | errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); 209 | } 210 | this.parseError(errStr, { 211 | text: lexer.match, 212 | token: this.terminals_[symbol] || symbol, 213 | line: lexer.yylineno, 214 | loc: yyloc, 215 | expected: expected 216 | }); 217 | } 218 | if (action[0] instanceof Array && action.length > 1) { 219 | throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); 220 | } 221 | switch (action[0]) { 222 | case 1: 223 | stack.push(symbol); 224 | vstack.push(lexer.yytext); 225 | lstack.push(lexer.yylloc); 226 | stack.push(action[1]); 227 | symbol = null; 228 | if (!preErrorSymbol) { 229 | yyleng = lexer.yyleng; 230 | yytext = lexer.yytext; 231 | yylineno = lexer.yylineno; 232 | yyloc = lexer.yylloc; 233 | if (recovering > 0) { 234 | recovering--; 235 | } 236 | } else { 237 | symbol = preErrorSymbol; 238 | preErrorSymbol = null; 239 | } 240 | break; 241 | case 2: 242 | len = this.productions_[action[1]][1]; 243 | yyval.$ = vstack[vstack.length - len]; 244 | yyval._$ = { 245 | first_line: lstack[lstack.length - (len || 1)].first_line, 246 | last_line: lstack[lstack.length - 1].last_line, 247 | first_column: lstack[lstack.length - (len || 1)].first_column, 248 | last_column: lstack[lstack.length - 1].last_column 249 | }; 250 | if (ranges) { 251 | yyval._$.range = [ 252 | lstack[lstack.length - (len || 1)].range[0], 253 | lstack[lstack.length - 1].range[1] 254 | ]; 255 | } 256 | r = this.performAction.apply(yyval, [ 257 | yytext, 258 | yyleng, 259 | yylineno, 260 | sharedState.yy, 261 | action[1], 262 | vstack, 263 | lstack 264 | ].concat(args)); 265 | if (typeof r !== 'undefined') { 266 | return r; 267 | } 268 | if (len) { 269 | stack = stack.slice(0, -1 * len * 2); 270 | vstack = vstack.slice(0, -1 * len); 271 | lstack = lstack.slice(0, -1 * len); 272 | } 273 | stack.push(this.productions_[action[1]][0]); 274 | vstack.push(yyval.$); 275 | lstack.push(yyval._$); 276 | newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; 277 | stack.push(newState); 278 | break; 279 | case 3: 280 | return true; 281 | } 282 | } 283 | return true; 284 | }}; 285 | var RESULT = {DONE: 0, RUNNING: 1, CONNECTED: 2, ERROR: 3, EXIT: 4, STOPPED: 5}; exports.RESULT = RESULT;/* generated by jison-lex 0.3.4 */ 286 | var lexer = (function(){ 287 | var lexer = ({ 288 | 289 | EOF:1, 290 | 291 | parseError:function parseError(str, hash) { 292 | if (this.yy.parser) { 293 | this.yy.parser.parseError(str, hash); 294 | } else { 295 | throw new Error(str); 296 | } 297 | }, 298 | 299 | // resets the lexer, sets new input 300 | setInput:function (input, yy) { 301 | this.yy = yy || this.yy || {}; 302 | this._input = input; 303 | this._more = this._backtrack = this.done = false; 304 | this.yylineno = this.yyleng = 0; 305 | this.yytext = this.matched = this.match = ''; 306 | this.conditionStack = ['INITIAL']; 307 | this.yylloc = { 308 | first_line: 1, 309 | first_column: 0, 310 | last_line: 1, 311 | last_column: 0 312 | }; 313 | if (this.options.ranges) { 314 | this.yylloc.range = [0,0]; 315 | } 316 | this.offset = 0; 317 | return this; 318 | }, 319 | 320 | // consumes and returns one char from the input 321 | input:function () { 322 | var ch = this._input[0]; 323 | this.yytext += ch; 324 | this.yyleng++; 325 | this.offset++; 326 | this.match += ch; 327 | this.matched += ch; 328 | var lines = ch.match(/(?:\r\n?|\n).*/g); 329 | if (lines) { 330 | this.yylineno++; 331 | this.yylloc.last_line++; 332 | } else { 333 | this.yylloc.last_column++; 334 | } 335 | if (this.options.ranges) { 336 | this.yylloc.range[1]++; 337 | } 338 | 339 | this._input = this._input.slice(1); 340 | return ch; 341 | }, 342 | 343 | // unshifts one char (or a string) into the input 344 | unput:function (ch) { 345 | var len = ch.length; 346 | var lines = ch.split(/(?:\r\n?|\n)/g); 347 | 348 | this._input = ch + this._input; 349 | this.yytext = this.yytext.substr(0, this.yytext.length - len); 350 | //this.yyleng -= len; 351 | this.offset -= len; 352 | var oldLines = this.match.split(/(?:\r\n?|\n)/g); 353 | this.match = this.match.substr(0, this.match.length - 1); 354 | this.matched = this.matched.substr(0, this.matched.length - 1); 355 | 356 | if (lines.length - 1) { 357 | this.yylineno -= lines.length - 1; 358 | } 359 | var r = this.yylloc.range; 360 | 361 | this.yylloc = { 362 | first_line: this.yylloc.first_line, 363 | last_line: this.yylineno + 1, 364 | first_column: this.yylloc.first_column, 365 | last_column: lines ? 366 | (lines.length === oldLines.length ? this.yylloc.first_column : 0) 367 | + oldLines[oldLines.length - lines.length].length - lines[0].length : 368 | this.yylloc.first_column - len 369 | }; 370 | 371 | if (this.options.ranges) { 372 | this.yylloc.range = [r[0], r[0] + this.yyleng - len]; 373 | } 374 | this.yyleng = this.yytext.length; 375 | return this; 376 | }, 377 | 378 | // When called from action, caches matched text and appends it on next action 379 | more:function () { 380 | this._more = true; 381 | return this; 382 | }, 383 | 384 | // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. 385 | reject:function () { 386 | if (this.options.backtrack_lexer) { 387 | this._backtrack = true; 388 | } else { 389 | return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { 390 | text: "", 391 | token: null, 392 | line: this.yylineno 393 | }); 394 | 395 | } 396 | return this; 397 | }, 398 | 399 | // retain first n characters of the match 400 | less:function (n) { 401 | this.unput(this.match.slice(n)); 402 | }, 403 | 404 | // displays already matched input, i.e. for error messages 405 | pastInput:function () { 406 | var past = this.matched.substr(0, this.matched.length - this.match.length); 407 | return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); 408 | }, 409 | 410 | // displays upcoming input, i.e. for error messages 411 | upcomingInput:function () { 412 | var next = this.match; 413 | if (next.length < 20) { 414 | next += this._input.substr(0, 20-next.length); 415 | } 416 | return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); 417 | }, 418 | 419 | // displays the character position where the lexing error occurred, i.e. for error messages 420 | showPosition:function () { 421 | var pre = this.pastInput(); 422 | var c = new Array(pre.length + 1).join("-"); 423 | return pre + this.upcomingInput() + "\n" + c + "^"; 424 | }, 425 | 426 | // test the lexed token: return FALSE when not a match, otherwise return token 427 | test_match:function (match, indexed_rule) { 428 | var token, 429 | lines, 430 | backup; 431 | 432 | if (this.options.backtrack_lexer) { 433 | // save context 434 | backup = { 435 | yylineno: this.yylineno, 436 | yylloc: { 437 | first_line: this.yylloc.first_line, 438 | last_line: this.last_line, 439 | first_column: this.yylloc.first_column, 440 | last_column: this.yylloc.last_column 441 | }, 442 | yytext: this.yytext, 443 | match: this.match, 444 | matches: this.matches, 445 | matched: this.matched, 446 | yyleng: this.yyleng, 447 | offset: this.offset, 448 | _more: this._more, 449 | _input: this._input, 450 | yy: this.yy, 451 | conditionStack: this.conditionStack.slice(0), 452 | done: this.done 453 | }; 454 | if (this.options.ranges) { 455 | backup.yylloc.range = this.yylloc.range.slice(0); 456 | } 457 | } 458 | 459 | lines = match[0].match(/(?:\r\n?|\n).*/g); 460 | if (lines) { 461 | this.yylineno += lines.length; 462 | } 463 | this.yylloc = { 464 | first_line: this.yylloc.last_line, 465 | last_line: this.yylineno + 1, 466 | first_column: this.yylloc.last_column, 467 | last_column: lines ? 468 | lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : 469 | this.yylloc.last_column + match[0].length 470 | }; 471 | this.yytext += match[0]; 472 | this.match += match[0]; 473 | this.matches = match; 474 | this.yyleng = this.yytext.length; 475 | if (this.options.ranges) { 476 | this.yylloc.range = [this.offset, this.offset += this.yyleng]; 477 | } 478 | this._more = false; 479 | this._backtrack = false; 480 | this._input = this._input.slice(match[0].length); 481 | this.matched += match[0]; 482 | token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); 483 | if (this.done && this._input) { 484 | this.done = false; 485 | } 486 | if (token) { 487 | return token; 488 | } else if (this._backtrack) { 489 | // recover context 490 | for (var k in backup) { 491 | this[k] = backup[k]; 492 | } 493 | return false; // rule action called reject() implying the next rule should be tested instead. 494 | } 495 | return false; 496 | }, 497 | 498 | // return next match in input 499 | next:function () { 500 | if (this.done) { 501 | return this.EOF; 502 | } 503 | if (!this._input) { 504 | this.done = true; 505 | } 506 | 507 | var token, 508 | match, 509 | tempMatch, 510 | index; 511 | if (!this._more) { 512 | this.yytext = ''; 513 | this.match = ''; 514 | } 515 | var rules = this._currentRules(); 516 | for (var i = 0; i < rules.length; i++) { 517 | tempMatch = this._input.match(this.rules[rules[i]]); 518 | if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { 519 | match = tempMatch; 520 | index = i; 521 | if (this.options.backtrack_lexer) { 522 | token = this.test_match(tempMatch, rules[i]); 523 | if (token !== false) { 524 | return token; 525 | } else if (this._backtrack) { 526 | match = false; 527 | continue; // rule action called reject() implying a rule MISmatch. 528 | } else { 529 | // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) 530 | return false; 531 | } 532 | } else if (!this.options.flex) { 533 | break; 534 | } 535 | } 536 | } 537 | if (match) { 538 | token = this.test_match(match, rules[index]); 539 | if (token !== false) { 540 | return token; 541 | } 542 | // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) 543 | return false; 544 | } 545 | if (this._input === "") { 546 | return this.EOF; 547 | } else { 548 | return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { 549 | text: "", 550 | token: null, 551 | line: this.yylineno 552 | }); 553 | } 554 | }, 555 | 556 | // return next match that has a token 557 | lex:function lex() { 558 | var r = this.next(); 559 | if (r) { 560 | return r; 561 | } else { 562 | return this.lex(); 563 | } 564 | }, 565 | 566 | // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) 567 | begin:function begin(condition) { 568 | this.conditionStack.push(condition); 569 | }, 570 | 571 | // pop the previously active lexer condition state off the condition stack 572 | popState:function popState() { 573 | var n = this.conditionStack.length - 1; 574 | if (n > 0) { 575 | return this.conditionStack.pop(); 576 | } else { 577 | return this.conditionStack[0]; 578 | } 579 | }, 580 | 581 | // produce the lexer rule set which is active for the currently active lexer condition state 582 | _currentRules:function _currentRules() { 583 | if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { 584 | return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; 585 | } else { 586 | return this.conditions["INITIAL"].rules; 587 | } 588 | }, 589 | 590 | // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available 591 | topState:function topState(n) { 592 | n = this.conditionStack.length - 1 - Math.abs(n || 0); 593 | if (n >= 0) { 594 | return this.conditionStack[n]; 595 | } else { 596 | return "INITIAL"; 597 | } 598 | }, 599 | 600 | // alias for begin(condition) 601 | pushState:function pushState(condition) { 602 | this.begin(condition); 603 | }, 604 | 605 | // return the number of states currently on the stack 606 | stateStackSize:function stateStackSize() { 607 | return this.conditionStack.length; 608 | }, 609 | options: {}, 610 | performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { 611 | var YYSTATE=YY_START; 612 | switch($avoiding_name_collisions) { 613 | case 0:return 10 614 | break; 615 | case 1:return 22; 616 | break; 617 | case 2:return yy_.yytext; 618 | break; 619 | case 3:return 14; 620 | break; 621 | case 4:/* skip whitespace */ 622 | break; 623 | case 5:return 9; 624 | break; 625 | } 626 | }, 627 | rules: [/^(?:[0-9]+)/,/^(?:"(\\.|[^"])*")/,/^(?:[\^\,=\[\]\{\}"*])/,/^(?:[^\^\,=\[\]\{\}"*\s]+)/,/^(?:\s+)/,/^(?:$)/], 628 | conditions: {"INITIAL":{"rules":[0,1,2,3,4,5],"inclusive":true}} 629 | }); 630 | return lexer; 631 | })(); 632 | parser.lexer = lexer; 633 | function Parser () { 634 | this.yy = {}; 635 | } 636 | Parser.prototype = parser;parser.Parser = Parser; 637 | return new Parser; 638 | })(); 639 | 640 | 641 | if (typeof require !== 'undefined' && typeof exports !== 'undefined') { 642 | exports.parser = parser; 643 | exports.Parser = parser.Parser; 644 | exports.parse = function () { return parser.parse.apply(parser, arguments); }; 645 | exports.main = function commonjsMain(args) { 646 | if (!args[1]) { 647 | console.log('Usage: '+args[0]+' FILE'); 648 | process.exit(1); 649 | } 650 | var source = require('fs').readFileSync(require('path').normalize(args[1]), "utf8"); 651 | return exports.parser.parse(source); 652 | }; 653 | if (typeof module !== 'undefined' && require.main === module) { 654 | exports.main(process.argv.slice(1)); 655 | } 656 | } -------------------------------------------------------------------------------- /lib/backend/gdb/gdb-mi-parser.json: -------------------------------------------------------------------------------- 1 | { 2 | "lex": { 3 | "rules": [ 4 | ["[0-9]+", "return 'NUMBER'"], 5 | ["\"(\\\\.|[^\"])*\"", "return 'CSTRING';"], 6 | ["[\\^\\,=\\[\\]\\{\\}\"*]", "return yytext;"], 7 | ["[^\\^\\,=\\[\\]\\{\\}\"*\\s]+","return 'STRING';"], 8 | ["\\s+", "/* skip whitespace */"], 9 | ["$", "return 'EOF';"] 10 | ] 11 | }, 12 | 13 | "start": "START", 14 | 15 | "moduleInclude": "var RESULT = {DONE: 0, RUNNING: 1, CONNECTED: 2, ERROR: 3, EXIT: 4, STOPPED: 5}; exports.RESULT = RESULT;", 16 | 17 | "bnf": { 18 | "START": [ 19 | ["result_record", "return $1"], 20 | ["async_output", "return $1"] 21 | ], 22 | "result_record": [["token ^ result_with_class EOF", "$$ = {token: $1, clazz: $3.clazz, result: $3.result};"]], 23 | "token": [["NUMBER", "$$ = Number(yytext);"]], 24 | "result_with_class": [ 25 | ["result_class", "$$ = {clazz: $1, result: null};"], 26 | ["result_class , result_one_more", "$$ = {clazz: $1, result: $3};"] 27 | ], 28 | "result_class": [["STRING", "$$ = RESULT[$1.toUpperCase()];"]], 29 | "result_one_more": [ 30 | ["result", "$$ = $1;"], 31 | ["result_one_more , result", "$$ = $1; var key = Object.keys($3)[0]; $$[key] = $3[key];"] 32 | ], 33 | "result": [["variable = value", "$$ = {}; $$[$1] = $3;"]], 34 | "variable": [["STRING", "$$ = $1;"]], 35 | "value": [ 36 | ["const", "$$ = $1;"], 37 | ["tuple", "$$ = $1;"], 38 | ["list", "$$ = $1;"] 39 | ], 40 | "const": [["CSTRING", "$$ = $1.substr(1, $1.length-2);"]], 41 | "tuple": [ 42 | ["{ }", "$$ = {};"], 43 | ["{ result_one_more }", "$$ = $2;"] 44 | ], 45 | "list": [ 46 | ["[ ]", "$$ = [];"], 47 | ["[ value_one_more ]", "$$ = $2;"], 48 | ["[ list_result_one_more ]", "$$ = $2;"] 49 | ], 50 | "value_one_more": [ 51 | ["value", "$$ = [$1];"], 52 | ["value_one_more , value", "$$ = $1.concat($3);"] 53 | ], 54 | "list_result_one_more": [ 55 | ["result", "var key = Object.keys($1)[0]; $$ = {}; $$[key] = [$1[key]];"], 56 | ["list_result_one_more , result", "var key = Object.keys($3)[0]; $$ = {}; $$[key] = $1[key].concat($3[key]);"] 57 | ], 58 | "async_output": [["result_with_class EOF", "$$ = $1;"]] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/backend/gdb/gdb.coffee: -------------------------------------------------------------------------------- 1 | {BufferedProcess, Emitter} = require 'atom' 2 | {RESULT, parser} = require './gdb-mi-parser' 3 | 4 | module.exports = 5 | class GDB 6 | 7 | STATUS = 8 | NOTHING: 0 9 | RUNNING: 1 10 | 11 | constructor: (target) -> 12 | @token = 0 13 | @handler = {} 14 | @emitter = new Emitter 15 | 16 | stdout = (lines) => 17 | for line in lines.split('\n') 18 | switch line[0] 19 | when '+' then null # status-async-output 20 | when '=' then null # notify-async-output 21 | when '~' then null # console-stream-output 22 | when '@' then null # target-stream-output 23 | when '&' then null # log-stream-output 24 | when '*' # exec-async-output 25 | {clazz, result} = parser.parse(line.substr(1)) 26 | @emitter.emit 'exec-async-output', {clazz, result} 27 | @emitter.emit "exec-async-running", result if clazz == RESULT.RUNNING 28 | @emitter.emit "exec-async-stopped", result if clazz == RESULT.STOPPED 29 | 30 | else # result-record 31 | if line[0] <= '9' and line[0] >= '0' 32 | {token, clazz, result} = parser.parse(line) 33 | @handler[token](clazz, result) 34 | delete @handler[token] 35 | 36 | stderr = (lines) => 37 | 38 | command = 'gdb' 39 | args = ['--interpreter=mi2', target] # 40 | console.log("target", target) 41 | @process = new BufferedProcess({command, args, stdout, stderr}).process 42 | @stdin = @process.stdin 43 | @status = STATUS.NOTHING 44 | 45 | destroy: -> 46 | @process.kill() 47 | @emitter.dispose() 48 | 49 | onExecAsyncOutput: (callback) -> 50 | @emitter.on 'exec-async-output', callback 51 | 52 | onExecAsyncStopped: (callback) -> 53 | @emitter.on 'exec-async-stopped', callback 54 | 55 | onExecAsyncRunning: (callback) -> 56 | @emitter.on 'exec-async-running', callback 57 | 58 | listFiles: (handler) -> 59 | @postCommand 'file-list-exec-source-files', (clazz, result) => 60 | files = [] 61 | if clazz == RESULT.DONE 62 | for file in result.files 63 | files.push(file.fullname) 64 | handler(files) 65 | 66 | listExecFile: (handler) -> 67 | @postCommand 'file-list-exec-source-file', (clazz, result) => 68 | file = null 69 | if clazz == RESULT.DONE 70 | file = result 71 | handler(file) 72 | 73 | setSourceDirectories: (directories, handler) -> 74 | args = [] 75 | args.push("\"#{directory}\"") for directory in directories 76 | 77 | command = 'environment-directory ' + args.join(' ') 78 | @postCommand command, (clazz, result) => 79 | handler(clazz == RESULT.DONE) 80 | 81 | listBreaks: (handler) -> 82 | @postCommand 'break-list', (clazz, result) => 83 | breaks = [] 84 | if clazz == RESULT.DONE and result.BreakpointTable.body.bkpt 85 | breaks = result.BreakpointTable.body.bkpt 86 | handler(breaks) 87 | 88 | deleteBreak: (number, handler) -> 89 | command = "break-delete #{number}" 90 | @postCommand command, (clazz, result) => 91 | handler(clazz == RESULT.DONE) 92 | 93 | disassembleData: ({address, file, mode}, handler) -> 94 | args = [] 95 | if address 96 | args.push("-s #{address.start}") 97 | args.push("-e #{address.end}") 98 | else if file 99 | args.push("-f #{file.name}") 100 | args.push("-l #{file.linenum}") 101 | args.push("-n #{file.lines}") if file.lines 102 | args.push("-- #{mode}") 103 | 104 | command = 'data-disassemble ' + args.join(' ') 105 | 106 | @postCommand command, (clazz, result) => 107 | instructions = [] 108 | if clazz == RESULT.DONE 109 | instructions = result.asm_insns.src_and_asm_line 110 | handler(instructions) 111 | 112 | insertBreak: ({location, condition, count, thread, temporary, hardware, disabled, tracepoint}, handler) -> 113 | args = [] 114 | args.push('-t') if temporary is true 115 | args.push('-h') if hardware is true 116 | args.push('-d') if disabled is true 117 | args.push('-a') if tracepoint is true 118 | args.push("-c #{condition}") if condition 119 | args.push("-i #{count}") if count 120 | args.push("-p #{thread}") if thread 121 | args.push(location) 122 | 123 | command = 'break-insert ' + args.join(' ') 124 | 125 | @postCommand command, (clazz, result) => 126 | abreak = null 127 | if clazz == RESULT.DONE 128 | abreak = result.bkpt 129 | handler(abreak) 130 | 131 | run: (handler) -> 132 | command = 'exec-run' 133 | @postCommand command, (clazz, result) => 134 | handler(clazz == RESULT.RUNNING) 135 | 136 | continue: (handler) -> 137 | command = 'exec-continue' 138 | @postCommand command, (clazz, result) => 139 | handler(clazz == RESULT.RUNNING) 140 | 141 | interrupt: (handler) -> 142 | command = 'exec-interrupt' 143 | @postCommand command, (clazz, result) => 144 | handler(clazz == RESULT.DONE) 145 | 146 | next: (handler) -> 147 | command = 'exec-next' 148 | @postCommand command, (clazz, result) => 149 | handler(clazz == RESULT.RUNNING) 150 | 151 | step: (handler) -> 152 | command = 'exec-step' 153 | @postCommand command, (clazz, result) => 154 | handler(clazz == RESULT.RUNNING) 155 | 156 | set: (key, value, handler) -> 157 | command = "gdb-set #{key} #{value}" 158 | @postCommand command, (clazz, result) => 159 | handler(clazz == RESULT.DONE) 160 | 161 | postCommand: (command, handler) -> 162 | @handler[@token] = handler 163 | @stdin.write("#{@token}-#{command}\n") 164 | @token = @token + 1 165 | -------------------------------------------------------------------------------- /lib/debugger-view.coffee: -------------------------------------------------------------------------------- 1 | {Point, Range, TextEditor, TextBuffer, CompositeDisposable} = require 'atom' 2 | {View} = require 'atom-space-pen-views' 3 | GDB = require './backend/gdb/gdb' 4 | fs = require 'fs' 5 | path = require 'path' 6 | AsmViewer = require './asm-viewer' 7 | 8 | 9 | module.exports = 10 | class DebuggerView extends View 11 | @content: -> 12 | @div class: 'atom-debugger', => 13 | @header class: 'header', => 14 | @span class: 'header-item title', 'Atom Debugger' 15 | @span class: 'header-item sub-title', outlet: 'targetLable' 16 | @div class: 'btn-toolbar', => 17 | @div class: 'btn-group', => 18 | @div class: 'btn', outlet: 'runButton', 'Run' 19 | @div class: 'btn disabled', outlet: 'continueButton', 'Continue' 20 | @div class: 'btn disabled', outlet: 'interruptButton', 'Interrupt' 21 | @div class: 'btn disabled', outlet: 'nextButton', 'Next' 22 | @div class: 'btn disabled', outlet: 'stepButton', 'Step' 23 | 24 | initialize: (target, mainBreak) -> 25 | @GDB = new GDB(target) 26 | @targetLable.text(target) 27 | 28 | @GDB.set 'target-async', 'on', (result) -> 29 | @GDB.setSourceDirectories atom.project.getPaths(), (done) -> 30 | 31 | @breaks = {} 32 | @stopped = {marker: null, fullpath: null, line: null} 33 | @asms = {} 34 | @cachedEditors = {} 35 | @handleEvents() 36 | 37 | contextMenuCreated = (event) => 38 | if editor = @getActiveTextEditor() 39 | component = atom.views.getView(editor).component 40 | position = component.screenPositionForMouseEvent(event) 41 | @contextLine = editor.bufferPositionForScreenPosition(position).row 42 | 43 | @menu = atom.contextMenu.add { 44 | 'atom-text-editor': [{ 45 | label: 'Toggle Breakpoint', 46 | command: 'debugger:toggle-breakpoint', 47 | created: contextMenuCreated 48 | }] 49 | } 50 | 51 | @panel = atom.workspace.addBottomPanel(item: @, visible: true) 52 | 53 | @insertMainBreak() if mainBreak 54 | @listExecFile() 55 | 56 | getActiveTextEditor: -> 57 | atom.workspace.getActiveTextEditor() 58 | 59 | exists: (fullpath) -> 60 | return fs.existsSync(fullpath) 61 | 62 | getEditor: (fullpath) -> 63 | return @cachedEditors[fullpath] 64 | 65 | goExitedStatus: -> 66 | @continueButton.addClass('disabled') 67 | @interruptButton.addClass('disabled') 68 | @stepButton.addClass('disabled') 69 | @nextButton.addClass('disabled') 70 | @removeClass('running') 71 | @addClass('stopped') 72 | 73 | goStoppedStatus: -> 74 | @continueButton.removeClass('disabled') 75 | @interruptButton.addClass('disabled') 76 | @stepButton.removeClass('disabled') 77 | @nextButton.removeClass('disabled') 78 | @removeClass('running') 79 | @addClass('stopped') 80 | 81 | goRunningStatus: -> 82 | @stopped.marker?.destroy() 83 | @stopped = {marker: null, fullpath: null, line: null} 84 | @continueButton.addClass('disabled') 85 | @interruptButton.removeClass('disabled') 86 | @stepButton.addClass('disabled') 87 | @nextButton.addClass('disabled') 88 | @removeClass('stopped') 89 | @addClass('running') 90 | 91 | insertMainBreak: -> 92 | @GDB.insertBreak {location: 'main'}, (abreak) => 93 | if abreak 94 | if abreak.fullname 95 | fullpath = path.resolve(abreak.fullname) 96 | line = Number(abreak.line)-1 97 | @insertBreakWithoutEditor(fullpath, line) 98 | else 99 | atom.confirm 100 | detailedMessage: "Can't find debugging symbols\nPlease recompile with `-g` option." 101 | buttons: 102 | Exit: => @destroy() 103 | 104 | listExecFile: -> 105 | @GDB.listExecFile (file) => 106 | if file 107 | fullpath = path.resolve(file.fullname) 108 | line = Number(file.line) - 1 109 | if @exists(fullpath) 110 | atom.workspace.open fullpath, (editor) => 111 | @moveToLine(editor, line) 112 | else 113 | atom.confirm 114 | detailedMessage: "Can't find file #{file.file}\nPlease add path to tree-view and try again." 115 | buttons: 116 | Exit: => @destroy() 117 | 118 | toggleBreak: (editor, line) -> 119 | if @hasBreak(editor, line) 120 | @deleteBreak(editor, line) 121 | else 122 | @insertBreak(editor, line) 123 | 124 | hasBreak: (editor, line) -> 125 | return line of @breaks[editor.getPath()] 126 | 127 | deleteBreak: (editor, line) -> 128 | fullpath = editor.getPath() 129 | {abreak, marker} = @breaks[fullpath][line] 130 | @GDB.deleteBreak abreak.number, (done) => 131 | if done 132 | marker.destroy() 133 | delete @breaks[fullpath][line] 134 | 135 | insertBreak: (editor, line) -> 136 | fullpath = editor.getPath() 137 | @GDB.insertBreak {location: "#{fullpath}:#{line+1}"}, (abreak) => 138 | if abreak 139 | marker = @markBreakLine(editor, line) 140 | @breaks[fullpath][line] = {abreak, marker} 141 | 142 | insertBreakWithoutEditor: (fullpath, line) -> 143 | @breaks[fullpath] ?= {} 144 | @GDB.insertBreak {location: "#{fullpath}:#{line+1}"}, (abreak) => 145 | if abreak 146 | if editor = @getEditor(fullpath) 147 | marker = @markBreakLine(editor, line) 148 | else 149 | marker = null 150 | @breaks[fullpath][line] = {abreak, marker} 151 | 152 | moveToLine: (editor, line) -> 153 | editor.scrollToBufferPosition(new Point(line)) 154 | editor.setCursorBufferPosition(new Point(line)) 155 | editor.moveToFirstCharacterOfLine() 156 | 157 | markBreakLine: (editor, line) -> 158 | range = new Range([line, 0], [line+1, 0]) 159 | marker = editor.markBufferRange(range, {invalidate: 'never'}) 160 | editor.decorateMarker(marker, {type: 'line-number', class: 'debugger-breakpoint-line'}) 161 | return marker 162 | 163 | markStoppedLine: (editor, line) -> 164 | range = new Range([line, 0], [line+1, 0]) 165 | marker = editor.markBufferRange(range, {invalidate: 'never'}) 166 | editor.decorateMarker(marker, {type: 'line-number', class: 'debugger-stopped-line'}) 167 | editor.decorateMarker(marker, {type: 'highlight', class: 'selection'}) 168 | 169 | @moveToLine(editor, line) 170 | return marker 171 | 172 | refreshBreakMarkers: (editor) -> 173 | fullpath = editor.getPath() 174 | for line, {abreak, marker} of @breaks[fullpath] 175 | marker = @markBreakLine(editor, Number(line)) 176 | @breaks[fullpath][line] = {abreak, marker} 177 | 178 | refreshStoppedMarker: (editor) -> 179 | fullpath = editor.getPath() 180 | if fullpath == @stopped.fullpath 181 | @stopped.marker = @markStoppedLine(editor, @stopped.line) 182 | 183 | hackGutterDblClick: (editor) -> 184 | component = atom.views.getView(editor).component 185 | # gutterComponent has been renamed to gutterContainerComponent 186 | gutter = component.gutterComponent 187 | gutter ?= component.gutterContainerComponent 188 | 189 | gutter.domNode.addEventListener 'dblclick', (event) => 190 | position = component.screenPositionForMouseEvent(event) 191 | line = editor.bufferPositionForScreenPosition(position).row 192 | @toggleBreak(editor, line) 193 | selection = editor.selectionsForScreenRows(line, line + 1)[0] 194 | selection?.clear() 195 | 196 | handleEvents: -> 197 | @subscriptions = new CompositeDisposable 198 | 199 | @subscriptions.add atom.commands.add 'atom-workspace', 'debugger:toggle-breakpoint', => 200 | @toggleBreak(@getActiveTextEditor(), @contextLine) 201 | 202 | @subscriptions.add atom.workspace.observeTextEditors (editor) => 203 | fullpath = editor.getPath() 204 | @cachedEditors[fullpath] = editor 205 | @breaks[fullpath] ?= {} 206 | @refreshBreakMarkers(editor) 207 | @refreshStoppedMarker(editor) 208 | @hackGutterDblClick(editor) 209 | 210 | @subscriptions.add atom.project.onDidChangePaths (paths) => 211 | @GDB.setSourceDirectories paths, (done) -> 212 | 213 | @runButton.on 'click', => 214 | @GDB.run (result) -> 215 | 216 | @continueButton.on 'click', => 217 | @GDB.continue (result) -> 218 | 219 | @interruptButton.on 'click', => 220 | @GDB.interrupt (result) -> 221 | 222 | @nextButton.on 'click', => 223 | @GDB.next (result) -> 224 | 225 | @stepButton.on 'click', => 226 | @GDB.step (result) -> 227 | 228 | @GDB.onExecAsyncRunning (result) => 229 | @goRunningStatus() 230 | 231 | @GDB.onExecAsyncStopped (result) => 232 | @goStoppedStatus() 233 | 234 | unless frame = result.frame 235 | @goExitedStatus() 236 | else 237 | fullpath = path.resolve(frame.fullname) 238 | line = Number(frame.line)-1 239 | 240 | if @exists(fullpath) 241 | atom.workspace.open(fullpath, {debugging: true, fullpath: fullpath, startline: line}).done (editor) => 242 | @stopped = {marker: @markStoppedLine(editor, line), fullpath, line} 243 | else 244 | @GDB.next (result) -> 245 | 246 | # Tear down any state and detach 247 | destroy: -> 248 | @GDB.destroy() 249 | @subscriptions.dispose() 250 | @stopped.marker?.destroy() 251 | @menu.dispose() 252 | 253 | for fullpath, breaks of @breaks 254 | for line, {abreak, marker} of breaks 255 | marker.destroy() 256 | 257 | for editor in atom.workspace.getTextEditors() 258 | component = atom.views.getView(editor).component 259 | gutter = component.gutterComponent 260 | gutter ?= component.gutterContainerComponent 261 | gutter.domNode.removeEventListener 'dblclick' 262 | 263 | @panel.destroy() 264 | @detach() 265 | -------------------------------------------------------------------------------- /lib/debugger.coffee: -------------------------------------------------------------------------------- 1 | OpenDialogView = require './open-dialog-view' 2 | DebuggerView = require './debugger-view' 3 | {CompositeDisposable} = require 'atom' 4 | fs = require 'fs' 5 | 6 | module.exports = Debugger = 7 | subscriptions: null 8 | 9 | activate: (state) -> 10 | # Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable 11 | @subscriptions = new CompositeDisposable 12 | 13 | # Register command that toggles this view 14 | @subscriptions.add atom.commands.add 'atom-workspace', 'debugger:toggle': => @toggle() 15 | @subscriptions.add atom.commands.add 'atom-workspace', 'core:close': => 16 | @debuggerView?.destroy() 17 | @debuggerView = null 18 | @subscriptions.add atom.commands.add 'atom-workspace', 'core:cancel': => 19 | @debuggerView?.destroy() 20 | @debuggerView = null 21 | 22 | deactivate: -> 23 | @subscriptions.dispose() 24 | @openDialogView.destroy() 25 | @debuggerView?.destroy() 26 | 27 | serialize: -> 28 | 29 | toggle: -> 30 | if @debuggerView and @debuggerView.hasParent() 31 | @debuggerView.destroy() 32 | @debuggerView = null 33 | else 34 | @openDialogView = new OpenDialogView (target, mainBreak) => 35 | if fs.existsSync(target) 36 | @debuggerView = new DebuggerView(target, mainBreak) 37 | else 38 | atom.confirm 39 | detailedMessage: "Can't find file #{target}." 40 | buttons: 41 | Exit: => 42 | -------------------------------------------------------------------------------- /lib/open-dialog-view.coffee: -------------------------------------------------------------------------------- 1 | {View, TextEditorView} = require 'atom-space-pen-views' 2 | 3 | module.exports = 4 | class OpenDialogView extends View 5 | @content: -> 6 | @div tabIndex: -1, class: 'atom-debugger', => 7 | @div class: 'block', => 8 | @label 'Atom Debugger' 9 | @subview 'targetEditor', new TextEditorView(mini: true, placeholderText: 'Target Binary File Path') 10 | @div class: 'checkbox', => 11 | @input type: 'checkbox', checked: 'true', outlet: 'mainBreakCheckbox' 12 | @label class: 'checkbox-label', 'Add breakpoint in `main` function' 13 | @div class: 'block', => 14 | @button class: 'inline-block btn', outlet: 'startButton', 'Start' 15 | @button class: 'inline-block btn', outlet: 'cancelButton', 'Cancel' 16 | 17 | initialize: (handler) -> 18 | @panel = atom.workspace.addModalPanel(item: this, visible: true) 19 | @targetEditor.focus() 20 | 21 | @cancelButton.on 'click', (e) => @destroy() 22 | @startButton.on 'click', (e) => 23 | handler(@targetEditor.getText(), @mainBreakCheckbox.prop('checked')) 24 | @destroy() 25 | 26 | destroy: -> 27 | @panel.destroy() 28 | -------------------------------------------------------------------------------- /menus/debugger.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/creating-a-package#menus for more details 2 | 'menu': [ 3 | { 4 | 'label': 'Packages' 5 | 'submenu': [ 6 | 'label': 'Atom Debugger' 7 | 'submenu': [ 8 | { 9 | 'label': 'Toggle Debugging...' 10 | 'command': 'debugger:toggle' 11 | } 12 | ] 13 | ] 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-debugger", 3 | "main": "./lib/debugger", 4 | "version": "0.1.6", 5 | "description": "GDB debugger for Atom", 6 | "activationCommands": { 7 | "atom-workspace": "debugger:toggle" 8 | }, 9 | "repository": "https://github.com/xndcn/atom-debugger", 10 | "license": "MIT", 11 | "engines": { 12 | "atom": ">=0.174.0 <2.0.0" 13 | }, 14 | "dependencies": { 15 | "atom-space-pen-views": "^2.0.0" 16 | }, 17 | "keywords": [ 18 | "debugger", 19 | "gdb", 20 | "debug" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xndcn/atom-debugger/2884e6edae34984c906ac6fb04542a54e3e40cae/screenshot.png -------------------------------------------------------------------------------- /spec/debugger-spec.coffee: -------------------------------------------------------------------------------- 1 | Debugger = require '../lib/debugger' 2 | 3 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 4 | # 5 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 6 | # or `fdescribe`). Remove the `f` to unfocus the block. 7 | 8 | describe "Debugger", -> 9 | [workspaceElement, activationPromise] = [] 10 | 11 | beforeEach -> 12 | workspaceElement = atom.views.getView(atom.workspace) 13 | activationPromise = atom.packages.activatePackage('debugger') 14 | 15 | describe "when the debugger:toggle event is triggered", -> 16 | it "hides and shows the modal panel", -> 17 | # Before the activation event the view is not on the DOM, and no panel 18 | # has been created 19 | expect(workspaceElement.querySelector('.debugger')).not.toExist() 20 | 21 | # This is an activation event, triggering it will cause the package to be 22 | # activated. 23 | atom.commands.dispatch workspaceElement, 'debugger:toggle' 24 | 25 | waitsForPromise -> 26 | activationPromise 27 | 28 | runs -> 29 | expect(workspaceElement.querySelector('.debugger')).toExist() 30 | 31 | debuggerElement = workspaceElement.querySelector('.debugger') 32 | expect(debuggerElement).toExist() 33 | 34 | debuggerPanel = atom.workspace.panelForItem(debuggerElement) 35 | expect(debuggerPanel.isVisible()).toBe true 36 | atom.commands.dispatch workspaceElement, 'debugger:toggle' 37 | expect(debuggerPanel.isVisible()).toBe false 38 | 39 | it "hides and shows the view", -> 40 | # This test shows you an integration test testing at the view level. 41 | 42 | # Attaching the workspaceElement to the DOM is required to allow the 43 | # `toBeVisible()` matchers to work. Anything testing visibility or focus 44 | # requires that the workspaceElement is on the DOM. Tests that attach the 45 | # workspaceElement to the DOM are generally slower than those off DOM. 46 | jasmine.attachToDOM(workspaceElement) 47 | 48 | expect(workspaceElement.querySelector('.debugger')).not.toExist() 49 | 50 | # This is an activation event, triggering it causes the package to be 51 | # activated. 52 | atom.commands.dispatch workspaceElement, 'debugger:toggle' 53 | 54 | waitsForPromise -> 55 | activationPromise 56 | 57 | runs -> 58 | # Now we can test for view visibility 59 | debuggerElement = workspaceElement.querySelector('.debugger') 60 | expect(debuggerElement).toBeVisible() 61 | atom.commands.dispatch workspaceElement, 'debugger:toggle' 62 | expect(debuggerElement).not.toBeVisible() 63 | -------------------------------------------------------------------------------- /styles/debugger.atom-text-editor.less: -------------------------------------------------------------------------------- 1 | // Note: This file is specially named! 2 | // Less files with the name `*.atom-text-editor.less` will be loaded into the text-editor! 3 | // 4 | // Since these styles applies to the editor, this file needs to be loaded in the 5 | // context of the editor. 6 | 7 | @import "ui-variables"; 8 | 9 | //* { 10 | // cursor: wait !important; 11 | //} 12 | 13 | .debugger-breakpoint-line { 14 | background-color: @background-color-error !important; 15 | } 16 | 17 | .debugger-stopped-line { 18 | .icon-right { 19 | visibility: visible; 20 | } 21 | 22 | .icon-right:before { 23 | content: "\f03e"; 24 | color: @background-color-warning; 25 | font-family: "Octicons Regular"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /styles/debugger.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | .atom-debugger { 8 | padding: @component-padding/2; 9 | 10 | .header { 11 | padding: @component-padding/4 @component-padding/2; 12 | } 13 | .header-item { 14 | display: inline-block; 15 | margin: @component-padding/4 0; 16 | } 17 | .sub-title { 18 | padding-left: @component-padding; 19 | color: @text-color-subtle; 20 | } 21 | 22 | .checkbox { 23 | padding-left: 2.25em; 24 | 25 | input[type="checkbox"] { 26 | display: inline-block; 27 | font-size: inherit; 28 | position: relative; 29 | width: 1.5em; 30 | height: 1.5em; 31 | margin: 0 0 0 -2.25em; 32 | border-radius: @component-border-radius; 33 | } 34 | 35 | .checkbox-label { 36 | -webkit-user-select: none; 37 | display: inline-block; 38 | font-size: 1.2em; 39 | } 40 | } 41 | } 42 | 43 | .atom-debugger.stopped { 44 | cursor: ''; 45 | } 46 | 47 | .atom-debugger.running { 48 | cursor: wait; 49 | } 50 | --------------------------------------------------------------------------------