├── .eslintignore ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .markdownlint.json ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── LICENSE_1_0.txt ├── README.md ├── images ├── gmrdb.gif └── icon.png ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── debugAdapter.ts ├── extension.ts └── gmrdbDebug.ts ├── test ├── __snapshots__ │ └── debugadapter.test.ts.snap ├── debugadapter.test.ts └── lua │ ├── get_global_variable_test.lua │ ├── get_local_variable_test.lua │ ├── get_upvalue_test.lua │ ├── loop_test.lua │ └── step_test.lua └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/**/* 2 | **/__tests__/**/* 3 | **/__mocks__/**/* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint', 'jest'], 5 | extends: [ 6 | 'eslint:recommended', 7 | 'plugin:@typescript-eslint/recommended', 8 | 'prettier', 9 | 'plugin:jest/recommended', 10 | ], 11 | rules: { 12 | semi: [2, 'never'], 13 | '@typescript-eslint/no-unused-vars': 0, 14 | '@typescript-eslint/no-explicit-any': 0, 15 | '@typescript-eslint/explicit-module-boundary-types': 0, 16 | '@typescript-eslint/no-non-null-assertion': 0, 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | schedule: 9 | - cron: '26 4 * * 0' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ 'javascript' ] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | 34 | - name: Autobuild 35 | uses: github/codeql-action/autobuild@v2 36 | 37 | - name: Perform CodeQL Analysis 38 | uses: github/codeql-action/analyze@v2 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .DS_Store 4 | .eslintcache -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD024": { "siblings_only": true } 4 | } 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | out 3 | coverage 4 | .github -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "overrides": [ 4 | { 5 | "files": ["*.md"], 6 | "options": { 7 | "tabWidth": 2 8 | } 9 | } 10 | ], 11 | "singleQuote": true, 12 | "endOfLine": "auto" 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outFiles": ["${workspaceRoot}/out/src/**/*.js"], 14 | "preLaunchTask": "TypeScript Compile" 15 | }, 16 | { 17 | "type": "node", 18 | "request": "launch", 19 | "name": "Server", 20 | "cwd": "${workspaceRoot}", 21 | "program": "${workspaceRoot}/src/debugAdapter.ts", 22 | "args": ["--server=4711"], 23 | "sourceMaps": true, 24 | "outFiles": ["${workspaceRoot}/out/**/*.js"] 25 | }, 26 | { 27 | "name": "Launch Tests", 28 | "type": "extensionHost", 29 | "request": "launch", 30 | "runtimeExecutable": "${execPath}", 31 | "args": [ 32 | "--extensionDevelopmentPath=${workspaceRoot}", 33 | "--extensionTestsPath=${workspaceRoot}/out/test" 34 | ], 35 | "stopOnEntry": false, 36 | "sourceMaps": true, 37 | "outFiles": ["${workspaceRoot}/out/test/**/*.js"], 38 | "preLaunchTask": "TypeScript Compile" 39 | }, 40 | { 41 | "type": "node", 42 | "request": "launch", 43 | "name": "Debug adapter tests", 44 | "cwd": "${workspaceRoot}", 45 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 46 | "args": ["--runInBand"], 47 | "sourceMaps": true, 48 | "outFiles": ["${workspaceRoot}/out/test/**/*.js"], 49 | "internalConsoleOptions": "openOnSessionStart", 50 | "preLaunchTask": "TypeScript Compile" 51 | } 52 | ], 53 | "compounds": [ 54 | { 55 | "name": "Extension + Server", 56 | "configurations": ["Launch Extension", "Server"] 57 | }, 58 | { 59 | "name": "Debug adapter tests + Server", 60 | "configurations": ["Debug adapter tests", "Server"] 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "2.0.0", 12 | "tasks": [ 13 | { 14 | "label": "TypeScript Compile", 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | }, 19 | "isBackground": true, 20 | "type": "shell", 21 | "presentation": { 22 | "echo": true, 23 | "reveal": "silent", 24 | "focus": false, 25 | "panel": "shared" 26 | }, 27 | "command": "npm", 28 | "args": ["run", "compile", "--loglevel", "silent"], 29 | "problemMatcher": "$tsc-watch" 30 | }, 31 | { 32 | "label": "Package into VSIX", 33 | "group": "build", 34 | "type": "shell", 35 | "presentation": { 36 | "reveal": "always", 37 | "panel": "shared" 38 | }, 39 | "command": "vsce", 40 | "args": ["package"] 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | node-modules/** 4 | out/test/** 5 | src/** 6 | test/** 7 | **/*.map 8 | .eslintcache 9 | .eslintignore 10 | .eslintrc.js 11 | .gitignore 12 | .markdownlint.json 13 | .prettierignore 14 | .prettierrc.json 15 | jest.config.js 16 | package-lock.json 17 | tsconfig.json 18 | -------------------------------------------------------------------------------- /LICENSE_1_0.txt: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Garry's Mod Remote DeBugger for Visual Studio Code 2 | 3 | ## Introduction 4 | 5 | This extension allows debugging Lua code and using the Source engine console 6 | of Garry's Mod clients or SRCDS (SouRCe Dedicated Server) instances, 7 | through Visual Studio Code. 8 | 9 | This works by running a [remote debugging server](https://github.com/danielga/gm_rdb) 10 | on SRCDS listening on a port. The VSCode extension is then used to attach a 11 | debugger to provide breakpoints. 12 | 13 | This fork works only with the Garry's Mod module 14 | [danielga/gm_rdb](https://github.com/danielga/gm_rdb). 15 | 16 | Based on the work from 17 | [satoren/vscode-lrdb](https://github.com/satoren/vscode-lrdb) and 18 | [kapecp/vscode-lrdb](https://github.com/kapecp/vscode-lrdb). 19 | 20 | ![Garry's Mod debug](https://raw.githubusercontent.com/danielga/vscode-gmrdb/master/images/gmrdb.gif) 21 | 22 | ## Features 23 | 24 | - Supports Windows, macOS and Linux 25 | - Add/remove breakpoints 26 | - Conditional breakpoints 27 | - Continue, pause, step over, step in, step out 28 | - Local, global, \_ENV, upvalue variables and arguments 29 | - Watch window 30 | - Evaluate expressions 31 | - Remote debugging over TCP 32 | 33 | ## Requirements 34 | 35 | - [Garry's Mod Remote Debugger binary modules](https://github.com/danielga/gm_rdb/releases) 36 | - SRCDS 32-bit or 64-bit 37 | 38 | ## Usage 39 | 40 | Be sure to use 64-bit or 32-bit modules on the respective platforms, otherwise 41 | the modules will not be loaded. 42 | 43 | ### Server-side debugging 44 | 45 | For this example, we're using SRCDS from the `x86-64` beta branch on Windows. 46 | 47 | The server will freeze _until_ we attach the debugger through VSCode and _resume_. 48 | 49 | 1. Place the `gmsv_rdb_win64.dll` binary module in `garrysmod/lua/bin` - [guide](https://wiki.facepunch.com/gmod/Creating_Binary_Modules) 50 | 2. (Optional) Add the following snippet wherever we want to start the server 51 | 52 | ```lua 53 | -- Fetch the remote debugging server binary module 54 | require("rdb") 55 | 56 | -- Start a debugging server 57 | -- This will pause the server until we attach a debugger 58 | -- Listens on port 21111 by default, use the first argument to change it 59 | rdb.activate() 60 | ``` 61 | 62 | #### Extension settings 63 | 64 | Feel free to use variables like `workspaceFolder` to specify paths as a shortcut. 65 | 66 | `launch.json` example: 67 | 68 | ```jsonc 69 | { 70 | "version": "0.2.0", 71 | "configurations": [ 72 | { 73 | "type": "gmrdb", 74 | "request": "attach", 75 | "host": "127.0.0.1", 76 | "port": 21111, 77 | "name": "Attach to Garry's Mod", 78 | "sourceRoot": "C:/example-srcds/garrysmod", 79 | // Important to map Lua source code to breakpoints 80 | // (otherwise we'll see missing file errors on VSCode) 81 | "sourceFileMap": { 82 | // Local absolute path: remote path 83 | "C:/example-srcds/garrysmod/addons/exampleaddon": "addons/exampleaddon", 84 | "C:/example-srcds/garrysmod/gamemode/examplerp": "gamemodes/examplerp" 85 | }, 86 | "stopOnEntry": true 87 | }, 88 | { 89 | "type": "gmrdb", 90 | "request": "launch", 91 | "name": "Launch Garry's Mod", 92 | "program": "C:/example-srcds/srcds_win64.exe", 93 | "cwd": "C:/example-srcds", 94 | "args": [ 95 | "-console", 96 | "-game", 97 | "garrysmod", 98 | "-ip", 99 | "127.0.0.1", 100 | "-port", 101 | "27015", 102 | "+map", 103 | "gm_construct", 104 | "+maxplayers", 105 | "2" 106 | ], 107 | "sourceRoot": "C:/example-srcds/garrysmod", 108 | "port": 21111, 109 | "sourceFileMap": { 110 | "C:/example-srcds/garrysmod/addons/test2": "addons/test2", 111 | "C:/example-srcds/garrysmod/gamemode/examplerp": "gamemodes/examplerp" 112 | }, 113 | "stopOnEntry": true 114 | } 115 | ] 116 | } 117 | ``` 118 | 119 | ### Client-side debugging 120 | 121 | This follows similar steps to server-side debugging on Windows 64-bit. 122 | 123 | The client will freeze _until_ we attach the debugger through VSCode and _resume_. 124 | 125 | It is possible to join a server that will load the module on your client. 126 | Just be wary if this is what you want, since ANY server can do this. 127 | The only effect of this should be your game freezing until you attach a debugger 128 | on it. Someone else remotely debugging your game should be considered a bug! 129 | 130 | 1. Place the `gmcl_rdb_win64.dll` binary module in `garrysmod/lua/bin` in our 131 | local Garry's Mod installation - [guide](https://wiki.facepunch.com/gmod/Creating_Binary_Modules) 132 | 1. (Optional) Add the following snippet wherever we want to start the debugging server 133 | 134 | ```lua 135 | -- Fetch the remote debugging server binary module 136 | require("rdb") 137 | 138 | -- Start a debugging server 139 | -- This will pause the server until we attach a debugger 140 | -- Listens on port 21111 by default, use the first argument to change it 141 | rdb.activate() 142 | ``` 143 | 144 | #### Extension settings 145 | 146 | Feel free to use variables like `workspaceFolder` to specify paths as a shortcut. 147 | 148 | `launch.json` example: 149 | 150 | ```jsonc 151 | { 152 | "version": "0.2.0", 153 | "configurations": [ 154 | { 155 | "type": "gmrdb", 156 | "request": "attach", 157 | "host": "127.0.0.1", 158 | "port": 21111, 159 | "name": "Attach to Garry's Mod", 160 | "sourceRoot": "C:/steamapps/common/garrysmod", 161 | // Important to map Lua source code to breakpoints 162 | // (otherwise we'll see missing file errors on VSCode) 163 | "sourceFileMap": { 164 | // Local absolute path: remote path 165 | "C:/steamapps/common/garrysmod/addons/exampleaddon": "addons/exampleaddon", 166 | "C:/steamapps/common/garrysmod/gamemode/examplerp": "gamemodes/examplerp" 167 | }, 168 | "stopOnEntry": true 169 | } 170 | ] 171 | } 172 | ``` 173 | 174 | ## Icon licensing 175 | 176 | [Lua icon](https://www.lua.org/images) 177 | 178 | [Search for virus (modified)](https://www.flaticon.com/free-icon/search-for-virus_95496) 179 | -------------------------------------------------------------------------------- /images/gmrdb.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielga/vscode-gmrdb/d2ccc0222f09aa5b8e60a8610e82007e168881a6/images/gmrdb.gif -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielga/vscode-gmrdb/d2ccc0222f09aa5b8e60a8610e82007e168881a6/images/icon.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: '.', 3 | testMatch: [ 4 | '**/__tests__/**/?(*.)+(spec|test).+(ts|tsx|js)', 5 | '**/?(*.)+(spec|test).+(ts|tsx|js)', 6 | '!**/dist/**/*', 7 | '!**/out/**/*', 8 | ], 9 | transform: { 10 | '^.+\\.(ts|tsx)$': 'ts-jest', 11 | }, 12 | moduleNameMapper: { 13 | '~(.*)$': '/$1', 14 | }, 15 | collectCoverageFrom: [ 16 | '**/src/**/*.{js,jsx,ts,tsx}', 17 | '!**/node_modules/', 18 | '!**/__tests__/**', 19 | '!**/__mocks__/**', 20 | '!**/examples/**', 21 | '!**/examples-*/**', 22 | ], 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gmrdb", 3 | "displayName": "GMRDB", 4 | "description": "Garry's Mod Remote DeBugger", 5 | "version": "0.4.4", 6 | "publisher": "metaman", 7 | "engines": { 8 | "vscode": "^1.78.1" 9 | }, 10 | "categories": [ 11 | "Debuggers" 12 | ], 13 | "dependencies": { 14 | "lrdb-debuggable-lua": "^0.7.0", 15 | "tree-kill": "^1.2.2", 16 | "@vscode/debugadapter": "^1.68.0" 17 | }, 18 | "activationEvents": [ 19 | "onDebug" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/danielga/vscode-gmrdb.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/danielga/vscode-gmrdb/issues" 27 | }, 28 | "main": "./out/extension.js", 29 | "scripts": { 30 | "vscode:prepublish": "tsc -p ./", 31 | "compile": "tsc -watch -p ./", 32 | "package": "vsce package", 33 | "publish": "vsce publish", 34 | "build": "tsc", 35 | "jest": "jest", 36 | "test": "run-s build jest", 37 | "lint": "run-s lint:eslint lint:prettier", 38 | "lint:eslint": "eslint . --cache --ext ts", 39 | "lint:prettier": "prettier . --check", 40 | "fix": "run-s fix:eslint fix:prettier", 41 | "fix:eslint": "eslint . --cache --ext ts --fix", 42 | "fix:prettier": "prettier . --check --write" 43 | }, 44 | "devDependencies": { 45 | "@types/jest": "^29.5.14", 46 | "@types/node": "^22.15.30", 47 | "@types/vscode": "^1.100.0", 48 | "@typescript-eslint/eslint-plugin": "^8.34.0", 49 | "@typescript-eslint/parser": "^8.34.0", 50 | "eslint": "^9.28.0", 51 | "eslint-config-prettier": "^10.1.5", 52 | "eslint-plugin-jest": "^28.13.0", 53 | "jest": "^29.7.0", 54 | "npm-run-all": "^4.1.5", 55 | "prettier": "^3.5.3", 56 | "ts-jest": "^29.3.4", 57 | "ts-node": "^10.9.2", 58 | "typescript": "^5.8.3", 59 | "@vscode/vsce": "^3.5.0", 60 | "@vscode/debugadapter-testsupport": "^1.68.0", 61 | "@vscode/debugprotocol": "^1.68.0", 62 | "vscode-test": "^1.6.1" 63 | }, 64 | "icon": "images/icon.png", 65 | "license": "BSL-1.0", 66 | "contributes": { 67 | "breakpoints": [ 68 | { 69 | "language": "lua" 70 | }, 71 | { 72 | "language": "glua" 73 | } 74 | ], 75 | "debuggers": [ 76 | { 77 | "type": "gmrdb", 78 | "label": "Garry's Mod Remote DeBugger", 79 | "program": "./out/debugAdapter.js", 80 | "runtime": "node", 81 | "languages": [ 82 | "lua", 83 | "glua" 84 | ], 85 | "variables": {}, 86 | "configurationSnippets": [ 87 | { 88 | "label": "Garry's Mod: (gmrdb) Launch", 89 | "description": "A new configuration for launching a Garry's Mod instance.", 90 | "body": { 91 | "type": "gmrdb", 92 | "request": "launch", 93 | "name": "${2:Launch}", 94 | "program": "Garry's Mod executable (either a client or SRCDS executable).", 95 | "args": [], 96 | "cwd": "${workspaceFolder}", 97 | "sourceFileMap": { 98 | "${workspaceFolder}": "." 99 | } 100 | } 101 | }, 102 | { 103 | "label": "Garry's Mod: (gmrdb) attach", 104 | "description": "A new configuration for remotely debugging a Garry's Mod instance.", 105 | "body": { 106 | "type": "gmrdb", 107 | "request": "attach", 108 | "name": "${1:Attach}", 109 | "host": "localhost", 110 | "port": 21111, 111 | "sourceFileMap": { 112 | "${workspaceFolder}": "." 113 | } 114 | } 115 | } 116 | ], 117 | "configurationAttributes": { 118 | "launch": { 119 | "required": [ 120 | "program" 121 | ], 122 | "properties": { 123 | "program": { 124 | "type": "string", 125 | "description": "Garry's Mod executable (either a client or SRCDS executable).", 126 | "default": "${file}" 127 | }, 128 | "cwd": { 129 | "type": "string", 130 | "description": "Working directory (usually path where the executable is).", 131 | "default": "${workspaceFolder}" 132 | }, 133 | "args": { 134 | "type": "array", 135 | "description": "Command line arguments.", 136 | "default": [] 137 | }, 138 | "port": { 139 | "type": "number", 140 | "description": "Port to connect to.", 141 | "default": 21111 142 | }, 143 | "sourceRoot": { 144 | "type": "string", 145 | "description": "Script source root directory (used in souce file matching at breakpoints).", 146 | "default": "${workspaceFolder}" 147 | }, 148 | "sourceFileMap": { 149 | "type": "object", 150 | "description": "Optional source file mappings passed to the debug engine (relates a local path to a target path).", 151 | "default": {} 152 | }, 153 | "stopOnEntry": { 154 | "type": "boolean", 155 | "description": "Automatically stop after launch.", 156 | "default": true 157 | } 158 | } 159 | }, 160 | "attach": { 161 | "required": [ 162 | "sourceRoot" 163 | ], 164 | "properties": { 165 | "host": { 166 | "type": "string", 167 | "description": "Host name to connect to.", 168 | "default": "localhost" 169 | }, 170 | "port": { 171 | "type": "number", 172 | "description": "Port to connect to.", 173 | "default": 21111 174 | }, 175 | "sourceRoot": { 176 | "type": "string", 177 | "description": "Script source root directory (used in souce file matching at breakpoints).", 178 | "default": "${workspaceFolder}" 179 | }, 180 | "sourceFileMap": { 181 | "type": "object", 182 | "description": "Optional source file mappings passed to the debug engine (relates a local path to a target path).", 183 | "default": {} 184 | }, 185 | "stopOnEntry": { 186 | "type": "boolean", 187 | "description": "Automatically stop after launch.", 188 | "default": true 189 | } 190 | } 191 | } 192 | } 193 | } 194 | ] 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/debugAdapter.ts: -------------------------------------------------------------------------------- 1 | import { GarrysModDebugSession } from './gmrdbDebug' 2 | 3 | GarrysModDebugSession.run(GarrysModDebugSession) 4 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import * as path from 'path' 4 | import * as vscode from 'vscode' 5 | import * as net from 'net' 6 | import { GarrysModDebugSession } from './gmrdbDebug' 7 | 8 | // The compile time flag 'runMode' controls how the debug adapter is run. 9 | // Please note: the test suite only supports 'external' mode. 10 | // 'inline' mode is great for debugging. 11 | const runMode: 'external' | 'server' | 'inline' = 'external' 12 | 13 | export function activate(context: vscode.ExtensionContext): void { 14 | context.subscriptions.push( 15 | vscode.debug.registerDebugConfigurationProvider( 16 | 'gmrdb', 17 | new GMRDBDebugConfigurationProvider() 18 | ) 19 | ) 20 | 21 | // debug adapters can be run in different ways by using a vscode.DebugAdapterDescriptorFactory: 22 | let factory: vscode.DebugAdapterDescriptorFactory | undefined 23 | switch (runMode) { 24 | case 'server': 25 | // run the debug adapter as a server inside the extension and communicating via a socket 26 | factory = new GMRDBServerDebugAdapterDescriptorFactory() 27 | break 28 | 29 | case 'inline': 30 | // run the debug adapter inside the extension and directly talk to it 31 | factory = new GMRDBInlineDebugAdapterDescriptorFactory() 32 | break 33 | 34 | case 'external': 35 | default: 36 | // run the debug adapter as a separate process (it's the default so we do nothing) 37 | break 38 | } 39 | 40 | if (factory) { 41 | context.subscriptions.push( 42 | vscode.debug.registerDebugAdapterDescriptorFactory('gmrdb', factory) 43 | ) 44 | } 45 | } 46 | 47 | export function deactivate(): void { 48 | // nothing to do 49 | } 50 | 51 | class GMRDBInlineDebugAdapterDescriptorFactory 52 | implements vscode.DebugAdapterDescriptorFactory 53 | { 54 | createDebugAdapterDescriptor( 55 | _session: vscode.DebugSession, 56 | _executable: vscode.DebugAdapterExecutable | undefined 57 | ): vscode.ProviderResult { 58 | return new vscode.DebugAdapterInlineImplementation( 59 | new GarrysModDebugSession() 60 | ) 61 | } 62 | } 63 | 64 | class GMRDBServerDebugAdapterDescriptorFactory 65 | implements vscode.DebugAdapterDescriptorFactory 66 | { 67 | private server?: net.Server 68 | 69 | createDebugAdapterDescriptor( 70 | _session: vscode.DebugSession, 71 | _executable: vscode.DebugAdapterExecutable | undefined 72 | ): vscode.ProviderResult { 73 | if (!this.server) { 74 | // start listening on a random port 75 | this.server = net 76 | .createServer((socket) => { 77 | const session = new GarrysModDebugSession() 78 | session.setRunAsServer(true) 79 | session.start(socket, socket) 80 | }) 81 | .listen(0) 82 | } 83 | 84 | // make VS Code connect to debug server 85 | const address = this.server.address() 86 | if (address && typeof address !== 'string') { 87 | return new vscode.DebugAdapterServer(address.port) 88 | } 89 | 90 | throw Error('failed') 91 | } 92 | } 93 | 94 | class GMRDBDebugConfigurationProvider 95 | implements vscode.DebugConfigurationProvider 96 | { 97 | /** 98 | * Try to add all missing attributes to the debug configuration being launched. 99 | */ 100 | resolveDebugConfiguration( 101 | folder: vscode.WorkspaceFolder | undefined, 102 | config: vscode.DebugConfiguration 103 | ): vscode.ProviderResult { 104 | // if launch.json is missing or empty 105 | if (!config.type && !config.request && !config.name) { 106 | const message = 'Cannot find a program to debug' 107 | return vscode.window.showInformationMessage(message).then(() => { 108 | return undefined // abort launch 109 | }) 110 | } 111 | 112 | // make sure that config has a 'cwd' attribute set 113 | if (!config.cwd) { 114 | if (folder) { 115 | config.cwd = folder.uri.fsPath 116 | } else if (config.program) { 117 | // derive 'cwd' from 'program' 118 | config.cwd = path.dirname(config.program) 119 | } 120 | } 121 | 122 | return config 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/gmrdbDebug.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DebugSession, 3 | InitializedEvent, 4 | TerminatedEvent, 5 | ContinuedEvent, 6 | StoppedEvent, 7 | OutputEvent, 8 | Thread, 9 | StackFrame, 10 | Scope, 11 | Source, 12 | Handles, 13 | Breakpoint, 14 | } from '@vscode/debugadapter' 15 | import { DebugProtocol } from '@vscode/debugprotocol' 16 | import { readFileSync } from 'fs' 17 | import { spawn, ChildProcess } from 'child_process' 18 | import * as path from 'path' 19 | import { LRDBAdapter, LRDBClient } from 'lrdb-debuggable-lua' 20 | import { JsonRpcNotify, JsonRpcRequest } from 'lrdb-debuggable-lua/dist/JsonRpc' 21 | import { 22 | DebugRequest, 23 | EvalRequest, 24 | ExitNotify, 25 | GetGlobalRequest, 26 | GetLocalVariableRequest, 27 | GetUpvaluesRequest, 28 | PausedNotify, 29 | RunningNotify, 30 | } from 'lrdb-debuggable-lua/dist/Client' 31 | import * as treeKill from 'tree-kill' 32 | 33 | export interface LaunchRequestArguments 34 | extends DebugProtocol.LaunchRequestArguments { 35 | program: string 36 | args?: string[] 37 | cwd?: string 38 | port?: number 39 | sourceRoot?: string 40 | sourceFileMap?: Record 41 | stopOnEntry?: boolean 42 | } 43 | 44 | export interface AttachRequestArguments 45 | extends DebugProtocol.AttachRequestArguments { 46 | host?: string 47 | port?: number 48 | sourceRoot: string 49 | sourceFileMap?: Record 50 | stopOnEntry?: boolean 51 | } 52 | 53 | type GetLocalVariableParam = { 54 | type: 'get_local_variable' 55 | params: GetLocalVariableRequest['params'] 56 | } 57 | type GetGlobalParam = { 58 | type: 'get_global' 59 | params: GetGlobalRequest['params'] 60 | } 61 | type GetUpvaluesParam = { 62 | type: 'get_upvalues' 63 | params: GetUpvaluesRequest['params'] 64 | } 65 | type EvalParam = { 66 | type: 'eval' 67 | params: EvalRequest['params'] 68 | } 69 | 70 | type VariableReference = 71 | | GetLocalVariableParam 72 | | GetGlobalParam 73 | | GetUpvaluesParam 74 | | EvalParam 75 | 76 | interface ConnectedNotify extends JsonRpcNotify { 77 | method: 'connected' 78 | params: { working_directory?: string } 79 | } 80 | 81 | interface Color { 82 | r: number 83 | g: number 84 | b: number 85 | a: number 86 | } 87 | 88 | interface NotificationOutput { 89 | channel_id: number 90 | severity: number 91 | color: Color 92 | message: string 93 | } 94 | 95 | interface OutputNotify extends JsonRpcNotify { 96 | method: 'output' 97 | params: NotificationOutput 98 | } 99 | 100 | declare type DebuggerNotify = 101 | | PausedNotify 102 | | ConnectedNotify 103 | | ExitNotify 104 | | RunningNotify 105 | | OutputNotify 106 | 107 | interface CommandRequest extends JsonRpcRequest { 108 | method: 'command' 109 | params: string 110 | } 111 | 112 | function stringify(value: unknown): string { 113 | if (value == null) { 114 | return 'nil' 115 | } else if (value == undefined) { 116 | return 'none' 117 | } else { 118 | return JSON.stringify(value) 119 | } 120 | } 121 | 122 | export class GarrysModDebugSession extends DebugSession { 123 | // Lua 124 | private static THREAD_ID = 1 125 | 126 | private _debug_server_process?: ChildProcess 127 | 128 | private _debug_client?: LRDBClient.Client 129 | 130 | // maps from sourceFile to array of Breakpoints 131 | private _breakPoints = new Map() 132 | 133 | private _breakPointID = 1000 134 | 135 | private _variableHandles = new Handles() 136 | 137 | private _sourceHandles = new Handles() 138 | 139 | private _stopOnEntry?: boolean 140 | 141 | private _working_directory?: string 142 | 143 | /** 144 | * Creates a new debug adapter that is used for one debug session. 145 | * We configure the default implementation of a debug adapter here. 146 | */ 147 | public constructor() { 148 | super() 149 | 150 | // this debugger uses zero-based lines and columns 151 | this.setDebuggerLinesStartAt1(false) 152 | this.setDebuggerColumnsStartAt1(false) 153 | } 154 | 155 | /** 156 | * The 'initialize' request is the first request called by the frontend 157 | * to interrogate the features the debug adapter provides. 158 | */ 159 | protected initializeRequest( 160 | response: DebugProtocol.InitializeResponse, 161 | _args: DebugProtocol.InitializeRequestArguments 162 | ): void { 163 | if (this._debug_server_process) { 164 | if (this._debug_server_process.pid) { 165 | treeKill(this._debug_server_process.pid) 166 | } 167 | 168 | delete this._debug_server_process 169 | } 170 | 171 | if (this._debug_client) { 172 | this._debug_client.end() 173 | delete this._debug_client 174 | } 175 | 176 | if (response.body) { 177 | // This debug adapter implements the configurationDoneRequest. 178 | response.body.supportsConfigurationDoneRequest = true 179 | 180 | response.body.supportsConditionalBreakpoints = true 181 | 182 | response.body.supportsHitConditionalBreakpoints = true 183 | 184 | // make VS Code to use 'evaluate' when hovering over source 185 | response.body.supportsEvaluateForHovers = true 186 | } 187 | 188 | this.sendResponse(response) 189 | } 190 | 191 | private setupSourceEnv( 192 | sourceRoot: string, 193 | sourceFileMap?: Record 194 | ) { 195 | this.convertClientLineToDebugger = (line: number): number => { 196 | return line 197 | } 198 | 199 | this.convertDebuggerLineToClient = (line: number): number => { 200 | return line 201 | } 202 | 203 | this.convertClientPathToDebugger = (clientPath: string): string => { 204 | if (sourceFileMap) { 205 | for (const sourceFileMapSource of Object.keys(sourceFileMap)) { 206 | const sourceFileMapTarget = sourceFileMap[sourceFileMapSource] 207 | const resolvedSource = path.resolve(sourceFileMapSource) 208 | const resolvedClient = path.resolve(clientPath) 209 | const relativePath = path.relative(resolvedSource, resolvedClient) 210 | if (!relativePath.startsWith('..')) { 211 | // client is child of source 212 | return path.join(sourceFileMapTarget, relativePath) 213 | } 214 | } 215 | } 216 | 217 | return path.relative(sourceRoot, clientPath) 218 | } 219 | 220 | this.convertDebuggerPathToClient = (debuggerPath: string): string => { 221 | if (!debuggerPath.startsWith('@')) { 222 | return '' 223 | } 224 | 225 | const filename = debuggerPath.substr(1) 226 | if (sourceFileMap) { 227 | for (const sourceFileMapSource of Object.keys(sourceFileMap)) { 228 | const sourceFileMapTarget = sourceFileMap[sourceFileMapSource] 229 | const relativePath = path.relative(sourceFileMapTarget, filename) 230 | if (!relativePath.startsWith('..')) { 231 | // filename is child of target 232 | return path.join(sourceFileMapSource, relativePath) 233 | } 234 | } 235 | } 236 | 237 | if (path.isAbsolute(filename)) { 238 | return filename 239 | } else { 240 | return path.join(sourceRoot, filename) 241 | } 242 | } 243 | } 244 | 245 | protected launchRequest( 246 | response: DebugProtocol.LaunchResponse, 247 | args: LaunchRequestArguments 248 | ): void { 249 | this._stopOnEntry = args.stopOnEntry 250 | 251 | const cwd = args.cwd ? args.cwd : process.cwd() 252 | const sourceRoot = args.sourceRoot ? args.sourceRoot : cwd 253 | 254 | this.setupSourceEnv(sourceRoot, args.sourceFileMap) 255 | 256 | const programArgs = args.args ? args.args : [] 257 | 258 | // only using the shell seems to be able to run SRCDS without causing engine errors and removing all output from its window 259 | this._debug_server_process = spawn(args.program, programArgs, { 260 | cwd: cwd, 261 | shell: true, 262 | windowsHide: true, 263 | }) 264 | 265 | const port = args.port ? args.port : 21111 266 | 267 | this._debug_client = new LRDBClient.Client( 268 | new LRDBAdapter.TcpAdapter(port, 'localhost') 269 | ) 270 | 271 | this._debug_client.onNotify.on((event) => { 272 | this.handleServerEvents(event as DebuggerNotify) 273 | }) 274 | 275 | this._debug_client.onOpen.on(() => { 276 | this.sendEvent(new InitializedEvent()) 277 | }) 278 | 279 | this._debug_server_process.on('error', (msg: string) => { 280 | this.sendEvent(new OutputEvent(msg, 'error')) 281 | }) 282 | 283 | this._debug_server_process.on('close', (code: number) => { 284 | this.sendEvent(new OutputEvent(`exit status: ${code}\n`)) 285 | this.sendEvent(new TerminatedEvent()) 286 | }) 287 | 288 | this.sendResponse(response) 289 | } 290 | 291 | protected attachRequest( 292 | response: DebugProtocol.AttachResponse, 293 | args: AttachRequestArguments 294 | ): void { 295 | this._stopOnEntry = args.stopOnEntry 296 | 297 | this.setupSourceEnv(args.sourceRoot, args.sourceFileMap) 298 | 299 | const port = args.port ? args.port : 21111 300 | const host = args.host ? args.host : 'localhost' 301 | 302 | this._debug_client = new LRDBClient.Client( 303 | new LRDBAdapter.TcpAdapter(port, host) 304 | ) 305 | 306 | this._debug_client.onNotify.on((event) => { 307 | this.handleServerEvents(event as DebuggerNotify) 308 | }) 309 | 310 | this._debug_client.onClose.on(() => { 311 | this.sendEvent(new TerminatedEvent()) 312 | }) 313 | 314 | this._debug_client.onOpen.on(() => { 315 | this.sendEvent(new InitializedEvent()) 316 | }) 317 | 318 | this.sendResponse(response) 319 | } 320 | 321 | protected configurationDoneRequest( 322 | response: DebugProtocol.ConfigurationDoneResponse 323 | ): void { 324 | this.sendResponse(response) 325 | } 326 | 327 | protected setBreakPointsRequest( 328 | response: DebugProtocol.SetBreakpointsResponse, 329 | args: DebugProtocol.SetBreakpointsArguments 330 | ): void { 331 | const path = args.source.path 332 | if (!this._debug_client || !path) { 333 | response.success = false 334 | this.sendResponse(response) 335 | return 336 | } 337 | 338 | // read file contents into array for direct access 339 | const lines = readFileSync(path).toString().split('\n') 340 | 341 | const breakpoints = new Array() 342 | 343 | const debuggerFilePath = this.convertClientPathToDebugger(path) 344 | 345 | this._debug_client.clearBreakPoints({ file: debuggerFilePath }) 346 | 347 | if (args.breakpoints) { 348 | // verify breakpoint locations 349 | for (const souceBreakpoint of args.breakpoints) { 350 | let l = this.convertClientLineToDebugger(souceBreakpoint.line) 351 | let verified = false 352 | while (l <= lines.length) { 353 | const line = lines[l - 1].trim() 354 | // if a line is empty or starts with '--' we don't allow to set a breakpoint but move the breakpoint down 355 | if (line.length == 0 || line.startsWith('--')) { 356 | l++ 357 | } else { 358 | verified = true // this breakpoint has been validated 359 | break 360 | } 361 | } 362 | 363 | const bp: DebugProtocol.Breakpoint = new Breakpoint( 364 | verified, 365 | this.convertDebuggerLineToClient(l) 366 | ) 367 | bp.id = this._breakPointID++ 368 | breakpoints.push(bp) 369 | if (verified) { 370 | const sendbreakpoint = { 371 | line: l, 372 | file: debuggerFilePath, 373 | condition: souceBreakpoint.condition, 374 | hit_condition: souceBreakpoint.hitCondition, 375 | } 376 | this._debug_client.addBreakPoint(sendbreakpoint) 377 | } 378 | } 379 | } 380 | 381 | this._breakPoints.set(path, breakpoints) 382 | 383 | // send back the actual breakpoint positions 384 | response.body = { 385 | breakpoints: breakpoints, 386 | } 387 | 388 | this.sendResponse(response) 389 | } 390 | 391 | protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { 392 | // return the default thread 393 | response.body = { 394 | threads: [new Thread(GarrysModDebugSession.THREAD_ID, 'thread 1')], 395 | } 396 | 397 | this.sendResponse(response) 398 | } 399 | 400 | /** 401 | * Returns a fake 'stacktrace' where every 'stackframe' is a word from the current line. 402 | */ 403 | protected stackTraceRequest( 404 | response: DebugProtocol.StackTraceResponse, 405 | args: DebugProtocol.StackTraceArguments 406 | ): void { 407 | if (!this._debug_client) { 408 | response.success = false 409 | this.sendResponse(response) 410 | return 411 | } 412 | 413 | this._debug_client.getStackTrace().then((res) => { 414 | if (res.result) { 415 | const startFrame = 416 | typeof args.startFrame === 'number' ? args.startFrame : 0 417 | const maxLevels = 418 | typeof args.levels === 'number' 419 | ? args.levels 420 | : res.result.length - startFrame 421 | const endFrame = Math.min(startFrame + maxLevels, res.result.length) 422 | const frames = new Array() 423 | for (let i = startFrame; i < endFrame; i++) { 424 | const frame = res.result[i] // use a word of the line as the stackframe name 425 | const filename = this.convertDebuggerPathToClient(frame.file) 426 | const source = new Source(frame.id, filename) 427 | if (!frame.file.startsWith('@')) { 428 | source.sourceReference = this._sourceHandles.create(frame.file) 429 | } 430 | 431 | frames.push( 432 | new StackFrame( 433 | i, 434 | frame.func, 435 | source, 436 | this.convertDebuggerLineToClient(frame.line), 437 | 0 438 | ) 439 | ) 440 | } 441 | 442 | response.body = { 443 | stackFrames: frames, 444 | totalFrames: res.result.length, 445 | } 446 | } else { 447 | response.success = false 448 | response.message = 'unknown error' 449 | } 450 | 451 | this.sendResponse(response) 452 | }) 453 | } 454 | 455 | protected scopesRequest( 456 | response: DebugProtocol.ScopesResponse, 457 | args: DebugProtocol.ScopesArguments 458 | ): void { 459 | const scopes = [ 460 | new Scope( 461 | 'Local', 462 | this._variableHandles.create({ 463 | type: 'get_local_variable', 464 | params: { 465 | stack_no: args.frameId, 466 | }, 467 | }), 468 | false 469 | ), 470 | new Scope( 471 | 'Upvalues', 472 | this._variableHandles.create({ 473 | type: 'get_upvalues', 474 | params: { 475 | stack_no: args.frameId, 476 | }, 477 | }), 478 | false 479 | ), 480 | new Scope( 481 | 'Global', 482 | this._variableHandles.create({ 483 | type: 'get_global', 484 | params: {}, 485 | }), 486 | true 487 | ), 488 | ] 489 | 490 | response.body = { 491 | scopes: scopes, 492 | } 493 | 494 | this.sendResponse(response) 495 | } 496 | 497 | protected variablesRequest( 498 | response: DebugProtocol.VariablesResponse, 499 | args: DebugProtocol.VariablesArguments 500 | ): void { 501 | if (!this._debug_client) { 502 | response.success = false 503 | this.sendResponse(response) 504 | return 505 | } 506 | 507 | const parent = this._variableHandles.get(args.variablesReference) 508 | if (parent != null) { 509 | const res = (() => { 510 | switch (parent.type) { 511 | case 'get_global': 512 | return this._debug_client 513 | .getGlobal(parent.params) 514 | .then((res) => res.result) 515 | case 'get_local_variable': 516 | return this._debug_client 517 | .getLocalVariable(parent.params) 518 | .then((res) => res.result) 519 | case 'get_upvalues': 520 | return this._debug_client 521 | .getUpvalues(parent.params) 522 | .then((res) => res.result) 523 | case 'eval': 524 | return this._debug_client.eval(parent.params).then((res) => { 525 | const results = res.result as any[] 526 | return results[0] 527 | }) 528 | default: 529 | return Promise.reject(Error('invalid')) 530 | } 531 | })() 532 | 533 | res 534 | .then((result) => 535 | this.variablesRequestResponse(response, result, parent) 536 | ) 537 | .catch((err) => { 538 | response.success = false 539 | response.message = err.message 540 | this.sendResponse(response) 541 | }) 542 | } else { 543 | response.success = false 544 | this.sendResponse(response) 545 | } 546 | } 547 | 548 | private variablesRequestResponse( 549 | response: DebugProtocol.VariablesResponse, 550 | variablesData: unknown, 551 | parent: VariableReference 552 | ): void { 553 | const evalParam = (k: string | number): EvalParam => { 554 | switch (parent.type) { 555 | case 'eval': { 556 | const key = typeof k === 'string' ? `"${k}"` : `${k}` 557 | return { 558 | type: 'eval', 559 | params: { 560 | ...parent.params, 561 | chunk: `(${parent.params.chunk})[${key}]`, 562 | }, 563 | } 564 | } 565 | default: { 566 | return { 567 | type: 'eval', 568 | params: { 569 | stack_no: 0, 570 | ...parent.params, 571 | chunk: `${k}`, 572 | upvalue: parent.type === 'get_upvalues', 573 | local: parent.type === 'get_local_variable', 574 | global: parent.type === 'get_global', 575 | }, 576 | } 577 | } 578 | } 579 | } 580 | 581 | const variables: DebugProtocol.Variable[] = [] 582 | if (variablesData instanceof Array) { 583 | variablesData.forEach((v, i) => { 584 | const typename = typeof v 585 | const k = i + 1 586 | const varRef = 587 | typename === 'object' ? this._variableHandles.create(evalParam(k)) : 0 588 | variables.push({ 589 | name: `${k}`, 590 | type: typename, 591 | value: stringify(v), 592 | variablesReference: varRef, 593 | }) 594 | }) 595 | } else if (typeof variablesData === 'object') { 596 | const varData = variablesData as Record 597 | for (const k in varData) { 598 | const typename = typeof varData[k] 599 | const varRef = 600 | typename === 'object' ? this._variableHandles.create(evalParam(k)) : 0 601 | variables.push({ 602 | name: k, 603 | type: typename, 604 | value: stringify(varData[k]), 605 | variablesReference: varRef, 606 | }) 607 | } 608 | } 609 | 610 | response.body = { 611 | variables: variables, 612 | } 613 | 614 | this.sendResponse(response) 615 | } 616 | 617 | protected continueRequest( 618 | response: DebugProtocol.ContinueResponse, 619 | _args: DebugProtocol.ContinueArguments 620 | ): void { 621 | this._debug_client?.continue() 622 | this.sendResponse(response) 623 | } 624 | 625 | protected nextRequest( 626 | response: DebugProtocol.NextResponse, 627 | _args: DebugProtocol.NextArguments 628 | ): void { 629 | this._debug_client?.step() 630 | this.sendResponse(response) 631 | } 632 | 633 | protected stepInRequest( 634 | response: DebugProtocol.StepInResponse, 635 | _args: DebugProtocol.StepInArguments 636 | ): void { 637 | this._debug_client?.stepIn() 638 | this.sendResponse(response) 639 | } 640 | 641 | protected stepOutRequest( 642 | response: DebugProtocol.StepOutResponse, 643 | _args: DebugProtocol.StepOutArguments 644 | ): void { 645 | this._debug_client?.stepOut() 646 | this.sendResponse(response) 647 | } 648 | 649 | protected pauseRequest( 650 | response: DebugProtocol.PauseResponse, 651 | _args: DebugProtocol.PauseArguments 652 | ): void { 653 | this._debug_client?.pause() 654 | this.sendResponse(response) 655 | } 656 | 657 | protected sourceRequest( 658 | response: DebugProtocol.SourceResponse, 659 | args: DebugProtocol.SourceArguments 660 | ): void { 661 | const id = this._sourceHandles.get(args.sourceReference) 662 | if (id) { 663 | response.body = { 664 | content: id, 665 | } 666 | } 667 | 668 | this.sendResponse(response) 669 | } 670 | 671 | protected disconnectRequest( 672 | response: DebugProtocol.DisconnectResponse, 673 | _args: DebugProtocol.DisconnectArguments 674 | ): void { 675 | if (this._debug_server_process) { 676 | if (this._debug_server_process.pid) { 677 | treeKill(this._debug_server_process.pid) 678 | } 679 | 680 | delete this._debug_server_process 681 | } 682 | 683 | if (this._debug_client) { 684 | this._debug_client.end() 685 | delete this._debug_client 686 | } 687 | 688 | this.sendResponse(response) 689 | } 690 | 691 | protected evaluateRequest( 692 | response: DebugProtocol.EvaluateResponse, 693 | args: DebugProtocol.EvaluateArguments 694 | ): void { 695 | if (!this._debug_client) { 696 | response.success = false 697 | this.sendResponse(response) 698 | return 699 | } 700 | 701 | if (args.context === 'repl' && args.expression.startsWith('con ')) { 702 | const request: CommandRequest = { 703 | jsonrpc: '2.0', 704 | method: 'command', 705 | params: args.expression.substr(4) + '\n', 706 | id: 0, 707 | } 708 | this._debug_client.send(request as unknown as DebugRequest) 709 | response.success = true 710 | this.sendResponse(response) 711 | return 712 | } 713 | 714 | const chunk = args.expression 715 | const requestParam: EvalRequest['params'] = { 716 | stack_no: args.frameId as number, 717 | chunk: chunk, 718 | depth: 0, 719 | } 720 | this._debug_client.eval(requestParam).then((res) => { 721 | if (res.result instanceof Array) { 722 | const ret = res.result.map((v) => stringify(v)).join('\t') 723 | let varRef = 0 724 | if (res.result.length == 1) { 725 | const refobj = res.result[0] 726 | const typename = typeof refobj 727 | if (refobj && typename == 'object') { 728 | varRef = this._variableHandles.create({ 729 | type: 'eval', 730 | params: requestParam, 731 | }) 732 | } 733 | } 734 | 735 | response.body = { 736 | result: ret, 737 | variablesReference: varRef, 738 | } 739 | } else { 740 | response.body = { 741 | result: '', 742 | variablesReference: 0, 743 | } 744 | 745 | response.success = false 746 | } 747 | 748 | this.sendResponse(response) 749 | }) 750 | } 751 | 752 | private handleServerEvents(event: DebuggerNotify) { 753 | switch (event.method) { 754 | case 'paused': 755 | if (event.params.reason === 'entry' && !this._stopOnEntry) { 756 | this._debug_client?.continue() 757 | } else { 758 | this.sendEvent( 759 | new StoppedEvent( 760 | event.params.reason, 761 | GarrysModDebugSession.THREAD_ID 762 | ) 763 | ) 764 | } 765 | 766 | break 767 | 768 | case 'running': 769 | this._variableHandles.reset() 770 | this.sendEvent(new ContinuedEvent(GarrysModDebugSession.THREAD_ID)) 771 | break 772 | 773 | case 'exit': 774 | break 775 | 776 | case 'connected': 777 | this._working_directory = event.params.working_directory 778 | break 779 | 780 | case 'output': 781 | this.sendEvent( 782 | new OutputEvent( 783 | `\u001b[38;2;${event.params.color.r};${event.params.color.g};${event.params.color.b}m${event.params.message}\u001b[0m`, 784 | 'stdout' 785 | ) 786 | ) 787 | break 788 | } 789 | } 790 | } 791 | -------------------------------------------------------------------------------- /test/__snapshots__/debugadapter.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Lua Debug Adapter global check global values 1`] = ` 4 | Object { 5 | "body": Object { 6 | "variables": Array [ 7 | Object { 8 | "name": "_G", 9 | "type": "object", 10 | "value": "{\\"_G\\":{\\"table\\":\\"0x0\\"},\\"_VERSION\\":\\"Lua 5.3\\",\\"assert\\":\\"function: 0x67\\",\\"collectgarbage\\":\\"function: 0x68\\",\\"coroutine\\":{\\"table\\":\\"0x0\\"},\\"debug\\":{\\"table\\":\\"0x0\\"},\\"dofile\\":\\"function: 0x69\\",\\"error\\":\\"function: 0x6a\\",\\"getmetatable\\":\\"function: 0x6b\\",\\"global_value\\":1,\\"io\\":{\\"table\\":\\"0x0\\"},\\"ipairs\\":\\"function: 0x6c\\",\\"load\\":\\"function: 0x6e\\",\\"loadfile\\":\\"function: 0x6d\\",\\"math\\":{\\"table\\":\\"0x0\\"},\\"next\\":\\"function: 0x65\\",\\"os\\":{\\"table\\":\\"0x0\\"},\\"package\\":{\\"table\\":\\"0x0\\"},\\"pairs\\":\\"function: 0x6f\\",\\"pcall\\":\\"function: 0x70\\",\\"print\\":\\"function: 0x71\\",\\"rawequal\\":\\"function: 0x72\\",\\"rawget\\":\\"function: 0x74\\",\\"rawlen\\":\\"function: 0x73\\",\\"rawset\\":\\"function: 0x75\\",\\"require\\":\\"function: 0x50bfe0\\",\\"select\\":\\"function: 0x76\\",\\"setmetatable\\":\\"function: 0x77\\",\\"string\\":{\\"table\\":\\"0x0\\"},\\"table\\":{\\"table\\":\\"0x0\\"},\\"tonumber\\":\\"function: 0x78\\",\\"tostring\\":\\"function: 0x79\\",\\"type\\":\\"function: 0x7a\\",\\"utf8\\":{\\"table\\":\\"0x0\\"},\\"xpcall\\":\\"function: 0x7b\\"}", 11 | "variablesReference": 1003, 12 | }, 13 | Object { 14 | "name": "_VERSION", 15 | "type": "string", 16 | "value": "\\"Lua 5.3\\"", 17 | "variablesReference": 0, 18 | }, 19 | Object { 20 | "name": "assert", 21 | "type": "string", 22 | "value": "function: 0x0", 23 | "variablesReference": 0, 24 | }, 25 | Object { 26 | "name": "collectgarbage", 27 | "type": "string", 28 | "value": "function: 0x0", 29 | "variablesReference": 0, 30 | }, 31 | Object { 32 | "name": "coroutine", 33 | "type": "object", 34 | "value": "{\\"create\\":\\"function: 0x7d\\",\\"isyieldable\\":\\"function: 0x83\\",\\"resume\\":\\"function: 0x7e\\",\\"running\\":\\"function: 0x7f\\",\\"status\\":\\"function: 0x80\\",\\"wrap\\":\\"function: 0x81\\",\\"yield\\":\\"function: 0x82\\"}", 35 | "variablesReference": 1004, 36 | }, 37 | Object { 38 | "name": "debug", 39 | "type": "object", 40 | "value": "{\\"debug\\":\\"function: 0x85\\",\\"gethook\\":\\"function: 0x87\\",\\"getinfo\\":\\"function: 0x88\\",\\"getlocal\\":\\"function: 0x89\\",\\"getmetatable\\":\\"function: 0x8b\\",\\"getregistry\\":\\"function: 0x8a\\",\\"getupvalue\\":\\"function: 0x8c\\",\\"getuservalue\\":\\"function: 0x86\\",\\"sethook\\":\\"function: 0x90\\",\\"setlocal\\":\\"function: 0x91\\",\\"setmetatable\\":\\"function: 0x92\\",\\"setupvalue\\":\\"function: 0x93\\",\\"setuservalue\\":\\"function: 0x8f\\",\\"traceback\\":\\"function: 0x94\\",\\"upvalueid\\":\\"function: 0x8e\\",\\"upvaluejoin\\":\\"function: 0x8d\\"}", 41 | "variablesReference": 1005, 42 | }, 43 | Object { 44 | "name": "dofile", 45 | "type": "string", 46 | "value": "function: 0x0", 47 | "variablesReference": 0, 48 | }, 49 | Object { 50 | "name": "error", 51 | "type": "string", 52 | "value": "function: 0x0", 53 | "variablesReference": 0, 54 | }, 55 | Object { 56 | "name": "getmetatable", 57 | "type": "string", 58 | "value": "function: 0x0", 59 | "variablesReference": 0, 60 | }, 61 | Object { 62 | "name": "global_value", 63 | "type": "number", 64 | "value": "1", 65 | "variablesReference": 0, 66 | }, 67 | Object { 68 | "name": "io", 69 | "type": "object", 70 | "value": "{\\"close\\":\\"function: 0xa3\\",\\"flush\\":\\"function: 0xa4\\",\\"input\\":\\"function: 0xa5\\",\\"lines\\":\\"function: 0xa6\\",\\"open\\":\\"function: 0xa7\\",\\"output\\":\\"function: 0xa8\\",\\"popen\\":\\"function: 0xa9\\",\\"read\\":\\"function: 0xaa\\",\\"stderr\\":\\"file (0x9220)\\",\\"stdin\\":\\"file (0x92b0)\\",\\"stdout\\":\\"file (0x9188)\\",\\"tmpfile\\":\\"function: 0xab\\",\\"type\\":\\"function: 0xac\\",\\"write\\":\\"function: 0xad\\"}", 71 | "variablesReference": 1006, 72 | }, 73 | Object { 74 | "name": "ipairs", 75 | "type": "string", 76 | "value": "function: 0x0", 77 | "variablesReference": 0, 78 | }, 79 | Object { 80 | "name": "load", 81 | "type": "string", 82 | "value": "function: 0x0", 83 | "variablesReference": 0, 84 | }, 85 | Object { 86 | "name": "loadfile", 87 | "type": "string", 88 | "value": "function: 0x0", 89 | "variablesReference": 0, 90 | }, 91 | Object { 92 | "name": "math", 93 | "type": "object", 94 | "value": "{\\"abs\\":\\"function: 0xb6\\",\\"acos\\":\\"function: 0xb7\\",\\"asin\\":\\"function: 0xb8\\",\\"atan\\":\\"function: 0xb9\\",\\"ceil\\":\\"function: 0xba\\",\\"cos\\":\\"function: 0xbb\\",\\"deg\\":\\"function: 0xbc\\",\\"exp\\":\\"function: 0xbd\\",\\"floor\\":\\"function: 0xbf\\",\\"fmod\\":\\"function: 0xc0\\",\\"huge\\":\\"Infinity\\",\\"log\\":\\"function: 0xc2\\",\\"max\\":\\"function: 0xc3\\",\\"maxinteger\\":9223372036854776000,\\"min\\":\\"function: 0xc4\\",\\"mininteger\\":-9223372036854776000,\\"modf\\":\\"function: 0xc5\\",\\"pi\\":3.141592653589793,\\"rad\\":\\"function: 0xc6\\",\\"random\\":\\"function: 0xc7\\",\\"randomseed\\":\\"function: 0xc8\\",\\"sin\\":\\"function: 0xc9\\",\\"sqrt\\":\\"function: 0xca\\",\\"tan\\":\\"function: 0xcb\\",\\"tointeger\\":\\"function: 0xbe\\",\\"type\\":\\"function: 0xcc\\",\\"ult\\":\\"function: 0xc1\\"}", 95 | "variablesReference": 1007, 96 | }, 97 | Object { 98 | "name": "next", 99 | "type": "string", 100 | "value": "function: 0x0", 101 | "variablesReference": 0, 102 | }, 103 | Object { 104 | "name": "os", 105 | "type": "object", 106 | "value": "{\\"clock\\":\\"function: 0xd5\\",\\"date\\":\\"function: 0xd6\\",\\"difftime\\":\\"function: 0xd7\\",\\"execute\\":\\"function: 0xd8\\",\\"exit\\":\\"function: 0xd9\\",\\"getenv\\":\\"function: 0xda\\",\\"remove\\":\\"function: 0xdb\\",\\"rename\\":\\"function: 0xdc\\",\\"setlocale\\":\\"function: 0xdd\\",\\"time\\":\\"function: 0xde\\",\\"tmpname\\":\\"function: 0xdf\\"}", 107 | "variablesReference": 1008, 108 | }, 109 | Object { 110 | "name": "package", 111 | "type": "object", 112 | "value": "{\\"config\\":\\"/\\\\n;\\\\n?\\\\n!\\\\n-\\\\n\\",\\"cpath\\":\\"/usr/local/lib/lua/5.3/?.so;/usr/local/lib/lua/5.3/loadall.so;./?.so\\",\\"loaded\\":{\\"table\\":\\"0x0\\"},\\"loadlib\\":\\"function: 0xd2\\",\\"path\\":\\"/usr/local/share/lua/5.3/?.lua;/usr/local/share/lua/5.3/?/init.lua;/usr/local/lib/lua/5.3/?.lua;/usr/local/lib/lua/5.3/?/init.lua;./?.lua;./?/init.lua\\",\\"preload\\":{\\"table\\":\\"0x0\\"},\\"searchers\\":{\\"table\\":\\"0x0\\"},\\"searchpath\\":\\"function: 0xd3\\"}", 113 | "variablesReference": 1009, 114 | }, 115 | Object { 116 | "name": "pairs", 117 | "type": "string", 118 | "value": "function: 0x0", 119 | "variablesReference": 0, 120 | }, 121 | Object { 122 | "name": "pcall", 123 | "type": "string", 124 | "value": "function: 0x0", 125 | "variablesReference": 0, 126 | }, 127 | Object { 128 | "name": "print", 129 | "type": "string", 130 | "value": "function: 0x0", 131 | "variablesReference": 0, 132 | }, 133 | Object { 134 | "name": "rawequal", 135 | "type": "string", 136 | "value": "function: 0x0", 137 | "variablesReference": 0, 138 | }, 139 | Object { 140 | "name": "rawget", 141 | "type": "string", 142 | "value": "function: 0x0", 143 | "variablesReference": 0, 144 | }, 145 | Object { 146 | "name": "rawlen", 147 | "type": "string", 148 | "value": "function: 0x0", 149 | "variablesReference": 0, 150 | }, 151 | Object { 152 | "name": "rawset", 153 | "type": "string", 154 | "value": "function: 0x0", 155 | "variablesReference": 0, 156 | }, 157 | Object { 158 | "name": "require", 159 | "type": "string", 160 | "value": "function: 0x0", 161 | "variablesReference": 0, 162 | }, 163 | Object { 164 | "name": "select", 165 | "type": "string", 166 | "value": "function: 0x0", 167 | "variablesReference": 0, 168 | }, 169 | Object { 170 | "name": "setmetatable", 171 | "type": "string", 172 | "value": "function: 0x0", 173 | "variablesReference": 0, 174 | }, 175 | Object { 176 | "name": "string", 177 | "type": "object", 178 | "value": "{\\"byte\\":\\"function: 0xe4\\",\\"char\\":\\"function: 0xe5\\",\\"dump\\":\\"function: 0xe6\\",\\"find\\":\\"function: 0xe7\\",\\"format\\":\\"function: 0xe8\\",\\"gmatch\\":\\"function: 0xe9\\",\\"gsub\\":\\"function: 0xea\\",\\"len\\":\\"function: 0xeb\\",\\"lower\\":\\"function: 0xec\\",\\"match\\":\\"function: 0xed\\",\\"pack\\":\\"function: 0xf2\\",\\"packsize\\":\\"function: 0xf3\\",\\"rep\\":\\"function: 0xee\\",\\"reverse\\":\\"function: 0xef\\",\\"sub\\":\\"function: 0xf0\\",\\"unpack\\":\\"function: 0xf4\\",\\"upper\\":\\"function: 0xf1\\"}", 179 | "variablesReference": 1010, 180 | }, 181 | Object { 182 | "name": "table", 183 | "type": "object", 184 | "value": "{\\"concat\\":\\"function: 0xf5\\",\\"insert\\":\\"function: 0xf6\\",\\"move\\":\\"function: 0xfa\\",\\"pack\\":\\"function: 0xf7\\",\\"remove\\":\\"function: 0xf9\\",\\"sort\\":\\"function: 0xfb\\",\\"unpack\\":\\"function: 0xf8\\"}", 185 | "variablesReference": 1011, 186 | }, 187 | Object { 188 | "name": "tonumber", 189 | "type": "string", 190 | "value": "function: 0x0", 191 | "variablesReference": 0, 192 | }, 193 | Object { 194 | "name": "tostring", 195 | "type": "string", 196 | "value": "function: 0x0", 197 | "variablesReference": 0, 198 | }, 199 | Object { 200 | "name": "type", 201 | "type": "string", 202 | "value": "function: 0x0", 203 | "variablesReference": 0, 204 | }, 205 | Object { 206 | "name": "utf8", 207 | "type": "object", 208 | "value": "{\\"char\\":\\"function: 0xff\\",\\"charpattern\\":\\"[\\",\\"codepoint\\":\\"function: 0xfe\\",\\"codes\\":\\"function: 0x101\\",\\"len\\":\\"function: 0x100\\",\\"offset\\":\\"function: 0xfd\\"}", 209 | "variablesReference": 1012, 210 | }, 211 | Object { 212 | "name": "xpcall", 213 | "type": "string", 214 | "value": "function: 0x0", 215 | "variablesReference": 0, 216 | }, 217 | ], 218 | }, 219 | "command": "variables", 220 | "request_seq": 7, 221 | "seq": 10, 222 | "success": true, 223 | "type": "response", 224 | } 225 | `; 226 | 227 | exports[`Lua Debug Adapter local check local_values 1`] = ` 228 | Object { 229 | "body": Object { 230 | "variables": Array [ 231 | Object { 232 | "name": "local_value1", 233 | "type": "number", 234 | "value": "1", 235 | "variablesReference": 0, 236 | }, 237 | Object { 238 | "name": "local_value2", 239 | "type": "string", 240 | "value": "\\"abc\\"", 241 | "variablesReference": 0, 242 | }, 243 | Object { 244 | "name": "local_value3", 245 | "type": "number", 246 | "value": "1", 247 | "variablesReference": 0, 248 | }, 249 | Object { 250 | "name": "local_value4", 251 | "type": "object", 252 | "value": "[4234.3]", 253 | "variablesReference": 1003, 254 | }, 255 | ], 256 | }, 257 | "command": "variables", 258 | "request_seq": 7, 259 | "seq": 10, 260 | "success": true, 261 | "type": "response", 262 | } 263 | `; 264 | 265 | exports[`Lua Debug Adapter upvalues check upvalues 1`] = ` 266 | Object { 267 | "body": Object { 268 | "variables": Array [ 269 | Object { 270 | "name": "a", 271 | "type": "number", 272 | "value": "1", 273 | "variablesReference": 0, 274 | }, 275 | Object { 276 | "name": "t", 277 | "type": "object", 278 | "value": "[{\\"table\\":\\"0x0\\"},5,6]", 279 | "variablesReference": 1003, 280 | }, 281 | ], 282 | }, 283 | "command": "variables", 284 | "request_seq": 7, 285 | "seq": 10, 286 | "success": true, 287 | "type": "response", 288 | } 289 | `; 290 | -------------------------------------------------------------------------------- /test/debugadapter.test.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Note: This example test is leveraging the Mocha test framework. 3 | // Please refer to their documentation on https://mochajs.org/ for help. 4 | // 5 | 6 | // The module 'assert' provides assertion methods from node 7 | import * as path from 'path' 8 | 9 | // You can import and use all API from the 'vscode' module 10 | // as well as import your extension to test it 11 | import { DebugClient } from '@vscode/debugadapter-testsupport' 12 | import { DebugProtocol } from '@vscode/debugprotocol' 13 | 14 | // DebugProtocol.Variable can have members with memory addresses, these addresses are not reliable. 15 | // This function will clean them out after validating them. 16 | function cleanVariable( 17 | variable: DebugProtocol.Variable 18 | ): DebugProtocol.Variable { 19 | const functionRegex = /^"function: 0x[a-fA-F1-9][a-fA-F0-9]*"$/ 20 | const tableRegex = /^0x[a-fA-F1-9][a-fA-F0-9]*$/ 21 | if (variable.type === 'string' && variable.value.match(functionRegex)) { 22 | variable.value = 'function: 0x0' 23 | return variable 24 | } else if (variable.type === 'object') { 25 | const variables: Record = 26 | JSON.parse(variable.value) 27 | for (const k in variables) { 28 | const v = variables[k] 29 | if (typeof v === 'string' && v.match(functionRegex)) { 30 | variables[k] = 'function: 0x0' 31 | } else if (typeof v === 'object' && v.table.match(tableRegex)) { 32 | v.table = '0x0' 33 | } 34 | } 35 | variable.value = JSON.stringify(variables) 36 | return variable 37 | } else { 38 | return variable 39 | } 40 | } 41 | 42 | function sequenceVariablesRequest( 43 | dc: DebugClient, 44 | varref: number, 45 | datapath: string[] 46 | ) { 47 | let req = dc.variablesRequest({ variablesReference: varref }) 48 | const last = datapath.pop() 49 | for (const p of datapath) { 50 | req = req.then((response) => { 51 | for (const va of response.body.variables) { 52 | if (va.name == p && va.variablesReference != 0) { 53 | return dc.variablesRequest({ 54 | variablesReference: va.variablesReference, 55 | }) 56 | } 57 | } 58 | 59 | throw Error( 60 | 'not found:' + p + ' in ' + JSON.stringify(response.body.variables) 61 | ) 62 | }) 63 | } 64 | 65 | return req.then((response) => { 66 | for (const va of response.body.variables) { 67 | if (va.name == last) { 68 | return va 69 | } 70 | } 71 | 72 | throw Error( 73 | 'not found:' + last + ' in ' + JSON.stringify(response.body.variables) 74 | ) 75 | }) 76 | } 77 | 78 | // Defines a Mocha test describe to group tests of similar kind together 79 | describe('Lua Debug Adapter', () => { 80 | const DEBUG_ADAPTER = './out/debugAdapter.js' 81 | const PROJECT_ROOT = path.join(__dirname, '../') 82 | const DATA_ROOT = path.join(PROJECT_ROOT, 'test/lua/') 83 | 84 | let dc: DebugClient 85 | 86 | beforeEach(() => { 87 | dc = new DebugClient('node', DEBUG_ADAPTER, 'lua') 88 | return dc.start() 89 | }) 90 | 91 | afterEach(() => dc.stop()) 92 | 93 | describe('basic', () => { 94 | test('unknown request should produce error', async () => { 95 | const response = dc.send('illegal_request') 96 | await expect(response).rejects.toThrow() 97 | }) 98 | }) 99 | 100 | describe('initialize', () => { 101 | test('should return supported features', async () => { 102 | const response = await dc.initializeRequest() 103 | expect(response.body?.supportsConfigurationDoneRequest).toBe(true) 104 | }) 105 | 106 | test("should produce error for invalid 'pathFormat'", async () => { 107 | const response = dc.initializeRequest({ 108 | adapterID: 'lua', 109 | linesStartAt1: true, 110 | columnsStartAt1: true, 111 | pathFormat: 'url', 112 | }) 113 | await expect(response).rejects.toThrow() 114 | }) 115 | }) 116 | 117 | describe('launch', () => { 118 | test('should run program to the end', async () => { 119 | const PROGRAM = path.join(DATA_ROOT, 'loop_test.lua') 120 | 121 | const response = Promise.all([ 122 | dc.launch({ program: PROGRAM }), 123 | dc.configurationSequence(), 124 | dc.waitForEvent('terminated'), 125 | ]) 126 | 127 | await expect(response).resolves.toHaveLength(3) 128 | }) 129 | 130 | test('should stop on entry', async () => { 131 | const PROGRAM = path.join(DATA_ROOT, 'loop_test.lua') 132 | const ENTRY_LINE = 1 133 | const response = Promise.all([ 134 | dc.launch({ program: PROGRAM, stopOnEntry: true }), 135 | dc.configurationSequence(), 136 | dc.waitForEvent('stopped'), 137 | dc.assertStoppedLocation('entry', { line: ENTRY_LINE }), 138 | ]) 139 | await expect(response).resolves.toHaveLength(4) 140 | }) 141 | }) 142 | 143 | describe('breakpoint', () => { 144 | test('should stop on breakpoint', async () => { 145 | const PROGRAM = path.join(DATA_ROOT, 'loop_test.lua') 146 | const BREAK_LINE = 5 147 | const response = dc.hitBreakpoint( 148 | { program: PROGRAM }, 149 | { path: PROGRAM, line: BREAK_LINE } 150 | ) 151 | await expect(response).resolves.toHaveLength(3) 152 | }) 153 | }) 154 | 155 | describe('evaluate', () => { 156 | beforeEach(async () => { 157 | const PROGRAM = path.join(DATA_ROOT, 'loop_test.lua') 158 | await Promise.all([ 159 | dc.launch({ program: PROGRAM, stopOnEntry: true }), 160 | dc.configurationSequence(), 161 | dc.waitForEvent('stopped'), 162 | ]) 163 | }) 164 | 165 | test('check watch results 1', async () => { 166 | const response = dc 167 | .evaluateRequest({ 168 | expression: '{{1}}', 169 | context: 'watch', 170 | frameId: 0, 171 | }) 172 | .then((response) => 173 | sequenceVariablesRequest(dc, response.body.variablesReference, [ 174 | '1', 175 | '1', 176 | ]) 177 | ) 178 | await expect(response).resolves.toMatchObject({ name: '1', value: '1' }) 179 | }) 180 | 181 | test('watch array value [1][1]', async () => { 182 | const response = dc 183 | .evaluateRequest({ 184 | expression: '{{1}}', 185 | context: 'watch', 186 | frameId: 0, 187 | }) 188 | .then((response) => 189 | sequenceVariablesRequest(dc, response.body.variablesReference, [ 190 | '1', 191 | '1', 192 | ]) 193 | ) 194 | await expect(response).resolves.toMatchObject({ name: '1', value: '1' }) 195 | }) 196 | 197 | test('watch array value [1][1][3]', async () => { 198 | const response = dc 199 | .evaluateRequest({ 200 | expression: '{{{5,4}}}', 201 | context: 'watch', 202 | frameId: 0, 203 | }) 204 | .then((response) => { 205 | return sequenceVariablesRequest( 206 | dc, 207 | response.body.variablesReference, 208 | ['1', '1', '2'] 209 | ) 210 | }) 211 | await expect(response).resolves.toMatchObject({ name: '2', value: '4' }) 212 | }) 213 | 214 | test('watch object value ["a"][2]', async () => { 215 | const response = dc 216 | .evaluateRequest({ 217 | expression: '{a={4,2}}', 218 | context: 'watch', 219 | frameId: 0, 220 | }) 221 | .then((response) => { 222 | return sequenceVariablesRequest( 223 | dc, 224 | response.body.variablesReference, 225 | ['a', '2'] 226 | ) 227 | }) 228 | await expect(response).resolves.toMatchObject({ name: '2', value: '2' }) 229 | }) 230 | }) 231 | 232 | describe('upvalues', () => { 233 | beforeEach(() => { 234 | const PROGRAM = path.join(DATA_ROOT, 'get_upvalue_test.lua') 235 | const BREAK_LINE = 6 236 | return dc.hitBreakpoint( 237 | { program: PROGRAM, stopOnEntry: false }, 238 | { path: PROGRAM, line: BREAK_LINE } 239 | ) 240 | }) 241 | 242 | function getUpvalueScope(frameID: number) { 243 | return dc.scopesRequest({ frameId: frameID }).then((response) => { 244 | for (const scope of response.body.scopes) { 245 | if (scope.name == 'Upvalues') { 246 | return scope 247 | } 248 | } 249 | 250 | throw Error('upvalue not found') 251 | }) 252 | } 253 | 254 | test('check upvalues', async () => { 255 | return dc 256 | .scopesRequest({ frameId: 0 }) 257 | .then((res) => { 258 | const ref = res.body.scopes.find((scope) => scope.name === 'Upvalues') 259 | if (!ref) { 260 | throw Error('scope not found') 261 | } 262 | 263 | return ref 264 | }) 265 | .then((ref) => dc.variablesRequest(ref)) 266 | .then((res) => { 267 | res.body.variables = res.body.variables.map(cleanVariable) 268 | return res 269 | }) 270 | .then((res) => expect(res).toMatchSnapshot()) 271 | }) 272 | 273 | test('check upvalue a', async () => { 274 | const response = getUpvalueScope(0).then((scope) => { 275 | return sequenceVariablesRequest(dc, scope.variablesReference, ['a']) 276 | }) 277 | await expect(response).resolves.toMatchObject({ name: 'a', value: '1' }) 278 | }) 279 | 280 | test('check upvalue table ["t"][1][1]', async () => { 281 | //local t={{1,2,3,4},5,6} 282 | const response = getUpvalueScope(0).then((scope) => { 283 | return sequenceVariablesRequest(dc, scope.variablesReference, [ 284 | 't', 285 | '1', 286 | '1', 287 | ]) 288 | }) 289 | 290 | await expect(response).resolves.toMatchObject({ name: '1', value: '1' }) 291 | }) 292 | 293 | test('check upvalue table ["t"][1][3]', async () => { 294 | const response = getUpvalueScope(0).then((scope) => { 295 | return sequenceVariablesRequest(dc, scope.variablesReference, [ 296 | 't', 297 | '1', 298 | '3', 299 | ]) 300 | }) 301 | await expect(response).resolves.toMatchObject({ name: '3', value: '3' }) 302 | }) 303 | 304 | test('check upvalue table ["t"][2]', async () => { 305 | const response = getUpvalueScope(0).then((scope) => { 306 | return sequenceVariablesRequest(dc, scope.variablesReference, [ 307 | 't', 308 | '2', 309 | ]) 310 | }) 311 | await expect(response).resolves.toMatchObject({ name: '2', value: '5' }) 312 | }) 313 | }) 314 | 315 | describe('global', () => { 316 | beforeEach(() => { 317 | const PROGRAM = path.join(DATA_ROOT, 'get_global_variable_test.lua') 318 | const BREAK_LINE = 7 319 | return dc.hitBreakpoint( 320 | { program: PROGRAM, stopOnEntry: false }, 321 | { path: PROGRAM, line: BREAK_LINE } 322 | ) 323 | }) 324 | 325 | test('check global values', async () => { 326 | return dc 327 | .scopesRequest({ frameId: 0 }) 328 | .then((res) => { 329 | const ref = res.body.scopes.find((scope) => scope.name === 'Global') 330 | if (!ref) { 331 | throw Error('scope not found') 332 | } 333 | 334 | return ref 335 | }) 336 | .then((ref) => dc.variablesRequest(ref)) 337 | .then((res) => { 338 | res.body.variables = res.body.variables.map(cleanVariable) 339 | return res 340 | }) 341 | .then((res) => expect(res).toMatchSnapshot()) 342 | }) 343 | }) 344 | 345 | describe('local', () => { 346 | beforeEach(() => { 347 | const PROGRAM = path.join(DATA_ROOT, 'get_local_variable_test.lua') 348 | const BREAK_LINE = 7 349 | return dc.hitBreakpoint( 350 | { program: PROGRAM, stopOnEntry: false }, 351 | { path: PROGRAM, line: BREAK_LINE } 352 | ) 353 | }) 354 | 355 | function getLocalScope(frameID: number) { 356 | return dc.scopesRequest({ frameId: frameID }).then((response) => { 357 | for (const scope of response.body.scopes) { 358 | if (scope.name == 'Local') { 359 | return scope 360 | } 361 | } 362 | 363 | throw Error('local value not found') 364 | }) 365 | } 366 | 367 | test('check local_values', async () => { 368 | return dc 369 | .scopesRequest({ frameId: 0 }) 370 | .then((res) => { 371 | const ref = res.body.scopes.find((scope) => scope.name === 'Local') 372 | if (!ref) { 373 | throw Error('scope not found') 374 | } 375 | 376 | return ref 377 | }) 378 | .then((ref) => dc.variablesRequest(ref)) 379 | .then((res) => { 380 | res.body.variables = res.body.variables.map(cleanVariable) 381 | return res 382 | }) 383 | .then((res) => expect(res).toMatchSnapshot()) 384 | }) 385 | 386 | test('get local_value1', async () => { 387 | const response = await getLocalScope(0).then((scope) => { 388 | return sequenceVariablesRequest(dc, scope.variablesReference, [ 389 | 'local_value1', 390 | ]) 391 | }) 392 | 393 | expect(response.value).toBe('1') 394 | }) 395 | 396 | test('get local_value2', async () => { 397 | const response = await getLocalScope(0).then((scope) => { 398 | return sequenceVariablesRequest(dc, scope.variablesReference, [ 399 | 'local_value2', 400 | ]) 401 | }) 402 | expect(response.value).toBe('"abc"') 403 | }) 404 | 405 | test('get local_value3', async () => { 406 | const response = await getLocalScope(0).then((scope) => { 407 | return sequenceVariablesRequest(dc, scope.variablesReference, [ 408 | 'local_value3', 409 | ]) 410 | }) 411 | expect(response.value).toBe('1') 412 | }) 413 | 414 | test('get local_value4', async () => { 415 | const response = await getLocalScope(0).then((scope) => { 416 | return sequenceVariablesRequest(dc, scope.variablesReference, [ 417 | 'local_value4', 418 | '1', 419 | ]) 420 | }) 421 | expect(response.value).toBe('4234.3') 422 | }) 423 | }) 424 | }) 425 | -------------------------------------------------------------------------------- /test/lua/get_global_variable_test.lua: -------------------------------------------------------------------------------- 1 | global_value = 1 2 | local function testfn() 3 | local local_value1 = 1 4 | local local_value2 = "abc" 5 | local local_value3 = value 6 | local local_value4 ={4234.3} 7 | return local_value1,local_value2,local_value3,local_value4 8 | end 9 | 10 | testfn() 11 | -------------------------------------------------------------------------------- /test/lua/get_local_variable_test.lua: -------------------------------------------------------------------------------- 1 | local value = 1 2 | local function testfn() 3 | local local_value1 = 1 4 | local local_value2 = "abc" 5 | local local_value3 = value 6 | local local_value4 ={4234.3} 7 | return local_value1,local_value2,local_value3,local_value4 8 | end 9 | 10 | testfn() 11 | -------------------------------------------------------------------------------- /test/lua/get_upvalue_test.lua: -------------------------------------------------------------------------------- 1 | local a=1 2 | local t={{1,2,3,4},5,6} 3 | local function testfn() 4 | local f=a 5 | local ta=t 6 | return f,ta 7 | end 8 | testfn() 9 | -------------------------------------------------------------------------------- /test/lua/loop_test.lua: -------------------------------------------------------------------------------- 1 | local value = 1 2 | 3 | function testfn() 4 | local local_value1 = 1 5 | local local_value2 = "abc" 6 | local local_value3 = 4234.33 7 | local local_value4 ={4234.3} 8 | end 9 | 10 | for i = 1, 10 do 11 | testfn() 12 | end 13 | -------------------------------------------------------------------------------- /test/lua/step_test.lua: -------------------------------------------------------------------------------- 1 | local a=1 2 | local b=1 3 | function testfn() 4 | local f=1 5 | local g=1 6 | end 7 | local c=1 8 | testfn() 9 | local d=1 10 | local e=1 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2020", 5 | "lib": ["ES2020"], 6 | "outDir": "out", 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "strict": true 10 | }, 11 | "exclude": ["node_modules", ".vscode-test"], 12 | "include": ["src"] 13 | } 14 | --------------------------------------------------------------------------------