├── .travis.yml ├── .gitignore ├── vscode-ruby-debug.png ├── .vscodeignore ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── src ├── tslint.json ├── tsconfig.json ├── extension.ts ├── rubyConfig.ts └── rubyDebug.ts ├── CHANGELOG.md ├── LICENSE.txt ├── README.md └── package.json /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7.9" 4 | sudo: false 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | out/ 4 | npm-debug.log 5 | mock-debug.txt 6 | *.vsix 7 | -------------------------------------------------------------------------------- /vscode-ruby-debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/castwide/vscode-ruby-debug/HEAD/vscode-ruby-debug.png -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/**/* 2 | .gitignore 3 | .travis.yml 4 | appveyor.yml 5 | src/**/* 6 | out/tests/**/* 7 | **/*.js.map 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings.FALSE 2 | { 3 | "javascript.validate.enable": false, 4 | "typescript.tsdk": "node_modules/typescript/lib", 5 | "files.trimTrailingWhitespace": true, 6 | "editor.insertSpaces": false 7 | } -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-unused-expression": true, 4 | "no-duplicate-variable": true, 5 | "curly": true, 6 | "class-name": true, 7 | "semicolon": [ "always" ], 8 | "triple-equals": true, 9 | "no-var-keyword": true, 10 | "no-bitwise": true 11 | } 12 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "watch", 7 | "problemMatcher": "$tsc-watch", 8 | "isBackground": true, 9 | "presentation": { 10 | "reveal": "never" 11 | }, 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | 6 | "noImplicitAny": false, 7 | "removeComments": false, 8 | "noUnusedLocals": true, 9 | "noImplicitThis": true, 10 | "inlineSourceMap": false, 11 | "sourceMap": true, 12 | "outDir": "../out", 13 | "preserveConstEnums": true, 14 | "strictNullChecks": true, 15 | "noUnusedParameters": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.4 - October 12, 2019 2 | - Updated README 3 | 4 | ## 0.3.3 - September 10, 2019 5 | - Console output 6 | 7 | ## 0.3.2 - August 11, 2019 8 | - Updated ruby-spawn 9 | 10 | ## 0.3.1 - August 10, 2019 11 | - Clean recompile 12 | 13 | ## 0.3.0 - August 10, 2019 14 | - Descriptive error message 15 | - Default `program` setting 16 | - Updated engine 17 | - Machine-scoped settings 18 | 19 | ## 0.2.0 - August 7, 2019 20 | - Workspace folder and program argument support 21 | - Debug external executables (e.g., RSpec) 22 | - Option to use Bundler 23 | - Additional launch configuration fixes 24 | 25 | ## 0.1.0 - August 5, 2019 26 | - First release 27 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as vscode from 'vscode'; 4 | import { RubyDebugAdapterDescriptorFactory } from './rubyDebug'; 5 | import { RubyConfigurationProvider } from './rubyConfig'; 6 | 7 | export function activate(context: vscode.ExtensionContext) { 8 | context.subscriptions.push(vscode.commands.registerCommand('ruby-debug.getProgramName', config => { 9 | return vscode.window.showInputBox({ 10 | placeHolder: "Enter the name of a Ruby file in the workspace folder", 11 | value: "main.rb" 12 | }); 13 | })); 14 | 15 | const provider = new RubyConfigurationProvider(); 16 | context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('ruby-debug', provider)); 17 | 18 | const factory = new RubyDebugAdapterDescriptorFactory(); 19 | context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('ruby-debug', factory)); 20 | context.subscriptions.push(factory); 21 | } 22 | 23 | export function deactivate() { 24 | // nothing to do 25 | } 26 | -------------------------------------------------------------------------------- /src/rubyConfig.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { WorkspaceFolder, DebugConfiguration, ProviderResult, CancellationToken } from 'vscode'; 3 | 4 | export class RubyConfigurationProvider implements vscode.DebugConfigurationProvider { 5 | 6 | /** 7 | * Massage a debug configuration just before a debug session is being launched, 8 | * e.g. add all missing attributes to the debug configuration. 9 | */ 10 | resolveDebugConfiguration(folder: WorkspaceFolder | undefined, config: DebugConfiguration, token?: CancellationToken): ProviderResult { 11 | // if launch.json is missing or empty 12 | if (!config.type && !config.request && !config.name) { 13 | const editor = vscode.window.activeTextEditor; 14 | if (editor && editor.document.languageId === 'ruby') { 15 | config.type = 'ruby-debug'; 16 | config.name = 'Launch'; 17 | config.request = 'launch'; 18 | config.program = '${file}'; 19 | } 20 | } 21 | 22 | if (config.name !== "Attach" && !config.program) { 23 | return vscode.window.showInformationMessage("Cannot find a Ruby file to debug").then(_ => { 24 | return undefined; // abort launch 25 | }); 26 | } 27 | 28 | return config; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Ruby Debug 2 | 3 | Copyright (c) Fred Snyder for Castwide Technologies 4 | 5 | All rights reserved. 6 | 7 | MIT License 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Extension", 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "runtimeExecutable": "${execPath}", 9 | "args": [ 10 | "--extensionDevelopmentPath=${workspaceFolder}" 11 | ], 12 | "outFiles": [ 13 | "${workspaceFolder}/out/**/*.js" 14 | ], 15 | "preLaunchTask": "npm: watch" 16 | }, 17 | { 18 | "name": "Server", 19 | "type": "node", 20 | "request": "launch", 21 | "cwd": "${workspaceFolder}", 22 | "program": "${workspaceFolder}/src/debugAdapter.ts", 23 | "args": [ 24 | "--server=4711" 25 | ], 26 | "outFiles": [ 27 | "${workspaceFolder}/out/**/*.js" 28 | ] 29 | }, 30 | { 31 | "name": "Tests", 32 | "type": "node", 33 | "request": "launch", 34 | "cwd": "${workspaceFolder}", 35 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 36 | "args": [ 37 | "-u", "tdd", 38 | "--timeout", "999999", 39 | "--colors", 40 | "./out/tests/" 41 | ], 42 | "outFiles": [ 43 | "${workspaceFolder}/out/**/*.js" 44 | ], 45 | "internalConsoleOptions": "openOnSessionStart" 46 | }, 47 | { 48 | "name": "Mock Sample", 49 | "type": "mock", 50 | "request": "launch", 51 | "program": "${workspaceFolder}/${command:AskForProgramName}", 52 | "stopOnEntry": true 53 | } 54 | ], 55 | "compounds": [ 56 | { 57 | "name": "Extension + Server", 58 | "configurations": [ "Extension", "Server" ] 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /src/rubyDebug.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as Net from 'net'; 3 | import { rubySpawn } from 'ruby-spawn'; 4 | import { ChildProcess } from 'child_process'; 5 | 6 | export class RubyDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory { 7 | createDebugAdapterDescriptor(session: vscode.DebugSession, executable: vscode.DebugAdapterExecutable | undefined): vscode.ProviderResult { 8 | return new Promise((resolve, reject) => { 9 | let socket = new Net.Socket(); 10 | let child: ChildProcess; 11 | 12 | socket.on('connect', () => { 13 | if (socket.remotePort && socket.remoteAddress) { 14 | resolve(new vscode.DebugAdapterServer(socket.remotePort, socket.remoteAddress)); 15 | } else { 16 | reject(new Error("Connection to debugger could not be resolved")); 17 | } 18 | }); 19 | socket.on('error', (err) => { 20 | if (child) { 21 | child.kill(); 22 | } 23 | reject(err); 24 | }); 25 | 26 | if (session.configuration.request === 'attach') { 27 | let host = session.configuration.host || '127.0.0.1'; 28 | let port = session.configuration.port || 1234; 29 | socket.connect(port, host); 30 | } else { 31 | let opts = {}; 32 | if (session.workspaceFolder) { 33 | opts['cwd'] = session.workspaceFolder.uri.fsPath; 34 | } 35 | let dbg = (session.configuration.debugger || 'readapt'); 36 | let dbgArgs = ['serve'].concat(session.configuration.debuggerArgs || []); 37 | if (session.configuration.useBundler) { 38 | dbgArgs.unshift(dbg); 39 | dbgArgs.unshift('exec'); 40 | dbg = 'bundle'; 41 | } 42 | child = rubySpawn(dbg, dbgArgs, opts, true); 43 | let started = false; 44 | child.stderr.on('data', (buffer: Buffer) => { 45 | let text = buffer.toString(); 46 | if (!started) { 47 | if (text.match(/^Readapt Debugger/)) { 48 | let match = text.match(/HOST=([^\s]*)[\s]+PORT=([0-9]*)/); 49 | if (match) { 50 | started = true; 51 | socket.connect(parseInt(match[2]), match[1]); 52 | } else { 53 | reject(new Error("Unable to determine debugger host and port")); 54 | } 55 | } 56 | } 57 | vscode.debug.activeDebugConsole.append(text); 58 | }); 59 | child.stdout.on('data', (buffer: Buffer) => { 60 | let text = buffer.toString(); 61 | vscode.debug.activeDebugConsole.append(text); 62 | }); 63 | child.on('exit', (code) => { 64 | if (!started) { 65 | let message = `Debugger exited without connecting (exit code ${code})`; 66 | if (session.configuration.useBundler) { 67 | message += "\nIs readapt included in your Gemfile?" 68 | } 69 | reject(new Error(message)); 70 | } 71 | }); 72 | } 73 | }); 74 | } 75 | 76 | dispose() { 77 | // Nothing to do 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ruby Debug 2 | 3 | **A native Ruby debugger using the Debug Adapter Protocol.** 4 | 5 | *This extension is still in early development. Please report bugs at [https://github.com/castwide/vscode-ruby-debug](https://github.com/castwide/vscode-ruby-debug).* 6 | 7 | ## Features 8 | 9 | * Standard breakpoints 10 | * Conditional breakpoints (with readapt >= 0.7.0) 11 | * Local and global variable data 12 | * Next, step in, step out 13 | * Pause while running 14 | * Evaluate expressions in debug console 15 | * Individual thread control 16 | * Remote workspace support 17 | 18 | ## Requirements 19 | 20 | Ruby Debug requires the `readapt` gem. Install it from the command line: 21 | 22 | ``` 23 | gem install readapt 24 | ``` 25 | 26 | Or add it to your project's Gemfile: 27 | 28 | ```ruby 29 | gem 'readapt', group :development 30 | ``` 31 | 32 | Readapt requires Ruby >= 2.2. 33 | 34 | ## Usage 35 | 36 | If you're not familiar with VS Code's debugger, see [the debugger documentation](https://code.visualstudio.com/docs/editor/debugging) for more information. 37 | 38 | ### Quick Start 39 | 40 | 1. Open a Ruby project folder in VS Code. 41 | 2. Go to the Debug view from the Activity bar. 42 | 3. Click the Play button at the top of the Debug view (or use the `F5` keyboard shortcut). 43 | (If you don't have a launch configuration in the current workspace, the extension will debug the active file.) 44 | 45 | ### Workspace Configurations 46 | 47 | Click the dropdown at the top of the Debug view and click "Add Configuration." 48 | 49 | The simplest configuration is "Launch." It will start the debugger with a Ruby file in your workspace. The default behavior is to prompt the user for a file. You can set the `program` option to a specific file instead; e.g., `"program": "${workspaceFolder}/path/to/your/file.rb"`. 50 | 51 | ### Debugging External Programs 52 | 53 | The debugger can also launch external Ruby executables. The debug configuration snippets include an example for debugging RSpec. 54 | 55 | ### Using Bundler 56 | 57 | Launch configurations include a `useBundler` option. If it's `true`, the debugger will start with `bundle exec readapt serve` before launching the program. 58 | 59 | Note that `readapt` must be included in your Gemfile for `useBundler` to work. 60 | 61 | ### Example Configurations 62 | 63 | The following snippets demonstrate some common launch configurations you can add to `launch.json`. 64 | 65 | #### Debug Active File 66 | 67 | ``` 68 | { 69 | "type": "ruby-debug", 70 | "request": "launch", 71 | "name": "Active File", 72 | "program": "${file}", 73 | "programArgs": [], 74 | "useBundler": false 75 | } 76 | ``` 77 | 78 | #### Debug Active RSpec File 79 | 80 | ``` 81 | { 82 | "type": "ruby-debug", 83 | "request": "launch", 84 | "name": "RSpec (Active File)", 85 | "program": "rspec", 86 | "programArgs": [ 87 | "-I", 88 | "${workspaceFolder}", 89 | "${file}" 90 | ], 91 | "useBundler": false 92 | } 93 | ``` 94 | 95 | #### Debug Rails 96 | 97 | ``` 98 | { 99 | "type": "ruby-debug", 100 | "request": "launch", 101 | "name": "Rails", 102 | "program": "${workspaceFolder}/bin/rails", 103 | "programArgs": ["s"], 104 | "useBundler": true 105 | } 106 | ``` 107 | 108 | **Note:** Make sure the readapt gem is included in your Gemfile, e.g.: 109 | 110 | ```ruby 111 | gem 'readapt', group: :development 112 | ``` 113 | 114 | ## Work in Progress 115 | 116 | * Multiple stack frames per thread 117 | * Hit counts 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ruby-debug", 3 | "displayName": "Ruby Debug", 4 | "version": "0.3.4", 5 | "publisher": "castwide", 6 | "description": "A Ruby debugger that uses the Debug Adapter Protocol.", 7 | "author": { 8 | "name": "Fred Snyder" 9 | }, 10 | "license": "MIT", 11 | "keywords": [ 12 | "ruby", 13 | "debug adapter protocol" 14 | ], 15 | "engines": { 16 | "vscode": "^1.31.0" 17 | }, 18 | "extensionKind": "workspace", 19 | "icon": "vscode-ruby-debug.png", 20 | "categories": [ 21 | "Debuggers" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/castwide/vscode-ruby-debug" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/castwide/vscode-ruby-debug/issues" 29 | }, 30 | "scripts": { 31 | "prepublish": "tsc -p ./src", 32 | "compile": "tsc -p ./src", 33 | "tslint": "tslint ./src/**/*.ts", 34 | "watch": "tsc -w -p ./src", 35 | "test": "mocha -u tdd ./out/tests/", 36 | "postinstall": "node ./node_modules/vscode/bin/install", 37 | "package": "vsce package", 38 | "publish": "vsce publish" 39 | }, 40 | "dependencies": { 41 | "await-notify": "1.0.1", 42 | "ps-tree": "1.2.0", 43 | "ruby-spawn": "1.2.0", 44 | "vscode-debugadapter": "1.33.0" 45 | }, 46 | "devDependencies": { 47 | "@types/mocha": "5.2.5", 48 | "@types/node": "8.9.3", 49 | "mocha": "5.2.0", 50 | "tslint": "5.11.0", 51 | "typescript": "3.1.6", 52 | "vsce": "1.53.2", 53 | "vscode": "^1.1.36", 54 | "vscode-debugadapter-testsupport": "1.33.0" 55 | }, 56 | "main": "./out/extension", 57 | "activationEvents": [ 58 | "onDebug", 59 | "onCommand:ruby-debug.getProgramName" 60 | ], 61 | "contributes": { 62 | "breakpoints": [ 63 | { 64 | "language": "ruby" 65 | } 66 | ], 67 | "debuggers": [ 68 | { 69 | "type": "ruby-debug", 70 | "label": "Ruby Debug", 71 | "configurationAttributes": { 72 | "launch": { 73 | "required": [ 74 | "program" 75 | ], 76 | "properties": { 77 | "program": { 78 | "type": "string", 79 | "description": "Absolute path to a Ruby file.", 80 | "default": "${workspaceFolder}/${command:AskForProgramName}" 81 | }, 82 | "programArgs": { 83 | "type": "array", 84 | "description": "Arguments for the program being debugged", 85 | "default": [] 86 | }, 87 | "debugger": { 88 | "type": "string", 89 | "description": "The path to the readapt executable.", 90 | "default": "readapt" 91 | }, 92 | "debuggerArgs": { 93 | "type": "array", 94 | "description": "Arguments for the debugger", 95 | "default": [ 96 | "--host", 97 | "127.0.0.1", 98 | "--port", 99 | "1234" 100 | ] 101 | }, 102 | "useBundler": { 103 | "type": "boolean", 104 | "description": "Start the debugger with Bundler", 105 | "default": false 106 | } 107 | } 108 | } 109 | }, 110 | "initialConfigurations": [ 111 | { 112 | "type": "ruby-debug", 113 | "request": "launch", 114 | "name": "Launch File", 115 | "program": "${workspaceFolder}/${command:AskForProgramName}", 116 | "programArgs": [], 117 | "useBundler": false 118 | } 119 | ], 120 | "configurationSnippets": [ 121 | { 122 | "label": "Ruby Debug: Launch", 123 | "description": "Run a file in the workspace", 124 | "body": { 125 | "type": "ruby-debug", 126 | "request": "launch", 127 | "name": "Launch File", 128 | "program": "^\"\\${workspaceFolder}/\\${command:AskForProgramName}\"", 129 | "programArgs": [], 130 | "useBundler": false 131 | } 132 | }, 133 | { 134 | "label": "Ruby Debug: RSpec", 135 | "description": "Run RSpec in a debugger", 136 | "body": { 137 | "type": "ruby-debug", 138 | "request": "launch", 139 | "name": "RSpec", 140 | "program": "rspec", 141 | "programArgs": [ 142 | "^\"\\${workspaceFolder}\"" 143 | ], 144 | "useBundler": false 145 | } 146 | } 147 | ], 148 | "variables": { 149 | "AskForProgramName": "ruby-debug.getProgramName" 150 | } 151 | } 152 | ] 153 | } 154 | } 155 | --------------------------------------------------------------------------------