├── .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 | [](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 |
--------------------------------------------------------------------------------