├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── lint.yml │ └── vimdoc.yaml ├── .gitignore ├── .luacheckrc ├── .stylua.toml ├── LICENSE ├── README.md ├── doc └── dap-cortex-debug.txt ├── lua ├── dap-cortex-debug.lua └── dap-cortex-debug │ ├── adapter.lua │ ├── buffer.lua │ ├── config.lua │ ├── consoles.lua │ ├── dapui │ └── rtt.lua │ ├── health.lua │ ├── hexdump.lua │ ├── listeners.lua │ ├── memory.lua │ ├── requests.lua │ ├── tcp.lua │ ├── terminal │ ├── base.lua │ ├── buf.lua │ ├── codes.lua │ ├── init.lua │ └── term.lua │ └── utils.lua ├── perf └── bytes-to-string.lua └── scripts └── check-readme-config.sh /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Something does not work correctly 4 | title: '' 5 | labels: bug 6 | assignees: jedrzejboczar 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | Describe the problem and what would be the expected behavior. Provide steps to reproduce the bug. 13 | 14 | **Context** 15 | 16 | Check the following things and provide them if there is something that might be helpful: 17 | 18 | 1. Provide your plugin config passed to `require('dap-cortex-debug').setup {}` 19 | 2. Run `:checkhealth dap-cortex-debug` and provide the output. 20 | 3. Provide DAP launch configuration (either `.vscode/launch.json` or directly in Lua) 21 | 4. Use `:DapSetLogLevel DEBUG`, then reproduce the issue. Attach the output of `:DapShowLog`. 22 | 5. Use `require('dap-cortex-debug').setup { debug = true, ... }`, reproduce the issue and attach the output. 23 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | luacheck: 11 | name: Luacheck 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Prepare 16 | run: | 17 | sudo apt-get update 18 | sudo apt-get install luarocks -y 19 | sudo luarocks install luacheck 20 | - name: Run Luacheck 21 | run: luacheck . 22 | 23 | stylua: 24 | name: StyLua 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: Lint with stylua 29 | uses: JohnnyMorganz/stylua-action@v1 30 | with: 31 | token: ${{ secrets.GITHUB_TOKEN }} 32 | args: --check . 33 | 34 | readme_config: 35 | name: Readme config 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | - name: Verify that defaults in README and docs are the same 40 | run: scripts/check-readme-config.sh lua/dap-cortex-debug/config.lua README.md 41 | -------------------------------------------------------------------------------- /.github/workflows/vimdoc.yaml: -------------------------------------------------------------------------------- 1 | name: panvimdoc 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | docs: 10 | runs-on: ubuntu-latest 11 | name: pandoc to vimdoc 12 | steps: 13 | - name: Wait until linting succeeds 14 | uses: lewagon/wait-on-check-action@v1.3.1 15 | with: 16 | ref: master 17 | check-name: Luacheck 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | wait-interval: 10 20 | 21 | - name: Wait until linting succeeds 22 | uses: lewagon/wait-on-check-action@v1.3.1 23 | with: 24 | ref: master 25 | check-name: StyLua 26 | repo-token: ${{ secrets.GITHUB_TOKEN }} 27 | wait-interval: 10 28 | 29 | - name: Wait until linting succeeds 30 | uses: lewagon/wait-on-check-action@v1.3.1 31 | with: 32 | ref: master 33 | check-name: Readme config 34 | repo-token: ${{ secrets.GITHUB_TOKEN }} 35 | wait-interval: 10 36 | - uses: actions/checkout@v2 37 | 38 | - name: Crate output direction 39 | run: mkdir -p ./doc 40 | 41 | - name: panvimdoc 42 | uses: kdheepak/panvimdoc@main 43 | with: 44 | vimdoc: dap-cortex-debug 45 | description: Project-local task management 46 | 47 | - uses: stefanzweifel/git-auto-commit-action@v4 48 | with: 49 | commit_message: "chore(ci): auto generate docs" 50 | branch: ${{ github.head_ref }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/tags 2 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | -- Rerun tests only if their modification time changed. 2 | cache = true 3 | codes = true 4 | 5 | -- Glorious list of warnings: https://luacheck.readthedocs.io/en/stable/warnings.html 6 | ignore = { 7 | "212", -- Unused argument, In the case of callback function, _arg_name is easier to understand than _, so this option is set to off. 8 | "411", -- Redefining a local variable. 9 | "412", -- Redefining an argument. 10 | "422", -- Shadowing an argument 11 | } 12 | 13 | -- Global objects defined by the C code 14 | read_globals = { 15 | "vim", 16 | } 17 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 120 2 | line_endings = "Unix" 3 | indent_type = "Spaces" 4 | indent_width = 4 5 | quote_style = "AutoPreferSingle" 6 | call_parentheses = "NoSingleTable" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jędrzej Boczar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Lint](https://github.com/jedrzejboczar/nvim-dap-cortex-debug/actions/workflows/lint.yml/badge.svg)](https://github.com/jedrzejboczar/nvim-dap-cortex-debug/actions/workflows/lint.yml) 2 | 3 | # nvim-dap-cortex-debug 4 | 5 | An extension for [nvim-dap](https://github.com/mfussenegger/nvim-dap) providing integration with VS Code's [cortex-debug](https://github.com/Marus/cortex-debug) debug adapter. 6 | 7 | ## Features 8 | 9 | - [x] Launch nvim-dap sessions using cortex-debug's `launch.json` 10 | - [x] Support J-Link and OpenOCD 11 | - [ ] Support other GDB servers (#mightwork) 12 | - [x] Globals and Static variable scopes 13 | - [x] Cortex Core Register Viewer (shown under "Registers" scope) 14 | - [ ] Peripheral Register Viewer from SVD file 15 | - [ ] SWO decoding 16 | - [x] SEGGER RTT using OpenOCD/J-Link (currently only "console") 17 | - [x] Raw Memory Viewer 18 | - [ ] Dissassembly viewer 19 | - [ ] RTOS support 20 | - [x] Integration with [nvim-dap-ui](https://github.com/rcarriga/nvim-dap-ui): RTT output 21 | - [x] Download cortex-debug with [mason.nvim](https://github.com/williamboman/mason.nvim) 22 | 23 | ## Installation 24 | 25 | Requirements: 26 | 27 | * [cortex-debug](https://github.com/Marus/cortex-debug) 28 | * [node](https://nodejs.org/en/) (to start cortex-debug) 29 | * [appropriate toolchain and debugger](https://github.com/Marus/cortex-debug#installation) 30 | 31 | To use this plugin you must first install [cortex-debug](https://github.com/Marus/cortex-debug) VS Code extension. 32 | There are a several options: 33 | 34 | * If you're using [mason.nvim](https://github.com/williamboman/mason.nvim) then just `:MasonInstall cortex-debug` 35 | * Install it in VS Code and point `extension_path` to appropriate location. 36 | * Download the extension from [releases](https://github.com/Marus/cortex-debug/releases) and unzip the `.vsix` file (it is just a zip archive) 37 | * [Clone the repo and build from sources](https://github.com/Marus/cortex-debug#how-to-build-from-sources). 38 | 39 | Make sure that the `extension_path` (see [Configuration](#configuration)) is correct. 40 | With the default value of `nil` nvim-dap-cortex-debug will try to detect the path from mason.nvim 41 | from the default VS Code extensions path. 42 | Otherwise configure it yourself - it should be the path to the directory in which `dist/debugadapter.js` is located. 43 | In most cases the directory will be named `marus25.cortex-debug-x.x.x` (so there should be a 44 | `marus25.cortex-debug-x.x.x/dist/debugadapter.js` file). 45 | 46 | Example using [packer.nvim](https://github.com/wbthomason/packer.nvim): 47 | 48 | ```lua 49 | use { 'jedrzejboczar/nvim-dap-cortex-debug', requires = 'mfussenegger/nvim-dap' } 50 | ``` 51 | 52 | ## Configuration 53 | 54 | Call `require('dap-cortex-debug').setup { ... }` in your config. 55 | Available options (with default values): 56 | 57 | ```lua 58 | require('dap-cortex-debug').setup { 59 | debug = false, -- log debug messages 60 | -- path to cortex-debug extension, supports vim.fn.glob 61 | -- by default tries to guess: mason.nvim or VSCode extensions 62 | extension_path = nil, 63 | lib_extension = nil, -- shared libraries extension, tries auto-detecting, e.g. 'so' on unix 64 | node_path = 'node', -- path to node.js executable 65 | dapui_rtt = true, -- register nvim-dap-ui RTT element 66 | -- make :DapLoadLaunchJSON register cortex-debug for C/C++, set false to disable 67 | dap_vscode_filetypes = { 'c', 'cpp' }, 68 | rtt = { 69 | buftype = 'Terminal', -- 'Terminal' or 'BufTerminal' for terminal buffer vs normal buffer 70 | }, 71 | } 72 | ``` 73 | 74 | This will configure nvim-dap adapter (i.e. assign to `dap.adapters['cortex-debug']`) and set up required nvim-dap listeners. 75 | 76 | Now define nvim-dap configuration for debugging, the format is the same as for 77 | [cortex-debug](https://github.com/Marus/cortex-debug/blob/master/debug_attributes.md). 78 | You can use a `launch.json` file (see 79 | [nvim-dap launch.json](https://github.com/mfussenegger/nvim-dap/blob/e71da68e59eec1df258acac20dad206366506438/doc/dap.txt#L276) 80 | for details) or define the configuration in Lua. 81 | When writing the configuration in Lua you may write the whole table manually or use one of the helper functions defined in 82 | [dap-cortex-debug.lua](https://github.com/jedrzejboczar/nvim-dap-cortex-debug/blob/master/lua/dap-cortex-debug.lua) which sets 83 | up some default values that get overwritten by the passed table, e.g. 84 | 85 | 86 | ```lua 87 | local dap_cortex_debug = require('dap-cortex-debug') 88 | require('dap').configurations.c = { 89 | dap_cortex_debug.openocd_config { 90 | name = 'Example debugging with OpenOCD', 91 | cwd = '${workspaceFolder}', 92 | executable = '${workspaceFolder}/build/app', 93 | configFiles = { '${workspaceFolder}/build/openocd/connect.cfg' }, 94 | gdbTarget = 'localhost:3333', 95 | rttConfig = dap_cortex_debug.rtt_config(0), 96 | showDevDebugOutput = false, 97 | }, 98 | } 99 | ``` 100 | 101 |

102 |

103 | which should be equivalent to the following: 104 | 105 | ```lua 106 | local dap_cortex_debug = require('dap-cortex-debug') 107 | require('dap').configurations.c = { 108 | { 109 | name = 'Example debugging with OpenOCD', 110 | type = 'cortex-debug', 111 | request = 'launch', 112 | servertype = 'openocd', 113 | serverpath = 'openocd', 114 | gdbPath = 'arm-none-eabi-gdb', 115 | toolchainPath = '/usr/bin', 116 | toolchainPrefix = 'arm-none-eabi', 117 | runToEntryPoint = 'main', 118 | swoConfig = { enabled = false }, 119 | showDevDebugOutput = false, 120 | gdbTarget = 'localhost:3333', 121 | cwd = '${workspaceFolder}', 122 | executable = '${workspaceFolder}/build/app', 123 | configFiles = { '${workspaceFolder}/build/openocd/connect.cfg' }, 124 | rttConfig = { 125 | address = 'auto', 126 | decoders = { 127 | { 128 | label = 'RTT:0', 129 | port = 0, 130 | type = 'console' 131 | } 132 | }, 133 | enabled = true 134 | }, 135 | } 136 | } 137 | ``` 138 | 139 |
140 |

141 | 142 | GDB server output can be seen in `cotex-debug://gdb-server-console` buffer. It is hidden by default, 143 | use `:buffer` or some buffer picker to open it. If RTT logging is enabled, a terminal buffer with 144 | the output will be opened (with the name `cortex-debug://rtt:PORT` where `PORT` is `rttConfig.decoders[i].port`). 145 | 146 | ### DAP UI 147 | 148 | This extension registers custom DAP UI element `rtt` for viewing RTT channel output, e.g. 149 | 150 | ```lua 151 | require('dapui').setup { 152 | layouts = { 153 | { 154 | position = 'left', 155 | size = 96, 156 | elements = { 157 | { id = 'scopes', size = 0.4 }, 158 | { id = 'rtt', size = 0.6 }, 159 | }, 160 | }, 161 | -- (...) 162 | }, 163 | } 164 | ``` 165 | 166 | ## Troubleshooting 167 | 168 | To verify common problems run [checkhealth](https://neovim.io/doc/user/pi_health.html#pi_health.txt): 169 | ```vim 170 | :checkhealth dap-cortex-debug 171 | ``` 172 | 173 | ## Implementation notes 174 | 175 | [cortex-debug](https://github.com/Marus/cortex-debug) implements 176 | [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/specification) server, 177 | so it should be possible to use it with [nvim-dap](https://github.com/mfussenegger/nvim-dap) 178 | which is a DAP client. However, there are some extensions to DAP that cortex-debug uses, which have 179 | to be implemented separately to make it work with nvim-dap. 180 | 181 | Cortex-debug [is split into two parts](https://github.com/Marus/cortex-debug#how-to-debug): frontend 182 | and backend. Backend is what acts as DAP server and does most of the job, fronted is mostly used for 183 | preparing configuration data and implementing additional functionality like RTT logging or SVD viewer. 184 | For more details see [Cortex Debug: Under the hood](https://github.com/Marus/cortex-debug/wiki/Cortex-Debug:-Under-the-hood). 185 | 186 | This plugin tries to reimplement cortex-debug frontend. It: 187 | 188 | * takes the launch configuration, fills in missing keys, sets default values and checks config correctness; 189 | see `adapter.lua` (backend expects a complete configuration - no missing values) 190 | * starts a server to which the output from gdb-server will be sent; this output is displayed in a terminal buffer 191 | (`cortex-debug://gdb-server-console`) 192 | * if RTT is enabled, the plugin connects via TCP and shows RTT output in a terminal buffer 193 | * hooks into nvim-dap event/command listeners to handle cortex-debug's custom events and fix some existing 194 | incompatibilities 195 | 196 | Implementing a missing cortex-debug feature most likely requires implementing some of the custom events 197 | and displaying the output in Neovim buffers. 198 | -------------------------------------------------------------------------------- /doc/dap-cortex-debug.txt: -------------------------------------------------------------------------------- 1 | *dap-cortex-debug.txt* Project-local task management 2 | 3 | ============================================================================== 4 | Table of Contents *dap-cortex-debug-table-of-contents* 5 | 6 | 1. nvim-dap-cortex-debug |dap-cortex-debug-nvim-dap-cortex-debug| 7 | - Features |dap-cortex-debug-nvim-dap-cortex-debug-features| 8 | - Installation |dap-cortex-debug-nvim-dap-cortex-debug-installation| 9 | - Configuration |dap-cortex-debug-nvim-dap-cortex-debug-configuration| 10 | - Troubleshooting |dap-cortex-debug-nvim-dap-cortex-debug-troubleshooting| 11 | - Implementation notes|dap-cortex-debug-nvim-dap-cortex-debug-implementation-notes| 12 | 2. Links |dap-cortex-debug-links| 13 | 14 | 15 | 16 | 17 | ============================================================================== 18 | 1. nvim-dap-cortex-debug *dap-cortex-debug-nvim-dap-cortex-debug* 19 | 20 | An extension for nvim-dap providing 21 | integration with VS Code’s cortex-debug 22 | debug adapter. 23 | 24 | 25 | FEATURES *dap-cortex-debug-nvim-dap-cortex-debug-features* 26 | 27 | - ☒ Launch nvim-dap sessions using cortex-debug’s `launch.json` 28 | - ☒ Support J-Link and OpenOCD 29 | - ☐ Support other GDB servers (#mightwork) 30 | - ☒ Globals and Static variable scopes 31 | - ☒ Cortex Core Register Viewer (shown under "Registers" scope) 32 | - ☐ Peripheral Register Viewer from SVD file 33 | - ☐ SWO decoding 34 | - ☒ SEGGER RTT using OpenOCD/J-Link (currently only "console") 35 | - ☒ Raw Memory Viewer 36 | - ☐ Dissassembly viewer 37 | - ☐ RTOS support 38 | - ☒ Integration with nvim-dap-ui : RTT output 39 | - ☒ Download cortex-debug with mason.nvim 40 | 41 | 42 | INSTALLATION *dap-cortex-debug-nvim-dap-cortex-debug-installation* 43 | 44 | Requirements: 45 | 46 | - cortex-debug 47 | - node (to start cortex-debug) 48 | - appropriate toolchain and debugger 49 | 50 | To use this plugin you must first install cortex-debug 51 | VS Code extension. There are a several 52 | options: 53 | 54 | - If you’re using mason.nvim then just `:MasonInstall cortex-debug` 55 | - Install it in VS Code and point `extension_path` to appropriate location. 56 | - Download the extension from releases and unzip the `.vsix` file (it is just a zip archive) 57 | - Clone the repo and build from sources . 58 | 59 | Make sure that the `extension_path` (see |dap-cortex-debug-configuration|) is 60 | correct. With the default value of `nil` nvim-dap-cortex-debug will try to 61 | detect the path from mason.nvim from the default VS Code extensions path. 62 | Otherwise configure it yourself - it should be the path to the directory in 63 | which `dist/debugadapter.js` is located. In most cases the directory will be 64 | named `marus25.cortex-debug-x.x.x` (so there should be a 65 | `marus25.cortex-debug-x.x.x/dist/debugadapter.js` file). 66 | 67 | Example using packer.nvim : 68 | 69 | >lua 70 | use { 'jedrzejboczar/nvim-dap-cortex-debug', requires = 'mfussenegger/nvim-dap' } 71 | < 72 | 73 | 74 | CONFIGURATION *dap-cortex-debug-nvim-dap-cortex-debug-configuration* 75 | 76 | Call `require('dap-cortex-debug').setup { ... }` in your config. Available 77 | options (with default values): 78 | 79 | >lua 80 | require('dap-cortex-debug').setup { 81 | debug = false, -- log debug messages 82 | -- path to cortex-debug extension, supports vim.fn.glob 83 | -- by default tries to guess: mason.nvim or VSCode extensions 84 | extension_path = nil, 85 | lib_extension = nil, -- shared libraries extension, tries auto-detecting, e.g. 'so' on unix 86 | node_path = 'node', -- path to node.js executable 87 | dapui_rtt = true, -- register nvim-dap-ui RTT element 88 | -- make :DapLoadLaunchJSON register cortex-debug for C/C++, set false to disable 89 | dap_vscode_filetypes = { 'c', 'cpp' }, 90 | rtt = { 91 | buftype = 'Terminal', -- 'Terminal' or 'BufTerminal' for terminal buffer vs normal buffer 92 | }, 93 | } 94 | < 95 | 96 | This will configure nvim-dap adapter (i.e. assign to 97 | `dap.adapters['cortex-debug']`) and set up required nvim-dap listeners. 98 | 99 | Now define nvim-dap configuration for debugging, the format is the same as for 100 | cortex-debug 101 | . You 102 | can use a `launch.json` file (see nvim-dap launch.json 103 | 104 | for details) or define the configuration in Lua. When writing the configuration 105 | in Lua you may write the whole table manually or use one of the helper 106 | functions defined in dap-cortex-debug.lua 107 | 108 | which sets up some default values that get overwritten by the passed table, 109 | e.g. 110 | 111 | >lua 112 | local dap_cortex_debug = require('dap-cortex-debug') 113 | require('dap').configurations.c = { 114 | dap_cortex_debug.openocd_config { 115 | name = 'Example debugging with OpenOCD', 116 | cwd = '${workspaceFolder}', 117 | executable = '${workspaceFolder}/build/app', 118 | configFiles = { '${workspaceFolder}/build/openocd/connect.cfg' }, 119 | gdbTarget = 'localhost:3333', 120 | rttConfig = dap_cortex_debug.rtt_config(0), 121 | showDevDebugOutput = false, 122 | }, 123 | } 124 | < 125 | 126 | which should be equivalent to the following: ~ 127 | 128 | >lua 129 | local dap_cortex_debug = require('dap-cortex-debug') 130 | require('dap').configurations.c = { 131 | { 132 | name = 'Example debugging with OpenOCD', 133 | type = 'cortex-debug', 134 | request = 'launch', 135 | servertype = 'openocd', 136 | serverpath = 'openocd', 137 | gdbPath = 'arm-none-eabi-gdb', 138 | toolchainPath = '/usr/bin', 139 | toolchainPrefix = 'arm-none-eabi', 140 | runToEntryPoint = 'main', 141 | swoConfig = { enabled = false }, 142 | showDevDebugOutput = false, 143 | gdbTarget = 'localhost:3333', 144 | cwd = '${workspaceFolder}', 145 | executable = '${workspaceFolder}/build/app', 146 | configFiles = { '${workspaceFolder}/build/openocd/connect.cfg' }, 147 | rttConfig = { 148 | address = 'auto', 149 | decoders = { 150 | { 151 | label = 'RTT:0', 152 | port = 0, 153 | type = 'console' 154 | } 155 | }, 156 | enabled = true 157 | }, 158 | } 159 | } 160 | < 161 | 162 | GDB server output can be seen in `cotex-debug://gdb-server-console` buffer. It 163 | is hidden by default, use `:buffer` or some buffer picker to open it. If RTT 164 | logging is enabled, a terminal buffer with the output will be opened (with the 165 | name `cortex-debug://rtt:PORT` where `PORT` is `rttConfig.decoders[i].port`). 166 | 167 | 168 | DAP UI ~ 169 | 170 | This extension registers custom DAP UI element `rtt` for viewing RTT channel 171 | output, e.g. 172 | 173 | >lua 174 | require('dapui').setup { 175 | layouts = { 176 | { 177 | position = 'left', 178 | size = 96, 179 | elements = { 180 | { id = 'scopes', size = 0.4 }, 181 | { id = 'rtt', size = 0.6 }, 182 | }, 183 | }, 184 | -- (...) 185 | }, 186 | } 187 | < 188 | 189 | 190 | TROUBLESHOOTING *dap-cortex-debug-nvim-dap-cortex-debug-troubleshooting* 191 | 192 | To verify common problems run |checkhealth|: 193 | 194 | >vim 195 | :checkhealth dap-cortex-debug 196 | < 197 | 198 | 199 | IMPLEMENTATION NOTES*dap-cortex-debug-nvim-dap-cortex-debug-implementation-notes* 200 | 201 | cortex-debug implements Debug Adapter 202 | Protocol 203 | server, so it should be possible to use it with nvim-dap 204 | which is a DAP client. However, 205 | there are some extensions to DAP that cortex-debug uses, which have to be 206 | implemented separately to make it work with nvim-dap. 207 | 208 | Cortex-debug is split into two parts 209 | : frontend and backend. 210 | Backend is what acts as DAP server and does most of the job, fronted is mostly 211 | used for preparing configuration data and implementing additional functionality 212 | like RTT logging or SVD viewer. For more details see Cortex Debug: Under the 213 | hood . 214 | 215 | This plugin tries to reimplement cortex-debug frontend. It: 216 | 217 | - takes the launch configuration, fills in missing keys, sets default values and checks config correctness; 218 | see `adapter.lua` (backend expects a complete configuration - no missing values) 219 | - starts a server to which the output from gdb-server will be sent; this output is displayed in a terminal buffer 220 | (`cortex-debug://gdb-server-console`) 221 | - if RTT is enabled, the plugin connects via TCP and shows RTT output in a terminal buffer 222 | - hooks into nvim-dap event/command listeners to handle cortex-debug’s custom events and fix some existing 223 | incompatibilities 224 | 225 | Implementing a missing cortex-debug feature most likely requires implementing 226 | some of the custom events and displaying the output in Neovim buffers. 227 | 228 | ============================================================================== 229 | 2. Links *dap-cortex-debug-links* 230 | 231 | 1. *Lint*: https://github.com/jedrzejboczar/nvim-dap-cortex-debug/actions/workflows/lint.yml/badge.svg 232 | 233 | Generated by panvimdoc 234 | 235 | vim:tw=78:ts=8:noet:ft=help:norl: 236 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local dap = require('dap') 4 | local config = require('dap-cortex-debug.config') 5 | local listeners = require('dap-cortex-debug.listeners') 6 | local adapter = require('dap-cortex-debug.adapter') 7 | local memory = require('dap-cortex-debug.memory') 8 | local requests = require('dap-cortex-debug.requests') 9 | local utils = require('dap-cortex-debug.utils') 10 | 11 | function M.setup(opts) 12 | config.setup(opts) 13 | listeners.setup() 14 | 15 | -- TODO: is this necessary? 16 | dap.defaults['cortex-debug'].auto_continue_if_many_stopped = false 17 | 18 | -- Could be a function(cb, config) to auto-generate docker command arguments 19 | dap.adapters['cortex-debug'] = adapter 20 | 21 | if config.dap_vscode_filetypes then 22 | require('dap.ext.vscode').type_to_filetypes['cortex-debug'] = config.dap_vscode_filetypes 23 | end 24 | 25 | local hex_mode_on = false 26 | local function set_hex_mode(on) 27 | hex_mode_on = on 28 | requests.set_var_format(nil, { hex = hex_mode_on }) 29 | end 30 | vim.api.nvim_create_user_command('CortexDebugVarHexModeOn', function() 31 | set_hex_mode(true) 32 | end, {}) 33 | vim.api.nvim_create_user_command('CortexDebugVarHexModeOff', function() 34 | set_hex_mode(false) 35 | end, {}) 36 | vim.api.nvim_create_user_command('CortexDebugVarHexModeToggle', function() 37 | set_hex_mode(not hex_mode_on) 38 | end, {}) 39 | 40 | -- TODO: completion of variable names that maps them to address? 41 | -- TODO: handle mods for location of window 42 | -- Keep CDMemory name for backwards compatibility 43 | for _, cmd_name in ipairs { 'CDMemory', 'CortexDebugMemory' } do 44 | vim.api.nvim_create_user_command(cmd_name, function(o) 45 | coroutine.wrap(function() 46 | local address, length 47 | if #o.fargs == 2 then 48 | address = utils.assert(tonumber(o.fargs[1]), 'Incorrect `address`: %s', o.fargs[1]) 49 | length = utils.assert(tonumber(o.fargs[2]), 'Incorrect `length`: %s', o.fargs[1]) 50 | elseif #o.fargs == 1 then 51 | local err, mem = memory.var_to_mem(o.fargs[1]) 52 | if err then 53 | utils.error('Error when evaluating "%s": %s', o.fargs[1], err.message or err) 54 | return 55 | end 56 | assert(mem ~= nil) 57 | address, length = mem.address, mem.length 58 | else 59 | utils.error('Incorrect number of arguments') 60 | return 61 | end 62 | memory.show { address = address, length = length, id = o.count } 63 | end)() 64 | end, { desc = 'Open memory viewer', nargs = '+', range = 1 }) 65 | end 66 | 67 | if config.dapui_rtt then 68 | local ok, dapui = pcall(require, 'dapui') 69 | if ok then 70 | dapui.register_element('rtt', require('dap-cortex-debug.dapui.rtt')) 71 | else 72 | utils.warn_once('nvim-dap-ui not installed, cannot register RTT element') 73 | end 74 | end 75 | end 76 | 77 | ---@class RTTChannel 78 | ---@field port number 79 | ---@field type "console"|"binary" 80 | 81 | ---Generate basic RTT configuration with decoders for given channels 82 | ---@param channels? number|number[]|RTTChannel[] Channels to use 83 | ---@return table Configuration assignable to "rttConfig" field 84 | function M.rtt_config(channels) 85 | if type(channels) ~= 'table' then 86 | channels = { channels } 87 | end 88 | return { 89 | enabled = #channels > 0, 90 | address = 'auto', 91 | decoders = vim.tbl_map(function(channel) 92 | local port = channel 93 | local typ = 'console' 94 | if type(channel) == 'table' then 95 | port = channel.port 96 | typ = channel.type 97 | end 98 | return { 99 | label = 'RTT:' .. port, 100 | port = port, 101 | type = typ, 102 | } 103 | end, channels), 104 | } 105 | end 106 | 107 | function M.jlink_config(overrides) 108 | local defaults = { 109 | type = 'cortex-debug', 110 | request = 'attach', 111 | servertype = 'jlink', 112 | interface = 'jtag', 113 | serverpath = 'JLinkGDBServerCLExe', 114 | gdbPath = 'arm-none-eabi-gdb', 115 | toolchainPath = '/usr/bin', 116 | toolchainPrefix = 'arm-none-eabi', 117 | runToEntryPoint = 'main', 118 | swoConfig = { enabled = false }, 119 | rttConfig = M.rtt_config(), 120 | } 121 | return vim.tbl_deep_extend('force', defaults, overrides) 122 | end 123 | 124 | function M.openocd_config(overrides) 125 | local defaults = { 126 | type = 'cortex-debug', 127 | request = 'launch', 128 | servertype = 'openocd', 129 | serverpath = 'openocd', 130 | gdbPath = 'arm-none-eabi-gdb', 131 | toolchainPath = '/usr/bin', 132 | toolchainPrefix = 'arm-none-eabi', 133 | runToEntryPoint = 'main', 134 | swoConfig = { enabled = false }, 135 | rttConfig = M.rtt_config(), 136 | } 137 | return vim.tbl_deep_extend('force', defaults, overrides) 138 | end 139 | 140 | return M 141 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/adapter.lua: -------------------------------------------------------------------------------- 1 | local config = require('dap-cortex-debug.config') 2 | local consoles = require('dap-cortex-debug.consoles') 3 | local utils = require('dap-cortex-debug.utils') 4 | 5 | local valid_rtos = { 6 | jlink = { 'Azure', 'ChibiOS', 'embOS', 'FreeRTOS', 'NuttX', 'Zephyr' }, 7 | openocd = { 'ChibiOS', 'eCos', 'embKernel', 'FreeRTOS', 'mqx', 'nuttx', 'ThreadX', 'uCOS-III', 'auto' }, 8 | } 9 | 10 | local verifiers = {} 11 | 12 | function verifiers.jlink(c) 13 | if not c.interface then 14 | c.interface = 'swd' 15 | end 16 | 17 | utils.assert( 18 | c.device, 19 | 'Device Identifier is required for J-Link configurations. ' 20 | .. 'Please see https://www.segger.com/downloads/supported-devices.php for supported devices' 21 | ) 22 | 23 | utils.assert( 24 | not ( 25 | vim.tbl_contains({ 'jtag', 'cjtag' }, c.interface) 26 | and c.swoConfig.enabled 27 | and c.swoConfig.source == 'probe' 28 | ), 29 | 'SWO Decoding cannot be performed through the J-Link Probe in JTAG mode.' 30 | ) 31 | 32 | local count = 0 33 | for _, decoder in ipairs(c.rttConfig.decoders) do 34 | utils.assert( 35 | decoder.port >= 0 and decoder.port <= 15, 36 | 'Invalid RTT port %s, must be between 0 and 15.', 37 | decoder.port 38 | ) 39 | count = count + 1 40 | end 41 | 42 | utils.assert(count < 2, 'JLink RTT only allows a single RTT port/channel per debugging session but got %s', count) 43 | 44 | if c.rtos then 45 | local valid = valid_rtos.jlink 46 | if vim.tbl_contains(valid, c.rtos) then 47 | c.rtos = string.format('GDBServer/RTOSPlugin_%s.%s', c.rtos, utils.get_lib_ext()) 48 | else 49 | if vim.fn.fnamemodify(c.rtos, ':e') == '' then 50 | c.rtos = c.rtos .. '.' .. utils.get_lib_ext() 51 | end 52 | utils.assert( 53 | vim.fn.filereadable(c.rtos), 54 | 'JLink RTOS plugin file not found: "%s". Supported RTOS values: %s. Or use full path to JLink plugin.', 55 | c.rtos, 56 | table.concat(valid, ', ') 57 | ) 58 | end 59 | end 60 | 61 | return c 62 | end 63 | 64 | function verifiers.openocd(c) 65 | utils.assert(c.configFiles and #c.configFiles > 0, 'At least one OpenOCD Configuration File must be specified.') 66 | c.searchDir = c.searchDir or {} 67 | 68 | if c.rtos then 69 | local valid = valid_rtos.openocd 70 | utils.assert( 71 | vim.tbl_contains(valid, c.rtos), 72 | 'Invalid RTOS for %s, available: %s', 73 | c.servertype, 74 | table.concat(valid, ', ') 75 | ) 76 | end 77 | 78 | return c 79 | end 80 | 81 | local function no_rtos(c, server_name) 82 | utils.assert(not c.rtos, server_name .. ' GDB server does not support "rtos"') 83 | end 84 | 85 | local function no_swo(c, server_name) 86 | if c.swoConfig and c.swoConfig.enabled and c.swoConfig.source == 'probe' then 87 | utils.warn('SWO from "probe" not available when using %s GDB server. Disabling.', server_name) 88 | c.swoConfig = { cpuFrequency = 0, enabled = false, ports = {}, swoFrequency = 0 } 89 | c.graphConfig = {} 90 | end 91 | end 92 | 93 | function verifiers.stutil(c) 94 | no_rtos(c, 'ST-Util') 95 | no_swo(c, 'ST-Util') 96 | return c 97 | end 98 | 99 | function verifiers.stlink(c) 100 | no_rtos(c, 'ST-Link') 101 | no_swo(c, 'ST-Link') 102 | return c 103 | end 104 | 105 | function verifiers.pyocd(c) 106 | no_rtos(c, 'PyOCD') 107 | if c.board and not c.boardId then 108 | c.boardId = c.board 109 | end 110 | if c.target and not c.targetId then 111 | c.targetId = c.target 112 | end 113 | return c 114 | end 115 | 116 | function verifiers.bmp(c) 117 | utils.assert(c.BMPGDBSerialPort, '"BMPGDBSerialPort" is required for Black Magic Proble GDB server') 118 | c.powerOverBMP = c.powerOverBMP or 'lastState' 119 | c.interface = c.interface or 'swd' 120 | c.targetId = c.targetId or 1 121 | no_rtos(c, 'Black Magic Probe') 122 | no_swo(c, 'Black Magic Probe') 123 | return c 124 | end 125 | 126 | function verifiers.pe(c) 127 | utils.assert(not (c.configFiles and #c.configFiles > 1), 'Only one pegdbserver Configuration File is allowed') 128 | utils.assert( 129 | c.device, 130 | 'Device Identifier is required for PE configurations. Check `pegdbserver_console.exe -devicelist`.' 131 | ) 132 | utils.assert( 133 | not (c.swoConfig and c.swoConfig.enabled and c.swoConfig.source ~= 'socket'), 134 | 'The PE GDB Server Only supports socket type SWO' 135 | ) 136 | 137 | return c 138 | end 139 | 140 | function verifiers.external(c) 141 | if c.swoConfig and c.swoConfig.enabled then 142 | if c.swoConfig.source == 'socket' and not c.swoConfig.swoPort then 143 | utils.warn('SWO source type "socket" requires "swoPort". Disabling SWO support.') 144 | config.swoConfig = { enabled = false } 145 | config.graphConfig = {} 146 | elseif c.swoConfig.source ~= 'socket' and not c.swoConfig.swoPath then 147 | utils.warn('SWO source type "%s" requires "swoPath". Disabling SWO support.', c.swoConfig.source) 148 | config.swoConfig = { enabled = false } 149 | config.graphConfig = {} 150 | end 151 | end 152 | utils.assert( 153 | c.gdbTarget, 154 | 'External GDB server type must specify the GDB target.' 155 | .. ' This should either be a "hostname:port" combination or a serial port.' 156 | ) 157 | return c 158 | end 159 | 160 | function verifiers.qemu(c) 161 | c.cpu = c.cpu or 'cortex-m3' 162 | c.machine = c.machine or 'lm3s6965evb' 163 | no_rtos(c, 'QEMU') 164 | no_swo(c, 'QEMU') 165 | return c 166 | end 167 | 168 | local function sanitize_dev_debug(c) 169 | local modes = { 170 | none = 'none', 171 | parsed = 'parsed', 172 | both = 'both', 173 | raw = 'raw', 174 | vscode = 'vscode', 175 | } 176 | if type(c.showDevDebugOutput) == 'string' then 177 | c.showDevDebugOutput = vim.trim(c.showDevDebugOutput:lower()) 178 | end 179 | if vim.tbl_contains({ false, 'false', '', 'none' }, c.showDevDebugOutput) then 180 | c.showDevDebugOutput = nil 181 | elseif vim.tbl_contains({ true, 'true' }, c.showDevDebugOutput) then 182 | c.showDevDebugOutput = modes.raw 183 | elseif not modes[c.showDevDebugOutput] then 184 | c.showDevDebugOutput = 'vscode' 185 | end 186 | end 187 | 188 | -- Imitate cortex-debug/src/frontend/configprovider.ts 189 | local function verify_config(c) 190 | -- Flatten platform specific config 191 | local platform = utils.get_platform() 192 | c = vim.tbl_extend('force', c, c[platform] or {}) 193 | c[platform] = nil 194 | 195 | -- There is some code that makes sure to resolve deprecated options but we won't support this. 196 | local assert_deprecated = function(old, new) 197 | local old_path = vim.split(old, '.', { plain = true }) 198 | local old_value = vim.tbl_get(c, unpack(old_path)) 199 | utils.assert(old_value == nil, '"%s" is not supported, use "%s"', old, new) 200 | end 201 | assert_deprecated('debugger_args', 'debuggerArgs') 202 | assert_deprecated('swoConfig.ports', 'swoConfig.decoders') 203 | assert_deprecated('runToMain', 'runToEntryPoint = "main"') 204 | assert_deprecated('armToolchainPath', 'toolchainPath') 205 | assert_deprecated('jlinkpath', 'serverpath') 206 | assert_deprecated('jlinkInterface', 'interface') 207 | assert_deprecated('openOCDPath', 'serverpath') 208 | 209 | -- TODO: pvtAvoidPorts 210 | -- TODO: chained configs? 211 | 212 | -- Ensure that following keys exist even if not provided or debug adapter may fail 213 | local defaults = { 214 | cwd = vim.fn.getcwd(), 215 | debuggerArgs = {}, 216 | swoConfig = { enabled = false, decoders = {}, cpuFrequency = 0, swoFrequency = 0, source = 'probe' }, 217 | rttConfig = { enabled = false, decoders = {} }, 218 | graphConfig = {}, 219 | preLaunchCommands = {}, 220 | postLaunchCommands = {}, 221 | preAttachCommands = {}, 222 | postAttachCommands = {}, 223 | preRestartCommands = {}, 224 | postRestartCommands = {}, 225 | toolchainPrefix = 'arm-none-eabi', 226 | registerUseNaturalFormat = true, 227 | variableUseNaturalFormat = true, 228 | } 229 | c = vim.tbl_deep_extend('keep', c, defaults) 230 | 231 | c.runToEntryPoint = c.runToEntryPoint and vim.trim(c.runToEntryPoint) 232 | 233 | if c.servertype ~= 'openocd' or not vim.tbl_get(c, 'ctiOpenOCDConfig', 'enabled') then 234 | c.ctiOpenOCDConfig = nil 235 | end 236 | 237 | sanitize_dev_debug(c) 238 | 239 | -- Warn because it might be confusing 240 | if vim.endswith(c.toolchainPrefix, '-') then 241 | utils.warn_once('toolchainPrefix should not end with "-", e.g. "arm-none-eabi"') 242 | end 243 | 244 | local verify = utils.assert(verifiers[c.servertype], 'Unsupported servertype: %s', c.servertype) 245 | c = verify(c) 246 | 247 | if platform == 'windows' then 248 | -- This is passed to GDB so must use forward slash instead of backslash 249 | c.extensionPath = c.extensionPath:gsub([[\]], '/') 250 | c.executable = c.executable:gsub([[\]], '/') 251 | end 252 | 253 | return c 254 | end 255 | 256 | ---Debug adapter configuration in functional variant; assignable to dap.adapters[...] 257 | ---@param callback function 258 | ---@param launch_config table 259 | local function adapter_fn(callback, launch_config) 260 | -- Currently it's not strictly necessary to use functional variant, but we'll see... 261 | local extension_path = launch_config.extensionPath or utils.get_extension_path() 262 | if not extension_path then 263 | utils.error('Missing cortex-debug extension_path') 264 | end 265 | launch_config.extensionPath = extension_path 266 | 267 | -- Ensure GDB server console has been started 268 | local port = consoles.gdb_server_console(launch_config.dbgServerLogfile).port 269 | 270 | callback { 271 | type = 'executable', 272 | command = config.node_path, 273 | args = { utils.get_debugadapter_path(extension_path) }, 274 | options = { detached = false }, 275 | enrich_config = function(conf, on_config) 276 | local ok, conf_or_err = utils.trace_pcall(verify_config, vim.deepcopy(conf)) 277 | if ok then 278 | conf = conf_or_err 279 | else 280 | utils.error('Launch config error: %s', conf_or_err) 281 | return false 282 | end 283 | 284 | conf.gdbServerConsolePort = port 285 | 286 | on_config(conf) 287 | end, 288 | } 289 | end 290 | 291 | return adapter_fn 292 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/buffer.lua: -------------------------------------------------------------------------------- 1 | local utils = require('dap-cortex-debug.utils') 2 | 3 | ---@alias BufferSetWin fun(buf: number): number, function? 4 | ---@alias Uri string 5 | 6 | ---@class CDBufferOpts 7 | ---Assigns buffer to a window, return window and optional callback to call when is ready. 8 | ---@field set_win BufferSetWin 9 | ---@field uri Uri 10 | ---@field on_delete? fun(b: CDBuffer) 11 | 12 | ---@class CDBuffer:Class 13 | ---@field buf number 14 | ---@field on_delete? fun(b: CDBuffer) 15 | local Buffer = utils.class() 16 | 17 | local augroup = vim.api.nvim_create_augroup('CortexDebugBuffer', { clear = true }) 18 | 19 | ---@type { [Uri]: CDBuffer } 20 | local buffers = {} 21 | 22 | ---Create new buffer object with its buffer. 23 | ---NOTE: For terminals this needs to open a window, at least temporarily. 24 | ---Will delete previous buffer with the same URI. `get_or_new` can be used instead. 25 | ---@param opts CDBufferOpts 26 | ---@return CDBuffer 27 | function Buffer:new(opts, instance) 28 | if buffers[opts.uri] then 29 | buffers[opts.uri]:delete() 30 | end 31 | 32 | local b = instance or self:_new() 33 | b.buf = nil 34 | b.on_delte = nil 35 | b.uri = assert(opts.uri) 36 | 37 | b:_create_buf(opts.set_win) 38 | b:_create_autocmds() 39 | 40 | buffers[b.uri] = b 41 | 42 | return b 43 | end 44 | 45 | function Buffer.get(uri) 46 | return buffers[uri] 47 | end 48 | 49 | function Buffer._get_or_new(cls) 50 | return function(opts) 51 | return cls.get(opts.uri) or cls:new(opts) 52 | end 53 | end 54 | 55 | --- Must be overwritten in deriving classes since this is a constructor 56 | Buffer.get_or_new = Buffer._get_or_new(Buffer) 57 | 58 | function Buffer:delete() 59 | pcall(vim.api.nvim_buf_delete, self.buf, { force = true }) 60 | end 61 | 62 | local function delete_buf_by_name(name) 63 | for _, buf in ipairs(vim.api.nvim_list_bufs()) do 64 | if vim.api.nvim_buf_get_name(buf) == name then 65 | vim.api.nvim_buf_delete(buf, { force = true }) 66 | return 67 | end 68 | end 69 | end 70 | 71 | local function set_buf_name(buf, uri) 72 | -- If the buffer already exists then it might e.g. have been restored by mksession - delete it 73 | -- We do not care for buffers with our names matching our URI scheme that are not owned by us, 74 | -- so delete forcefully. 75 | delete_buf_by_name(uri) 76 | vim.api.nvim_buf_set_name(buf, uri) 77 | end 78 | 79 | ---Set buffer URI 80 | ---@param uri Uri 81 | function Buffer:set_uri(uri) 82 | if buffers[uri] then 83 | utils.error('Buffer with given URI already exists: "%s"', uri) 84 | return 85 | end 86 | -- TODO: set user friendly b:term_title? 87 | set_buf_name(self.buf, uri) 88 | buffers[self.uri] = nil 89 | self.uri = uri 90 | buffers[self.uri] = self 91 | end 92 | 93 | function Buffer:_create_buf(set_win) 94 | self.buf = vim.api.nvim_create_buf(true, true) 95 | self:set_uri(self.uri) 96 | 97 | local win, on_ready = set_win(self.buf) 98 | vim.api.nvim_set_option_value('number', false, { win = win, scope = 'local' }) 99 | vim.api.nvim_set_option_value('relativenumber', false, { win = win, scope = 'local' }) 100 | vim.api.nvim_set_option_value('spell', false, { win = win, scope = 'local' }) 101 | 102 | self:_create_buf_final() 103 | 104 | if on_ready then 105 | on_ready(self) 106 | end 107 | end 108 | 109 | function Buffer:_create_buf_final() end 110 | 111 | function Buffer:_create_autocmds() 112 | vim.api.nvim_create_autocmd('BufDelete', { 113 | group = augroup, 114 | buffer = self.buf, 115 | once = true, 116 | callback = function() 117 | buffers[self.uri] = nil 118 | if self.on_delete then 119 | self:on_delete() 120 | end 121 | end, 122 | }) 123 | end 124 | 125 | function Buffer:is_visible() 126 | return vim.api.nvim_win_is_valid(vim.fn.bufwinid(self.buf)) 127 | end 128 | 129 | function Buffer:is_valid() 130 | return vim.api.nvim_buf_is_valid(self.buf) 131 | end 132 | 133 | function Buffer.temporary_win(buf) 134 | local curr_win = vim.api.nvim_get_current_win() 135 | local new_win = vim.api.nvim_open_win(buf, false, { 136 | relative = 'win', 137 | win = curr_win, 138 | width = vim.api.nvim_win_get_width(curr_win), 139 | height = vim.api.nvim_win_get_height(curr_win), 140 | row = 0, 141 | col = 0, 142 | style = 'minimal', 143 | }) 144 | return new_win, function() 145 | vim.api.nvim_win_close(new_win, false) 146 | end 147 | end 148 | 149 | function Buffer.open_in_split(opts) 150 | return function(buf) 151 | local prev_win = vim.api.nvim_get_current_win() 152 | vim.cmd(table.concat({ opts.mods or '', opts.size or '', 'split' }, ' ')) 153 | local new_win = vim.api.nvim_get_current_win() 154 | vim.api.nvim_win_set_buf(new_win, buf) 155 | if not opts.focus then 156 | vim.api.nvim_set_current_win(prev_win) 157 | end 158 | return new_win 159 | end 160 | end 161 | 162 | return Buffer 163 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/config.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | -- Use a function to always get a new table, even if some deep field is modified, 4 | -- like `config.commands.save = ...`. Returning a "constant" still seems to allow 5 | -- the LSP completion to work. 6 | local function defaults() 7 | -- stylua: ignore 8 | return { 9 | debug = false, 10 | extension_path = nil, 11 | lib_extension = nil, 12 | node_path = 'node', 13 | dapui_rtt = true, 14 | dap_vscode_filetypes = { 'c', 'cpp' }, 15 | rtt = { 16 | buftype = 'Terminal', 17 | }, 18 | } 19 | end 20 | 21 | local config = defaults() 22 | 23 | function M.setup(opts) 24 | local new_config = vim.tbl_deep_extend('force', {}, defaults(), opts or {}) 25 | -- Do _not_ replace the table pointer with `config = ...` because this 26 | -- wouldn't change the tables that have already been `require`d by other 27 | -- modules. Instead, clear all the table keys and then re-add them. 28 | for _, key in ipairs(vim.tbl_keys(config)) do 29 | config[key] = nil 30 | end 31 | for key, val in pairs(new_config) do 32 | config[key] = val 33 | end 34 | end 35 | 36 | -- Return the config table (getting completion!) but fall back to module methods. 37 | return setmetatable(config, { __index = M }) 38 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/consoles.lua: -------------------------------------------------------------------------------- 1 | local tcp = require('dap-cortex-debug.tcp') 2 | local utils = require('dap-cortex-debug.utils') 3 | local config = require('dap-cortex-debug.config') 4 | local terminal = require('dap-cortex-debug.terminal') 5 | 6 | local M = {} 7 | 8 | local gdb_server_console = { 9 | server = nil, 10 | port = nil, 11 | } 12 | 13 | local function log_timestamp() 14 | return os.date('%Y-%m-%d_%H:%M:%S') 15 | end 16 | 17 | ---@class dap-cortex-debug.Logfile 18 | ---@field fd number? file descriptor 19 | ---@field filename string? 20 | local Logfile = {} 21 | Logfile.__index = Logfile 22 | 23 | function Logfile:new(filename) 24 | local o = setmetatable({ 25 | fd = nil, 26 | filename = filename, 27 | }, self) 28 | if filename then 29 | o:_open() 30 | o:write('\n') -- mark new "open" with a newline 31 | o:write(string.format('LOG START: %s\n', log_timestamp())) 32 | end 33 | return o 34 | end 35 | 36 | function Logfile:_open() 37 | self.fd = vim.loop.fs_open(self.filename, 'a', 438) 38 | if not self.fd then 39 | utils.warn('Could not open logfile: %s', self.filename) 40 | return 41 | end 42 | end 43 | 44 | function Logfile:write(data) 45 | if not self.fd then 46 | return 47 | end 48 | local ok = vim.loop.fs_write(self.fd, data) 49 | if not ok then 50 | utils.warn_once('Writing to logfile failed: %s', self.filename) 51 | end 52 | end 53 | 54 | function Logfile:close() 55 | if not self.fd then 56 | return 57 | end 58 | self:write(string.format('LOG END: %s\n', log_timestamp())) 59 | vim.loop.fs_close(self.fd) 60 | end 61 | 62 | function M.gdb_server_console_term() 63 | return terminal.Terminal.get_or_new { 64 | set_win = terminal.Terminal.temporary_win, 65 | uri = [[cortex-debug://gdb-server-console]], 66 | on_delete = function() 67 | if gdb_server_console.server then 68 | local server = gdb_server_console.server 69 | gdb_server_console.server = nil 70 | if server then 71 | server:shutdown(function() 72 | server:close() 73 | end) 74 | end 75 | end 76 | end, 77 | } --[[@as CDTerminal]] 78 | end 79 | 80 | function M.gdb_server_console(logfile) 81 | if not gdb_server_console.server then 82 | gdb_server_console.port = tcp.get_free_port(55878) 83 | gdb_server_console.server = tcp.serve { 84 | port = gdb_server_console.port, 85 | on_connect = function(sock) 86 | local sock_info = sock:getsockname() 87 | -- Cannot create terminal in callback so do wait for loop 88 | vim.schedule(function() 89 | local term = M.gdb_server_console_term() 90 | term:scroll() 91 | term:send_line(string.format('Connected from %s:%d', sock_info.ip, sock_info.port), { bold = true }) 92 | 93 | local log = Logfile:new(logfile) 94 | 95 | sock:read_start(function(err, data) 96 | if err then 97 | term:send_line('ERROR: ' .. err, { bold = true, error = true }) 98 | elseif data then 99 | log:write(data) 100 | term:send(data) 101 | else 102 | sock:close() 103 | log:close() 104 | term:send_line('Disconnected\n', { bold = true }) 105 | end 106 | end) 107 | end) 108 | end, 109 | on_error = function(err) 110 | utils.error('Could not open gdb server console: %s', err) 111 | gdb_server_console.server = nil 112 | gdb_server_console.port = nil 113 | end, 114 | } 115 | end 116 | return gdb_server_console 117 | end 118 | 119 | function M.rtt_term(channel, set_win) 120 | local Term = assert(terminal[config.rtt.buftype], 'Invalid value for rtt.buftype') 121 | local default_set_win = config.dapui_rtt and Term.temporary_win 122 | or Term.open_in_split { size = 80, mods = 'vertical' } 123 | return Term.get_or_new { 124 | uri = string.format([[cortex-debug://rtt:%d]], channel), 125 | set_win = set_win or default_set_win, 126 | } --[[@as CDTerminal]] 127 | end 128 | 129 | ---@class dap-cortex-debug.RTTConnectOpts 130 | ---@field channel number 131 | ---@field tcp_port number 132 | ---@field logfile? string 133 | 134 | local function datetime() 135 | return os.date('%H:%M:%S %Y-%m-%d') 136 | end 137 | 138 | ---@param opts dap-cortex-debug.RTTConnectOpts 139 | ---@param on_connected fun(client, term) 140 | ---@param on_client_connected? fun(client) raw client connection callback, without vim.schedule 141 | function M.rtt_connect(opts, on_connected, on_client_connected) 142 | local on_connect = vim.schedule_wrap(function(client) 143 | local term = M.rtt_term(opts.channel) 144 | 145 | if getmetatable(term) == terminal.BufTerminal then 146 | -- TODO: support more sessions but simulate 'scrollback' 147 | term:clear() -- single session only 148 | end 149 | 150 | term:send_line(string.format('Connected on port %d at %s', opts.tcp_port, datetime()), { bold = true }) 151 | 152 | local log = Logfile:new(opts.logfile) 153 | 154 | client:read_start(function(err, data) 155 | if err then 156 | term:send_line('ERROR: ' .. err, { bold = true, error = true }) 157 | elseif data then 158 | if #data > 0 then 159 | log:write(data) 160 | term:send(data) 161 | end 162 | else 163 | client:shutdown() 164 | client:close() 165 | log:close() 166 | pcall(vim.api.nvim_buf_delete, term.buf, { force = true }) 167 | term:send_line(string.format('Disconnected at %s\n', datetime()), { bold = true }) 168 | end 169 | end) 170 | 171 | on_connected(client, term) 172 | end) 173 | 174 | local on_success = function(client) 175 | if on_client_connected then 176 | on_client_connected(client) 177 | end 178 | on_connect(client) 179 | end 180 | 181 | tcp.connect { 182 | host = '0.0.0.0', 183 | port = opts.tcp_port, 184 | delay = 10, 185 | delay_multiplier = 2, 186 | delay_total_max = 5000, 187 | on_error = vim.schedule_wrap(function(err) 188 | utils.error('Failed to connect RTT:%d on TCP port %d: %s', opts.channel, opts.tcp_port, err) 189 | end), 190 | on_success = on_success, 191 | } 192 | end 193 | 194 | return M 195 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/dapui/rtt.lua: -------------------------------------------------------------------------------- 1 | local consoles = require('dap-cortex-debug.consoles') 2 | 3 | -- Find first open RTT channel 4 | local function find_rtt_channel() 5 | for _, buf in ipairs(vim.api.nvim_list_bufs()) do 6 | local name = vim.api.nvim_buf_get_name(buf) 7 | local channel = name:match([[cortex%-debug://rtt:([0-9]+)]]) 8 | channel = vim.F.npcall(tonumber, channel) 9 | if channel then 10 | return channel 11 | end 12 | end 13 | end 14 | 15 | local tmp_buf 16 | 17 | ---@type dapui.Element 18 | return { 19 | render = function() end, 20 | buffer = function() 21 | local channel = find_rtt_channel() 22 | if not channel then 23 | if not tmp_buf or not vim.api.nvim_buf_is_valid(tmp_buf) then 24 | tmp_buf = vim.api.nvim_create_buf(false, true) 25 | end 26 | return tmp_buf 27 | end 28 | local term = consoles.rtt_term(channel) 29 | return term.buf 30 | end, 31 | float_defaults = function() 32 | return { width = 80, height = 20, enter = true } 33 | end, 34 | on_rtt_connect = function(channel) 35 | -- Force dap-ui to reevaluate buffer() by changing the temporary buffer in our window 36 | if tmp_buf and vim.api.nvim_buf_is_valid(tmp_buf) then 37 | -- if our tmp buf is being displayed 38 | local win = vim.fn.bufwinid(tmp_buf) 39 | if vim.api.nvim_win_is_valid(win) then 40 | -- replace it with term buffer 41 | local term = consoles.rtt_term(channel) 42 | vim.api.nvim_win_set_buf(win, term.buf) 43 | end 44 | end 45 | end, 46 | } 47 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/health.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local config = require('dap-cortex-debug.config') 4 | local utils = require('dap-cortex-debug.utils') 5 | 6 | function M.check() 7 | vim.health.start('nvim-dap-cortex-debug') 8 | 9 | if vim.fn.executable(config.node_path) == 1 then 10 | local ok, version = pcall(vim.fn.system, 'node --version') 11 | if ok and version then 12 | vim.health.ok('Node.js installed: ' .. vim.trim(version)) 13 | else 14 | vim.health.error('Node.js executable but `node --version` failed') 15 | end 16 | else 17 | vim.health.error('Node.js not installed') 18 | end 19 | 20 | local extension_path = utils.get_extension_path() 21 | if extension_path and vim.fn.isdirectory(extension_path) == 1 then 22 | vim.health.ok('cortex-debug extension found: ' .. extension_path) 23 | 24 | local debugadapter_path = utils.get_debugadapter_path(extension_path) 25 | if vim.fn.filereadable(debugadapter_path) == 1 then 26 | vim.health.ok('Found debugadapter.js: ' .. debugadapter_path) 27 | else 28 | vim.health.error('debugadapter.js not found: ' .. debugadapter_path) 29 | end 30 | elseif extension_path then 31 | vim.health.error('cortex-debug extension path not a directory: ' .. extension_path) 32 | else 33 | vim.health.error('cortex-debug extension not found') 34 | end 35 | end 36 | 37 | return M 38 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/hexdump.lua: -------------------------------------------------------------------------------- 1 | local utils = require('dap-cortex-debug.utils') 2 | 3 | ---@class HexDump:Class 4 | ---@field start_addr integer Starting address 5 | ---@field per_line integer Number of bytes shown in each line 6 | ---@field word_bytes integer Number of bytes grouped into a single word (without spaces) 7 | ---@field endianess 'little'|'big' Show words as little-endian / big-endian 8 | ---@field group_by integer Size of group (in bytes), groups are separated by additional space 9 | ---@field addr_0x boolean Whether to add 0x prefix to address 10 | ---@field spaces integer Number of spaces used between sections 11 | ---@field _fmt { [string]: { [1]: string, [2]: integer }} 12 | local HexDump = utils.class() 13 | 14 | ---@class HexDumpOpts 15 | ---@field start_addr? integer 16 | ---@field per_line? integer 17 | ---@field word_bytes? integer 18 | ---@field endianess? 'little'|'big' 19 | ---@field group_by? integer 20 | ---@field addr_0x? boolean 21 | ---@field spaces? integer 22 | 23 | ---@param opts? HexDumpOpts 24 | ---@return HexDump 25 | function HexDump:new(opts) 26 | opts = opts or {} 27 | local o = self:_new { 28 | start_addr = opts.start_addr or 0, 29 | per_line = opts.per_line or 16, 30 | word_bytes = opts.word_bytes or 1, 31 | endianess = opts.endianess or 'little', 32 | group_by = opts.group_by or 8, 33 | addr_0x = vim.F.if_nil(opts.addr_0x, false), 34 | spaces = opts.spaces or 3, 35 | } 36 | -- to allow for construction of invalid objects with settings for hot-reload 37 | pcall(o._update_fmt, o) 38 | return o 39 | end 40 | 41 | function HexDump:_update_fmt() 42 | assert(self.per_line % self.word_bytes == 0, 'per_line must be multiple of word_bytes') 43 | assert(self.group_by % self.word_bytes == 0, 'group_by must be multiple of word_bytes') 44 | self._fmt = { 45 | addr = self.addr_0x and { '0x%08x', 10 } or { '%08x', 8 }, 46 | word = { string.rep('%02x', self.word_bytes), 2 * self.word_bytes }, 47 | } 48 | end 49 | 50 | --- Given a range of values where there is a breaking point at which criteria `test(x)` changes from 51 | --- returning false (left) to returning true (right), performs binary search to find that value. 52 | ---@param left integer 53 | ---@param right integer 54 | ---@param test fun(val: integer): boolean 55 | ---@return integer? 56 | local function binary_search(left, right, test) 57 | assert(left <= right) 58 | while left < right do 59 | local mid = math.floor((left + right) / 2) 60 | if test(mid) then 61 | right = mid 62 | else 63 | left = mid + 1 64 | end 65 | end 66 | return left 67 | end 68 | 69 | --- Get maximum number of values that can be passed to unpack() without an error. 70 | ---@return integer 71 | local max_unpack_size = utils.lazy(function() 72 | local min_estimate = 1 73 | local max_estimate = 16 * 1024 74 | local tbl = {} 75 | local first_err = binary_search(min_estimate, max_estimate, function(n) 76 | return not pcall(unpack, tbl, 1, n) 77 | end) 78 | assert(first_err and first_err > 1, 'Could not determine max number of arguments for unpack()') 79 | return first_err - 1 80 | end) 81 | 82 | --- Optimized function for converting a list of bytes into a byte-string making use of large unpack() calls. 83 | ---@param bytes integer[] 84 | ---@return string 85 | local function bytes_to_string(bytes) 86 | local max_chunk = max_unpack_size() 87 | local taken, len = 0, #bytes 88 | local parts = {} 89 | while taken < len do 90 | local chunk = math.min(max_chunk, len - taken) 91 | table.insert(parts, string.char(unpack(bytes, taken + 1, taken + 1 + chunk - 1))) 92 | taken = taken + chunk 93 | end 94 | return table.concat(parts) 95 | end 96 | 97 | ---@param data string|integer[] prefer using byte-string instead of a list-table 98 | ---@return string[] 99 | function HexDump:lines(data) 100 | vim.validate { data = { data, { 'string', 'table' } } } 101 | if type(data) == 'table' then 102 | data = bytes_to_string(data) 103 | end 104 | 105 | self:_update_fmt() 106 | 107 | local spaces = string.rep(' ', self.spaces) 108 | local lines = {} 109 | local row = 0 110 | 111 | for chunk in utils.string_chunks(data, self.per_line) do 112 | local line = {} 113 | 114 | local addr = self.start_addr + row * self.per_line 115 | table.insert(line, string.format(self._fmt.addr[1], addr)) 116 | table.insert(line, spaces) 117 | 118 | -- Hex section 119 | local nbytes = 0 120 | local word_i = 1 121 | local hex_len = 0 122 | for word in utils.string_chunks(chunk, self.word_bytes) do 123 | nbytes = nbytes + #word 124 | 125 | if self.endianess == 'little' then 126 | word = word:reverse() 127 | end 128 | table.insert(line, string.format(self._fmt.word[1], word:byte(1, #word))) 129 | hex_len = hex_len + self._fmt.word[2] 130 | 131 | if word_i ~= self:words_per_line() then -- no space after last word 132 | local more_space = word_i % self:words_per_group() == 0 133 | table.insert(line, more_space and ' ' or ' ') 134 | hex_len = hex_len + (more_space and 2 or 1) 135 | word_i = word_i + 1 136 | end 137 | end 138 | 139 | -- Padding 140 | if nbytes ~= self.per_line then 141 | local n = (self:_ascii_col(0) - self._fmt.addr[2] - 2 * self.spaces) - hex_len 142 | table.insert(line, string.rep(' ', n)) 143 | end 144 | 145 | table.insert(line, spaces) 146 | 147 | -- Ascii section 148 | for i = 1, #chunk do 149 | local byte = chunk:byte(i) 150 | table.insert(line, self:_printable(byte) and string.char(byte) or '.') 151 | end 152 | 153 | table.insert(lines, table.concat(line)) 154 | row = row + 1 155 | end 156 | 157 | return lines 158 | end 159 | 160 | --- Buffer position of text for given byte hex 161 | ---@param b integer 1-indexed byte number 162 | ---@return integer row 163 | ---@return integer start_col 164 | ---@return integer end_col non-inclusive 165 | function HexDump:pos_hex(b) 166 | local row = self:_byte_row(b - 1) 167 | local col = self:_byte_col(self:_byte_in_row(b - 1)) 168 | return row, col, col + 2 169 | end 170 | 171 | --- Buffer position of text for given byte ascii 172 | ---@param b integer 1-indexed byte number 173 | ---@return integer row 174 | ---@return integer start_col 175 | ---@return integer end_col non-inclusive 176 | function HexDump:pos_ascii(b) 177 | local row = self:_byte_row(b - 1) 178 | local col = self:_ascii_col(self:_byte_in_row(b - 1)) 179 | return row, col, col + 1 180 | end 181 | 182 | function HexDump:words_per_line() 183 | return math.floor(self.per_line / self.word_bytes) 184 | end 185 | function HexDump:words_per_group() 186 | return math.floor(self.group_by / self.word_bytes) 187 | end 188 | 189 | -- Everyting 0-indexed 190 | function HexDump:_byte_row(b) 191 | return math.floor(b / self.per_line) 192 | end 193 | function HexDump:_byte_in_row(b) 194 | return b % self.per_line 195 | end 196 | -- All following inputs `b` are modulo line (aka _byte_in_row) 197 | function HexDump:_byte_word(b) 198 | return math.floor(b / self.word_bytes) 199 | end 200 | function HexDump:_byte_in_word(b) 201 | local i = b % self.word_bytes 202 | return self.endianess == 'little' and (self.word_bytes - 1 - i) or i 203 | end 204 | function HexDump:_byte_groups(b) 205 | return math.floor(b / self.group_by) 206 | end 207 | function HexDump:_byte_col(b) 208 | local addr_w = self._fmt.addr[2] + self.spaces 209 | local word_w = self._fmt.word[2] + 1 210 | return addr_w + self:_byte_word(b) * word_w + self:_byte_in_word(b) * 2 + self:_byte_groups(b) 211 | end 212 | function HexDump:_ascii_col(b) 213 | -- return self:_byte_col(self.per_line - 1) + 2 + self.spaces + b 214 | local addr_w = self._fmt.addr[2] + self.spaces 215 | local word_w = self._fmt.word[2] + 1 216 | local hex_end = addr_w + (self:words_per_line() * word_w - 1) + self:_byte_groups(self.per_line - 1) 217 | return hex_end + self.spaces + b 218 | end 219 | 220 | function HexDump:_printable(byte) 221 | return byte >= 32 and byte <= 126 222 | end 223 | 224 | --- Open test buffer with live-update of settings on key presses 225 | ---@private 226 | -- stylua: ignore 227 | function HexDump._test_buf_open(opts) 228 | local dump = HexDump:new(opts) 229 | 230 | local bytes = { 231 | 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 232 | 113, 114, 115, 116, 117, 119, 120, 121, 122, 10, 113, 119, 101, 114, 116, 121, 233 | 121, 117, 105, 111, 112, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 97, 234 | 115, 100, 102, 103, 104, 106, 107, 108, 59, 122, 120, 99, 118, 98, 110, 109, 235 | 44, 46, 47, 10, 1, 2, 3, 4, 5, 5, 4, 5, 6, 7, 8, 6, 236 | 4, 56, 9, 4, 6, 4, 6, 4, 237 | } 238 | 239 | local buf = vim.api.nvim_create_buf(true, true) 240 | vim.api.nvim_buf_set_lines(buf, 0, -1, false, dump:lines(bytes)) 241 | 242 | vim.cmd('enew') 243 | vim.api.nvim_set_current_buf(buf) 244 | 245 | local last_n_lines = 0 246 | local update = function() 247 | local ok, lines = pcall(dump.lines, dump, bytes) 248 | local err_line = '' 249 | if ok then 250 | vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) 251 | last_n_lines = vim.api.nvim_buf_line_count(buf) 252 | else 253 | err_line = 'ERROR: ' .. lines 254 | end 255 | 256 | local info = vim.split(vim.inspect(vim.tbl_extend('force', {}, dump)), '\n') 257 | table.insert(info, err_line) 258 | 259 | vim.api.nvim_buf_set_lines(buf, last_n_lines, -1, false, info) 260 | 261 | -- Run a position test 262 | local tests = {} 263 | local n_ok = 0 264 | for i, byte in ipairs(bytes) do 265 | local row, start_col, end_col = dump:pos_hex(i) 266 | local hex = vim.api.nvim_buf_get_text(buf, row, start_col, row, end_col, {})[1] 267 | local hex_expected = string.format('%02x', byte) 268 | if hex ~= hex_expected then 269 | table.insert(tests, 270 | string.format('ERR at %2d: %s vs %s : %d %d %d', i, hex, hex_expected, row, start_col, end_col)) 271 | else 272 | n_ok = n_ok + 1 273 | table.insert(tests, 274 | string.format('ok at %2d: %s vs %s : %d %d %d', i, hex, hex_expected, row, start_col, end_col)) 275 | end 276 | end 277 | 278 | vim.api.nvim_buf_set_lines(buf, -1, -1, false, { string.format('Tests OK: %d / %d', n_ok, #bytes) }) 279 | vim.api.nvim_buf_set_lines(buf, -1, -1, false, tests) 280 | end 281 | 282 | local nmap = function(lhs, fn) 283 | vim.keymap.set('n', lhs, function() 284 | fn() 285 | update() 286 | end, { buffer = buf }) 287 | end 288 | 289 | nmap('-', function() dump.per_line = dump.per_line - 1 end) 290 | nmap('+', function() dump.per_line = dump.per_line + 1 end) 291 | nmap('', function() dump.word_bytes = math.ceil(dump.word_bytes / 2) end) 292 | nmap('', function() dump.word_bytes = dump.word_bytes * 2 end) 293 | nmap('', function() dump.group_by = dump.group_by - 1 end) 294 | nmap('', function() dump.group_by = dump.group_by + 1 end) 295 | nmap('', function() dump.addr_0x = not dump.addr_0x end) 296 | nmap('', function() dump.endianess = dump.endianess == 'big' and 'little' or 'big' end) 297 | nmap('', function() dump.spaces = dump.spaces - 1 end) 298 | nmap('', function() dump.spaces = dump.spaces + 1 end) 299 | 300 | update() 301 | end 302 | 303 | return HexDump 304 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/listeners.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local dap = require('dap') 4 | local consoles = require('dap-cortex-debug.consoles') 5 | local memory = require('dap-cortex-debug.memory') 6 | local utils = require('dap-cortex-debug.utils') 7 | 8 | local PLUGIN = 'cortex-debug' 9 | M.debug = true 10 | 11 | local function set_listener(when, name, handler) 12 | handler = handler or function() end 13 | 14 | local log_handler = function(...) 15 | local args = { ... } 16 | if debug then 17 | utils.debug('cortex-debug.%s.%s: %s', when, name, vim.inspect(args)) 18 | end 19 | end 20 | 21 | dap.listeners[when][name][PLUGIN] = function(session, ...) 22 | if session.config.type ~= PLUGIN then 23 | return 24 | end 25 | log_handler(...) 26 | return handler(session, ...) 27 | end 28 | end 29 | 30 | ---@type fun(when: string, cb?: fun(session: dap.Session, ...)) 31 | local before = utils.bind(set_listener, 'before') 32 | ---@type fun(when: string, cb?: fun(session: dap.Session, ...)) 33 | local after = utils.bind(set_listener, 'after') 34 | 35 | -- Create handlers for cortex-debug custom events 36 | function M.setup() 37 | after('event_capabilities', function(session, body) end, 'after') 38 | 39 | before('event_custom-event-ports-allocated', function(session, body) 40 | local ports = body and body.info 41 | session.used_ports = session.used_ports or {} 42 | vim.list_extend(session.used_ports, ports or {}) 43 | end) 44 | 45 | before('event_custom-event-ports-done') 46 | 47 | before('event_custom-event-popup', function(_session, body) 48 | local msg = body.info and body.info.message or '' 49 | local level = ({ 50 | warning = vim.log.levels.WARN, 51 | error = vim.log.levels.ERROR, 52 | })[body.info and body.info.type] or vim.log.levels.INFO 53 | vim.notify(msg, level) 54 | end) 55 | 56 | before('event_custom-stop') 57 | before('event_custom-continued') 58 | before('event_swo-configure') 59 | 60 | before('event_rtt-configure', function(session, body) 61 | assert(body and body.type == 'socket') 62 | assert(body.decoder.type == 'console') 63 | 64 | local channel = body.decoder.port 65 | local rtt_opts = { 66 | channel = channel, 67 | tcp_port = body.decoder.tcpPort, 68 | logfile = body.decoder.logfile, 69 | } 70 | 71 | local start = vim.uv.hrtime() 72 | local on_client_connected = function(client) 73 | -- See: cortex-debug/src/frontend/swo/sources/socket.ts:123 74 | -- When the TCP connection to the RTT port is established, send config commands 75 | -- within 100ms to configure the RTT channel. See 76 | -- https://wiki.segger.com/RTT#SEGGER_TELNET_Config_String for more information 77 | -- on the config string format. 78 | if session.config.servertype == 'jlink' then 79 | client:write(string.format('$$SEGGER_TELNET_ConfigStr=RTTCh;%d$$', channel)) 80 | utils.debug('sending jlink rtt channel request after %.6f ms', (vim.uv.hrtime() - start) / 1e6) 81 | end 82 | end 83 | 84 | consoles.rtt_connect(rtt_opts, function(client, term) 85 | -- Notify our dapui element to update 86 | require('dap-cortex-debug.dapui.rtt').on_rtt_connect(channel) 87 | 88 | session:request('rtt-poll', nil, function(_, _) end) 89 | 90 | term:scroll() 91 | end, on_client_connected) 92 | end) 93 | 94 | before('event_record-event') 95 | before('event_custom-event-open-disassembly') 96 | before('event_custom-event-post-start-server') 97 | before('event_custom-event-post-start-gdb') 98 | before('event_custom-event-session-terminating') 99 | before('event_custom-event-session-restart') 100 | before('event_custom-event-session-reset') 101 | 102 | before('initialize', function(_session, _err, _response, _payload) end) 103 | 104 | -- HACK: work around cortex-debug's workaround for vscode's bug... 105 | -- Cortex-debug includes a workaround for some bug in VS code, which causes cortex-debug 106 | -- to send the first frame/thread as "cortex-debug-dummy". It is solved by runToEntryPoint 107 | -- which will result in a breakpoint stop and then we will get correct stack trace. But we 108 | -- need to re-request threads, and force nvim-dap to jump to the new frame received (it 109 | -- won't jump because it sees that session.stopped_thread_id ~= nil). 110 | after('stackTrace', function(session, err, response, _payload) 111 | if not err and vim.tbl_get(response, 'stackFrames', 1, 'name') == 'cortex-debug-dummy' then 112 | session._cortex_debug_dummy = (session._cortex_debug_dummy or 0) + 1 113 | if session._cortex_debug_dummy <= 3 then 114 | session.stopped_thread_id = nil 115 | vim.defer_fn(function() 116 | utils.debug('Re-requesting threads to fix dummy frame') 117 | session:update_threads(function(_err) end) 118 | end, 50) 119 | else 120 | utils.warn_once('Failed to update stack trace after 3 cortex-debug-dummy frames') 121 | end 122 | end 123 | end) 124 | 125 | -- Cortex-debug sends tooltips (multi-line info) under variable.type, e.g. 126 | -- SystemCoreClock undefined SystemCoreClock; 127 | -- dec: 168000000 128 | -- hex: 0x0a037a00 129 | -- oct: 001200675000 130 | -- bin: 00001010 00000011 01111010 00000000 = 168000000 131 | -- where: "SystemCoreClock {TYPE} = 168000000". 132 | -- Try to extract actual type from the first line, and store the whole string under _tooltip. 133 | -- TODO: find a way to use this tooltip on hover. 134 | local function fix_variable_type(var) 135 | if not var.type then 136 | return 137 | end 138 | 139 | var._tooltip = var.type 140 | local line = vim.split(var.type, '\n', { plain = true, trimempty = true })[1] 141 | 142 | -- Remove trailing semicolon 143 | if vim.endswith(line, ';') then 144 | line = line:sub(1, #line - 1) 145 | end 146 | -- Remove variable name 147 | local tokens = vim.tbl_filter(function(token) 148 | return token ~= var.name 149 | end, vim.split(line, '%s+')) 150 | 151 | -- Remove redundant registers info 152 | if tokens[1] == 'Register:' and vim.endswith(tokens[2], var.name) then 153 | tokens = vim.list_slice(tokens, 3) 154 | end 155 | 156 | var.type = table.concat(tokens, ' ') 157 | end 158 | 159 | before('variables', function(_session, _err, response, _payload) 160 | if not response then 161 | return 162 | end 163 | -- first remove all the vim.NIL values 164 | response.variables = vim.tbl_filter(function(v) 165 | return v ~= vim.NIL 166 | end, response.variables) 167 | -- then fix types 168 | for _, var in ipairs(response.variables or {}) do 169 | fix_variable_type(var) 170 | end 171 | end) 172 | 173 | after('event_stopped', function(_session, _body) 174 | memory.update() 175 | end) 176 | end 177 | 178 | return M 179 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/memory.lua: -------------------------------------------------------------------------------- 1 | local dap = require('dap') 2 | local utils = require('dap-cortex-debug.utils') 3 | local Buffer = require('dap-cortex-debug.buffer') 4 | local HexDump = require('dap-cortex-debug.hexdump') 5 | 6 | local ns = vim.api.nvim_create_namespace('cortex-debug-memory') 7 | 8 | ---@type { [integer]: MemoryView } 9 | local mem_views = {} 10 | 11 | --- Memory viewer that attaches to a buffer to display hexdump 12 | ---@class MemoryView:Class 13 | ---@field id integer Memory view window number 14 | ---@field address integer Memory start address 15 | ---@field length integer Memory length in bytes 16 | ---@field bytes nil|integer[] Current memory bytes 17 | ---@field buffer CDBuffer Display buffer 18 | ---@field update_id integer Incremented on each update 19 | ---@field highlight_time integer Duration [ms] of change highlight (-1 -> disabled, 0 -> until next update) 20 | ---@field _hexdump HexDumpOpts Hexdump display options 21 | local MemoryView = utils.class() 22 | 23 | ---@class MemoryViewOpts 24 | ---@field id integer 25 | ---@field address integer 26 | ---@field length integer 27 | ---@field hexdump? HexDumpOpts 28 | ---@field highlight_time? integer 29 | 30 | ---@param opts MemoryViewOpts 31 | ---@return MemoryView 32 | function MemoryView:new(opts) 33 | vim.validate { opts = { opts, 'table' } } 34 | 35 | local mem = self:_new() 36 | mem.id = opts.id 37 | mem.address = assert(opts.address) 38 | mem.length = assert(opts.length) 39 | mem._hexdump = opts.hexdump or {} 40 | mem.highlight_time = opts.highlight_time or 0 41 | mem.bytes = nil 42 | mem.update_id = 0 43 | mem.buffer = Buffer.get_or_new { 44 | uri = MemoryView._uri(mem.id), 45 | set_win = Buffer.open_in_split { size = 90, mods = 'vertical' }, 46 | on_delete = function() 47 | mem_views[self.id] = nil 48 | end, 49 | } 50 | mem.buffer._memview = mem 51 | 52 | mem_views[mem.id] = mem 53 | 54 | mem:_set_keymaps() 55 | 56 | return mem 57 | end 58 | 59 | function MemoryView:_set_keymaps() 60 | -- stylua: ignore 61 | local fn = { 62 | inc = function(v) return v + 1 end, 63 | dec = function(v) return v - 1 end, 64 | mul2 = function(v) return 2 * v end, 65 | div2 = function(v) return math.ceil(v / 2) end, -- avoid going to 0 for mul2 to work 66 | swap_endianess = function(v) return v == 'big' and 'little' or 'big' end, 67 | invert = function(v) return not v end, 68 | } 69 | 70 | local maps = { 71 | { 'g?', 'Show help' }, 72 | { 'gr', 'Refresh memory from DUT' }, 73 | { 'ge', 'Toggle byte-word endianess', 'endianess', fn.swap_endianess }, 74 | { 'gx', 'Toggle address 0x prefix', 'addr_0x', fn.invert }, 75 | { '-', 'Decrement bytes per line', 'per_line', fn.dec }, 76 | { '+', 'Increment bytes per line', 'per_line', fn.inc }, 77 | { '[w', 'Decrease bytes per word (/2)', 'word_bytes', fn.div2 }, 78 | { ']w', 'Increase bytes per word (x2)', 'word_bytes', fn.mul2 }, 79 | { '[g', 'Decrement byte group size', 'group_by', fn.dec }, 80 | { ']g', 'Increment byte group size', 'group_by', fn.inc }, 81 | { '[s', 'Decrement number of spaces', 'spaces', fn.dec }, 82 | { ']s', 'Increment number of spaces', 'spaces', fn.inc }, 83 | } 84 | for _, map in ipairs(maps) do 85 | maps[map[1]] = map 86 | end 87 | 88 | local nmap = function(lhs, rhs) 89 | vim.keymap.set('n', lhs, rhs, { buffer = self.buffer.buf, desc = maps[lhs][2] }) 90 | end 91 | 92 | -- Automatically create parameter update mappings 93 | for _, map in ipairs(maps) do 94 | if map[3] then 95 | local lhs, desc, param, modify_fn = unpack(map) 96 | local rhs = function() 97 | self:with { hexdump = { [param] = modify_fn(self:hexdump()[param]) } } 98 | self:set(self.bytes) 99 | end 100 | vim.keymap.set('n', lhs, rhs, { buffer = self.buffer.buf, desc = desc }) 101 | end 102 | end 103 | 104 | nmap('gr', function() 105 | self:update() 106 | end) 107 | nmap('g?', function() 108 | local fmt = '%5s %s' 109 | local lines = { fmt:format('LHS', 'Description') } 110 | for _, map in ipairs(maps) do 111 | local lhs, desc = unpack(map) 112 | table.insert(lines, fmt:format('`' .. lhs .. '`', desc)) 113 | end 114 | table.insert(lines, 'Move cursor to go back') 115 | 116 | vim.api.nvim_buf_clear_namespace(self.buffer.buf, ns, 0, -1) 117 | vim.api.nvim_buf_set_lines(self.buffer.buf, 0, -1, false, lines) 118 | vim.schedule(function() 119 | vim.api.nvim_create_autocmd('CursorMoved', { 120 | once = true, 121 | buffer = self.buffer.buf, 122 | callback = function() 123 | if self.bytes then 124 | self:set(self.bytes) 125 | end 126 | end, 127 | }) 128 | end) 129 | end) 130 | end 131 | 132 | ---@param opts MemoryViewOpts 133 | ---@return MemoryView 134 | function MemoryView:with(opts) 135 | if opts.id and opts.id ~= self.id then 136 | utils.warn('Cannot reconfigure MemoryView.id') 137 | end 138 | self.address = vim.F.if_nil(opts.address, self.address) 139 | self.length = vim.F.if_nil(opts.length, self.length) 140 | self._hexdump = vim.tbl_extend('force', self._hexdump, opts.hexdump or {}) 141 | self.highlight_time = vim.F.if_nil(opts.highlight_time, self.highlight_time) 142 | return self 143 | end 144 | 145 | function MemoryView._uri(id) 146 | return string.format([[cortex-debug://memory:%d]], id) 147 | end 148 | 149 | ---@param id integer 150 | ---@return MemoryView? 151 | function MemoryView.get(id) 152 | local buffer = Buffer.get(MemoryView._uri(id)) 153 | return buffer and buffer._memview 154 | end 155 | 156 | ---@param opts MemoryViewOpts 157 | ---@return MemoryView 158 | function MemoryView.get_or_new(opts) 159 | local buffer = Buffer.get(MemoryView._uri(opts.id)) 160 | return buffer and buffer._memview:with(opts) or MemoryView:new(opts) 161 | end 162 | 163 | function MemoryView:hexdump() 164 | local opts = vim.tbl_extend('error', { start_addr = self.address }, self._hexdump) 165 | return HexDump:new(opts) 166 | end 167 | 168 | function MemoryView:set(bytes) 169 | if not self.buffer:is_valid() then 170 | return 171 | end 172 | local buf = self.buffer.buf 173 | 174 | self.update_id = self.update_id + 1 175 | local changes = self:changes(bytes) 176 | self.bytes = bytes 177 | 178 | vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1) 179 | 180 | -- Handle hexdump generation errors due to incorrect parameters 181 | local ok, dump, lines = pcall(function() 182 | local dump = self:hexdump() 183 | local lines = dump:lines(bytes) 184 | return dump, lines 185 | end) 186 | 187 | if not ok then 188 | local err = dump 189 | local line1 = vim.api.nvim_buf_get_lines(buf, 0, 1, false)[1] 190 | local end_col = vim.startswith(line1 or '', 'ERROR') and 1 or 0 191 | vim.api.nvim_buf_set_lines(buf, 0, end_col, false, { 'ERROR: ' .. err }) 192 | vim.api.nvim_buf_add_highlight(buf, ns, 'Error', 0, 0, -1) 193 | return 194 | end 195 | 196 | vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) 197 | 198 | if self.highlight_time >= 0 then 199 | for _, c in ipairs(changes) do 200 | vim.api.nvim_buf_add_highlight(buf, ns, 'DiffChange', dump:pos_hex(c)) 201 | vim.api.nvim_buf_add_highlight(buf, ns, 'DiffChange', dump:pos_ascii(c)) 202 | end 203 | end 204 | 205 | if self.highlight_time > 0 then 206 | local id = self.update_id 207 | vim.defer_fn(function() 208 | if self.update_id == id and self.buffer:is_valid() then 209 | vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1) 210 | end 211 | end, self.highlight_time) 212 | end 213 | end 214 | 215 | function MemoryView:update() 216 | local session = dap.session() 217 | utils.assert(session ~= nil, 'No DAP session is running') 218 | utils.assert(session.config.type == 'cortex-debug', 'DAP session is not cortex-debug') 219 | session:request('read-memory', { address = self.address, length = self.length }, function(err, response) 220 | if err then 221 | utils.error('read-memory failed: %s', err.message or vim.inspect(err)) 222 | return 223 | end 224 | if tonumber(response.startAddress) ~= self.address then 225 | utils.warn('Address mismatch 0x%08x vs 0x%08x', response.startAddress, self.address) 226 | end 227 | self:set(response.bytes) 228 | end) 229 | end 230 | 231 | --- Find positions of modified bytes 232 | ---@param bytes integer[] 233 | ---@return integer[] 234 | function MemoryView:changes(bytes) 235 | local changes = {} 236 | if self.bytes then 237 | for i = 1, #bytes do 238 | if self.bytes[i] ~= bytes[i] then 239 | table.insert(changes, i) 240 | end 241 | end 242 | end 243 | return changes 244 | end 245 | 246 | ---@param opts MemoryViewOpts 247 | local function show(opts) 248 | MemoryView.get_or_new(opts):update() 249 | end 250 | 251 | local function update() 252 | for _, view in pairs(mem_views) do 253 | view:update() 254 | end 255 | end 256 | 257 | ---@async 258 | --- Try to evaluate a variable to get its memory range 259 | ---@param var string Should be variable value, & will be prepended 260 | ---@param opts? { frame_id?: integer } 261 | ---@return any|nil error 262 | ---@return nil|{ address: integer, length: integer } 263 | local function var_to_mem(var, opts) 264 | opts = vim.tbl_extend('force', { 265 | frame_id = dap.session().current_frame.id, 266 | }, opts or {}) 267 | 268 | local evaluate = function(expr) 269 | return utils.session_request('evaluate', { 270 | expression = expr, 271 | frameId = opts.frame_id, 272 | context = 'variables', 273 | }) 274 | end 275 | 276 | local err, response = evaluate('&' .. var) 277 | if err then 278 | return err 279 | end 280 | local address = tonumber(response.memoryReference) 281 | if not address then 282 | return 'Could not get address of ' .. var 283 | end 284 | 285 | err, response = evaluate(string.format('sizeof(%s)', var)) 286 | if err then 287 | return err 288 | end 289 | local length = tonumber(response.result) 290 | if not length then 291 | return 'Could not get size of ' .. var 292 | end 293 | 294 | return nil, { address = address, length = length } 295 | end 296 | 297 | return { 298 | show = show, 299 | update = update, 300 | var_to_mem = var_to_mem, 301 | MemoryView = MemoryView, 302 | } 303 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/requests.lua: -------------------------------------------------------------------------------- 1 | --- Custom requests implemented by cortex-debug. 2 | --- See gdb.ts (GDBDebugSession.customRequest) and frontend.extensions.ts. 3 | local M = {} 4 | 5 | local utils = require('dap-cortex-debug.utils') 6 | 7 | --- FIXME: does not work? try to understand how to use it, or can we just use dap.pause? 8 | ---@param session? dap.Session 9 | ---@param callback? fun(err: table, result: any) 10 | function M.reset_device(session, callback) 11 | local dap = require('dap') 12 | session = assert(session or dap.session(), 'No DAP session') 13 | session:request('reset-device', 'reset', function(err, result) 14 | if err then 15 | utils.error('Could not reset device: %s', vim.inspect(result)) 16 | else 17 | utils.debug('Reset device: %s', vim.inspect(result)) 18 | end 19 | end) 20 | end 21 | 22 | ---@param session? dap.Session 23 | ---@param format { hex: boolean } 24 | function M.set_var_format(session, format) 25 | local dap = require('dap') 26 | session = assert(session or dap.session(), 'No DAP session') 27 | session:request('set-var-format', format, function(err, result) 28 | if err then 29 | utils.error('Could not set var format: %s', result) 30 | end 31 | end) 32 | end 33 | 34 | return M 35 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/tcp.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local utils = require('dap-cortex-debug.utils') 4 | 5 | local localhost = '127.0.0.1' 6 | 7 | ---@class ConnectOpts 8 | ---@field host? string 9 | ---@field port number 10 | ---@field retries? number Maximum number of retries (default 0 or math.huge if delay_total_max!=nil) 11 | ---@field delay? number Delay in milliseconds between retriesa (default 100) 12 | ---@field delay_multiplier? number Each subsequent delay is X times longer (default 1.0) 13 | ---@field delay_max? number Max delay after multiplication 14 | ---@field delay_total_max? number Max total delay, can be used limit by delay instead of retries 15 | ---@field on_success fun(client: userdata) 16 | ---@field on_error fun(err: string?) 17 | 18 | ---Connect to a server with retries 19 | ---@param opts ConnectOpts 20 | function M.connect(opts) 21 | coroutine.wrap(function() 22 | local resume = utils.coroutine_resume() 23 | 24 | local retries = opts.retries and opts.retries or (opts.delay_total_max and math.huge or 0) 25 | local attempts = retries + 1 26 | local host = opts.host or localhost 27 | local delay = opts.delay or 100 28 | local delay_mul = opts.delay_multiplier or 1.0 29 | local delay_max = opts.delay_max or math.huge 30 | local delay_total_max = opts.delay_total_max or math.huge 31 | local delay_total = 0 32 | 33 | local err 34 | for attempt = 1, attempts do 35 | local client = assert(vim.loop.new_tcp()) 36 | client:connect(host, opts.port, resume) 37 | err = coroutine.yield() 38 | 39 | if not err then 40 | opts.on_success(client) 41 | return 42 | end 43 | client:shutdown() 44 | client:close() 45 | 46 | if attempt ~= attempts then 47 | -- check if the total time after sleeping would exceed the limit 48 | delay_total = delay_total + delay 49 | if delay_total > delay_total_max then 50 | break 51 | end 52 | 53 | vim.defer_fn(resume, delay) 54 | coroutine.yield() 55 | 56 | -- multiply and limit 57 | delay = math.ceil(math.min(delay * delay_mul, delay_max)) 58 | -- make the last attempt exactly at delay_total_max 59 | if delay_total ~= delay_total_max then 60 | delay = math.min(delay, delay_total_max - delay_total) 61 | end 62 | end 63 | end 64 | 65 | opts.on_error(err) 66 | end)() 67 | end 68 | 69 | ---@class ServeOpts 70 | ---@field host? string 71 | ---@field port number 72 | ---@field backlog? number Maximum number of pending connections 73 | ---@field on_connect fun(socket: userdata) 74 | ---@field on_error fun(err: string?) 75 | 76 | ---Start serving on given port 77 | ---@param opts ServeOpts 78 | ---@return userdata LibUV server userdata 79 | function M.serve(opts) 80 | local host = opts.host or localhost 81 | local backlog = opts.backlog or 128 82 | 83 | local server = assert(vim.loop.new_tcp()) 84 | -- TODO: handle bind/listen errors 85 | server:bind(host, opts.port) 86 | server:listen(backlog, function(err) 87 | if err then 88 | opts.on_error(err) 89 | else 90 | local socket = vim.loop.new_tcp() 91 | server:accept(socket) 92 | opts.on_connect(socket) 93 | end 94 | end) 95 | 96 | return server 97 | end 98 | 99 | ---Check if given port is available 100 | ---@param port integer 101 | ---@param host? string 102 | ---@return boolean 103 | function M.try_port_listen(port, host) 104 | local tcp = assert(vim.loop.new_tcp()) 105 | local ok = pcall(function() 106 | assert(tcp:bind(host or localhost, port)) 107 | assert(tcp:listen(1, function() end)) 108 | end) 109 | tcp:shutdown() 110 | tcp:close() 111 | return ok 112 | end 113 | 114 | ---Find a free port 115 | ---@param preferred? integer Try to use this port if possible 116 | ---@param host? string 117 | ---@return integer Port that is free for use 118 | function M.get_free_port(preferred, host) 119 | if preferred and M.try_port_listen(preferred) then 120 | return preferred 121 | end 122 | local tcp = vim.loop.new_tcp() 123 | tcp:bind(host or localhost, 0) -- 0 finds a free port 124 | local port = tcp:getsockname().port 125 | tcp:shutdown() 126 | tcp:close() 127 | return port 128 | end 129 | 130 | return M 131 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/terminal/base.lua: -------------------------------------------------------------------------------- 1 | local utils = require('dap-cortex-debug.utils') 2 | local Buffer = require('dap-cortex-debug.buffer') 3 | 4 | ---@class CDTerminalSendOpts 5 | ---@field newline? boolean 6 | ---@field bold? boolean 7 | ---@field error? boolean 8 | 9 | ---@class CDTerminalLineBufferOpts 10 | ---@field timeout? number Timeout [ms] after which buffered data is pushed even without newline 11 | 12 | ---@class CDTerminalOpts:CDBufferOpts 13 | ---Assigns terminal buffer to a window, return window and optional callback to call when terminal is ready. 14 | ---@field on_input? fun(term: CDTerminal, data: string) 15 | ---@field scroll_on_open? boolean Scroll to end when opening with new output (default true) 16 | ---@field line_buffer? CDTerminalLineBufferOpts Perform line-buffering when sending data (default true) 17 | 18 | ---@class CDTerminal:CDBuffer 19 | ---@field on_input? fun(term: CDTerminal, data: string) 20 | ---@field scroll_on_open boolean 21 | ---@field line_buf_timeout number? 22 | ---@field line_buf_timer userdata? 23 | ---@field line_buf string[] 24 | local Terminal = utils.class(Buffer) 25 | 26 | local augroup = vim.api.nvim_create_augroup('CortexDebugTerminal', { clear = true }) 27 | 28 | ---Create new terminal object with its buffer. This needs to open a window, at least temporarily. 29 | ---Will delete previous terminal with the same URI. `get_or_new` can be used instead. 30 | ---@param opts CDTerminalOpts 31 | ---@return CDTerminal 32 | function Terminal:new(opts, instance) 33 | local term = Buffer:new(opts --[[@as CDBufferOpts]], instance or self:_new()) --[[@as CDTerminal]] 34 | 35 | term.needs_scroll = false 36 | term.on_input = opts.on_input 37 | term.scroll_on_open = vim.F.if_nil(opts.scroll_on_open, true) 38 | 39 | term.line_buf = {} 40 | local line_buffer = vim.F.if_nil(opts.line_buffer, {}) 41 | if line_buffer then 42 | term.line_buf_timeout = line_buffer.timeout or 100 43 | term.line_buf_timer = vim.uv.new_timer() 44 | end 45 | 46 | return term 47 | end 48 | 49 | Terminal.get_or_new = function() 50 | error('NOT IMPLEMENTED') 51 | end 52 | 53 | function Terminal:_create_buf_final() 54 | error('NOT IMPLEMENTED') 55 | end 56 | 57 | -- Set up buffer autocommands 58 | function Terminal:_create_autocmds() 59 | Buffer._create_autocmds(self) 60 | vim.api.nvim_create_autocmd('BufWinEnter', { 61 | group = augroup, 62 | buffer = self.buf, 63 | callback = function() 64 | if self.needs_scroll then 65 | self.needs_scroll = false 66 | -- Do it in next loop when the window is valid 67 | vim.schedule(function() 68 | self:scroll() 69 | end) 70 | end 71 | end, 72 | }) 73 | end 74 | 75 | ---Clear terminal buffer. Safe to call from |lua-loop-callbacks|. 76 | function Terminal:clear() 77 | error('NOT IMPLEMENTED') 78 | end 79 | 80 | ---Send data to terminal. Safe to call from |lua-loop-callbacks|. 81 | ---@param data string 82 | ---@param opts? CDTerminalSendOpts 83 | function Terminal:send(data, opts) 84 | if self.line_buf_timeout then 85 | self:_send_line_buffered(data, opts) 86 | else 87 | self:_send(data, opts) 88 | end 89 | end 90 | 91 | ---@param data string 92 | ---@param opts? CDTerminalSendOpts 93 | function Terminal:_send(data, opts) 94 | error('NOT IMPLEMENTED') 95 | end 96 | 97 | ---@param data string 98 | ---@param opts? CDTerminalSendOpts 99 | function Terminal:send_line(data, opts) 100 | return self:send(data, vim.tbl_extend('force', opts or {}, { newline = true })) 101 | end 102 | 103 | function Terminal:is_visible() 104 | return vim.api.nvim_win_is_valid(vim.fn.bufwinid(self.buf)) 105 | end 106 | 107 | ---Scroll terminal to the end. Safe to call from |lua-loop-callbacks|. 108 | function Terminal:scroll() 109 | utils.call_api(function() 110 | if not vim.api.nvim_buf_is_valid(self.buf) then 111 | return 112 | end 113 | -- scroll all windows 114 | local nlines = vim.api.nvim_buf_line_count(self.buf) 115 | self.needs_scroll = true 116 | for _, win in ipairs(vim.fn.win_findbuf(self.buf)) do 117 | vim.api.nvim_win_set_cursor(win, { nlines, 0 }) 118 | self.needs_scroll = false 119 | end 120 | end) 121 | end 122 | 123 | function Terminal:_commit_buffered() 124 | if #self.line_buf == 0 then 125 | return 126 | end 127 | local line = table.concat(self.line_buf, '') 128 | if line ~= '' then 129 | self:_send(line) 130 | end 131 | self.line_buf = {} 132 | end 133 | 134 | ---@param data string 135 | ---@param opts? CDTerminalSendOpts 136 | function Terminal:_send_line_buffered(data, opts) 137 | opts = opts or {} 138 | if opts.newline then 139 | table.insert(self.line_buf, data .. '\n') 140 | self:_commit_buffered() 141 | return 142 | end 143 | 144 | while #data do 145 | local newline = data:find('\n') 146 | if not newline then 147 | table.insert(self.line_buf, data) 148 | self.line_buf_timer:start(self.line_buf_timeout, 0, function() 149 | self:_commit_buffered() 150 | end) 151 | return 152 | else 153 | self.line_buf_timer:stop() 154 | table.insert(self.line_buf, data:sub(1, newline)) 155 | self:_commit_buffered() 156 | data = data:sub(newline + 1) 157 | end 158 | end 159 | end 160 | 161 | ---@type CDTerminal 162 | return Terminal 163 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/terminal/buf.lua: -------------------------------------------------------------------------------- 1 | local utils = require('dap-cortex-debug.utils') 2 | local codes = require('dap-cortex-debug.terminal.codes') 3 | local BaseTerminal = require('dap-cortex-debug.terminal.base') 4 | 5 | ---@class CDTerminal.Buf:CDTerminal 6 | local Terminal = utils.class(BaseTerminal) 7 | 8 | Terminal.ns = vim.api.nvim_create_namespace('dap-cortex-debug.terminal.buf') 9 | 10 | ---Create new terminal object with its buffer. This needs to open a window, at least temporarily. 11 | ---Will delete previous terminal with the same URI. `get_or_new` can be used instead. 12 | ---@param opts CDTerminalOpts 13 | ---@return CDTerminal.Buf 14 | function Terminal:new(opts) 15 | local term = BaseTerminal:new(opts, self:_new()) 16 | term.has_newline = false 17 | return term 18 | end 19 | 20 | Terminal.get_or_new = Terminal._get_or_new(Terminal) 21 | 22 | function Terminal:_create_buf_final() 23 | -- luacheck: push ignore 122 24 | vim.bo[self.buf].buftype = 'nofile' 25 | -- luacheck: pop 26 | end 27 | 28 | function Terminal:clear() 29 | vim.api.nvim_buf_set_lines(self.buf, 0, -1, true, {}) 30 | vim.api.nvim_buf_clear_namespace(self.buf, self.ns, 0, -1) 31 | end 32 | 33 | local function cursor_at_end(win) 34 | local api = vim.api 35 | return api.nvim_win_is_valid(win) 36 | and api.nvim_win_get_cursor(win)[1] == api.nvim_buf_line_count(api.nvim_win_get_buf(win)) 37 | end 38 | 39 | ---Send data to terminal. Safe to call from |lua-loop-callbacks|. 40 | ---@param data string 41 | ---@param opts? CDTerminalSendOpts 42 | function Terminal:_send(data, opts) 43 | opts = opts or {} 44 | 45 | data = data:gsub('\r\n', '\n') 46 | if opts.newline then 47 | data = data .. '\n' 48 | end 49 | 50 | -- replace escape sequences: https://stackoverflow.com/a/24005600 51 | -- TODO: parse some escape sequences into extmark highlights 52 | data = data:gsub(codes.escape_sequence('%[[^@-~]*[@-~]'), '') 53 | 54 | local lines = vim.split(data, '\n', { plain = true }) 55 | 56 | utils.call_api(function() 57 | local srow, scol, erow, ecol 58 | 59 | if opts.bold or opts.error then 60 | srow = vim.api.nvim_buf_line_count(self.buf) - 1 61 | scol = #vim.api.nvim_buf_get_lines(self.buf, -2, -1, true)[1] 62 | end 63 | 64 | -- check which windows need scroll before appending text 65 | local to_scroll = vim.tbl_filter(cursor_at_end, vim.fn.win_findbuf(self.buf)) 66 | 67 | vim.api.nvim_buf_set_text(self.buf, -1, -1, -1, -1, lines) 68 | 69 | if opts.bold or opts.error then 70 | erow = vim.api.nvim_buf_line_count(self.buf) - 1 71 | ecol = #vim.api.nvim_buf_get_lines(self.buf, -2, -1, true)[1] 72 | local set_mark = function(hl_group) 73 | vim.api.nvim_buf_set_extmark( 74 | self.buf, 75 | self.ns, 76 | srow, 77 | scol, 78 | { end_row = erow, end_col = ecol, hl_group = hl_group } 79 | ) 80 | end 81 | if opts.bold then 82 | set_mark('Bold') 83 | end 84 | if opts.error then 85 | set_mark('ErrorMsg') 86 | end 87 | end 88 | 89 | local nlines = vim.api.nvim_buf_line_count(self.buf) 90 | self.needs_scroll = true 91 | for _, win in ipairs(to_scroll) do 92 | vim.api.nvim_win_set_cursor(win, { nlines, 0 }) 93 | self.needs_scroll = false 94 | end 95 | end) 96 | end 97 | 98 | return Terminal 99 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/terminal/codes.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | function M.escape_sequence(seq) 4 | -- In lua this decimal, so it's the equivalent of the usual \033 (octal) 5 | return '\027' .. seq 6 | end 7 | 8 | -- See: https://en.wikipedia.org/wiki/ANSI_M.escape_code 9 | -- PERF: could merge multiple into one when needed, like "[1;31m"; probably not worth it 10 | ---@class CDTerminalDisplay 11 | M.display = { 12 | reset = M.escape_sequence('[0m'), 13 | clear = M.escape_sequence('[0m'), 14 | bold = M.escape_sequence('[1m'), 15 | dim = M.escape_sequence('[2m'), 16 | italic = M.escape_sequence('[2m'), 17 | underline = M.escape_sequence('[2m'), 18 | fg = { 19 | black = M.escape_sequence('[30m'), 20 | red = M.escape_sequence('[31m'), 21 | green = M.escape_sequence('[32m'), 22 | yellow = M.escape_sequence('[33m'), 23 | blue = M.escape_sequence('[34m'), 24 | magenta = M.escape_sequence('[35m'), 25 | cyan = M.escape_sequence('[36m'), 26 | white = M.escape_sequence('[37m'), 27 | bright_black = M.escape_sequence('[90m'), 28 | bright_red = M.escape_sequence('[91m'), 29 | bright_green = M.escape_sequence('[92m'), 30 | bright_yellow = M.escape_sequence('[93m'), 31 | bright_blue = M.escape_sequence('[94m'), 32 | bright_magenta = M.escape_sequence('[95m'), 33 | bright_cyan = M.escape_sequence('[96m'), 34 | bright_white = M.escape_sequence('[97m'), 35 | }, 36 | bg = { 37 | black = M.escape_sequence('[40m'), 38 | red = M.escape_sequence('[41m'), 39 | green = M.escape_sequence('[42m'), 40 | yellow = M.escape_sequence('[43m'), 41 | blue = M.escape_sequence('[44m'), 42 | magenta = M.escape_sequence('[45m'), 43 | cyan = M.escape_sequence('[46m'), 44 | white = M.escape_sequence('[47m'), 45 | bright_black = M.escape_sequence('[100m'), 46 | bright_red = M.escape_sequence('[101m'), 47 | bright_green = M.escape_sequence('[102m'), 48 | bright_yellow = M.escape_sequence('[103m'), 49 | bright_blue = M.escape_sequence('[104m'), 50 | bright_magenta = M.escape_sequence('[105m'), 51 | bright_cyan = M.escape_sequence('[106m'), 52 | bright_white = M.escape_sequence('[107m'), 53 | }, 54 | } 55 | 56 | return M 57 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/terminal/init.lua: -------------------------------------------------------------------------------- 1 | return { 2 | ---@type CDTerminal 3 | Terminal = require('dap-cortex-debug.terminal.term'), 4 | ---@type CDTerminal 5 | BufTerminal = require('dap-cortex-debug.terminal.buf'), 6 | } 7 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/terminal/term.lua: -------------------------------------------------------------------------------- 1 | local utils = require('dap-cortex-debug.utils') 2 | local BaseTerminal = require('dap-cortex-debug.terminal.base') 3 | local codes = require('dap-cortex-debug.terminal.codes') 4 | 5 | ---@class CDTerminal.Term:CDTerminal 6 | ---@field term number CDTerminal job channel 7 | local Terminal = utils.class(BaseTerminal) 8 | 9 | ---Create new terminal object with its buffer. This needs to open a window, at least temporarily. 10 | ---Will delete previous terminal with the same URI. `get_or_new` can be used instead. 11 | ---@param opts CDTerminalOpts 12 | ---@return CDTerminal.Term 13 | function Terminal:new(opts) 14 | local instance = self:_new() 15 | return BaseTerminal:new(opts, instance) 16 | end 17 | 18 | Terminal.get_or_new = Terminal._get_or_new(Terminal) 19 | 20 | function Terminal:_create_buf_final() 21 | -- Needs to be stored as &channel doesn't work with buffers created using nvim_open_term 22 | self.term = vim.api.nvim_open_term(self.buf, { 23 | on_input = function(_input, _term, _buf, data) 24 | if self.on_input then 25 | self:on_input(data) 26 | end 27 | end, 28 | }) 29 | end 30 | 31 | function Terminal:clear() 32 | self:send(codes.escape_sequence('c')) 33 | end 34 | 35 | ---Send data to terminal. Safe to call from |lua-loop-callbacks|. 36 | ---@param data string 37 | ---@param opts? CDTerminalSendOpts 38 | function Terminal:_send(data, opts) 39 | opts = opts or {} 40 | 41 | -- FIXME: is it always needed 42 | data = data:gsub('\n', '\r\n') 43 | if opts.newline then 44 | data = data .. '\r\n' 45 | end 46 | 47 | local text = {} 48 | if opts.bold then 49 | table.insert(text, codes.display.bold) 50 | end 51 | if opts.error then 52 | table.insert(text, codes.display.fg.red) 53 | end 54 | table.insert(text, data) 55 | if opts.bold or opts.error then 56 | table.insert(text, codes.display.clear) 57 | end 58 | data = table.concat(text) 59 | 60 | local is_first = not self.__was_first_send 61 | self.__was_first_send = true 62 | 63 | utils.call_api(function() 64 | pcall(vim.api.nvim_chan_send, self.term, data) 65 | if is_first then 66 | self:scroll() 67 | elseif not self:is_visible() then 68 | self.needs_scroll = true 69 | end 70 | end) 71 | end 72 | 73 | return Terminal 74 | -------------------------------------------------------------------------------- /lua/dap-cortex-debug/utils.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local dap = require('dap') 4 | local config = require('dap-cortex-debug.config') 5 | 6 | ---Wrap a function with given values for N first arguments 7 | ---@param fn function 8 | ---@param ... any 9 | ---@return function 10 | function M.bind(fn, ...) 11 | local args = { ... } 12 | return function(...) 13 | return fn(unpack(args), ...) 14 | end 15 | end 16 | 17 | -- Wrap `fn` caching the result of its call. It won't recompute 18 | -- `fn` unless `recompute_cond` returns true. 19 | function M.cached(fn, recompute_cond) 20 | local cached 21 | return function(...) 22 | if cached == nil or recompute_cond(cached) then 23 | cached = fn(...) 24 | assert(cached ~= nil, 'cached: fn returned nil') 25 | end 26 | return cached 27 | end 28 | end 29 | 30 | -- Lazily evaluate a function, caching the result of the first call 31 | -- for all subsequent calls ever. 32 | function M.lazy(fn) 33 | return M.cached(fn, function() 34 | return false 35 | end) 36 | end 37 | 38 | local function logger(level, notify_fn, cond) 39 | return function(...) 40 | if cond and not cond() then 41 | return 42 | end 43 | -- Use notify_fn as string to get correct function if user 44 | -- replaced it later via vim.notify = ... 45 | local notify = vim[notify_fn] 46 | notify(string.format(...), level) 47 | end 48 | end 49 | 50 | local function debug_enabled() 51 | return config.debug 52 | end 53 | 54 | local function info_enabled() 55 | return not config.silent 56 | end 57 | 58 | M.debug = logger(vim.log.levels.DEBUG, 'notify', debug_enabled) 59 | M.debug_once = logger(vim.log.levels.DEBUG, 'notify_once', debug_enabled) 60 | M.info = logger(vim.log.levels.INFO, 'notify', info_enabled) 61 | M.info_once = logger(vim.log.levels.INFO, 'notify_once', info_enabled) 62 | M.warn = logger(vim.log.levels.WARN, 'notify') 63 | M.warn_once = logger(vim.log.levels.WARN, 'notify_once') 64 | M.error = logger(vim.log.levels.ERROR, 'notify') 65 | M.error_once = logger(vim.log.levels.ERROR, 'notify_once') 66 | 67 | ---Assert a condition or raise an error with formatted message 68 | ---@param val any Value treated as assertion condition 69 | ---@param err string Error message with optional format string placeholders 70 | ---@param ... any Arguments to the format string 71 | ---@return any Value if it was true-ish 72 | function M.assert(val, err, ...) 73 | if not val then 74 | -- Use level 2 to show the error at caller location 75 | error(string.format(err, ...), 2) 76 | end 77 | return val 78 | end 79 | 80 | ---Run function in protected mode like pcall but preserve traceback information 81 | ---@param fn function 82 | ---@param ... any 83 | ---@return boolean Success 84 | ---@return any Function return value or error message 85 | function M.trace_pcall(fn, ...) 86 | return xpcall(fn, debug.traceback, ...) 87 | end 88 | 89 | ---Make path absolute, remove repeated/trailing slashes 90 | ---@param path string 91 | ---@return string 92 | function M.path_sanitize(path) 93 | path = vim.fn.fnamemodify(path, ':p'):gsub('/+', '/'):gsub('/$', '') 94 | return path 95 | end 96 | 97 | ---Run `fn`, scheduling it if called in fast event 98 | ---@param fn function 99 | function M.call_api(fn) 100 | if vim.in_fast_event() then 101 | vim.schedule(fn) 102 | else 103 | fn() 104 | end 105 | end 106 | 107 | ---Create a callback that will resume currently running coroutine 108 | ---@return function 109 | function M.coroutine_resume() 110 | local co = assert(coroutine.running()) 111 | return function(...) 112 | local ret = { coroutine.resume(co, ...) } 113 | -- Re-raise errors with correct traceback 114 | local ok, err = unpack(ret) 115 | if not ok then 116 | error(debug.traceback(co, err)) 117 | end 118 | return unpack(ret, 2) 119 | end 120 | end 121 | 122 | ---@async 123 | --- Execute session request as sync call 124 | ---@param command string 125 | ---@param arguments? any 126 | ---@param session? dap.Session 127 | ---@return table err 128 | ---@return any result 129 | function M.session_request(command, arguments, session) 130 | session = session or dap.session() 131 | local resume = M.coroutine_resume() 132 | session:request(command, arguments, function(err, response) 133 | resume(err, response) 134 | end) 135 | return coroutine.yield() 136 | end 137 | 138 | ---Determine system platform 139 | ---@return 'darwin'|'windows'|'linux' 140 | function M.get_platform() 141 | if vim.fn.has('macos') == 1 or vim.fn.has('osx') == 1 then 142 | return 'darwin' 143 | elseif vim.fn.has('win32') == 1 or vim.fn.has('win64') == 1 then 144 | return 'windows' 145 | else 146 | return 'linux' 147 | end 148 | end 149 | 150 | function M.get_lib_ext() 151 | local extensions = { 152 | darwin = 'dylib', 153 | windows = 'dll', 154 | linux = 'so', 155 | } 156 | return config.lib_extension or extensions[M.get_platform()] 157 | end 158 | 159 | local function get_mason_extension_path() 160 | if vim.env.MASON then 161 | local path = vim.env.MASON .. '/share/cortex-debug' 162 | if vim.fn.isdirectory(path) then 163 | return path 164 | end 165 | end 166 | end 167 | 168 | local function default_extension_path() 169 | local extension_path = get_mason_extension_path() 170 | if not extension_path then 171 | local home = M.get_platform() == 'windows' and '$USERPROFILE' or '~' 172 | extension_path = home .. '/.vscode/extensions/marus25.cortex-debug-*/' 173 | end 174 | return extension_path 175 | end 176 | 177 | --- Resolve and sanitize path to cortex-debug extension 178 | ---@return string? 179 | function M.get_extension_path() 180 | local extension_path = config.extension_path or default_extension_path() 181 | local paths = vim.fn.glob(extension_path, false, true) 182 | if paths and paths[1] then 183 | return M.path_sanitize(paths[1]) 184 | end 185 | end 186 | 187 | --- Resolve path to debugadapter.js 188 | ---@param extension_path string 189 | ---@return string 190 | function M.get_debugadapter_path(extension_path) 191 | local paths = vim.fn.glob(extension_path .. '/dist/debugadapter.js', true, true) 192 | return paths and M.path_sanitize(paths[1]) 193 | end 194 | 195 | ---@class Class 196 | ---@field _new fun(cls: table, fields?: table): table Object constructor 197 | 198 | --- Create a class that inherits from given class 199 | ---@param base_cls table? 200 | ---@return Class 201 | function M.class(base_cls) 202 | -- New class table and metatable 203 | local cls = {} 204 | cls.__index = cls 205 | 206 | function cls:_new(fields) 207 | return setmetatable(fields or {}, cls) 208 | end 209 | 210 | -- Inheritance: indexing cls first checks cls due to object's metatable 211 | -- and then base_cls due to the metatable of `cls` itself 212 | if base_cls then 213 | setmetatable(cls, { __index = base_cls }) 214 | end 215 | 216 | return cls 217 | end 218 | 219 | --- Returns an iterator over list items grouped in chunks 220 | ---@generic T 221 | ---@param list T[] 222 | ---@param len integer 223 | ---@return fun(): (T[])? 224 | function M.list_chunks(list, len) 225 | local head = 1 226 | local tail = len 227 | return function() 228 | if head > #list then 229 | return 230 | end 231 | local chunk = vim.list_slice(list, head, tail) 232 | head = head + len 233 | tail = tail + len 234 | return chunk 235 | end 236 | end 237 | 238 | --- Iterate over chunks of `s` with length `len`. Last chunk may be shorter. 239 | ---@param s string 240 | ---@param len integer 241 | ---@return fun(): string? 242 | function M.string_chunks(s, len) 243 | local slen = #s 244 | local head = 1 245 | local tail = len 246 | return function() 247 | if head > slen then 248 | return 249 | end 250 | local chunk = s:sub(head, tail) 251 | head = head + len 252 | tail = tail + len 253 | return chunk 254 | end 255 | end 256 | 257 | ---@generic T 258 | ---@param list T[] 259 | function M.reverse_in_place(list) 260 | for i = 1, math.floor(#list / 2) do 261 | local j = #list + 1 - i 262 | local tmp = list[i] 263 | list[i] = list[j] 264 | list[j] = tmp 265 | end 266 | end 267 | 268 | return M 269 | -------------------------------------------------------------------------------- /perf/bytes-to-string.lua: -------------------------------------------------------------------------------- 1 | ---@param cfg BenchmarkConfig 2 | ---@return BenchmarkResult 3 | local function benchmark(cfg, fn, ...) 4 | local mem_usage = 0 5 | local total_time = 0 6 | local count = 0 7 | 8 | local max_count = cfg.max_iterations - 1 9 | local min_time = cfg.min_time * 1e9 10 | local end_time = vim.uv.hrtime() + cfg.max_duration * 1e9 11 | while vim.uv.hrtime() < end_time and (count < max_count or total_time < min_time) do 12 | collectgarbage('collect') 13 | 14 | local mem_start = collectgarbage('count') 15 | local time_start = vim.uv.hrtime() 16 | fn(...) 17 | local time_end = vim.uv.hrtime() 18 | local mem_end = collectgarbage('count') 19 | 20 | total_time = total_time + (time_end - time_start) 21 | mem_usage = mem_usage + (mem_end - mem_start) 22 | count = count + 1 23 | end 24 | 25 | return { 26 | n = count, 27 | time = total_time, 28 | mem = mem_usage, 29 | } 30 | end 31 | 32 | local function human_size(size) 33 | local unit = 1 34 | local units = { '', 'K', 'M', 'G' } 35 | while unit < #units and size >= 1000 do 36 | size = size / 1024 37 | unit = unit + 1 38 | end 39 | return string.format('%6.2f %s', size, units[unit]) 40 | end 41 | 42 | ---@param name string 43 | ---@param result BenchmarkResult 44 | local function summarize(name, input, result) 45 | return string.format( 46 | '%-16s %9.3f ms, %6.2f MB mem, %sB/s bw, %3d iters, %3.1f s total', 47 | name, 48 | result.time / 1e6 / result.n, 49 | result.mem / 1024 / result.n, 50 | human_size(#input.data / (result.time / 1e9)), 51 | result.n, 52 | result.time / 1e9 53 | ) 54 | end 55 | 56 | -------------------------------------------------------------------------------- 57 | 58 | local impls = {} 59 | 60 | -- function impls.iter1(data) 61 | -- local chars = {} 62 | -- for _, byte in ipairs(data) do 63 | -- table.insert(chars, string.char(byte)) 64 | -- end 65 | -- return table.concat(chars) 66 | -- end 67 | -- 68 | -- function impls.iter2(data) 69 | -- local chars = {} 70 | -- for _, byte in ipairs(data) do 71 | -- chars[#chars + 1] = string.char(byte) 72 | -- end 73 | -- return table.concat(chars) 74 | -- end 75 | 76 | function impls.iter(data) 77 | local chars = {} 78 | for i, byte in ipairs(data) do 79 | chars[i] = string.char(byte) 80 | end 81 | return table.concat(chars) 82 | end 83 | 84 | function impls.unpack(data) 85 | local CHUNK = 7997 86 | local n = #data 87 | local got = 0 88 | local parts = {} 89 | while got < n do 90 | local chunk = math.min(CHUNK, n - got) 91 | table.insert(parts, string.char(unpack(data, got + 1, got + 1 + chunk - 1))) 92 | got = got + chunk 93 | end 94 | return table.concat(parts) 95 | end 96 | 97 | function impls.stringbuf(data) 98 | local stringbuffer = require('string.buffer') 99 | local n = #data 100 | local buf = stringbuffer.new(n) 101 | for i = 1, n do 102 | buf:put(string.char(data[i])) 103 | end 104 | return tostring(buf) 105 | end 106 | 107 | local cfg = { min_time = 0.5, max_duration = 3, max_iterations = 100 } 108 | local lines = { 'Benchmark:' } 109 | 110 | local KB, MB = 1024, 1024 * 1024 111 | local counts = { small = 200, mid = 16 * KB, big = 2 * MB } 112 | local inputs = {} 113 | for count, n in pairs(counts) do 114 | local data = {} 115 | inputs[count] = { data = data } 116 | for _ = 1, n do 117 | table.insert(data, math.random(0, 255)) 118 | end 119 | end 120 | 121 | for count, input in pairs(inputs) do 122 | for impl, fn in pairs(impls) do 123 | local name = table.concat({ count, impl }, '.') 124 | local result = benchmark(cfg, fn, input.data) 125 | table.insert(lines, ' ' .. summarize(name, input, result)) 126 | end 127 | end 128 | 129 | print(table.concat(lines, '\n')) 130 | 131 | -- Benchmark: (CHUNK=7997) 132 | -- big.iter 46.924 ms, 17.75 MB mem, 1.52 MB/s bw, 28 iters, 1.3 s total 133 | -- big.unpack 5.491 ms, 5.08 MB mem, 7.92 MB/s bw, 46 iters, 0.3 s total 134 | -- big.stringbuf 6.088 ms, 4.00 MB mem, 7.14 MB/s bw, 46 iters, 0.3 s total 135 | -- small.iter 0.037 ms, 0.00 MB mem, 102.57 KB/s bw, 51 iters, 0.0 s total 136 | -- small.unpack 0.011 ms, 0.00 MB mem, 360.83 KB/s bw, 51 iters, 0.0 s total 137 | -- small.stringbuf 0.008 ms, 0.00 MB mem, 459.16 KB/s bw, 51 iters, 0.0 s total 138 | -- mid.iter 0.399 ms, 0.14 MB mem, 785.39 KB/s bw, 51 iters, 0.0 s total 139 | -- mid.unpack 0.063 ms, 0.11 MB mem, 4.90 MB/s bw, 51 iters, 0.0 s total 140 | -- mid.stringbuf 0.045 ms, 0.03 MB mem, 6.72 MB/s bw, 52 iters, 0.0 s total 141 | -------------------------------------------------------------------------------- /scripts/check-readme-config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Check if the defaults from config.lua are the same in README.md 3 | # Poor but simple implementation based on sed and other standard tools. 4 | 5 | defaults_file="$1" 6 | readme_file="$2" 7 | 8 | from_line() { 9 | tail -n "+$1" 10 | } 11 | 12 | ignore_last() { 13 | head -n "-$1" 14 | } 15 | 16 | remove_indent() { 17 | # 4 spaces 18 | sed 's/^ //' 19 | } 20 | 21 | remove_comments() { 22 | sed 's/\s*--.*$//' 23 | } 24 | 25 | # Retrieve the defaults table from config.lua 26 | # 1. Get lines of the defaults() function (including start/end) 27 | # 2. Get lines since return to end 28 | # 3. Remove the "return" line 29 | # 4. Remove "}\nend" (2 lines) 30 | config=$( 31 | sed -n '/^local function defaults/,/^end/ p' "$defaults_file" \ 32 | | sed -n '/^\s*return/,$ p' \ 33 | | from_line 2 \ 34 | | ignore_last 2 \ 35 | | remove_indent 36 | ) 37 | 38 | config_section() { 39 | sed -n '/^## Configuration/,/^##/ p' 40 | } 41 | 42 | code_block() { 43 | sed -n '/^```lua$/,/^```$/ p' 44 | } 45 | 46 | # Get defaults from README 47 | # 1. Lines of the Configuration section to the end of first code block 48 | # 2. Lines of the lua code block 49 | # 3. Remove starting lines in code block 50 | # 4. Remove ending lines in code block 51 | # 5. Remove Lua comments 52 | readme_config=$( 53 | sed -n '/^## Configuration/,/```$/ p' "$readme_file" \ 54 | | sed -n '/^```lua$/,/^```$/ p' \ 55 | | from_line 3 \ 56 | | ignore_last 2 \ 57 | | remove_comments 58 | ) 59 | 60 | diff --ignore-blank-lines <(echo "$config") <(echo "$readme_config") 61 | --------------------------------------------------------------------------------