├── .busted ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── feature.yml ├── linters │ └── .luacheckrc └── workflows │ ├── linter.yml │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .luacheckrc ├── .luarc.json ├── LICENSE.txt ├── README.md ├── doc └── dap.txt ├── lua ├── dap.lua └── dap │ ├── _cmds.lua │ ├── async.lua │ ├── breakpoints.lua │ ├── entity.lua │ ├── ext │ ├── autocompl.lua │ └── vscode.lua │ ├── health.lua │ ├── log.lua │ ├── progress.lua │ ├── protocol.lua │ ├── repl.lua │ ├── rpc.lua │ ├── session.lua │ ├── ui.lua │ ├── ui │ └── widgets.lua │ └── utils.lua ├── nvim-dap-scm-1.rockspec ├── plugin └── dap.lua └── spec ├── bad_adapter.py ├── breakpoints_spec.lua ├── debugpy_spec.lua ├── example.py ├── ext_vscode_spec.lua ├── helpers.lua ├── integration_spec.lua ├── launch.json ├── pipe_spec.lua ├── progress_spec.lua ├── repl_spec.lua ├── run_server.lua ├── server.lua ├── server_executable_spec.lua ├── sessions_spec.lua ├── ui_spec.lua ├── utils_spec.lua └── widgets_spec.lua /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | _all = { 3 | coverage = false, 4 | lpath = "lua/?.lua;lua/?/init.lua", 5 | lua = "~/.luarocks/bin/nlua", 6 | }, 7 | default = { 8 | verbose = true 9 | }, 10 | tests = { 11 | verbose = true 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.lua] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug Report 2 | description: Tell about something that's not working the way you think it should 3 | body: 4 | - type: textarea 5 | id: config 6 | attributes: 7 | label: Debug adapter definition and debug configuration 8 | description: dap.adapters. and dap.configurations.. 9 | Please reduce it to a minimum with which the problem can be reproduced. 10 | Make sure to format code snippets (Using ```) 11 | placeholder: |- 12 | Installed the adapter via ... 13 | Configured it as described in the wiki: 14 | 15 | ```lua 16 | dap.adapters.abc = { 17 | type = 'executable'; 18 | command = 'abc'; 19 | ... 20 | } 21 | 22 | dap.configurations.xy = { 23 | { 24 | ... 25 | } 26 | } 27 | ``` 28 | - type: input 29 | id: debug_adapter_version 30 | attributes: 31 | label: Debug adapter version 32 | placeholder: 3.4.5 33 | validations: 34 | required: false 35 | - type: textarea 36 | id: repro 37 | attributes: 38 | label: Steps to Reproduce 39 | description: How can we see what you're seeing? Please be specific 40 | placeholder: |- 41 | 1. In a project like: ... 42 | 2. Set a breakpoint on... 43 | 3. Run `continue()` and then ... 44 | validations: 45 | required: true 46 | - type: textarea 47 | id: expected 48 | attributes: 49 | label: Expected Result 50 | validations: 51 | required: true 52 | - type: textarea 53 | id: actual 54 | attributes: 55 | label: Actual Result 56 | description: Output? Logs?(:help dap.set_log_level) 57 | validations: 58 | required: true 59 | - type: markdown 60 | attributes: 61 | value: |- 62 | ## Thanks 🙏 63 | validations: 64 | required: false 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question or start a discussion 4 | url: https://github.com/mfussenegger/nvim-dap/discussions 5 | about: Use the Github discussions feature 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Feature Request 2 | description: Tell about a problem you'd like to solve with nvim-dap 3 | body: 4 | - type: textarea 5 | id: problem 6 | attributes: 7 | label: Problem Statement 8 | description: Describe your problem 9 | validations: 10 | required: true 11 | - type: textarea 12 | id: expected 13 | attributes: 14 | label: Possible Solutions 15 | description: |- 16 | Describe how a solution could look like. 17 | 18 | Please focus on how you'd use the feature, this isn't about 19 | implementation details. 20 | placeholder: |- 21 | 🤷 22 | validations: 23 | required: false 24 | - type: textarea 25 | id: alternatives 26 | attributes: 27 | label: Considered Alternatives 28 | description: |- 29 | If you tried alternatives and they didn't work or were insufficient, 30 | please mention them here. 31 | validations: 32 | required: false 33 | - type: markdown 34 | attributes: 35 | value: |- 36 | ## Thanks 🙏 37 | -------------------------------------------------------------------------------- /.github/linters/.luacheckrc: -------------------------------------------------------------------------------- 1 | ../../.luacheckrc -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint Code Base 3 | on: 4 | pull_request: ~ 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | name: Lint Code Base 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout Code 16 | uses: actions/checkout@v2 17 | 18 | - name: Lint Code Base 19 | uses: github/super-linter/slim@v4 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | VALIDATE_JSCPD: false 23 | VALIDATE_PYTHON_BLACK: false 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "release" 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | luarocks-upload: 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: LuaRocks Upload 12 | uses: nvim-neorocks/luarocks-tag-release@v7 13 | env: 14 | LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }} 15 | with: 16 | detailed_description: | 17 | nvim-dap allows you to: 18 | 19 | * Launch an application to debug 20 | * Attach to running applications and debug them 21 | * Set breakpoints and step through code 22 | * Inspect the state of the application 23 | copy_directories: 24 | doc 25 | plugin 26 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Run tests 3 | on: 4 | pull_request: ~ 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | name: Run tests 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | neovim_version: ['nightly', 'v0.10.4', 'v0.11.0'] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.ref }} 21 | 22 | - name: Install neovim 23 | uses: rhysd/action-setup-vim@v1 24 | with: 25 | neovim: true 26 | version: ${{ matrix.neovim_version }} 27 | 28 | - name: Install Lua 29 | uses: leso-kn/gh-actions-lua@master 30 | with: 31 | luaVersion: "5.1" 32 | 33 | - name: Install Luarocks 34 | uses: hishamhm/gh-actions-luarocks@master 35 | with: 36 | luarocksVersion: "3.11.0" 37 | 38 | - name: Run tests 39 | run: | 40 | luarocks test --local 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | ignore = { 2 | "631", -- max_line_length 3 | } 4 | globals = { 5 | "vim", 6 | } 7 | read_globals = { 8 | "describe", 9 | "it", 10 | "before_each", 11 | "after_each", 12 | "assert" 13 | } 14 | -------------------------------------------------------------------------------- /.luarc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", 3 | "Lua.workspace.checkThirdParty": false 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DAP (Debug Adapter Protocol) 2 | 3 | `nvim-dap` is a Debug Adapter Protocol client implementation for [Neovim][1]. 4 | `nvim-dap` allows you to: 5 | 6 | - Launch an application to debug 7 | - Attach to running applications and debug them 8 | - Set breakpoints and step through code 9 | - Inspect the state of the application 10 | 11 | ![demo][demo] 12 | 13 | ## Installation 14 | 15 | [![LuaRocks](https://img.shields.io/luarocks/v/mfussenegger/nvim-dap?logo=lua&color=purple)](https://luarocks.org/modules/mfussenegger/nvim-dap) 16 | 17 | - Install nvim-dap like any other Neovim plugin: 18 | - `git clone https://codeberg.org/mfussenegger/nvim-dap.git ~/.config/nvim/pack/plugins/start/nvim-dap` 19 | - Or with [vim-plug][11]: `Plug 'mfussenegger/nvim-dap'` 20 | - Or with [packer.nvim][12]: `use 'mfussenegger/nvim-dap'` 21 | - Generate the documentation for nvim-dap using `:helptags ALL` or 22 | `:helptags ` 23 | 24 | Supported Neovim versions: 25 | 26 | - Latest nightly 27 | - 0.11.x (Recommended) 28 | - 0.10.4 29 | 30 | You'll need to install and configure a debug adapter per language. See 31 | 32 | - [:help dap.txt](doc/dap.txt) 33 | - the [Debug-Adapter Installation][5] wiki 34 | - `:help dap-adapter` 35 | - `:help dap-configuration` 36 | 37 | ## Usage 38 | 39 | A typical debug flow consists of: 40 | 41 | - Setting breakpoints via `:lua require'dap'.toggle_breakpoint()`. 42 | - Launching debug sessions and resuming execution via `:lua require'dap'.continue()`. 43 | - Stepping through code via `:lua require'dap'.step_over()` and `:lua require'dap'.step_into()`. 44 | - Inspecting the state via the built-in REPL: `:lua require'dap'.repl.open()` 45 | or using the widget UI (`:help dap-widgets`) 46 | 47 | See [:help dap.txt](doc/dap.txt), `:help dap-mapping` and `:help dap-api`. 48 | 49 | ## Supported languages 50 | 51 | In theory all of the languages for which a debug adapter exists should be 52 | supported. 53 | 54 | - [Available debug adapters][13] 55 | - [nvim-dap Debug-Adapter Installation & Configuration][5] 56 | 57 | The Wiki is community maintained. If you got an adapter working that isn't 58 | listed yet, please extend the Wiki. 59 | 60 | Some debug adapters have [language specific 61 | extensions](https://codeberg.org/mfussenegger/nvim-dap/wiki/Extensions#language-specific-extensions). 62 | Using them over a manual configuration is recommended, as they're 63 | usually better maintained. 64 | 65 | If the instructions in the wiki for a debug adapter are not working, consider 66 | that debug adapters may have made changes since the instructions were written. 67 | You may want to read the release notes of the debug adapters or try with an 68 | older version. Please update the wiki if you discover outdated examples. 69 | 70 | ## Goals 71 | 72 | - Have a basic debugger in Neovim. 73 | - Extensibility and double as a DAP client library. This allows other plugins 74 | to extend the debugging experience. Either by improving the UI or by making 75 | it easier to debug parts of an application. 76 | 77 | - Examples of UI/UX extensions are [nvim-dap-virtual-text][7] and [nvim-dap-ui][15] 78 | - Examples for language specific extensions include [nvim-jdtls][8] and [nvim-dap-python][9] 79 | 80 | ## Extensions 81 | 82 | All known extensions are listed in the [Wiki][10]. The wiki is community 83 | maintained. Please add new extensions if you built one or if you discovered one 84 | that's not listed. 85 | 86 | ## Non-Goals 87 | 88 | - Debug adapter installations are out of scope. It's not the business of an 89 | editor plugin to re-invent a package manager. Use your system package 90 | manager. Use Nix. Use Ansible. 91 | 92 | - [nvim-dapconfig](https://github.com/nvim-lua/wishlist/issues/37#issuecomment-1023363686) 93 | 94 | - Vim support. It's not going to happen. Use [vimspector][2] instead. 95 | 96 | ## Alternatives 97 | 98 | - [vimspector][2] 99 | 100 | 101 | ## Contributing 102 | 103 | Contributions are welcome: 104 | 105 | - Give concrete feedback about usability. 106 | - Triage issues. Many of the problems people encounter are debug 107 | adapter specific. 108 | - Improve upstream debug adapter documentation to make them more editor 109 | agnostic. 110 | - Improve the Wiki. But please refrain from turning it into comprehensive debug 111 | adapter documentation that should go upstream. 112 | - Write extensions. 113 | 114 | Before making direct code contributions, please create a discussion or issue to 115 | clarify whether the change is in scope of the nvim-dap core. 116 | 117 | Please keep pull requests focused and don't change multiple things at the same 118 | time. 119 | 120 | ## Features 121 | 122 | - [x] launch debug adapter 123 | - [x] attach to debug adapter 124 | - [x] toggle breakpoints 125 | - [x] breakpoints with conditions 126 | - [x] logpoints 127 | - [x] set exception breakpoints 128 | - [x] step over, step into, step out 129 | - [x] step back, reverse continue 130 | - [x] Goto 131 | - [x] restart 132 | - [x] stop 133 | - [x] pause 134 | - [x] evaluate expressions 135 | - [x] REPL (incl. commands to show threads, frames and scopes) 136 | 137 | 138 | [1]: https://neovim.io/ 139 | [2]: https://github.com/puremourning/vimspector 140 | [5]: https://codeberg.org/mfussenegger/nvim-dap/wiki/Debug-Adapter-installation 141 | [7]: https://github.com/theHamsta/nvim-dap-virtual-text 142 | [8]: https://codeberg.org/mfussenegger/nvim-jdtls 143 | [9]: https://codeberg.org/mfussenegger/nvim-dap-python 144 | [10]: https://codeberg.org/mfussenegger/nvim-dap/wiki/Extensions 145 | [11]: https://github.com/junegunn/vim-plug 146 | [12]: https://github.com/wbthomason/packer.nvim 147 | [13]: https://microsoft.github.io/debug-adapter-protocol/implementors/adapters/ 148 | [15]: https://github.com/rcarriga/nvim-dap-ui 149 | [demo]: https://user-images.githubusercontent.com/38700/124292938-669a7100-db56-11eb-93b8-77b66994fc8a.gif 150 | 151 | -------------------------------------------------------------------------------- /lua/dap/_cmds.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | local M = {} 3 | 4 | 5 | ---@param args vim.api.keyset.create_user_command.command_args 6 | function M.eval(args) 7 | local oldbuf = api.nvim_get_current_buf() 8 | local name = string.format("dap-eval://%s", vim.bo[oldbuf].filetype) 9 | if args.smods.vertical then 10 | vim.cmd.vsplit({name}) 11 | elseif args.smods.tab == 1 then 12 | vim.cmd.tabedit(name) 13 | else 14 | local size = math.max(5, math.floor(vim.o.lines * 1/5)) 15 | vim.cmd.split({name, mods = args.smods, range = { size }}) 16 | end 17 | local newbuf = api.nvim_get_current_buf() 18 | if args.range ~= 0 then 19 | local lines = api.nvim_buf_get_lines(oldbuf, args.line1 -1 , args.line2, true) 20 | local indent = math.huge 21 | for _, line in ipairs(lines) do 22 | indent = math.min(line:find("[^ ]") or math.huge, indent) 23 | end 24 | if indent ~= math.huge and indent > 0 then 25 | for i, line in ipairs(lines) do 26 | lines[i] = line:sub(indent) 27 | end 28 | end 29 | api.nvim_buf_set_lines(newbuf, 0, -1, true, lines) 30 | vim.bo[newbuf].modified = false 31 | end 32 | if args.bang then 33 | vim.cmd.w() 34 | end 35 | end 36 | 37 | 38 | ---@param args vim.api.keyset.create_user_command.command_args 39 | function M.new(args) 40 | local dap = require("dap") 41 | local fargs = args.fargs 42 | if not next(fargs) then 43 | dap.continue({ new = true }) 44 | return 45 | end 46 | local bufnr = api.nvim_get_current_buf() 47 | require("dap.async").run(function() 48 | for _, get_configs in pairs(dap.providers.configs) do 49 | local configs = get_configs(bufnr) 50 | for _, config in ipairs(configs) do 51 | if vim.tbl_contains(fargs, config.name) then 52 | dap.run(config) 53 | end 54 | end 55 | end 56 | end) 57 | end 58 | 59 | 60 | function M.new_complete() 61 | local bufnr = api.nvim_get_current_buf() 62 | local dap = require("dap") 63 | local candidates = {} 64 | local done = false 65 | require("dap.async").run(function() 66 | for _, get_configs in pairs(dap.providers.configs) do 67 | local configs = get_configs(bufnr) 68 | for _, config in ipairs(configs) do 69 | local name = config.name:gsub(" ", "\\ ") 70 | table.insert(candidates, name) 71 | end 72 | end 73 | done = true 74 | end) 75 | vim.wait(2000, function() return done == true end) 76 | return candidates 77 | end 78 | 79 | 80 | function M.bufread_eval() 81 | local bufnr = api.nvim_get_current_buf() 82 | local fname = api.nvim_buf_get_name(bufnr) 83 | vim.bo[bufnr].swapfile = false 84 | vim.bo[bufnr].buftype = "acwrite" 85 | vim.bo[bufnr].bufhidden = "wipe" 86 | local ft = fname:match("dap%-eval://(%w+)(.*)") 87 | if ft and ft ~= "" then 88 | vim.bo[bufnr].filetype = ft 89 | else 90 | local altbuf = vim.fn.bufnr("#", false) 91 | if altbuf then 92 | vim.bo[bufnr].filetype = vim.bo[altbuf].filetype 93 | end 94 | end 95 | api.nvim_create_autocmd("BufWriteCmd", { 96 | buffer = bufnr, 97 | callback = function(args) 98 | vim.bo[args.buf].modified = false 99 | local repl = require("dap.repl") 100 | local lines = api.nvim_buf_get_lines(args.buf, 0, -1, true) 101 | repl.execute(table.concat(lines, "\n")) 102 | repl.open() 103 | end, 104 | }) 105 | end 106 | 107 | 108 | ---@param args vim.api.keyset.create_autocmd.callback_args 109 | function M.newlaunchjson(args) 110 | if vim.snippet then 111 | local text = [[{ 112 | "\$schema": "https://raw.githubusercontent.com/mfussenegger/dapconfig-schema/master/dapconfig-schema.json", 113 | "version": "0.2.0", 114 | "configurations": [ 115 | { 116 | "type": "${1:adaptername}", 117 | "request": "${2|launch,request|}", 118 | "name": "${3:run}"${0} 119 | } 120 | ] 121 | }]] 122 | api.nvim_buf_call(args.buf, function() 123 | vim.snippet.expand(text) 124 | end) 125 | else 126 | local lines = { 127 | '{', 128 | ' "$schema": "https://raw.githubusercontent.com/mfussenegger/dapconfig-schema/master/dapconfig-schema.json",', 129 | ' "version": "0.2.0",', 130 | ' "configurations": [', 131 | ' {', 132 | ' "type": "",', 133 | ' "request": "launch",', 134 | ' "name": "Launch"', 135 | ' }', 136 | ' ]', 137 | '}' 138 | } 139 | api.nvim_buf_set_lines(args.buf, 0, -1, true, lines) 140 | end 141 | end 142 | 143 | 144 | function M.yank_evalname() 145 | if vim.v.event.operator ~= "y" or vim.v.event.visual == true then 146 | return 147 | end 148 | local buf = api.nvim_get_current_buf() 149 | local layer = require("dap.ui").get_layer(buf) 150 | if not layer then 151 | return 152 | end 153 | local lnum = api.nvim_win_get_cursor(0)[1] - 1 154 | local item = (layer.get(lnum) or {}).item 155 | if item and item.evaluateName then 156 | vim.fn.setreg("e", item.evaluateName) 157 | end 158 | end 159 | 160 | 161 | function M.show_logs() 162 | local log = require("dap.log") 163 | log.create_logger("dap.log") 164 | for _, logger in pairs(log._loggers) do 165 | vim.cmd.tabnew(logger._path) 166 | end 167 | end 168 | 169 | 170 | return M 171 | -------------------------------------------------------------------------------- /lua/dap/async.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | --- Run a function in a coroutine with error handling via vim.notify 4 | --- 5 | --- If run is called within a coroutine, no new coroutine is created. 6 | function M.run(fn) 7 | local co, is_main = coroutine.running() 8 | if co and not is_main then 9 | fn() 10 | else 11 | coroutine.wrap(function() 12 | xpcall(fn, function(err) 13 | local msg = debug.traceback(err, 2) 14 | require("dap.utils").notify(msg, vim.log.levels.ERROR) 15 | end) 16 | end)() 17 | end 18 | end 19 | 20 | return M 21 | -------------------------------------------------------------------------------- /lua/dap/breakpoints.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | local non_empty = require('dap.utils').non_empty 3 | 4 | local bp_by_sign = {} 5 | local ns = 'dap_breakpoints' 6 | local M = {} 7 | 8 | 9 | local function get_breakpoint_signs(bufexpr) 10 | if bufexpr then 11 | return vim.fn.sign_getplaced(bufexpr, {group = ns}) 12 | end 13 | local bufs_with_signs = vim.fn.sign_getplaced() 14 | local result = {} 15 | for _, buf_signs in ipairs(bufs_with_signs) do 16 | buf_signs = vim.fn.sign_getplaced(buf_signs.bufnr, {group = ns})[1] 17 | if #buf_signs.signs > 0 then 18 | table.insert(result, buf_signs) 19 | end 20 | end 21 | return result 22 | end 23 | 24 | 25 | local function get_sign_name(bp) 26 | if bp.verified == false then 27 | return 'DapBreakpointRejected' 28 | elseif non_empty(bp.condition) then 29 | return 'DapBreakpointCondition' 30 | elseif non_empty(bp.logMessage) then 31 | return 'DapLogPoint' 32 | else 33 | return 'DapBreakpoint' 34 | end 35 | end 36 | 37 | 38 | ---@param breakpoint dap.Breakpoint 39 | function M.update(breakpoint) 40 | assert(breakpoint.id, "To update a breakpoint it must have an id property") 41 | for sign_id, bp in pairs(bp_by_sign) do 42 | if bp.state and bp.state.id == breakpoint.id then 43 | local verified_changed = 44 | bp.state.verified == false and breakpoint.verified 45 | or breakpoint.verified == false and bp.state.verified 46 | if verified_changed then 47 | vim.fn.sign_place( 48 | sign_id, 49 | ns, 50 | get_sign_name(bp), 51 | bp.buf, 52 | { lnum = bp.line; priority = 21; } 53 | ) 54 | end 55 | bp.state.verified = breakpoint.verified 56 | bp.state.message = breakpoint.message 57 | return 58 | end 59 | end 60 | end 61 | 62 | 63 | ---@param bufnr integer 64 | ---@param state dap.Breakpoint 65 | function M.set_state(bufnr, state) 66 | local ok, placements = pcall(vim.fn.sign_getplaced, bufnr, { group = ns; lnum = state.line; }) 67 | if not ok then 68 | return 69 | end 70 | local signs = (placements[1] or {}).signs 71 | if not signs or next(signs) == nil then 72 | return 73 | end 74 | for _, sign in pairs(signs) do 75 | local bp = bp_by_sign[sign.id] 76 | if bp then 77 | bp.state = state 78 | end 79 | if not state.verified then 80 | vim.fn.sign_place( 81 | sign.id, 82 | ns, 83 | 'DapBreakpointRejected', 84 | bufnr, 85 | { lnum = state.line; priority = 21; } 86 | ) 87 | end 88 | end 89 | end 90 | 91 | 92 | function M.remove(bufnr, lnum) 93 | local placements = vim.fn.sign_getplaced(bufnr, { group = ns; lnum = lnum; }) 94 | local signs = placements[1].signs 95 | if signs and #signs > 0 then 96 | for _, sign in pairs(signs) do 97 | vim.fn.sign_unplace(ns, { buffer = bufnr; id = sign.id; }) 98 | bp_by_sign[sign.id] = nil 99 | end 100 | return true 101 | else 102 | return false 103 | end 104 | end 105 | 106 | 107 | function M.toggle(opts, bufnr, lnum) 108 | opts = opts or {} 109 | bufnr = bufnr or api.nvim_get_current_buf() 110 | lnum = lnum or api.nvim_win_get_cursor(0)[1] 111 | if M.remove(bufnr, lnum) and not opts.replace then 112 | return 113 | end 114 | local bp = { 115 | buf = bufnr, 116 | condition = opts.condition, 117 | logMessage = opts.log_message, 118 | hitCondition = opts.hit_condition 119 | } 120 | local sign_name = get_sign_name(bp) 121 | local sign_id = vim.fn.sign_place( 122 | 0, 123 | ns, 124 | sign_name, 125 | bufnr, 126 | { lnum = lnum; priority = 21; } 127 | ) 128 | if sign_id ~= -1 then 129 | bp_by_sign[sign_id] = bp 130 | end 131 | end 132 | 133 | 134 | function M.set(opts, bufnr, lnum) 135 | opts = opts or {} 136 | opts.replace = true 137 | M.toggle(opts, bufnr, lnum) 138 | end 139 | 140 | 141 | --- Returns all breakpoints grouped by bufnr 142 | function M.get(bufexpr) 143 | local signs = get_breakpoint_signs(bufexpr) 144 | if #signs == 0 then 145 | return {} 146 | end 147 | local result = {} 148 | for _, buf_bp_signs in pairs(signs) do 149 | local breakpoints = {} 150 | local bufnr = buf_bp_signs.bufnr 151 | result[bufnr] = breakpoints 152 | for _, bp in pairs(buf_bp_signs.signs) do 153 | local bp_entry = bp_by_sign[bp.id] or {} 154 | table.insert(breakpoints, { 155 | line = bp.lnum; 156 | condition = bp_entry.condition; 157 | hitCondition = bp_entry.hitCondition; 158 | logMessage = bp_entry.logMessage; 159 | state = bp_entry.state, 160 | }) 161 | end 162 | end 163 | return result 164 | end 165 | 166 | 167 | function M.clear() 168 | vim.fn.sign_unplace(ns) 169 | bp_by_sign = {} 170 | end 171 | 172 | 173 | do 174 | local function not_nil(x) 175 | return x ~= nil 176 | end 177 | 178 | function M.to_qf_list(breakpoints) 179 | local qf_list = {} 180 | for bufnr, buf_bps in pairs(breakpoints) do 181 | for _, bp in pairs(buf_bps) do 182 | local state = bp.state or {} 183 | local text_parts = { 184 | unpack(api.nvim_buf_get_lines(bufnr, bp.line - 1, bp.line, false), 1), 185 | state.verified == false and (state.message and 'Rejected: ' .. state.message or 'Rejected') or nil, 186 | non_empty(bp.logMessage) and "Log message: " .. bp.logMessage or nil, 187 | non_empty(bp.condition) and "Condition: " .. bp.condition or nil, 188 | non_empty(bp.hitCondition) and "Hit condition: " .. bp.hitCondition or nil, 189 | } 190 | local text = table.concat(vim.tbl_filter(not_nil, text_parts), ', ') 191 | table.insert(qf_list, { 192 | bufnr = bufnr, 193 | lnum = bp.line, 194 | col = 0, 195 | text = text, 196 | }) 197 | end 198 | end 199 | return qf_list 200 | end 201 | end 202 | 203 | 204 | return M 205 | -------------------------------------------------------------------------------- /lua/dap/entity.lua: -------------------------------------------------------------------------------- 1 | local utils = require('dap.utils') 2 | local M = {} 3 | 4 | 5 | local variable = {} 6 | M.variable = variable 7 | 8 | local types_to_hl_group = { 9 | boolean = "Boolean", 10 | string = "String", 11 | int = "Number", 12 | long = "Number", 13 | number = "Number", 14 | double = "Float", 15 | float = "Float", 16 | ["function"] = "Function", 17 | } 18 | 19 | 20 | ---@param var dap.Variable|dap.EvaluateResponse 21 | function variable.get_key(var) 22 | return var.name or var.result 23 | end 24 | 25 | 26 | function variable.is_lazy(var) 27 | return (var.presentationHint or {}).lazy 28 | end 29 | 30 | 31 | ---@alias dap.entity.hl [string, integer, integer][] 32 | 33 | 34 | ---@param var dap.Variable|dap.EvaluateResponse 35 | ---@result string, dap.entity.hl[] 36 | function variable.render_parent(var) 37 | if var.name then 38 | return variable.render_child(var --[[@as dap.Variable]], 0) 39 | end 40 | local syntax_group = var.type and types_to_hl_group[var.type:lower()] 41 | if syntax_group then 42 | return var.result, {{syntax_group, 0, -1},} 43 | end 44 | return var.result 45 | end 46 | 47 | ---@param var dap.Variable 48 | ---@param indent integer 49 | ---@result string, dap.entity.hl[] 50 | function variable.render_child(var, indent) 51 | indent = indent or 0 52 | local hl_regions = { 53 | {'Identifier', indent, #var.name + indent + 1} 54 | } 55 | local prefix = string.rep(' ', indent) .. var.name .. ': ' 56 | local syntax_group = var.type and types_to_hl_group[var.type:lower()] 57 | if syntax_group then 58 | table.insert(hl_regions, {syntax_group, #prefix, -1}) 59 | end 60 | return prefix .. var.value, hl_regions 61 | end 62 | 63 | function variable.has_children(var) 64 | return (var.variables and #var.variables > 0) or var.variablesReference ~= 0 65 | end 66 | 67 | ---@param var dap.Variable|dap.Scope 68 | ---@result dap.Variable[] 69 | function variable.get_children(var) 70 | return var.variables or {} 71 | end 72 | 73 | 74 | ---@param a dap.Variable 75 | ---@param b dap.Variable 76 | local function cmp_vars(a, b) 77 | local num_a = string.match(a.name, '^%[?(%d+)%]?$') 78 | local num_b = string.match(b.name, '^%[?(%d+)%]?$') 79 | if num_a and num_b then 80 | return tonumber(num_a) < tonumber(num_b) 81 | else 82 | return a.name < b.name 83 | end 84 | end 85 | 86 | 87 | ---@param var dap.Variable|dap.Scope 88 | ---@param cb fun(variables: dap.Variable[]) 89 | function variable.fetch_children(var, cb) 90 | local session = require('dap').session() 91 | if var.variables then 92 | cb(variable.get_children(var)) 93 | elseif session and var.variablesReference > 0 then 94 | 95 | ---@param err? dap.ErrorResponse 96 | ---@param resp? dap.VariableResponse 97 | local function on_variables(err, resp) 98 | if err then 99 | utils.notify('Error fetching variables: ' .. err.message, vim.log.levels.ERROR) 100 | elseif resp then 101 | local variables = resp.variables 102 | local unloaded = #variables 103 | local function countdown() 104 | unloaded = unloaded - 1 105 | if unloaded == 0 then 106 | var.variables = variables 107 | cb(variables) 108 | end 109 | end 110 | 111 | table.sort(variables, cmp_vars) 112 | for i, v in ipairs(variables) do 113 | v.parent = var 114 | if variable.is_lazy(v) then 115 | variable.load_value(v, function(loaded_v) 116 | variables[i] = loaded_v 117 | countdown() 118 | end) 119 | else 120 | countdown() 121 | end 122 | end 123 | end 124 | end 125 | ---@type dap.VariablesArguments 126 | local params = { variablesReference = var.variablesReference } 127 | session:request('variables', params, on_variables) 128 | else 129 | cb({}) 130 | end 131 | end 132 | 133 | 134 | function variable.load_value(var, cb) 135 | assert(variable.is_lazy(var), "Must not call load_value if not lazy") 136 | local session = require('dap').session() 137 | if not session then 138 | cb(var) 139 | else 140 | ---@type dap.VariablesArguments 141 | local params = { variablesReference = var.variablesReference } 142 | ---@param err? dap.ErrorResponse 143 | ---@param resp? dap.VariableResponse 144 | local function on_variables(err, resp) 145 | if err then 146 | utils.notify('Error fetching variable: ' .. err.message, vim.log.levels.ERROR) 147 | elseif resp then 148 | local new_var = resp.variables[1] 149 | -- keep using the old variable; 150 | -- it has parent references and the parent contains references to the child 151 | var.value = new_var.value 152 | var.presentationHint = new_var.presentationHint 153 | var.variablesReference = new_var.variablesReference 154 | var.namedVariables = new_var.namedVariables 155 | var.indexedVariables = new_var.indexedVariables 156 | cb(var) 157 | end 158 | end 159 | session:request('variables', params, on_variables) 160 | end 161 | end 162 | 163 | 164 | ---@param item dap.Variable 165 | local function set_variable(_, item, _, context) 166 | local session = require('dap').session() 167 | if not session then 168 | utils.notify('No active session, cannot set variable') 169 | return 170 | end 171 | if not session.current_frame then 172 | utils.notify('Session has no active frame, cannot set variable') 173 | return 174 | end 175 | local parent = item.parent 176 | if not parent then 177 | utils.notify(string.format( 178 | "Cannot set variable on %s, couldn't find its parent container", 179 | item.name 180 | )) 181 | return 182 | end 183 | local view = context.view 184 | if view and vim.bo.bufhidden == 'wipe' then 185 | view.close() 186 | end 187 | local value = vim.fn.input(string.format('New `%s` value: ', item.name)) 188 | local params = { 189 | variablesReference = parent.variablesReference, 190 | name = item.name, 191 | value = value, 192 | } 193 | session:request('setVariable', params, function(err) 194 | if err then 195 | utils.notify('Error setting variable: ' .. err.message, vim.log.levels.WARN) 196 | else 197 | session:_request_scopes(session.current_frame) 198 | end 199 | end) 200 | end 201 | 202 | 203 | local function set_expression(_, item, _, context) 204 | local session = require('dap').session() 205 | if not session then 206 | utils.notify('No activate session, cannot set expression') 207 | return 208 | end 209 | local view = context.view 210 | if view and vim.bo.bufhidden == 'wipe' then 211 | view.close() 212 | end 213 | local value = vim.fn.input(string.format('New `%s` expression: ', item.name)) 214 | local params = { 215 | expression = item.evaluateName, 216 | value = value, 217 | frameId = session.current_frame and session.current_frame.id 218 | } 219 | session:request('setExpression', params, function(err) 220 | if err then 221 | utils.notify('Error on setExpression: ' .. tostring(err), vim.log.levels.WARN) 222 | else 223 | session:_request_scopes(session.current_frame) 224 | end 225 | end) 226 | end 227 | 228 | 229 | ---@param item dap.Variable 230 | local function copy_evalname(_, item, _, _) 231 | vim.fn.setreg("", item.evaluateName) 232 | end 233 | 234 | 235 | variable.tree_spec = { 236 | get_key = variable.get_key, 237 | render_parent = variable.render_parent, 238 | render_child = variable.render_child, 239 | has_children = variable.has_children, 240 | get_children = variable.get_children, 241 | is_lazy = variable.is_lazy, 242 | load_value = variable.load_value, 243 | fetch_children = variable.fetch_children, 244 | compute_actions = function(info) 245 | local session = require('dap').session() 246 | if not session then 247 | return {} 248 | end 249 | local result = {} 250 | local capabilities = session.capabilities 251 | ---@type dap.Variable 252 | local item = info.item 253 | if item.evaluateName then 254 | table.insert(result, { label = "Copy as expression", fn = copy_evalname, }) 255 | end 256 | if item.evaluateName and capabilities.supportsSetExpression then 257 | table.insert(result, { label = 'Set expression', fn = set_expression, }) 258 | elseif capabilities.supportsSetVariable then 259 | table.insert(result, { label = 'Set variable', fn = set_variable, }) 260 | end 261 | return result 262 | end 263 | } 264 | 265 | 266 | local scope = {} 267 | M.scope = scope 268 | 269 | 270 | function scope.render_parent(value) 271 | return value.name 272 | end 273 | 274 | scope.tree_spec = vim.tbl_extend('force', variable.tree_spec, { 275 | render_parent = scope.render_parent, 276 | }) 277 | 278 | 279 | local frames = {} 280 | M.frames = frames 281 | 282 | function frames.render_item(frame) 283 | local session = require('dap').session() 284 | local line 285 | if session and frame.id == (session.current_frame or {}).id then 286 | line = '→ ' .. frame.name .. ':' .. frame.line 287 | else 288 | line = ' ' .. frame.name .. ':' .. frame.line 289 | end 290 | if frame.presentationHint == 'subtle' then 291 | return line, {{'Comment', 0, -1},} 292 | end 293 | return line 294 | end 295 | 296 | 297 | M.threads = { 298 | tree_spec = { 299 | implicit_expand_action = false, 300 | }, 301 | } 302 | local threads_spec = M.threads.tree_spec 303 | 304 | function threads_spec.get_key(thread) 305 | return thread.id 306 | end 307 | 308 | function threads_spec.render_parent(thread) 309 | return thread.name 310 | end 311 | 312 | function threads_spec.render_child(thread_or_frame) 313 | if thread_or_frame.line then 314 | -- it's a frame 315 | return frames.render_item(thread_or_frame) 316 | end 317 | if thread_or_frame.stopped then 318 | return '⏸️ ' .. thread_or_frame.name 319 | else 320 | return '▶️ ' .. thread_or_frame.name 321 | end 322 | end 323 | 324 | function threads_spec.has_children(thread_or_frame) 325 | -- Threads have frames 326 | return thread_or_frame.line == nil 327 | end 328 | 329 | function threads_spec.get_children(thread) 330 | if thread.threads then 331 | return thread.threads or {} 332 | end 333 | return thread.frames or {} 334 | end 335 | 336 | 337 | function threads_spec.fetch_children(thread, cb) 338 | local session = require('dap').session() 339 | if thread.line then 340 | -- this is a frame, not a thread 341 | cb({}) 342 | elseif thread.threads then 343 | cb(thread.threads) 344 | elseif session then 345 | coroutine.wrap(function() 346 | local co = coroutine.running() 347 | local is_stopped = thread.stopped 348 | if not is_stopped then 349 | session:_pause(thread.id, function(err, result) 350 | coroutine.resume(co, err, result) 351 | end) 352 | coroutine.yield() 353 | end 354 | local params = { threadId = thread.id } 355 | local err, resp = session:request('stackTrace', params) 356 | if err then 357 | utils.notify('Error fetching stackTrace: ' .. tostring(err), vim.log.levels.WARN) 358 | else 359 | thread.frames = resp.stackFrames 360 | end 361 | if not is_stopped then 362 | local err0 = session:request('continue', params) 363 | if err0 then 364 | utils.notify('Error on continue: ' .. tostring(err0), vim.log.levels.WARN) 365 | else 366 | thread.stopped = false 367 | local progress = require('dap.progress') 368 | progress.report('Thread resumed: ' .. tostring(thread.id)) 369 | progress.report('Running: ' .. session.config.name) 370 | end 371 | end 372 | cb(threads_spec.get_children(thread)) 373 | end)() 374 | else 375 | cb({}) 376 | end 377 | end 378 | 379 | 380 | function threads_spec.compute_actions(info) 381 | local session = require('dap').session() 382 | if not session then 383 | return {} 384 | end 385 | local context = info.context 386 | local thread = info.item 387 | local result = {} 388 | if thread.line then 389 | -- this is a frame, not a thread 390 | table.insert(result, { 391 | label = 'Jump to frame', 392 | fn = function(_, frame) 393 | session:_frame_set(frame) 394 | if context.view and vim.bo[context.view.buf].bufhidden == 'wipe' then 395 | context.view.close() 396 | end 397 | end 398 | }) 399 | else 400 | table.insert(result, { label = 'Expand', fn = context.tree.toggle }) 401 | if thread.stopped then 402 | table.insert(result, { 403 | label = 'Resume thread', 404 | fn = function() 405 | if session.stopped_thread_id == thread.id then 406 | session:_step('continue') 407 | context.refresh() 408 | else 409 | thread.stopped = false 410 | session:request('continue', { threadId = thread.id }, function(err) 411 | if err then 412 | utils.notify('Error on continue: ' .. tostring(err), vim.log.levels.WARN) 413 | end 414 | context.refresh() 415 | end) 416 | end 417 | end 418 | }) 419 | else 420 | table.insert(result, { 421 | label = 'Stop thread', 422 | fn = function() 423 | session:_pause(thread.id, context.refresh) 424 | end 425 | }) 426 | end 427 | end 428 | return result 429 | end 430 | 431 | 432 | return M 433 | -------------------------------------------------------------------------------- /lua/dap/ext/autocompl.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local api = vim.api 3 | local timer = nil 4 | 5 | 6 | local function destroy_timer() 7 | if timer then 8 | timer:stop() 9 | timer:close() 10 | timer = nil 11 | end 12 | end 13 | 14 | 15 | local function trigger_completion(buf) 16 | destroy_timer() 17 | if api.nvim_get_current_buf() == buf then 18 | api.nvim_feedkeys(api.nvim_replace_termcodes('', true, false, true), 'm', true) 19 | end 20 | end 21 | 22 | 23 | function M._InsertCharPre() 24 | if timer then 25 | return 26 | end 27 | if tonumber(vim.fn.pumvisible()) == 1 then 28 | return 29 | end 30 | local buf = api.nvim_get_current_buf() 31 | local char = api.nvim_get_vvar('char') 32 | local session = require('dap').session() 33 | local trigger_characters = ((session or {}).capabilities or {}).completionTriggerCharacters 34 | local triggers 35 | if trigger_characters and next(trigger_characters) then 36 | triggers = trigger_characters 37 | else 38 | triggers = {'.'} 39 | end 40 | if vim.tbl_contains(triggers, char) then 41 | timer = vim.loop.new_timer() 42 | timer:start(50, 0, vim.schedule_wrap(function() 43 | trigger_completion(buf) 44 | end)) 45 | end 46 | end 47 | 48 | 49 | function M._InsertLeave() 50 | destroy_timer() 51 | end 52 | 53 | 54 | function M.attach(bufnr) 55 | bufnr = bufnr or api.nvim_get_current_buf() 56 | if api.nvim_create_autocmd then 57 | local group = api.nvim_create_augroup(("dap.ext.autocmpl-%d"):format(bufnr), { clear = true }) 58 | api.nvim_create_autocmd("InsertCharPre", { 59 | group = group, 60 | buffer = bufnr, 61 | callback = function() 62 | pcall(M._InsertCharPre) 63 | end, 64 | }) 65 | api.nvim_create_autocmd("InsertLeave", { 66 | group = group, 67 | buffer = bufnr, 68 | callback = destroy_timer 69 | }) 70 | else 71 | vim.cmd(string.format([[ 72 | augroup dap_autocomplete-%d 73 | au! 74 | autocmd InsertCharPre lua require('dap.ext.autocompl')._InsertCharPre() 75 | autocmd InsertLeave lua require('dap.ext.autocompl')._InsertLeave() 76 | augroup end 77 | ]], 78 | bufnr, 79 | bufnr, 80 | bufnr 81 | )) 82 | end 83 | end 84 | 85 | 86 | return M 87 | -------------------------------------------------------------------------------- /lua/dap/ext/vscode.lua: -------------------------------------------------------------------------------- 1 | local dap = require('dap') 2 | local notify = require('dap.utils').notify 3 | local M = {} 4 | 5 | M.json_decode = vim.json.decode 6 | M.type_to_filetypes = {} 7 | 8 | 9 | ---@class dap.vscode.launch.Input 10 | ---@field id string 11 | ---@field type "promptString"|"pickString" 12 | ---@field description string 13 | ---@field default? string 14 | ---@field options string[]|{label: string, value: string}[] 15 | 16 | 17 | ---@param input dap.vscode.launch.Input 18 | ---@return function 19 | local function create_input(input) 20 | if input.type == "promptString" then 21 | return function() 22 | local description = input.description or 'Input' 23 | if not vim.endswith(description, ': ') then 24 | description = description .. ': ' 25 | end 26 | if vim.ui.input then 27 | local co = coroutine.running() 28 | local opts = { 29 | prompt = description, 30 | default = input.default or '', 31 | } 32 | vim.ui.input(opts, function(result) 33 | vim.schedule(function() 34 | coroutine.resume(co, result) 35 | end) 36 | end) 37 | return coroutine.yield() 38 | else 39 | return vim.fn.input(description, input.default or '') 40 | end 41 | end 42 | elseif input.type == "pickString" then 43 | return function() 44 | local options = assert(input.options, "input of type pickString must have an `options` property") 45 | local opts = { 46 | prompt = input.description, 47 | format_item = function(x) 48 | return x.label and x.label or x 49 | end, 50 | } 51 | local co = coroutine.running() 52 | vim.ui.select(options, opts, function(option) 53 | vim.schedule(function() 54 | local value = option and option.value or option 55 | coroutine.resume(co, value or (input.default or '')) 56 | end) 57 | end) 58 | return coroutine.yield() 59 | end 60 | else 61 | local msg = "Unsupported input type in vscode launch.json: " .. input.type 62 | notify(msg, vim.log.levels.WARN) 63 | return function() 64 | return "${input:" .. input.id .. "}" 65 | end 66 | end 67 | end 68 | 69 | 70 | ---@param inputs dap.vscode.launch.Input[] 71 | ---@return table inputs map from ${input:} to function resolving the input value 72 | local function create_inputs(inputs) 73 | local result = {} 74 | for _, input in ipairs(inputs) do 75 | local id = assert(input.id, "input must have a `id`") 76 | local key = "${input:" .. id .. "}" 77 | assert(input.type, "input must have a `type`") 78 | local fn = create_input(input) 79 | if fn then 80 | result[key] = fn 81 | end 82 | end 83 | return result 84 | end 85 | 86 | 87 | ---@param inputs table 88 | ---@param value any 89 | ---@param cache table 90 | local function apply_input(inputs, value, cache) 91 | if type(value) == "table" then 92 | local new_value = {} 93 | for k, v in pairs(value) do 94 | new_value[k] = apply_input(inputs, v, cache) 95 | end 96 | value = new_value 97 | end 98 | if type(value) ~= "string" then 99 | return value 100 | end 101 | 102 | local matches = string.gmatch(value, "${input:([%w_]+)}") 103 | for input_id in matches do 104 | local input_key = "${input:" .. input_id .. "}" 105 | local result = cache[input_key] 106 | if not result then 107 | local input = inputs[input_key] 108 | if not input then 109 | local msg = "No input with id `" .. input_id .. "` found in inputs" 110 | notify(msg, vim.log.levels.WARN) 111 | else 112 | result = input() 113 | cache[input_key] = result 114 | end 115 | end 116 | if result then 117 | value = value:gsub(input_key, result) 118 | end 119 | end 120 | return value 121 | end 122 | 123 | 124 | ---@param config table 125 | ---@param inputs table 126 | local function apply_inputs(config, inputs) 127 | local result = {} 128 | local cache = {} 129 | for key, value in pairs(config) do 130 | result[key] = apply_input(inputs, value, cache) 131 | end 132 | return result 133 | end 134 | 135 | 136 | --- Lift properties of a child table to top-level 137 | local function lift(tbl, key) 138 | local child = tbl[key] 139 | if child then 140 | tbl[key] = nil 141 | return vim.tbl_extend('force', tbl, child) 142 | end 143 | return tbl 144 | end 145 | 146 | 147 | function M._load_json(jsonstr) 148 | local ok, data = pcall(M.json_decode, jsonstr) 149 | if not ok then 150 | error("Error parsing launch.json: " .. data) 151 | end 152 | assert(type(data) == "table", "launch.json must contain a JSON object") 153 | local inputs = create_inputs(data.inputs or {}) 154 | local has_inputs = next(inputs) ~= nil 155 | 156 | local sysname 157 | if vim.fn.has('linux') == 1 then 158 | sysname = 'linux' 159 | elseif vim.fn.has('mac') == 1 then 160 | sysname = 'osx' 161 | elseif vim.fn.has('win32') == 1 then 162 | sysname = 'windows' 163 | end 164 | 165 | local configs = {} 166 | for _, config in ipairs(data.configurations or {}) do 167 | config = lift(config, sysname) 168 | if (has_inputs) then 169 | config = setmetatable(config, { 170 | __call = function() 171 | local c = vim.deepcopy(config) 172 | return apply_inputs(c, inputs) 173 | end 174 | }) 175 | end 176 | table.insert(configs, config) 177 | end 178 | return configs 179 | end 180 | 181 | ---@param path string? 182 | ---@return dap.Configuration[] 183 | function M.getconfigs(path) 184 | local resolved_path = path or (vim.fn.getcwd() .. '/.vscode/launch.json') 185 | if not vim.loop.fs_stat(resolved_path) then 186 | return {} 187 | end 188 | local lines = {} 189 | for line in io.lines(resolved_path) do 190 | if not vim.startswith(vim.trim(line), '//') then 191 | table.insert(lines, line) 192 | end 193 | end 194 | local contents = table.concat(lines, '\n') 195 | return M._load_json(contents) 196 | end 197 | 198 | 199 | --- Extends dap.configurations with entries read from .vscode/launch.json 200 | ---@deprecated 201 | function M.load_launchjs(path, type_to_filetypes) 202 | type_to_filetypes = vim.tbl_extend('keep', type_to_filetypes or {}, M.type_to_filetypes) 203 | local configurations = M.getconfigs(path) 204 | 205 | assert(configurations, "launch.json must have a 'configurations' key") 206 | for _, config in ipairs(configurations) do 207 | assert(config.type, "Configuration in launch.json must have a 'type' key") 208 | assert(config.name, "Configuration in launch.json must have a 'name' key") 209 | local filetypes = type_to_filetypes[config.type] or { config.type, } 210 | for _, filetype in pairs(filetypes) do 211 | local dap_configurations = dap.configurations[filetype] or {} 212 | for i, dap_config in pairs(dap_configurations) do 213 | if dap_config.name == config.name then 214 | -- remove old value 215 | table.remove(dap_configurations, i) 216 | end 217 | end 218 | table.insert(dap_configurations, config) 219 | dap.configurations[filetype] = dap_configurations 220 | end 221 | end 222 | end 223 | 224 | return M 225 | -------------------------------------------------------------------------------- /lua/dap/health.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---@param command string? 4 | local function check_executable(command) 5 | local health = vim.health 6 | if not command then 7 | health.error("Missing required `command` property") 8 | else 9 | if vim.fn.executable(command) ~= 1 then 10 | health.error(table.concat({ 11 | "`command` is not executable.", 12 | "Check path and permissions.", 13 | "Use vim.fn.expand to handle ~ or $HOME:\n ", 14 | command 15 | }, " ")) 16 | else 17 | health.ok("is executable: " .. command) 18 | end 19 | end 20 | end 21 | 22 | 23 | function M.check() 24 | local health = vim.health 25 | if not health or not health.start then 26 | return 27 | end 28 | health.start("dap: Adapters") 29 | local dap = require("dap") 30 | for t, adapter in pairs(dap.adapters) do 31 | health.start("dap.adapter: " .. t) 32 | if type(adapter) == "function" then 33 | health.info("Adapter is a function. Can't validate it") 34 | else 35 | if adapter.type == "executable" then 36 | adapter = adapter --[[@as dap.ExecutableAdapter]] 37 | check_executable(adapter.command) 38 | elseif adapter.type == "server" then 39 | adapter = adapter --[[@as dap.ServerAdapter]] 40 | if not adapter.port then 41 | health.error("Missing required `port` property") 42 | end 43 | if adapter.executable then 44 | check_executable(adapter.executable.command) 45 | end 46 | elseif adapter.type == "pipe" then 47 | adapter = adapter --[[@as dap.PipeAdapter]] 48 | if not adapter.pipe then 49 | health.error("Missing required `pipe` property") 50 | end 51 | else 52 | health.error(adapter.type .. " must be one of: executable, server or pipe") 53 | end 54 | end 55 | end 56 | 57 | health.start("dap: Sessions") 58 | local sessions = dap.sessions() 59 | if not next(sessions) then 60 | health.ok("No active sessions") 61 | else 62 | for _, session in pairs(sessions) do 63 | if session.initialized then 64 | health.ok(" id: " .. session.id .. "\n type: " .. session.config.type) 65 | else 66 | health.warn(table.concat({ 67 | "\n id: ", session.id, 68 | "\n type: ", session.config.type, 69 | "\n started, but not initialized. ", 70 | "Either the adapter definition or the used configuration is wrong, ", 71 | "or the defined adapter doesn't speak DAP", 72 | })) 73 | end 74 | end 75 | end 76 | end 77 | 78 | 79 | return M 80 | -------------------------------------------------------------------------------- /lua/dap/log.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---@type table 4 | local loggers = {} 5 | 6 | M._loggers = loggers 7 | 8 | ---@enum dap.log.Level 9 | M.levels = { 10 | TRACE = 0, 11 | DEBUG = 1, 12 | INFO = 2, 13 | WARN = 3, 14 | ERROR = 4, 15 | } 16 | 17 | local log_date_format = "!%F %H:%M:%S" 18 | 19 | 20 | ---@class dap.log.Log 21 | ---@field _fname string 22 | ---@field _path string 23 | ---@field _file file*? 24 | ---@field _level dap.log.Level 25 | 26 | 27 | ---@class dap.log.Log 28 | local Log = {} 29 | local log_mt = { 30 | __index = Log 31 | } 32 | 33 | 34 | function Log:write(...) 35 | self:open() 36 | self._file:write(...) 37 | end 38 | 39 | function Log:open() 40 | if not self._file then 41 | local f = assert(io.open(self._path, "w+")) 42 | self._file = f 43 | end 44 | end 45 | 46 | ---@param level dap.log.Level|string 47 | function Log:set_level(level) 48 | if type(level) == "string" then 49 | self._level = assert( 50 | M.levels[tostring(level):upper()], 51 | string.format('Log level must be one of (trace, debug, info, warn, error), got: %q', level) 52 | ) 53 | else 54 | self._level = level 55 | end 56 | end 57 | 58 | function Log:get_path() 59 | return self._path 60 | end 61 | 62 | 63 | function Log:close() 64 | if self._file then 65 | self._file:flush() 66 | self._file:close() 67 | self._file = nil 68 | end 69 | end 70 | 71 | function Log:remove() 72 | self:close() 73 | os.remove(self._path) 74 | loggers[self._fname] = nil 75 | end 76 | 77 | 78 | ---@param level string 79 | ---@param levelnr integer 80 | ---@return boolean 81 | function Log:_log(level, levelnr, ...) 82 | local argc = select('#', ...) 83 | if levelnr < self._level then 84 | return false 85 | end 86 | if argc == 0 then 87 | return true 88 | end 89 | local info = debug.getinfo(3, 'Sl') 90 | local _, end_ = info.short_src:find("nvim-dap/lua", 1, true) 91 | local src = end_ and info.short_src:sub(end_ + 2) or info.short_src 92 | local fileinfo = string.format('%s:%s', src, info.currentline) 93 | local parts = { 94 | table.concat({'[', level, '] ', os.date(log_date_format), ' ', fileinfo}, '') 95 | } 96 | for i = 1, argc do 97 | local arg = select(i, ...) 98 | if arg == nil then 99 | table.insert(parts, "nil") 100 | else 101 | table.insert(parts, vim.inspect(arg)) 102 | end 103 | end 104 | self:write(table.concat(parts, '\t'), '\n') 105 | return true 106 | end 107 | 108 | 109 | 110 | --- Not generating methods below in a loop to help out luals 111 | 112 | 113 | function Log:trace(...) 114 | self:_log("TRACE", M.levels.TRACE, ...) 115 | end 116 | 117 | function Log:debug(...) 118 | self:_log("DEBUG", M.levels.DEBUG, ...) 119 | end 120 | 121 | function Log:info(...) 122 | self:_log("INFO", M.levels.INFO, ...) 123 | end 124 | 125 | function Log:warn(...) 126 | self:_log("WARN", M.levels.WARN, ...) 127 | end 128 | 129 | function Log:error(...) 130 | self:_log("ERROR", M.levels.ERROR, ...) 131 | end 132 | 133 | 134 | ---@param fname string 135 | ---@return string path 136 | ---@return string cache_dir 137 | local function getpath(fname) 138 | local path_sep = vim.loop.os_uname().sysname == "Windows" and "\\" or "/" 139 | local joinpath = (vim.fs or {}).joinpath or function(...) 140 | ---@diagnostic disable-next-line: deprecated 141 | return table.concat(vim.tbl_flatten{...}, path_sep) 142 | end 143 | local cache_dir = vim.fn.stdpath('cache') 144 | assert(type(cache_dir) == "string") 145 | return joinpath(cache_dir, fname), cache_dir 146 | end 147 | 148 | 149 | ---@return dap.log.Log 150 | function M.create_logger(filename) 151 | local logger = loggers[filename] 152 | if logger then 153 | logger:open() 154 | return logger 155 | end 156 | local path, cache_dir = getpath(filename) 157 | local log = { 158 | _fname = filename, 159 | _path = path, 160 | _level = M.levels.INFO 161 | } 162 | logger = setmetatable(log, log_mt) 163 | loggers[filename] = logger 164 | 165 | vim.fn.mkdir(cache_dir, "p") 166 | logger:open() 167 | return logger 168 | end 169 | 170 | 171 | return M 172 | -------------------------------------------------------------------------------- /lua/dap/progress.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local messages = {} 3 | 4 | local max_size = 11 5 | local idx_read = 0 6 | local idx_write = 0 7 | 8 | local last_msg = nil 9 | 10 | 11 | function M.reset() 12 | messages = {} 13 | idx_read = 0 14 | idx_write = 0 15 | last_msg = nil 16 | end 17 | 18 | 19 | ---@param msg string 20 | function M.report(msg) 21 | messages[idx_write] = msg 22 | idx_write = (idx_write + 1) % max_size 23 | if idx_write == idx_read then 24 | idx_read = (idx_read + 1) % max_size 25 | end 26 | 27 | if vim.in_fast_event() then 28 | vim.schedule(function() 29 | vim.cmd('doautocmd User DapProgressUpdate') 30 | end) 31 | else 32 | vim.cmd('doautocmd User DapProgressUpdate') 33 | end 34 | end 35 | 36 | 37 | ---@return string? 38 | function M.poll_msg() 39 | if idx_read == idx_write then 40 | return nil 41 | end 42 | local msg = messages[idx_read] 43 | messages[idx_read] = nil 44 | idx_read = (idx_read + 1) % max_size 45 | return msg 46 | end 47 | 48 | 49 | ---@return string 50 | function M.status() 51 | local msg = M.poll_msg() or last_msg 52 | if msg then 53 | last_msg = msg 54 | return msg 55 | else 56 | return '' 57 | end 58 | end 59 | 60 | 61 | return M 62 | -------------------------------------------------------------------------------- /lua/dap/protocol.lua: -------------------------------------------------------------------------------- 1 | ---@meta 2 | 3 | ---@class dap.ProtocolMessage 4 | ---@field seq number 5 | ---@field type "request"|"response"|"event"|string 6 | 7 | ---@class dap.Request: dap.ProtocolMessage 8 | ---@field type "request" 9 | ---@field command string 10 | ---@field arguments? any 11 | 12 | 13 | ---@class dap.Event: dap.ProtocolMessage 14 | ---@field type "event" 15 | ---@field event string 16 | ---@field body? any 17 | 18 | 19 | ---@class dap.Response: dap.ProtocolMessage 20 | ---@field type "response" 21 | ---@field request_seq number 22 | ---@field success boolean 23 | ---@field command string 24 | ---@field message? "cancelled"|"notStopped"|string 25 | ---@field body? any 26 | 27 | 28 | ---@class dap.ErrorResponse: dap.Response 29 | ---@field message? "cancelled"|"notStopped"|string 30 | ---@field body {error?: dap.Message} 31 | 32 | 33 | ---@class dap.Message 34 | ---@field id number 35 | ---@field format string 36 | ---@field variables nil|table 37 | ---@field showUser nil|boolean 38 | 39 | 40 | ---@class dap.Thread 41 | ---@field id number 42 | ---@field name string 43 | ---@field frames nil|dap.StackFrame[] not part of the spec; added by nvim-dap 44 | ---@field stopped nil|boolean not part of the spec; added by nvim-dap 45 | 46 | 47 | ---@class dap.ThreadResponse 48 | ---@field threads dap.Thread[] 49 | 50 | ---@class dap.StackFrame 51 | ---@field id number 52 | ---@field name string 53 | ---@field source dap.Source|nil 54 | ---@field line number 55 | ---@field column number 56 | ---@field endLine nil|number 57 | ---@field endColumn nil|number 58 | ---@field canRestart boolean|nil 59 | ---@field presentationHint nil|"normal"|"label"|"subtle"; 60 | ---@field scopes? dap.Scope[] Not part of spec; added by nvim-dap 61 | 62 | 63 | ---@class dap.StackFrameFormat : dap.ValueFormat 64 | --- Displays parameters for the stack frame. 65 | --- @field parameters? boolean 66 | --- 67 | --- Displays the types of parameters for the stack frame. 68 | --- @field parameterTypes? boolean 69 | --- 70 | --- Displays the names of parameters for the stack frame. 71 | --- @field parameterNames? boolean 72 | --- 73 | --- Displays the values of parameters for the stack frame. 74 | --- @field parameterValues? boolean 75 | --- 76 | --- Displays the line number of the stack frame. 77 | --- @field line? boolean 78 | --- 79 | --- Displays the module of the stack frame. 80 | --- @field module? boolean 81 | --- 82 | --- Includes all stack frames, including those the debug adapter might 83 | --- otherwise hide. 84 | --- @field includeAll? boolean 85 | 86 | 87 | ---@class dap.StackTraceArguments 88 | ---@field threadId number thread for which to retrieve the stackTrace 89 | ---@field startFrame? number index of the first frame to return. If omitted frames start at 0 90 | ---@field levels? number maximum number of frames to return. If absent or 0 all frames are returned 91 | ---@field format? dap.StackFrameFormat only honored with supportsValueFormattingOptions capability 92 | 93 | ---@class dap.StackTraceResponse 94 | ---@field stackFrames dap.StackFrame[] 95 | ---@field totalFrames? number 96 | 97 | 98 | ---@class dap.Scope 99 | ---@field name string 100 | ---@field presentationHint? "arguments"|"locals"|"registers"|string 101 | ---@field variablesReference number 102 | ---@field namedVariables? number 103 | ---@field indexedVariables? number 104 | ---@field expensive boolean 105 | ---@field source? dap.Source 106 | ---@field line? number 107 | ---@field column? number 108 | ---@field endLine? number 109 | ---@field endColumn? number 110 | ---@field variables? table by variable name. Not part of spec 111 | 112 | 113 | ---@class dap.ScopesResponse 114 | ---@field scopes dap.Scope[] 115 | 116 | 117 | ---@class dap.ValueFormat 118 | ---@field hex? boolean Display the value in hex 119 | 120 | ---@class dap.VariablesArguments 121 | ---@field variablesReference number variable for which to retrieve its children 122 | ---@field filter? "indexed"|"named" filter to limit child variables. Both are fetched if nil 123 | ---@field start? number index of the first variable to return. If nil children start at 0. Requires `supportsVariablePaging` 124 | ---@field count? number number of variables to return. If missing or 0, all variables are returned. Requires `supportsVariablePaging` 125 | ---@field format? dap.ValueFormat 126 | 127 | ---@class dap.VariableResponse 128 | ---@field variables dap.Variable[] 129 | 130 | ---@class dap.Variable 131 | ---@field name string 132 | ---@field value string 133 | ---@field type? string 134 | ---@field presentationHint? dap.VariablePresentationHint 135 | ---@field evaluateName? string 136 | ---@field variablesReference number if > 0 the variable is structured 137 | ---@field namedVariables? number 138 | ---@field indexedVariables? number 139 | ---@field memoryReference? string 140 | ---@field declarationLocationReference? number 141 | ---@field valueLocationReference? number 142 | ---@field variables? dap.Variable[] resolved variablesReference. Not part of the spec; added by nvim-dap 143 | ---@field parent? dap.Variable|dap.Scope injected by nvim-dap 144 | 145 | ---@class dap.EvaluateArguments 146 | ---@field expression string 147 | ---@field frameId? number 148 | ---@field context? "watch"|"repl"|"hover"|"clipboard"|"variables"|string 149 | ---@field format? dap.ValueFormat 150 | 151 | ---@class dap.EvaluateResponse 152 | ---@field result string 153 | ---@field type? string 154 | ---@field presentationHint? dap.VariablePresentationHint 155 | ---@field variablesReference number 156 | ---@field namedVariables? number 157 | ---@field indexedVariables? number 158 | ---@field memoryReference? string 159 | ---@field valueLocationReference? number 160 | 161 | 162 | ---@class dap.VariablePresentationHint 163 | ---@field kind? 164 | ---|'property' 165 | ---|'method' 166 | ---|'class' 167 | ---|'data' 168 | ---|'event' 169 | ---|'baseClass' 170 | ---|'innerClass' 171 | ---|'interface' 172 | ---|'mostDerivedClass' 173 | ---|'virtual' 174 | ---|'dataBreakpoint' 175 | ---|string; 176 | ---@field attributes? ('static'|'constant'|'readOnly'|'rawString'|'hasObjectId'|'canHaveObjectId'|'hasSideEffects'|'hasDataBreakpoint'|string)[] 177 | ---@field visibility? 178 | ---|'public' 179 | ---|'private' 180 | ---|'protected' 181 | ---|'internal' 182 | ---|'final' 183 | ---|string 184 | ---@field lazy? boolean 185 | 186 | 187 | ---@class dap.Source 188 | ---@field name nil|string 189 | ---@field path nil|string 190 | ---@field sourceReference nil|number 191 | ---@field presentationHint nil|"normal"|"emphasize"|"deemphasize" 192 | ---@field origin nil|string 193 | ---@field sources nil|dap.Source[] 194 | ---@field adapterData nil|any 195 | 196 | 197 | ---@class dap.SourceResponse 198 | ---@field content string 199 | ---@field mimeType? string 200 | 201 | 202 | ---@class dap.Capabilities 203 | ---@field supportsConfigurationDoneRequest boolean|nil 204 | ---@field supportsFunctionBreakpoints boolean|nil 205 | ---@field supportsConditionalBreakpoints boolean|nil 206 | ---@field supportsHitConditionalBreakpoints boolean|nil 207 | ---@field supportsEvaluateForHovers boolean|nil 208 | ---@field exceptionBreakpointFilters dap.ExceptionBreakpointsFilter[]|nil 209 | ---@field supportsStepBack boolean|nil 210 | ---@field supportsSetVariable boolean|nil 211 | ---@field supportsRestartFrame boolean|nil 212 | ---@field supportsGotoTargetsRequest boolean|nil 213 | ---@field supportsStepInTargetsRequest boolean|nil 214 | ---@field supportsCompletionsRequest boolean|nil 215 | ---@field completionTriggerCharacters string[]|nil 216 | ---@field supportsModulesRequest boolean|nil 217 | ---@field additionalModuleColumns dap.ColumnDescriptor[]|nil 218 | ---@field supportedChecksumAlgorithms dap.ChecksumAlgorithm[]|nil 219 | ---@field supportsRestartRequest boolean|nil 220 | ---@field supportsExceptionOptions boolean|nil 221 | ---@field supportsValueFormattingOptions boolean|nil 222 | ---@field supportsExceptionInfoRequest boolean|nil 223 | ---@field supportTerminateDebuggee boolean|nil 224 | ---@field supportSuspendDebuggee boolean|nil 225 | ---@field supportsDelayedStackTraceLoading boolean|nil 226 | ---@field supportsLoadedSourcesRequest boolean|nil 227 | ---@field supportsLogPoints boolean|nil 228 | ---@field supportsTerminateThreadsRequest boolean|nil 229 | ---@field supportsSetExpression boolean|nil 230 | ---@field supportsTerminateRequest boolean|nil 231 | ---@field supportsDataBreakpoints boolean|nil 232 | ---@field supportsReadMemoryRequest boolean|nil 233 | ---@field supportsWriteMemoryRequest boolean|nil 234 | ---@field supportsDisassembleRequest boolean|nil 235 | ---@field supportsCancelRequest boolean|nil 236 | ---@field supportsBreakpointLocationsRequest boolean|nil 237 | ---@field supportsClipboardContext boolean|nil 238 | ---@field supportsSteppingGranularity boolean|nil 239 | ---@field supportsInstructionBreakpoints boolean|nil 240 | ---@field supportsExceptionFilterOptions boolean|nil 241 | ---@field supportsSingleThreadExecutionRequests boolean|nil 242 | 243 | ---@class dap.InitializeRequestArguments 244 | ---@field clientId? string 245 | ---@field clientName? string 246 | ---@field adapterId string 247 | ---@field locale string 248 | ---@field linesStartAt1? boolean 249 | ---@field columnsStartAt1? boolean 250 | ---@field pathFormat? "path" | "uri" | string 251 | ---@field supportsVariableType? boolean 252 | ---@field supportsVariablePaging? boolean 253 | ---@field supportsRunInTerminalRequest? boolean 254 | ---@field supportsMemoryReferences? boolean 255 | ---@field supportsProgressReporting? boolean 256 | ---@field supportsInvalidatedEvent? boolean 257 | ---@field supportsMemoryEvent? boolean 258 | ---@field supportsArgsCanBeInterpretedByShell? boolean 259 | ---@field supportsStartDebuggingRequest? boolean 260 | ---@field supportsANSIStyling? boolean 261 | 262 | ---@class dap.ExceptionBreakpointsFilter 263 | ---@field filter string 264 | ---@field label string 265 | ---@field description string|nil 266 | ---@field default boolean|nil 267 | ---@field supportsCondition boolean|nil 268 | ---@field conditionDescription string|nil 269 | 270 | ---@class dap.ColumnDescriptor 271 | ---@field attributeName string 272 | ---@field label string 273 | ---@field format string|nil 274 | ---@field type nil|"string"|"number"|"number"|"unixTimestampUTC" 275 | ---@field width number|nil 276 | 277 | 278 | ---@class dap.ChecksumAlgorithm 279 | ---@field algorithm "MD5"|"SHA1"|"SHA256"|"timestamp" 280 | ---@field checksum string 281 | 282 | ---@class dap.SetBreakpointsResponse 283 | ---@field breakpoints dap.Breakpoint[] 284 | 285 | 286 | ---@class dap.SetBreakpointsArguments 287 | --- 288 | --- location of the breakpoint. 289 | --- Either source.path or source.sourceReference must be specified. 290 | ---@field source dap.Source 291 | ---@field breakpoints? dap.SourceBreakpoint[] 292 | ---@field sourceModified? boolean 293 | 294 | 295 | ---@class dap.SourceBreakpoint 296 | ---@field line integer 297 | ---@field column? integer 298 | ---@field condition? string 299 | ---@field hitCondition? string 300 | ---@field logMessage? string 301 | ---@field mode? string 302 | 303 | 304 | ---@class dap.Breakpoint 305 | ---@field id? number 306 | ---@field verified boolean 307 | ---@field message? string 308 | ---@field source? dap.Source 309 | ---@field line? number 310 | ---@field column? number 311 | ---@field endLine? number 312 | ---@field endColumn? number 313 | ---@field instructionReference? string 314 | ---@field offset? number 315 | 316 | ---@class dap.InitializedEvent 317 | 318 | ---@class dap.StoppedEvent 319 | ---@field reason "step"|"breakpoint"|"exception"|"pause"|"entry"|"goto"|"function breakpoint"|"data breakpoint"|"instruction breakpoint"|string; 320 | ---@field description nil|string 321 | ---@field threadId nil|number 322 | ---@field preserveFocusHint nil|boolean 323 | ---@field text nil|string 324 | ---@field allThreadsStopped nil|boolean 325 | ---@field hitBreakpointIds nil|number[] 326 | 327 | ---@class dap.TerminatedEvent 328 | ---@field restart? any 329 | 330 | ---@class dap.TerminateArguments 331 | ---@field restart? boolean 332 | 333 | ---@class dap.DisconnectArguments 334 | ---@field restart? boolean 335 | ---@field terminateDebuggee? boolean requires `supportTerminateDebuggee` capability 336 | ---@field suspendDebuggee? boolean requires `supportSuspendDebuggee` capability 337 | 338 | 339 | ---@class dap.ThreadEvent 340 | ---@field reason "started"|"exited"|string 341 | ---@field threadId number 342 | 343 | 344 | ---@class dap.ContinuedEvent 345 | ---@field threadId number 346 | ---@field allThreadsContinued? boolean 347 | 348 | 349 | ---@class dap.BreakpointEvent 350 | ---@field reason "changed"|"new"|"removed"|string 351 | ---@field breakpoint dap.Breakpoint 352 | 353 | 354 | ---@class dap.ProgressStartEvent 355 | ---@field progressId string 356 | ---@field title string 357 | ---@field requestId? number 358 | ---@field cancellable? boolean 359 | ---@field message? string 360 | ---@field percentage? number 361 | 362 | ---@class dap.ProgressUpdateEvent 363 | ---@field progressId string 364 | ---@field message? string 365 | ---@field percentage? number 366 | 367 | ---@class dap.ProgressEndEvent 368 | ---@field progressId string 369 | ---@field message? string 370 | 371 | 372 | ---@class dap.OutputEvent 373 | ---@field category? "console"|"important"|"stdout"|"stderr"|"telemetry"|string 374 | ---@field output string 375 | ---@field group? "start"|"startCollapsed"|"end" 376 | --- 377 | --- if > 0 the output contains objects which 378 | --- can be retrieved by passing `variablesReference` to the `variables` request 379 | --- as long as execution remains suspended. 380 | ---@field variablesReference? number 381 | ---@field source? dap.Source 382 | ---@field line? integer 383 | ---@field column? integer 384 | ---@field data? any 385 | 386 | 387 | ---@class dap.StartDebuggingRequestArguments 388 | ---@field configuration table 389 | ---@field request 'launch'|'attach' 390 | 391 | 392 | ---@class dap.CompletionsResponse 393 | ---@field targets dap.CompletionItem[] 394 | 395 | 396 | ---@class dap.LocationsArguments 397 | ---@field locationReference number 398 | 399 | 400 | ---@class dap.LocationsResponse 401 | ---@field source dap.Source 402 | ---@field line integer 403 | ---@field column? integer 404 | ---@field endLine? integer 405 | ---@field endColumn? integer 406 | 407 | 408 | ---@alias dap.CompletionItemType 409 | ---|'method' 410 | ---|'function' 411 | ---|'constructor' 412 | ---|'field' 413 | ---|'variable' 414 | ---|'class' 415 | ---|'interface' 416 | ---|'module' 417 | ---|'property' 418 | ---|'unit' 419 | ---|'value' 420 | ---|'enum' 421 | ---|'keyword' 422 | ---|'snippet' 423 | ---|'text' 424 | ---|'color' 425 | ---|'file' 426 | ---|'reference' 427 | ---|'customcolor' 428 | 429 | ---@class dap.CompletionsArguments 430 | ---@field frameId? number 431 | ---@field text string 432 | ---@field column integer utf-16 code units, 0- or 1-based depending on columnsStartAt1 433 | ---@field line? integer 434 | 435 | ---@class dap.CompletionItem 436 | ---@field label string By default this is also the text that is inserted when selecting this completion 437 | ---@field text? string If present and not empty this is inserted instead of the label 438 | ---@field sortText? string Used to sort completion items if present and not empty. Otherwise label is used 439 | ---@field detail? string human-readable string with additional information about this item. Like type or symbol information 440 | ---@field type? dap.CompletionItemType 441 | ---@field start? number Start position in UTF-16 code units. (within the `text` attribute of the `completions` request) 0- or 1-based depending on `columnsStartAt1` capability. If omitted, the text is added at the location of the `column` attribute of the `completions` request. 442 | ---@field length? number How many characters are overwritten by the completion text. Measured in UTF-16 code units. If missing the value 0 is assumed which results in the completion text being inserted. 443 | ---@field selectionStart? number 444 | ---@field selectionLength? number 445 | 446 | 447 | ---@alias dap.SteppingGranularity 'statement'|'line'|'instruction' 448 | -------------------------------------------------------------------------------- /lua/dap/repl.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | local ui = require('dap.ui') 3 | local utils = require('dap.utils') 4 | local prompt = "dap> " 5 | local M = {} 6 | 7 | local history = { 8 | last = nil, 9 | entries = {}, 10 | idx = 1, 11 | max_size = 100, 12 | } 13 | 14 | local autoscroll = vim.fn.has('nvim-0.7') == 1 15 | 16 | local function get_session() 17 | return require('dap').session() 18 | end 19 | 20 | local execute -- required for forward reference 21 | 22 | 23 | local function new_buf() 24 | local prev_buf = api.nvim_get_current_buf() 25 | local buf = api.nvim_create_buf(true, true) 26 | api.nvim_buf_set_name(buf, string.format('[dap-repl-%d]', buf)) 27 | vim.b[buf]["dap-srcft"] = vim.bo[prev_buf].filetype 28 | vim.bo[buf].buftype = "prompt" 29 | vim.bo[buf].omnifunc = "v:lua.require'dap.repl'.omnifunc" 30 | vim.bo[buf].buflisted = false 31 | vim.bo[buf].tagfunc = "v:lua.require'dap'._tagfunc" 32 | local path = vim.bo[prev_buf].path 33 | if path and path ~= "" then 34 | vim.bo[buf].path = path 35 | end 36 | api.nvim_buf_set_keymap(buf, 'n', '', "lua require('dap.ui').trigger_actions({ mode = 'first' })", {}) 37 | api.nvim_buf_set_keymap(buf, 'n', 'o', "lua require('dap.ui').trigger_actions()", {}) 38 | api.nvim_buf_set_keymap(buf, 'i', '', "lua require('dap.repl').on_up()", {}) 39 | api.nvim_buf_set_keymap(buf, 'i', '', "lua require('dap.repl').on_down()", {}) 40 | vim.keymap.set("n", "]]", function() 41 | local lnum = api.nvim_win_get_cursor(0)[1] - 1 42 | local lines = api.nvim_buf_get_lines(buf, lnum + 1, -1, false) 43 | for i, line in ipairs(lines) do 44 | if vim.startswith(line, prompt) then 45 | api.nvim_win_set_cursor(0, { i + lnum + 1, #line - 1 }) 46 | break 47 | end 48 | end 49 | end, { buffer = buf, desc = "Move to next prompt" }) 50 | vim.keymap.set("n", "[[", function() 51 | local lnum = api.nvim_win_get_cursor(0)[1] - 1 52 | local lines = api.nvim_buf_get_lines(buf, 0, lnum, true) 53 | local num_lines = #lines 54 | for i = num_lines, 1, -1 do 55 | local line = lines[i] 56 | if vim.startswith(line, prompt) then 57 | api.nvim_win_set_cursor(0, { lnum - (num_lines - i), #line - 1 }) 58 | break 59 | end 60 | end 61 | end, { buffer = buf, desc = "Move to previous prompt" }) 62 | api.nvim_create_autocmd("TextYankPost", { 63 | buffer = buf, 64 | callback = function() 65 | require("dap._cmds").yank_evalname() 66 | end, 67 | }) 68 | vim.fn.prompt_setprompt(buf, prompt) 69 | vim.fn.prompt_setcallback(buf, execute) 70 | if vim.fn.has('nvim-0.7') == 1 then 71 | vim.keymap.set('n', 'G', function() 72 | autoscroll = vim.v.count == 0 73 | vim.cmd(string.format('normal! %dG', vim.v.count)) 74 | end, { silent = true, buffer = buf }) 75 | api.nvim_create_autocmd({'InsertEnter', 'CursorMoved'}, { 76 | group = api.nvim_create_augroup('dap-repl-au', {clear = true}), 77 | buffer = buf, 78 | callback = function() 79 | local active_buf = api.nvim_win_get_buf(0) 80 | if active_buf == buf then 81 | local lnum = api.nvim_win_get_cursor(0)[1] 82 | autoscroll = lnum == api.nvim_buf_line_count(buf) 83 | end 84 | end 85 | }) 86 | end 87 | vim.bo[buf].filetype = "dap-repl" 88 | return buf 89 | end 90 | 91 | 92 | local function new_win(buf, winopts, wincmd) 93 | assert(not wincmd or type(wincmd) == 'string', 'wincmd must be nil or a string') 94 | api.nvim_command(wincmd or 'belowright split') 95 | local win = api.nvim_get_current_win() 96 | api.nvim_win_set_buf(win, buf) 97 | if vim.fn.has("nvim-0.11") == 1 then 98 | vim.wo[win][0].relativenumber = false 99 | vim.wo[win][0].number = false 100 | vim.wo[win][0].foldcolumn = "0" 101 | vim.wo[win][0].signcolumn = "auto" 102 | vim.wo[win][0].wrap = false 103 | else 104 | vim.wo[win].wrap = false 105 | end 106 | ui.apply_winopts(win, winopts) 107 | return win 108 | end 109 | 110 | local repl = ui.new_view( 111 | new_buf, 112 | new_win, { 113 | before_open = function() 114 | return api.nvim_get_current_win() 115 | end, 116 | after_open = function(_, prev_win) 117 | api.nvim_set_current_win(prev_win) 118 | end 119 | } 120 | ) 121 | 122 | 123 | M.commands = { 124 | continue = {'.continue', '.c'}, 125 | next_ = {'.next', '.n'}, 126 | step_back = {'.back', '.b'}, 127 | reverse_continue = {'.reverse-continue', '.rc'}, 128 | into = {'.into'}, 129 | into_targets = {'.into-targets'}, 130 | out = {'.out'}, 131 | scopes = {'.scopes'}, 132 | threads = {'.threads'}, 133 | frames = {'.frames'}, 134 | exit = {'exit', '.exit'}, 135 | up = {'.up'}, 136 | down = {'.down'}, 137 | goto_ = {'.goto'}, 138 | pause = {'.pause', '.p'}, 139 | clear = {'.clear'}, 140 | capabilities = {'.capabilities'}, 141 | help = {'help', '.help', '.h'}, 142 | custom_commands = {} 143 | } 144 | 145 | 146 | function M.print_stackframes(frames) 147 | if not repl.buf then 148 | return 149 | end 150 | local session = get_session() 151 | frames = frames or (session and session.threads[session.stopped_thread_id] or {}).frames or {} 152 | local context = {} 153 | M.append('(press enter on line to jump to frame)') 154 | local start = api.nvim_buf_line_count(repl.buf) - 1 155 | local render_frame = require('dap.entity').frames.render_item 156 | context.actions = { 157 | { 158 | label = 'Jump to frame', 159 | fn = function(layer, frame) 160 | local s = get_session() 161 | if s then 162 | s:_frame_set(frame) 163 | layer.render(frames, render_frame, context, start, start + #frames) 164 | else 165 | utils.notify('Cannot navigate to frame without active session', vim.log.levels.INFO) 166 | end 167 | end, 168 | }, 169 | } 170 | local layer = ui.layer(repl.buf) 171 | layer.render(frames, render_frame, context) 172 | end 173 | 174 | 175 | local function print_commands() 176 | M.append('Commands:') 177 | for _, commands in pairs(M.commands) do 178 | if #commands > 0 then 179 | M.append(' ' .. table.concat(commands, ', ')) 180 | end 181 | end 182 | 183 | M.append('Custom commands:') 184 | for command, _ in pairs(M.commands.custom_commands or {}) do 185 | M.append(' ' .. command) 186 | end 187 | end 188 | 189 | 190 | local function evaluate_handler(err, resp) 191 | if err then 192 | M.append(tostring(err), nil, { newline = false }) 193 | return 194 | end 195 | local layer = ui.layer(repl.buf) 196 | local attributes = (resp.presentationHint or {}).attributes or {} 197 | if resp.variablesReference > 0 or vim.tbl_contains(attributes, 'rawString') then 198 | local spec = require('dap.entity').variable.tree_spec 199 | local tree = ui.new_tree(spec) 200 | -- tree.render would "append" twice, once for the top element and once for the children 201 | -- Appending twice would result in a intermediate "dap> " prompt 202 | -- To avoid that this eagerly fetches the children; pre-renders the region 203 | -- and lets tree.render override it 204 | local lnum = api.nvim_buf_line_count(repl.buf) - 1 205 | if spec.has_children(resp) then 206 | spec.fetch_children(resp, function() 207 | tree.render(layer, resp, nil, lnum, -1) 208 | end) 209 | else 210 | tree.render(layer, resp, nil, lnum, -1) 211 | end 212 | else 213 | M.append(resp.result, nil, { newline = false }) 214 | end 215 | end 216 | 217 | 218 | local function print_scopes(frame) 219 | if not frame then return end 220 | local tree = ui.new_tree(require('dap.entity').scope.tree_spec) 221 | local layer = ui.layer(repl.buf) 222 | for _, scope in pairs(frame.scopes or {}) do 223 | tree.render(layer, scope) 224 | end 225 | end 226 | 227 | 228 | local function print_threads(threads) 229 | if not threads then 230 | return 231 | end 232 | local spec = vim.deepcopy(require('dap.entity').threads.tree_spec) 233 | spec.extra_context = { 234 | refresh = function() 235 | local session = get_session() 236 | if session then 237 | print_threads(vim.tbl_values(session.threads)) 238 | end 239 | end 240 | } 241 | local tree = ui.new_tree(spec) 242 | local layer = ui.layer(repl.buf) 243 | local root = { 244 | id = 0, 245 | name = 'Threads', 246 | threads = vim.tbl_values(threads) 247 | } 248 | tree.render(layer, root) 249 | end 250 | 251 | 252 | ---@param confname string 253 | ---@return dap.Session? 254 | local function trystart(confname) 255 | assert(coroutine.running() ~= nil, "Must run in coroutine") 256 | local dap = require("dap") 257 | local bufnr = api.nvim_get_current_buf() 258 | for _, get_configs in pairs(dap.providers.configs) do 259 | local configs = get_configs(bufnr) 260 | for _, config in ipairs(configs) do 261 | if confname == config.name then 262 | dap.run(config) 263 | end 264 | end 265 | end 266 | return dap.session() 267 | end 268 | 269 | 270 | ---@param text string 271 | ---@param opts? dap.repl.execute.Opts 272 | local function coexecute(text, opts) 273 | assert(coroutine.running() ~= nil, "Must run in coroutine") 274 | opts = opts or {} 275 | 276 | local session = get_session() 277 | if not session then 278 | local ft = vim.b["dap-srcft"] or vim.bo.filetype 279 | local autostart = require("dap").defaults[ft].autostart 280 | if autostart then 281 | session = trystart(autostart) 282 | end 283 | if not session then 284 | M.append('No active debug session') 285 | return 286 | end 287 | end 288 | local words = vim.split(text, ' ', { plain = true }) 289 | if vim.tbl_contains(M.commands.continue, text) then 290 | require('dap').continue() 291 | elseif vim.tbl_contains(M.commands.next_, text) then 292 | require('dap').step_over() 293 | elseif vim.tbl_contains(M.commands.capabilities, text) then 294 | M.append(vim.inspect(session.capabilities)) 295 | elseif vim.tbl_contains(M.commands.into, text) then 296 | require('dap').step_into() 297 | elseif vim.tbl_contains(M.commands.into_targets, text) then 298 | require('dap').step_into({askForTargets=true}) 299 | elseif vim.tbl_contains(M.commands.out, text) then 300 | require('dap').step_out() 301 | elseif vim.tbl_contains(M.commands.up, text) then 302 | session:_frame_delta(1) 303 | M.print_stackframes() 304 | elseif vim.tbl_contains(M.commands.step_back, text) then 305 | require('dap').step_back() 306 | elseif vim.tbl_contains(M.commands.pause, text) then 307 | session:_pause() 308 | elseif vim.tbl_contains(M.commands.reverse_continue, text) then 309 | require('dap').reverse_continue() 310 | elseif vim.tbl_contains(M.commands.down, text) then 311 | session:_frame_delta(-1) 312 | M.print_stackframes() 313 | elseif vim.tbl_contains(M.commands.goto_, words[1]) then 314 | if words[2] then 315 | session:_goto(tonumber(words[2])) 316 | end 317 | elseif vim.tbl_contains(M.commands.scopes, text) then 318 | print_scopes(session.current_frame) 319 | elseif vim.tbl_contains(M.commands.threads, text) then 320 | print_threads(vim.tbl_values(session.threads)) 321 | elseif vim.tbl_contains(M.commands.frames, text) then 322 | M.print_stackframes() 323 | elseif M.commands.custom_commands[words[1]] then 324 | local command = words[1] 325 | local args = string.sub(text, string.len(command)+2) 326 | M.commands.custom_commands[command](args) 327 | else 328 | ---@type dap.EvaluateArguments 329 | local params = { 330 | expression = text, 331 | context = opts.context or "repl" 332 | } 333 | session:evaluate(params, evaluate_handler) 334 | end 335 | end 336 | 337 | 338 | ---@class dap.repl.execute.Opts 339 | ---@field context? "watch"|"repl"|"hover"|"variables"|"clipboard" 340 | 341 | 342 | ---@param text string 343 | ---@param opts? dap.repl.execute.Opts 344 | function execute(text, opts) 345 | if text == '' then 346 | if history.last then 347 | text = history.last 348 | else 349 | return 350 | end 351 | else 352 | history.last = text 353 | if #history.entries == history.max_size then 354 | table.remove(history.entries, 1) 355 | end 356 | table.insert(history.entries, text) 357 | history.idx = #history.entries + 1 358 | end 359 | if vim.tbl_contains(M.commands.exit, text) then 360 | local session = get_session() 361 | if session then 362 | -- Should result in a `terminated` event which closes the session and sets it to nil 363 | session:disconnect() 364 | end 365 | api.nvim_command('close') 366 | return 367 | end 368 | if vim.tbl_contains(M.commands.help, text) then 369 | print_commands() 370 | elseif vim.tbl_contains(M.commands.clear, text) then 371 | if repl.buf and api.nvim_buf_is_loaded(repl.buf) then 372 | M.clear() 373 | end 374 | else 375 | require("dap.async").run(function() 376 | coexecute(text, opts) 377 | end) 378 | end 379 | end 380 | 381 | 382 | --- Add and execute text as if entered directly 383 | ---@param text string 384 | ---@param opts? dap.repl.execute.Opts 385 | function M.execute(text, opts) 386 | M.append(prompt .. text .. "\n", "$", { newline = true }) 387 | local numlines = api.nvim_buf_line_count(repl.buf) 388 | if repl.win and api.nvim_win_is_valid(repl.win) then 389 | pcall(api.nvim_win_set_cursor, repl.win, { numlines, 0 }) 390 | api.nvim_win_call(repl.win, function() 391 | vim.cmd.normal({"zt", bang = true }) 392 | end) 393 | end 394 | execute(text, opts) 395 | end 396 | 397 | 398 | --- Close the REPL if it is open. 399 | -- 400 | -- Does not disconnect an active session. 401 | -- 402 | -- Returns true if the REPL was open and got closed. false otherwise 403 | M.close = repl.close 404 | 405 | --- Open the REPL 406 | -- 407 | --@param winopts optional table which may include: 408 | -- `height` to set the window height 409 | -- `width` to set the window width 410 | -- 411 | -- Any other key/value pair, that will be treated as window 412 | -- option. 413 | -- 414 | --@param wincmd command that is used to create the window for the REPL. 415 | -- Defaults to 'belowright split' 416 | M.open = repl.open 417 | 418 | --- Open the REPL if it is closed, close it if it is open. 419 | M.toggle = repl.toggle 420 | 421 | 422 | local function select_history(delta) 423 | if not repl.buf then 424 | return 425 | end 426 | history.idx = history.idx + delta 427 | if history.idx < 1 then 428 | history.idx = #history.entries 429 | elseif history.idx > #history.entries then 430 | history.idx = 1 431 | end 432 | local text = history.entries[history.idx] 433 | if text then 434 | local lnum = vim.fn.line('$') 435 | local lines = vim.split(text, "\n", { plain = true }) 436 | lines[1] = prompt .. lines[1] 437 | api.nvim_buf_set_lines(repl.buf, lnum - 1, -1, true, lines) 438 | vim.fn.setcursorcharpos({ vim.fn.line("$"), vim.fn.col('$') }) -- move cursor to the end of line 439 | end 440 | end 441 | 442 | 443 | function M.on_up() 444 | select_history(-1) 445 | end 446 | 447 | function M.on_down() 448 | select_history(1) 449 | end 450 | 451 | 452 | ---@param line string 453 | ---@param lnum (integer|string)? 454 | ---@param opts? {newline: boolean} 455 | function M.append(line, lnum, opts) 456 | opts = opts or {} 457 | local buf = repl._init_buf() 458 | if api.nvim_get_current_win() == repl.win and lnum == '$' then 459 | lnum = nil 460 | end 461 | if vim.bo[buf].fileformat ~= "dos" then 462 | line = line:gsub('\r\n', '\n') 463 | end 464 | local lines = vim.split(line, '\n') 465 | if lnum == '$' or not lnum then 466 | lnum = api.nvim_buf_line_count(buf) - 1 467 | if opts.newline == false then 468 | local last_line = api.nvim_buf_get_lines(buf, -2, -1, true)[1] 469 | local insert_pos = #last_line 470 | if last_line == prompt then 471 | -- insert right in front of the empty prompt 472 | insert_pos = 0 473 | if lines[#lines] ~= '' then 474 | table.insert(lines, #lines + 1, '') 475 | end 476 | elseif vim.startswith(last_line, prompt) then 477 | table.insert(lines, 1, '') 478 | end 479 | api.nvim_buf_set_text(buf, lnum, insert_pos, lnum, insert_pos, lines) 480 | else 481 | api.nvim_buf_set_lines(buf, -1, -1, true, lines) 482 | end 483 | else 484 | api.nvim_buf_set_lines(buf, lnum, lnum, true, lines) 485 | end 486 | if autoscroll and repl.win and api.nvim_win_is_valid(repl.win) then 487 | pcall(api.nvim_win_set_cursor, repl.win, { lnum + 2, 0 }) 488 | end 489 | return lnum 490 | end 491 | 492 | 493 | function M.clear() 494 | if repl.buf and api.nvim_buf_is_loaded(repl.buf) then 495 | local layer = ui.layer(repl.buf) 496 | layer.render({}, tostring, {}, 0, - 1) 497 | end 498 | end 499 | 500 | do 501 | 502 | ---@param candidates dap.CompletionItem[] 503 | local function completions_to_items(candidates) 504 | table.sort(candidates, function(a, b) 505 | return (a.sortText or a.label) < (b.sortText or b.label) 506 | end) 507 | local items = {} 508 | for _, candidate in pairs(candidates) do 509 | table.insert(items, { 510 | word = candidate.text or candidate.label, 511 | abbr = candidate.label, 512 | dup = 0; 513 | icase = 1; 514 | }) 515 | end 516 | return items 517 | end 518 | 519 | --- Finds word boundary for [vim.fn.complete] 520 | --- 521 | ---@param items dap.CompletionItem[] 522 | ---@return boolean mixed, integer? start 523 | local function get_start(items, prefix) 524 | local start = nil 525 | local mixed = false 526 | for _, item in ipairs(items) do 527 | if item.start and (item.length or 0) > 0 then 528 | if start and start ~= item.start then 529 | mixed = true 530 | start = math.min(start, item.start) 531 | else 532 | start = item.start 533 | end 534 | end 535 | if not start and (item.text or item.label):sub(1, #prefix) == prefix then 536 | start = 0 537 | end 538 | end 539 | return mixed, start 540 | end 541 | 542 | function M.omnifunc(findstart, base) 543 | local session = get_session() 544 | local col = api.nvim_win_get_cursor(0)[2] 545 | local line = api.nvim_get_current_line() 546 | local offset = vim.startswith(line, prompt) and 5 or 0 547 | local line_to_cursor = line:sub(offset + 1, col) 548 | local text_match = vim.fn.match(line_to_cursor, '\\k*$') 549 | if vim.startswith(line_to_cursor, '.') or base ~= '' then 550 | if findstart == 1 then 551 | return offset 552 | end 553 | local completions = {} 554 | for key, values in pairs(M.commands) do 555 | if key ~= "custom_commands" then 556 | for _, val in pairs(values) do 557 | if vim.startswith(val, base) then 558 | table.insert(completions, val) 559 | end 560 | end 561 | end 562 | end 563 | for key, _ in pairs(M.commands.custom_commands or {}) do 564 | if vim.startswith(key, base) then 565 | table.insert(completions, key) 566 | end 567 | end 568 | 569 | return completions 570 | end 571 | local supportsCompletionsRequest = ((session or {}).capabilities or {}).supportsCompletionsRequest; 572 | if not supportsCompletionsRequest then 573 | if findstart == 1 then 574 | return -1 575 | else 576 | return {} 577 | end 578 | end 579 | assert(session, 'Session must exist if supportsCompletionsRequest is true') 580 | ---@type dap.CompletionsArguments 581 | local args = { 582 | frameId = (session.current_frame or {}).id, 583 | text = line_to_cursor, 584 | column = col + 1 - offset 585 | } 586 | ---@param err dap.ErrorResponse? 587 | ---@param response dap.CompletionsResponse? 588 | local function on_response(err, response) 589 | if err then 590 | require('dap.utils').notify('completions request failed: ' .. err.message, vim.log.levels.WARN) 591 | elseif response then 592 | local items = response.targets 593 | local mixed, start = get_start(items, line_to_cursor) 594 | if start and not mixed then 595 | vim.fn.complete(offset + start + 1, completions_to_items(items)) 596 | else 597 | vim.fn.complete(offset + text_match + 1, completions_to_items(items)) 598 | end 599 | end 600 | end 601 | session:request('completions', args, on_response) 602 | 603 | -- cancel but stay in completion mode for completion via `completions` callback 604 | return -2 605 | end 606 | end 607 | 608 | 609 | return M 610 | -------------------------------------------------------------------------------- /lua/dap/rpc.lua: -------------------------------------------------------------------------------- 1 | local utils = require('dap.utils') 2 | local M = {} 3 | 4 | ---@param header string 5 | ---@return integer? 6 | local function get_content_length(header) 7 | for line in header:gmatch("(.-)\r\n") do 8 | local key, value = line:match('^%s*(%S+)%s*:%s*(%d+)%s*$') 9 | if key and key:lower() == "content-length" then 10 | return tonumber(value) 11 | end 12 | end 13 | end 14 | 15 | 16 | local parse_chunk_loop 17 | local has_strbuffer, strbuffer = pcall(require, "string.buffer") 18 | 19 | if has_strbuffer then 20 | parse_chunk_loop = function() 21 | local buf = strbuffer.new() 22 | while true do 23 | local msg = buf:tostring() 24 | local header_end = msg:find('\r\n\r\n', 1, true) 25 | if header_end then 26 | local header = buf:get(header_end + 1) 27 | buf:skip(2) -- skip past header boundary 28 | local content_length = get_content_length(header) 29 | if not content_length then 30 | error("Content-Length not found in headers: " .. header) 31 | end 32 | while #buf < content_length do 33 | local chunk = coroutine.yield() 34 | buf:put(chunk) 35 | end 36 | local body = buf:get(content_length) 37 | coroutine.yield(body) 38 | else 39 | local chunk = coroutine.yield() 40 | buf:put(chunk) 41 | end 42 | end 43 | end 44 | else 45 | parse_chunk_loop = function() 46 | local buffer = '' 47 | while true do 48 | local header_end, body_start = buffer:find('\r\n\r\n', 1, true) 49 | if header_end then 50 | local header = buffer:sub(1, header_end + 1) 51 | local content_length = get_content_length(header) 52 | if not content_length then 53 | error("Content-Length not found in headers: " .. header) 54 | end 55 | local body_chunks = {buffer:sub(body_start + 1)} 56 | local body_length = #body_chunks[1] 57 | while body_length < content_length do 58 | local chunk = coroutine.yield() 59 | or error("Expected more data for the body. The server may have died.") 60 | table.insert(body_chunks, chunk) 61 | body_length = body_length + #chunk 62 | end 63 | local last_chunk = body_chunks[#body_chunks] 64 | 65 | body_chunks[#body_chunks] = last_chunk:sub(1, content_length - body_length - 1) 66 | local rest = '' 67 | if body_length > content_length then 68 | rest = last_chunk:sub(content_length - body_length) 69 | end 70 | local body = table.concat(body_chunks) 71 | buffer = rest .. (coroutine.yield(body) 72 | or error("Expected more data for the body. The server may have died.")) 73 | else 74 | buffer = buffer .. (coroutine.yield() 75 | or error("Expected more data for the header. The server may have died.")) 76 | end 77 | end 78 | end 79 | end 80 | 81 | 82 | function M.create_read_loop(handle_body, on_no_chunk) 83 | local parse_chunk = coroutine.wrap(parse_chunk_loop) 84 | parse_chunk() 85 | return function (err, chunk) 86 | if err then 87 | utils.notify(err, vim.log.levels.ERROR) 88 | return 89 | end 90 | if not chunk then 91 | if on_no_chunk then 92 | on_no_chunk() 93 | end 94 | return 95 | end 96 | while true do 97 | local body = parse_chunk(chunk) 98 | if body then 99 | handle_body(body) 100 | chunk = "" 101 | else 102 | break 103 | end 104 | end 105 | end 106 | end 107 | 108 | 109 | function M.msg_with_content_length(msg) 110 | return table.concat { 111 | 'Content-Length: '; 112 | tostring(#msg); 113 | '\r\n\r\n'; 114 | msg 115 | } 116 | end 117 | 118 | 119 | return M 120 | -------------------------------------------------------------------------------- /lua/dap/ui.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | local utils = require('dap.utils') 3 | local if_nil = utils.if_nil 4 | local M = {} 5 | 6 | 7 | ---@param win integer 8 | ---@param opts table? 9 | function M.apply_winopts(win, opts) 10 | if not opts then 11 | return 12 | end 13 | assert( 14 | type(opts) == 'table', 15 | 'winopts must be a table, not ' .. type(opts) .. ': ' .. vim.inspect(opts) 16 | ) 17 | for k, v in pairs(opts) do 18 | if k == 'width' then 19 | api.nvim_win_set_width(win, v) 20 | elseif k == 'height' then 21 | api.nvim_win_set_height(win, v) 22 | elseif vim.tbl_contains({ 'border', 'title' }, k) then 23 | api.nvim_win_set_config(win, {[k]=v}) 24 | else 25 | vim.wo[win][k] = v 26 | end 27 | end 28 | end 29 | 30 | 31 | --- Same as M.pick_one except that it skips the selection prompt if `items` 32 | -- contains exactly one item. 33 | function M.pick_if_many(items, prompt, label_fn, cb) 34 | if #items == 1 then 35 | if not cb then 36 | return items[1] 37 | else 38 | cb(items[1]) 39 | end 40 | else 41 | return M.pick_one(items, prompt, label_fn, cb) 42 | end 43 | end 44 | 45 | 46 | function M.pick_one_sync(items, prompt, label_fn) 47 | local choices = {prompt} 48 | for i, item in ipairs(items) do 49 | table.insert(choices, string.format('%d: %s', i, label_fn(item))) 50 | end 51 | local choice = vim.fn.inputlist(choices) 52 | if choice < 1 or choice > #items then 53 | return nil 54 | end 55 | return items[choice] 56 | end 57 | 58 | 59 | function M.pick_one(items, prompt, label_fn, cb) 60 | local co 61 | if not cb then 62 | co = coroutine.running() 63 | if co then 64 | cb = function(item) 65 | coroutine.resume(co, item) 66 | end 67 | end 68 | end 69 | cb = vim.schedule_wrap(cb) 70 | if vim.ui then 71 | vim.ui.select(items, { 72 | prompt = prompt, 73 | format_item = label_fn, 74 | }, cb) 75 | else 76 | local result = M.pick_one_sync(items, prompt, label_fn) 77 | cb(result) 78 | end 79 | if co then 80 | return coroutine.yield() 81 | end 82 | end 83 | 84 | 85 | local function with_indent(indent, fn) 86 | local move_cols = function(hl_group) 87 | local end_col = hl_group[3] == -1 and -1 or hl_group[3] + indent 88 | return {hl_group[1], hl_group[2] + indent, end_col} 89 | end 90 | return function(...) 91 | local text, hl_groups = fn(...) 92 | return string.rep(' ', indent) .. text, vim.tbl_map(move_cols, hl_groups or {}) 93 | end 94 | end 95 | 96 | 97 | function M.new_tree(opts) 98 | assert(opts.render_parent, 'opts for tree requires a `render_parent` function') 99 | assert(opts.get_children, 'opts for tree requires a `get_children` function') 100 | assert(opts.has_children, 'opts for tree requires a `has_children` function') 101 | local get_key = opts.get_key or function(x) return x end 102 | opts.fetch_children = opts.fetch_children or function(item, cb) 103 | cb(opts.get_children(item)) 104 | end 105 | opts.render_child = opts.render_child or opts.render_parent 106 | local compute_actions = opts.compute_actions or function() return {} end 107 | local extra_context = opts.extra_context or {} 108 | local implicit_expand_action = if_nil(opts.implicit_expand_action, true) 109 | local is_lazy = opts.is_lazy or function(_) return false end 110 | local load_value = opts.load_value or function(_, _) assert(false, "load_value not implemented") end 111 | 112 | local self -- forward reference 113 | 114 | -- tree supports to re-draw with new data while retaining previously 115 | -- expansion information. 116 | -- 117 | -- Since the data is completely changed, the expansion information must be 118 | -- held separately. 119 | -- 120 | -- The structure must supports constructs like this: 121 | -- 122 | -- root 123 | -- / \ 124 | -- a b 125 | -- / \ 126 | -- x x 127 | -- / \ 128 | -- aa bb 129 | -- 130 | -- It must be possible to distinguish the two `x` 131 | -- This assumes that `get_key` within a level is unique and that it is 132 | -- deterministic between two `render` operations. 133 | local expanded_root = {} 134 | 135 | local function get_expanded(item) 136 | local ancestors = {} 137 | local parent = item 138 | while true do 139 | parent = parent.__parent 140 | if parent then 141 | table.insert(ancestors, parent.key) 142 | else 143 | break 144 | end 145 | end 146 | local expanded = expanded_root 147 | for i = #ancestors, 1, -1 do 148 | local parent_expanded = expanded[ancestors[i]] 149 | if parent_expanded then 150 | expanded = parent_expanded 151 | else 152 | break 153 | end 154 | end 155 | return expanded 156 | end 157 | 158 | local function set_expanded(item, value) 159 | local expanded = get_expanded(item) 160 | expanded[get_key(item)] = value 161 | end 162 | 163 | local function is_expanded(item) 164 | local expanded = get_expanded(item) 165 | return expanded[get_key(item)] ~= nil 166 | end 167 | 168 | local expand = function(layer, value, lnum, context) 169 | set_expanded(value, {}) 170 | opts.fetch_children(value, function(children) 171 | local ctx = { 172 | actions = context.actions, 173 | indent = context.indent + 2, 174 | compute_actions = context.compute_actions, 175 | tree = self, 176 | } 177 | ctx = vim.tbl_deep_extend('keep', ctx, extra_context) 178 | for _, child in pairs(children) do 179 | if opts.has_children(child) then 180 | child.__parent = { key = get_key(value), __parent = value.__parent } 181 | end 182 | end 183 | local render = with_indent(ctx.indent, opts.render_child) 184 | layer.render(children, render, ctx, lnum + 1) 185 | end) 186 | end 187 | 188 | local function eager_fetch_expanded_children(value, cb, ctx) 189 | ctx = ctx or { to_traverse = 1 } 190 | opts.fetch_children(value, function(children) 191 | ctx.to_traverse = ctx.to_traverse + #children 192 | for _, child in pairs(children) do 193 | if opts.has_children(child) then 194 | child.__parent = { key = get_key(value), __parent = value.__parent } 195 | end 196 | if is_expanded(child) then 197 | eager_fetch_expanded_children(child, cb, ctx) 198 | else 199 | ctx.to_traverse = ctx.to_traverse - 1 200 | end 201 | end 202 | ctx.to_traverse = ctx.to_traverse - 1 203 | if ctx.to_traverse == 0 then 204 | cb() 205 | end 206 | end) 207 | end 208 | 209 | local function render_all_expanded(layer, value, indent) 210 | indent = indent or 2 211 | local context = { 212 | actions = implicit_expand_action and { { label ='Expand', fn = self.toggle, }, } or {}, 213 | indent = indent, 214 | compute_actions = compute_actions, 215 | tree = self, 216 | } 217 | context = vim.tbl_deep_extend('keep', context, extra_context) 218 | for _, child in pairs(opts.get_children(value)) do 219 | local ok, line_count = pcall(api.nvim_buf_line_count, layer.buf) 220 | if not ok then 221 | -- User might have closed the buffer 222 | return 223 | end 224 | layer.render({child}, with_indent(indent, opts.render_child), context, line_count) 225 | if is_expanded(child) then 226 | render_all_expanded(layer, child, indent + 2) 227 | end 228 | end 229 | end 230 | 231 | local collapse = function(layer, value, lnum, context) 232 | if not is_expanded(value) then 233 | return 234 | end 235 | local num_vars = 1 236 | local collapse_child 237 | collapse_child = function(parent) 238 | num_vars = num_vars + 1 239 | if is_expanded(parent) then 240 | for _, child in pairs(opts.get_children(parent)) do 241 | collapse_child(child) 242 | end 243 | set_expanded(parent, nil) 244 | end 245 | end 246 | for _, child in ipairs(opts.get_children(value)) do 247 | collapse_child(child) 248 | end 249 | set_expanded(value, nil) 250 | layer.render({}, tostring, context, lnum + 1, lnum + num_vars) 251 | end 252 | 253 | self = { 254 | toggle = function(layer, value, lnum, context) 255 | if is_lazy(value) then 256 | load_value(value, function(var) 257 | local render = with_indent(context.indent, opts.render_child) 258 | layer.render({var}, render, context, lnum, lnum + 1) 259 | end) 260 | elseif is_expanded(value) then 261 | collapse(layer, value, lnum, context) 262 | elseif opts.has_children(value) then 263 | expand(layer, value, lnum, context) 264 | else 265 | utils.notify("No children on line " .. tostring(lnum) .. ". Can't expand", vim.log.levels.INFO) 266 | end 267 | end, 268 | 269 | render = function(layer, value, on_done, lnum, end_) 270 | layer.render({value}, opts.render_parent, nil, lnum, end_) 271 | if not opts.has_children(value) then 272 | if on_done then 273 | on_done() 274 | end 275 | return 276 | end 277 | if not is_expanded(value) then 278 | set_expanded(value, {}) 279 | end 280 | eager_fetch_expanded_children(value, function() 281 | render_all_expanded(layer, value) 282 | if on_done then 283 | on_done() 284 | end 285 | end) 286 | end, 287 | } 288 | return self 289 | end 290 | 291 | 292 | --- Create a view that can be opened, closed and toggled. 293 | -- 294 | -- The view manages a single buffer and a single window. Both are created when 295 | -- the view is opened and destroyed when the view is closed. 296 | -- 297 | -- Arguments passed to `view.open()` are forwarded to the `new_win` function 298 | -- 299 | -- @param new_buf (view -> number): function to create a new buffer. Must return the bufnr 300 | -- @param new_win (-> number): function to create a new window. Must return the winnr 301 | -- @param opts A dictionary with `before_open` and `after_open` hooks. 302 | function M.new_view(new_buf, new_win, opts) 303 | assert(new_buf, 'new_buf must not be nil') 304 | assert(new_win, 'new_win must not be nil') 305 | opts = opts or {} 306 | local self 307 | self = { 308 | buf = nil, 309 | win = nil, 310 | 311 | toggle = function(...) 312 | if not self.close({ mode = 'toggle' }) then 313 | self.open(...) 314 | end 315 | end, 316 | 317 | close = function(close_opts) 318 | close_opts = close_opts or {} 319 | local closed = false 320 | local win = self.win 321 | local buf = self.buf 322 | if win and api.nvim_win_is_valid(win) and api.nvim_win_get_buf(win) == buf then 323 | api.nvim_win_close(win, true) 324 | self.win = nil 325 | closed = true 326 | end 327 | local hide = close_opts.mode == 'toggle' 328 | if buf and not hide then 329 | pcall(api.nvim_buf_delete, buf, {force=true}) 330 | self.buf = nil 331 | end 332 | return closed 333 | end, 334 | 335 | ---@return integer 336 | _init_buf = function() 337 | if self.buf then 338 | return self.buf 339 | end 340 | local buf = new_buf(self) 341 | assert(buf, 'The `new_buf` function is supposed to return a buffer') 342 | api.nvim_buf_attach(buf, false, { on_detach = function() self.buf = nil end }) 343 | self.buf = buf 344 | return buf 345 | end, 346 | 347 | open = function(...) 348 | local win = self.win 349 | local before_open_result 350 | if opts.before_open then 351 | before_open_result = opts.before_open(self, ...) 352 | end 353 | local buf = self._init_buf() 354 | if not win or not api.nvim_win_is_valid(win) then 355 | win = new_win(buf, ...) 356 | end 357 | api.nvim_win_set_buf(win, buf) 358 | 359 | -- Trigger filetype again to ensure ftplugin files can change window settings 360 | local ft = vim.bo[buf].filetype 361 | vim.bo[buf].filetype = ft 362 | 363 | self.buf = buf 364 | self.win = win 365 | if opts.after_open then 366 | opts.after_open(self, before_open_result, ...) 367 | end 368 | return buf, win 369 | end 370 | } 371 | return self 372 | end 373 | 374 | 375 | function M.trigger_actions(opts) 376 | opts = opts or {} 377 | local buf = api.nvim_get_current_buf() 378 | local layer = M.get_layer(buf) 379 | if not layer then return end 380 | local lnum, col = unpack(api.nvim_win_get_cursor(0)) 381 | lnum = lnum - 1 382 | local info = layer.get(lnum, 0, col) or {} 383 | local context = info.context or {} 384 | local actions = {} 385 | vim.list_extend(actions, context.actions or {}) 386 | if context.compute_actions then 387 | vim.list_extend(actions, context.compute_actions(info)) 388 | end 389 | if opts.filter then 390 | local filter = (type(opts.filter) == 'function' 391 | and opts.filter 392 | or function(x) return x.label == opts.filter end 393 | ) 394 | actions = vim.tbl_filter(filter, actions) 395 | end 396 | if #actions == 0 then 397 | utils.notify('No action possible on: ' .. api.nvim_buf_get_lines(buf, lnum, lnum + 1, true)[1], vim.log.levels.INFO) 398 | return 399 | end 400 | if opts.mode == 'first' then 401 | local action = actions[1] 402 | action.fn(layer, info.item, lnum, info.context) 403 | return 404 | end 405 | M.pick_if_many( 406 | actions, 407 | 'Actions> ', 408 | function(x) return type(x.label) == 'string' and x.label or x.label(info.item) end, 409 | function(action) 410 | if action then 411 | action.fn(layer, info.item, lnum, info.context) 412 | end 413 | end 414 | ) 415 | end 416 | 417 | 418 | ---@type table 419 | local layers = {} 420 | 421 | --- Return an existing layer 422 | --- 423 | ---@param buf integer 424 | ---@return nil|dap.ui.Layer 425 | function M.get_layer(buf) 426 | return layers[buf] 427 | end 428 | 429 | ---@class dap.ui.LineInfo 430 | ---@field mark_id number 431 | ---@field item any 432 | ---@field context table|nil 433 | 434 | --- Returns a layer, creating it if it's missing. 435 | ---@param buf integer 436 | ---@return dap.ui.Layer 437 | function M.layer(buf) 438 | assert(buf, 'Need a buffer to operate on') 439 | local layer = layers[buf] 440 | if layer then 441 | return layer 442 | end 443 | 444 | ---@type table 445 | local marks = {} 446 | local ns = api.nvim_create_namespace('dap.ui_layer_' .. buf) 447 | local nshl = api.nvim_create_namespace('dap.ui_layer_hl_' .. buf) 448 | local remove_marks = function(extmarks) 449 | for _, mark in pairs(extmarks) do 450 | local mark_id = mark[1] 451 | marks[mark_id] = nil 452 | api.nvim_buf_del_extmark(buf, ns, mark_id) 453 | end 454 | end 455 | 456 | ---@class dap.ui.Layer 457 | layer = { 458 | buf = buf, 459 | __marks = marks, 460 | 461 | --- Render the items and associate each item to the rendered line 462 | --- The item and context can then be retrieved using `.get(lnum)` 463 | --- 464 | --- lines between start and end_ are replaced 465 | --- If start == end_, new lines are inserted at the given position 466 | --- If start == nil, appends to the end of the buffer 467 | --- 468 | ---@generic T 469 | ---@param xs T[] 470 | ---@param render_fn? fun(T):string 471 | ---@param context table|nil 472 | ---@param start nil|number 0-indexed 473 | ---@param end_ nil|number 0-indexed exclusive 474 | render = function(xs, render_fn, context, start, end_) 475 | if not api.nvim_buf_is_valid(buf) then 476 | return 477 | end 478 | local modifiable = vim.bo[buf].modifiable 479 | vim.bo[buf].modifiable = true 480 | if not start and not end_ then 481 | start = api.nvim_buf_line_count(buf) 482 | -- Avoid inserting a new line at the end of the buffer 483 | -- The case of no lines and one empty line are ambiguous; 484 | -- set_lines(buf, 0, 0) would "preserve" the "empty buffer line" while set_lines(buf, 0, -1) replaces it 485 | -- Need to use regular end_ = start in other cases to support injecting lines in all other cases 486 | if start == 1 and (api.nvim_buf_get_lines(buf, 0, -1, true))[1] == "" then 487 | start = 0 488 | end_ = -1 489 | else 490 | end_ = start 491 | end 492 | else 493 | start = start or (api.nvim_buf_line_count(buf) - 1) 494 | end_ = end_ or start 495 | end 496 | render_fn = render_fn or tostring 497 | if end_ > start then 498 | remove_marks(api.nvim_buf_get_extmarks(buf, ns, {start, 0}, {end_ - 1, -1}, {})) 499 | elseif end_ == -1 then 500 | remove_marks(api.nvim_buf_get_extmarks(buf, ns, {start, 0}, {-1, -1}, {})) 501 | end 502 | -- This is a dummy call to insert new lines in a region 503 | -- the loop below will add the actual values 504 | local lines = vim.tbl_map(function() return '' end, xs) 505 | api.nvim_buf_set_lines(buf, start, end_, true, lines) 506 | if start == -1 then 507 | start = api.nvim_buf_line_count(buf) - #lines 508 | end 509 | 510 | for i = start, start + #lines - 1 do 511 | local item = xs[i + 1 - start] 512 | local text, hl_regions = render_fn(item) 513 | if not text then 514 | local debuginfo = debug.getinfo(render_fn) 515 | error(('render function must return a string, got nil instead. render_fn: ' 516 | .. debuginfo.short_src .. ':' .. debuginfo.linedefined 517 | .. ' ' 518 | .. vim.inspect(xs) 519 | )) 520 | end 521 | text = text:gsub('\n', '\\n') 522 | api.nvim_buf_set_lines(buf, i, i + 1, true, {text}) 523 | if hl_regions then 524 | for _, hl_region in pairs(hl_regions) do 525 | api.nvim_buf_add_highlight( 526 | buf, nshl, hl_region[1], i, hl_region[2], hl_region[3]) 527 | end 528 | end 529 | 530 | local end_col = math.max(0, #text - 1) 531 | local mark_id = api.nvim_buf_set_extmark(buf, ns, i, 0, {end_col=end_col}) 532 | marks[mark_id] = { mark_id = mark_id, item = item, context = context } 533 | end 534 | vim.bo[buf].modifiable = modifiable 535 | end, 536 | 537 | --- Get the information associated with a line 538 | --- 539 | ---@param lnum number 0-indexed line number 540 | ---@param start_col nil|number 541 | ---@param end_col nil|number 542 | ---@return nil|dap.ui.LineInfo 543 | get = function(lnum, start_col, end_col) 544 | local line = api.nvim_buf_get_lines(buf, lnum, lnum + 1, true)[1] 545 | start_col = start_col or 0 546 | end_col = end_col or #line 547 | local start = {lnum, start_col} 548 | local end_ = {lnum, end_col} 549 | local extmarks = api.nvim_buf_get_extmarks(buf, ns, start, end_, {}) 550 | if not extmarks or #extmarks == 0 then 551 | return 552 | end 553 | assert(#extmarks == 1, 'Expecting only a single mark per line and region: ' .. vim.inspect(extmarks)) 554 | local extmark = extmarks[1] 555 | return marks[extmark[1]] 556 | end 557 | } 558 | layers[buf] = layer 559 | api.nvim_buf_attach(buf, false, { on_detach = function(_, b) layers[b] = nil end }) 560 | return layer 561 | end 562 | 563 | 564 | return M 565 | -------------------------------------------------------------------------------- /lua/dap/ui/widgets.lua: -------------------------------------------------------------------------------- 1 | local ui = require('dap.ui') 2 | local utils = require('dap.utils') 3 | local api = vim.api 4 | local M = {} 5 | 6 | 7 | local function set_default_bufopts(buf) 8 | vim.bo[buf].modifiable = false 9 | vim.bo[buf].buftype = "nofile" 10 | api.nvim_buf_set_keymap( 11 | buf, "n", "", "lua require('dap.ui').trigger_actions({ mode = 'first' })", {}) 12 | api.nvim_buf_set_keymap( 13 | buf, "n", "a", "lua require('dap.ui').trigger_actions()", {}) 14 | api.nvim_buf_set_keymap( 15 | buf, "n", "o", "lua require('dap.ui').trigger_actions()", {}) 16 | api.nvim_buf_set_keymap( 17 | buf, "n", "<2-LeftMouse>", "lua require('dap.ui').trigger_actions()", {}) 18 | end 19 | 20 | 21 | local function new_buf() 22 | local buf = api.nvim_create_buf(false, true) 23 | set_default_bufopts(buf) 24 | return buf 25 | end 26 | 27 | 28 | function M.new_cursor_anchored_float_win(buf) 29 | vim.bo[buf].bufhidden = "wipe" 30 | local opts = vim.lsp.util.make_floating_popup_options(50, 30, {border = 'single'}) 31 | local win = api.nvim_open_win(buf, true, opts) 32 | if vim.fn.has("nvim-0.11") == 1 then 33 | vim.wo[win][0].scrolloff = 0 34 | vim.wo[win][0].wrap = false 35 | else 36 | vim.wo[win].scrolloff = 0 37 | vim.wo[win].wrap = false 38 | end 39 | vim.bo[buf].filetype = "dap-float" 40 | return win 41 | end 42 | 43 | 44 | function M.new_centered_float_win(buf) 45 | vim.bo[buf].bufhidden = "wipe" 46 | local columns = vim.o.columns 47 | local lines = vim.o.lines 48 | local width = math.floor(columns * 0.9) 49 | local height = math.floor(lines * 0.8) 50 | local opts = { 51 | relative = 'editor', 52 | style = 'minimal', 53 | row = math.floor((lines - height) * 0.5), 54 | col = math.floor((columns - width) * 0.5), 55 | width = width, 56 | height = height, 57 | border = 'single', 58 | } 59 | local win = api.nvim_open_win(buf, true, opts) 60 | if vim.fn.has("nvim-0.11") == 1 then 61 | vim.wo[win][0].scrolloff = 0 62 | vim.wo[win][0].wrap = false 63 | else 64 | vim.wo[win].scrolloff = 0 65 | vim.wo[win].wrap = false 66 | end 67 | vim.bo[buf].filetype = "dap-float" 68 | return win 69 | end 70 | 71 | 72 | local function with_winopts(new_win, winopts) 73 | return function(...) 74 | local win = new_win(...) 75 | ui.apply_winopts(win, winopts) 76 | return win 77 | end 78 | end 79 | 80 | 81 | local function mk_sidebar_win_func(winopts, wincmd) 82 | return function() 83 | vim.cmd(wincmd or '30 vsplit') 84 | local win = api.nvim_get_current_win() 85 | vim.wo[win].number = false 86 | vim.wo[win].relativenumber = false 87 | vim.wo[win].statusline = ' ' 88 | ui.apply_winopts(win, winopts) 89 | return win 90 | end 91 | end 92 | 93 | 94 | --- Decorates a `new_win` function, adding a hook that will cause the window to 95 | -- be resized if the content changes. 96 | function M.with_resize(new_win) 97 | return setmetatable({resize=true}, { 98 | __call = function(_, buf) 99 | return new_win(buf) 100 | end 101 | }) 102 | end 103 | 104 | 105 | local function resize_window(win, buf) 106 | if not api.nvim_win_is_valid(win) then 107 | -- Could happen if the user moves the buffer into a new window 108 | return 109 | end 110 | local lines = api.nvim_buf_get_lines(buf, 0, -1, true) 111 | local width = 0 112 | local height = #lines 113 | for _, line in pairs(lines) do 114 | width = math.max(width, #line) 115 | end 116 | local columns = vim.o.columns 117 | local max_win_width = math.floor(columns * 0.9) 118 | width = math.min(width, max_win_width) 119 | local max_win_height = vim.o.lines 120 | height = math.min(height, max_win_height) 121 | api.nvim_win_set_width(win, width) 122 | api.nvim_win_set_height(win, height) 123 | end 124 | 125 | 126 | local function resizing_layer(win, buf) 127 | local layer = ui.layer(buf) 128 | local orig_render = layer.render 129 | ---@diagnostic disable-next-line: inject-field 130 | layer.render = function(...) 131 | orig_render(...) 132 | if api.nvim_win_get_config(win).relative ~= '' then 133 | resize_window(win, buf) 134 | end 135 | end 136 | return layer 137 | end 138 | 139 | 140 | M.scopes = { 141 | refresh_listener = 'scopes', 142 | new_buf = function(view) 143 | local dap = require('dap') 144 | local function reset_tree() 145 | view.tree = nil 146 | end 147 | dap.listeners.after['event_terminated'][view] = reset_tree 148 | dap.listeners.after['event_exited'][view] = reset_tree 149 | local buf = new_buf() 150 | api.nvim_create_autocmd("TextYankPost", { 151 | buffer = buf, 152 | callback = function() 153 | require("dap._cmds").yank_evalname() 154 | end, 155 | }) 156 | vim.bo[buf].tagfunc = "v:lua.require'dap'._tagfunc" 157 | api.nvim_buf_attach(buf, false, { 158 | on_detach = function() 159 | dap.listeners.after['event_terminated'][view] = nil 160 | dap.listeners.after['event_exited'][view] = nil 161 | end 162 | }) 163 | api.nvim_buf_set_name(buf, 'dap-scopes-' .. tostring(buf)) 164 | return buf 165 | end, 166 | render = function(view) 167 | local session = require('dap').session() 168 | local frame = session and session.current_frame or {} 169 | local tree = view.tree 170 | if not tree then 171 | local spec = vim.deepcopy(require('dap.entity').scope.tree_spec) 172 | spec.extra_context = { view = view } 173 | tree = ui.new_tree(spec) 174 | view.tree = tree 175 | end 176 | local layer = view.layer() 177 | local scopes = frame.scopes or {} 178 | local render 179 | render = function(idx, scope, replace) 180 | if not scope then 181 | return 182 | end 183 | 184 | tree.render(layer, scope, function() 185 | render(next(scopes, idx)) 186 | end, replace and 0 or nil, replace and -1 or nil) 187 | end 188 | local idx, scope = next(scopes) 189 | render(idx, scope, true) 190 | end, 191 | } 192 | 193 | 194 | M.threads = { 195 | refresh_listener = 'event_thread', 196 | new_buf = function() 197 | local buf = new_buf() 198 | api.nvim_buf_set_name(buf, 'dap-threads-' .. tostring(buf)) 199 | return buf 200 | end, 201 | render = function(view) 202 | local layer = view.layer() 203 | local session = require('dap').session() 204 | if not session then 205 | layer.render({'No active session'}) 206 | return 207 | end 208 | 209 | ---@diagnostic disable-next-line: invisible 210 | if session.dirty.threads then 211 | session:update_threads(function() 212 | M.threads.render(view) 213 | end) 214 | return 215 | end 216 | 217 | local tree = view.tree 218 | if not tree then 219 | local spec = vim.deepcopy(require('dap.entity').threads.tree_spec) 220 | spec.extra_context = { 221 | view = view, 222 | refresh = view.refresh, 223 | } 224 | tree = ui.new_tree(spec) 225 | view.tree = tree 226 | end 227 | 228 | local root = { 229 | id = 0, 230 | name = 'Threads', 231 | threads = vim.tbl_values(session.threads) 232 | } 233 | tree.render(layer, root) 234 | end, 235 | } 236 | 237 | 238 | M.frames = { 239 | refresh_listener = 'scopes', 240 | new_buf = function() 241 | local buf = new_buf() 242 | api.nvim_buf_set_name(buf, 'dap-frames-' .. tostring(buf)) 243 | return buf 244 | end, 245 | render = function(view) 246 | local session = require('dap').session() 247 | local layer = view.layer() 248 | if not session then 249 | layer.render({'No active session'}) 250 | return 251 | end 252 | if not session.stopped_thread_id then 253 | layer.render({'Not stopped at any breakpoint. No frames available'}) 254 | return 255 | end 256 | local thread = session.threads[session.stopped_thread_id] 257 | if not thread then 258 | local msg = string.format("Stopped thread (%d) not found. Can't display frames", session.stopped_thread_id) 259 | layer.render({msg}) 260 | return 261 | end 262 | 263 | local frames = thread.frames 264 | require("dap.async").run(function() 265 | if not frames then 266 | local err, response = session:request("stackTrace", { threadId = thread.id }) 267 | ---@cast response dap.StackTraceResponse 268 | if err or not response then 269 | layer.render({"Stopped thread has no frames"}) 270 | return 271 | end 272 | frames = response.stackFrames 273 | end 274 | local context = {} 275 | context.actions = { 276 | { 277 | label = "Jump to frame", 278 | fn = function(_, frame) 279 | if session then 280 | local close = vim.bo.bufhidden == "wipe" 281 | session:_frame_set(frame) 282 | if close then 283 | view.close() 284 | end 285 | else 286 | utils.notify('Cannot navigate to frame without active session', vim.log.levels.INFO) 287 | end 288 | end 289 | }, 290 | } 291 | local render_frame = require('dap.entity').frames.render_item 292 | layer.render(frames, render_frame, context) 293 | end) 294 | end 295 | } 296 | 297 | 298 | M.sessions = { 299 | refresh_listener = { 300 | 'event_initialized', 301 | 'event_terminated', 302 | 'disconnect', 303 | 'event_stopped' 304 | }, 305 | new_buf = function() 306 | local buf = new_buf() 307 | api.nvim_buf_set_name(buf, 'dap-sessions-' .. tostring(buf)) 308 | return buf 309 | end, 310 | render = function(view) 311 | local dap = require('dap') 312 | local sessions = dap.sessions() 313 | local layer = view.layer() 314 | local lsessions = {} 315 | 316 | local add 317 | add = function(s) 318 | table.insert(lsessions, s) 319 | for _, child in pairs(s.children) do 320 | add(child) 321 | end 322 | end 323 | for _, s in pairs(sessions) do 324 | add(s) 325 | end 326 | local context = {} 327 | context.actions = { 328 | { 329 | label = "Focus session", 330 | fn = function(_, s) 331 | local close = vim.bo.bufhidden == "wipe" 332 | if s then 333 | dap.set_session(s) 334 | view.refresh() 335 | end 336 | if close then 337 | view.close() 338 | end 339 | end 340 | } 341 | } 342 | local focused = dap.session() 343 | local render_session = function(s) 344 | local text = s.id .. ': ' .. s.config.name 345 | local parent = s.parent 346 | local num_parents = 0 347 | while parent ~= nil do 348 | parent = parent.parent 349 | num_parents = num_parents + 1 350 | end 351 | local prefix 352 | if focused and s.id == focused.id then 353 | prefix = "→ " 354 | else 355 | prefix = " " 356 | end 357 | return prefix .. string.rep(" ", num_parents) .. text 358 | end 359 | layer.render({}, tostring, nil, 0, -1) 360 | layer.render(lsessions, render_session, context) 361 | end, 362 | } 363 | 364 | 365 | do 366 | 367 | ---@param scopes dap.Scope[] 368 | ---@param expression string 369 | ---@return dap.Variable? 370 | local function find_var(scopes, expression) 371 | for _, s in ipairs(scopes) do 372 | for _, var in ipairs(s.variables or {}) do 373 | if var.name == expression then 374 | return var 375 | end 376 | end 377 | end 378 | return nil 379 | end 380 | 381 | M.expression = { 382 | new_buf = function() 383 | local buf = new_buf() 384 | vim.bo[buf].tagfunc = "v:lua.require'dap'._tagfunc" 385 | api.nvim_create_autocmd("TextYankPost", { 386 | buffer = buf, 387 | callback = function() 388 | require("dap._cmds").yank_evalname() 389 | end, 390 | }) 391 | return buf 392 | end, 393 | before_open = function(view) 394 | view.__expression = vim.fn.expand('') 395 | end, 396 | render = function(view, expr) 397 | local session = require('dap').session() 398 | local layer = view.layer() 399 | if not session then 400 | layer.render({'No active session'}) 401 | return 402 | end 403 | local expression = expr or view.__expression 404 | local context = session.capabilities.supportsEvaluateForHovers and "hover" or "repl" 405 | local args = { 406 | expression = expression, 407 | context = context 408 | } 409 | local frame = session.current_frame or {} 410 | local scopes = frame.scopes or {} 411 | session:evaluate(args, function(err, resp) 412 | local spec = vim.deepcopy(require('dap.entity').variable.tree_spec) 413 | spec.extra_context = { view = view } 414 | if err then 415 | local variable = find_var(scopes, expression) 416 | if variable then 417 | local tree = ui.new_tree(spec) 418 | tree.render(view.layer(), variable) 419 | else 420 | local msg = 'Cannot evaluate "'..expression..'"!' 421 | layer.render({msg}) 422 | end 423 | elseif resp and resp.result then 424 | local attributes = (resp.presentationHint or {}).attributes or {} 425 | if resp.variablesReference > 0 or vim.tbl_contains(attributes, "rawString") then 426 | local tree = ui.new_tree(spec) 427 | tree.render(layer, resp) 428 | else 429 | local lines = vim.split(resp.result, "\n", { plain = true }) 430 | layer.render(lines) 431 | end 432 | end 433 | end) 434 | end, 435 | } 436 | end 437 | 438 | 439 | function M.builder(widget) 440 | assert(widget, 'widget is required') 441 | local nwin 442 | local nbuf = widget.new_buf 443 | local hooks = {{widget.before_open, widget.after_open},} 444 | local builder = {} 445 | 446 | function builder.add_hooks(before_open, after_open) 447 | table.insert(hooks, {before_open, after_open}) 448 | return builder 449 | end 450 | 451 | function builder.keep_focus() 452 | builder.add_hooks( 453 | function() 454 | return api.nvim_get_current_win() 455 | end, 456 | function(_, win) 457 | api.nvim_set_current_win(win) 458 | end 459 | ) 460 | return builder 461 | end 462 | 463 | function builder.new_buf(val) 464 | assert(val and type(val) == "function", '`new_buf` must be a function') 465 | nbuf = val 466 | return builder 467 | end 468 | 469 | function builder.new_win(val) 470 | assert(val, '`new_win` must be a callable') 471 | nwin = val 472 | return builder 473 | end 474 | 475 | function builder.build() 476 | assert(nwin, '`new_win` function must be set') 477 | local before_open_results 478 | local view = ui.new_view(nbuf, nwin, { 479 | 480 | before_open = function(view) 481 | before_open_results = {} 482 | for _, hook in pairs(hooks) do 483 | local result = hook[1] and hook[1](view) or vim.NIL 484 | table.insert(before_open_results, result) 485 | end 486 | end, 487 | 488 | after_open = function(view, _, ...) 489 | for idx, hook in pairs(hooks) do 490 | if hook[2] then 491 | hook[2](view, before_open_results[idx]) 492 | end 493 | end 494 | before_open_results = {} 495 | return widget.render(view, ...) 496 | end 497 | }) 498 | 499 | view.layer = function() 500 | if type(nwin) == "table" and nwin.resize then 501 | return resizing_layer(view.win, view.buf) 502 | else 503 | return ui.layer(view.buf) 504 | end 505 | end 506 | 507 | view.refresh = function() 508 | local layer = view.layer() 509 | layer.render({}, tostring, nil, 0, -1) 510 | widget.render(view) 511 | end 512 | return view 513 | end 514 | return builder 515 | end 516 | 517 | 518 | ---@param expr nil|string|fun():string 519 | ---@return string 520 | local function eval_expression(expr) 521 | local mode = api.nvim_get_mode() 522 | if mode.mode == 'v' then 523 | -- [bufnum, lnum, col, off]; 1-indexed 524 | local start = vim.fn.getpos('v') 525 | local end_ = vim.fn.getpos('.') 526 | 527 | local start_row = start[2] 528 | local start_col = start[3] 529 | 530 | local end_row = end_[2] 531 | local end_col = end_[3] 532 | 533 | if start_row == end_row and end_col < start_col then 534 | end_col, start_col = start_col, end_col 535 | elseif end_row < start_row then 536 | start_row, end_row = end_row, start_row 537 | start_col, end_col = end_col, start_col 538 | end 539 | 540 | api.nvim_feedkeys(api.nvim_replace_termcodes('', true, false, true), 'n', false) 541 | 542 | -- buf_get_text is 0-indexed; end-col is exclusive 543 | local lines = api.nvim_buf_get_text(0, start_row - 1, start_col - 1, end_row - 1, end_col, {}) 544 | return table.concat(lines, '\n') 545 | end 546 | expr = expr or '' 547 | if type(expr) == "function" then 548 | return expr() 549 | else 550 | return vim.fn.expand(expr) 551 | end 552 | end 553 | 554 | 555 | ---@param expr nil|string|fun():string 556 | ---@param winopts table? 557 | function M.hover(expr, winopts) 558 | local value = eval_expression(expr) 559 | local view = M.builder(M.expression) 560 | .new_win(M.with_resize(with_winopts(M.new_cursor_anchored_float_win, winopts))) 561 | .build() 562 | local buf = view.open(value) 563 | api.nvim_buf_set_name(buf, 'dap-hover-' .. tostring(buf) .. ': ' .. value) 564 | api.nvim_win_set_cursor(view.win, {1, 0}) 565 | return view 566 | end 567 | 568 | 569 | function M.cursor_float(widget, winopts) 570 | local view = M.builder(widget) 571 | .new_win(M.with_resize(with_winopts(M.new_cursor_anchored_float_win, winopts))) 572 | .build() 573 | view.open() 574 | return view 575 | end 576 | 577 | 578 | function M.centered_float(widget, winopts) 579 | local view = M.builder(widget) 580 | .new_win(with_winopts(M.new_centered_float_win, winopts)) 581 | .build() 582 | view.open() 583 | return view 584 | end 585 | 586 | 587 | --- View the value of the expression under the cursor in a preview window 588 | --- 589 | ---@param expr nil|string|fun():string 590 | ---@param opts? {listener?: string[]} 591 | function M.preview(expr, opts) 592 | opts = opts or {} 593 | local value = eval_expression(expr) 594 | 595 | local function new_preview_buf() 596 | vim.cmd('pedit ' .. 'dap-preview: ' .. value) 597 | for _, win in pairs(api.nvim_list_wins()) do 598 | if vim.wo[win].previewwindow then 599 | local buf = api.nvim_win_get_buf(win) 600 | set_default_bufopts(buf) 601 | vim.bo[buf].bufhidden = 'delete' 602 | return buf 603 | end 604 | end 605 | end 606 | 607 | local function new_preview_win() 608 | -- Avoid pedit call if window is already open 609 | -- Otherwise on_detach is triggered 610 | for _, win in ipairs(api.nvim_list_wins()) do 611 | if vim.wo[win].previewwindow then 612 | return win 613 | end 614 | end 615 | vim.cmd('pedit ' .. 'dap-preview: ' .. value) 616 | for _, win in ipairs(api.nvim_list_wins()) do 617 | if vim.wo[win].previewwindow then 618 | return win 619 | end 620 | end 621 | end 622 | 623 | if opts.listener and next(opts.listener) then 624 | new_preview_buf = M.with_refresh(new_preview_buf, opts.listener) 625 | end 626 | local view = M.builder(M.expression) 627 | .new_buf(new_preview_buf) 628 | .new_win(new_preview_win) 629 | .build() 630 | view.open(value) 631 | view.__expression = value 632 | return view 633 | end 634 | 635 | 636 | --- Decorate a `new_buf` function so that it will register a 637 | -- `dap.listeners.after[listener]` which will trigger a `view.refresh` call. 638 | -- 639 | -- Use this if you want a widget to live-update. 640 | ---@param listener string|string[] 641 | function M.with_refresh(new_buf_, listener) 642 | local listeners 643 | if type(listener) == "table" then 644 | listeners = listener 645 | else 646 | listeners = {listener} 647 | end 648 | return function(view) 649 | local dap = require('dap') 650 | for _, l in pairs(listeners) do 651 | dap.listeners.after[l][view] = view.refresh 652 | end 653 | local buf = new_buf_(view) 654 | api.nvim_buf_attach(buf, false, { 655 | on_detach = function() 656 | for _, l in pairs(listeners) do 657 | dap.listeners.after[l][view] = nil 658 | end 659 | end 660 | }) 661 | return buf 662 | end 663 | end 664 | 665 | 666 | --- Open the given widget in a sidebar 667 | --@param winopts with options that configure the window 668 | --@param wincmd command used to create the sidebar 669 | function M.sidebar(widget, winopts, wincmd) 670 | return M.builder(widget) 671 | .keep_focus() 672 | .new_win(mk_sidebar_win_func(winopts, wincmd)) 673 | .new_buf(M.with_refresh(widget.new_buf, widget.refresh_listener or 'event_stopped')) 674 | .build() 675 | end 676 | 677 | 678 | ---@param session dap.Session 679 | ---@param expr string 680 | ---@param max_level integer 681 | local function get_var_lines(session, expr, max_level) 682 | local req_args = { 683 | expression = expr, 684 | context = "repl", 685 | frameId = (session.current_frame or {}).id 686 | } 687 | local eval_err, eval_result = session:request("evaluate", req_args) 688 | assert(not eval_err, vim.inspect(eval_err)) 689 | 690 | local lines = {} 691 | local value = eval_result.result:gsub("\n", "\\n") 692 | table.insert(lines, value) 693 | 694 | local function add_children(ref, level) 695 | require("dap.progress").report("Fetching " .. tostring(ref)) 696 | 697 | ---@type dap.VariablesArguments 698 | local vargs = { 699 | variablesReference = ref, 700 | } 701 | ---@type dap.ErrorResponse, dap.VariableResponse 702 | local err, result = session:request("variables", vargs) 703 | assert(not err, vim.inspect(err)) 704 | for _, variable in ipairs(result.variables) do 705 | local val = variable.value:gsub("\n", "\\n") 706 | local indent = level * 2 707 | local line = string.rep(" ", indent) .. variable.name .. ": " .. val 708 | table.insert(lines, line) 709 | if level < max_level and variable.variablesReference > 0 then 710 | add_children(variable.variablesReference, level + 1) 711 | end 712 | end 713 | end 714 | 715 | if eval_result.variablesReference > 0 then 716 | add_children(eval_result.variablesReference, 0) 717 | end 718 | 719 | return lines 720 | end 721 | 722 | 723 | --- Generate a diff between two expressions 724 | --- 725 | --- Opens a new tab with two windows and buffers in diff mode. 726 | --- The diff is based on the lines of the variable tree's, expanded up to `max_level` 727 | --- 728 | ---@param expr1 string 729 | ---@param expr2 string 730 | ---@param max_level? integer default: 1 731 | function M.diff_var(expr1, expr2, max_level) 732 | local dap = require("dap") 733 | local session = dap.session() 734 | if not session then 735 | utils.notify("No active session", vim.log.levels.INFO) 736 | return 737 | end 738 | max_level = max_level or 1 739 | require("dap.async").run(function() 740 | local lines1 = get_var_lines(session, expr1, max_level) 741 | local lines2 = get_var_lines(session, expr2, max_level) 742 | require("dap.progress").report("Diff operation done") 743 | require("dap.progress").report("Running: " .. session.config.name) 744 | vim.cmd.tabnew() 745 | local buf1 = api.nvim_get_current_buf() 746 | api.nvim_buf_set_lines(buf1, 0, -1, true, lines1) 747 | vim.bo[buf1].modified = false 748 | vim.bo[buf1].bufhidden = "wipe" 749 | vim.cmd.diffthis() 750 | 751 | vim.cmd.vnew() 752 | local buf2 = api.nvim_get_current_buf() 753 | api.nvim_buf_set_lines(buf2, 0, -1, true, lines2) 754 | vim.bo[buf2].modified = false 755 | vim.bo[buf2].bufhidden = "wipe" 756 | vim.cmd.diffthis() 757 | end) 758 | end 759 | 760 | 761 | return M 762 | -------------------------------------------------------------------------------- /lua/dap/utils.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | 4 | ---@param err dap.ErrorResponse 5 | ---@return string? 6 | function M.fmt_error(err) 7 | local body = err.body or {} 8 | if body.error and body.error.showUser then 9 | local msg = body.error.format 10 | for key, val in pairs(body.error.variables or {}) do 11 | msg = msg:gsub('{' .. key .. '}', val) 12 | end 13 | return msg 14 | end 15 | return err.message 16 | end 17 | 18 | 19 | -- Group values (a list) into a dictionary. 20 | -- `get_key` is used to get the key from an element of values 21 | -- `get_value` is used to set the value from an element of values and 22 | -- defaults to the full element 23 | ---@deprecated 24 | function M.to_dict(values, get_key, get_value) 25 | if vim.notify_once then 26 | vim.notify_once("dap.utils.to_dict is deprecated for removal in nvim-dap 0.10.0") 27 | end 28 | local rtn = {} 29 | get_value = get_value or function(v) return v end 30 | for _, v in pairs(values or {}) do 31 | rtn[get_key(v)] = get_value(v) 32 | end 33 | return rtn 34 | end 35 | 36 | 37 | ---@param object? table|string 38 | ---@return boolean 39 | function M.non_empty(object) 40 | if type(object) == "table" then 41 | return next(object) ~= nil 42 | end 43 | return object and #object > 0 or false 44 | end 45 | 46 | 47 | ---@generic T 48 | ---@param items T[] 49 | ---@param predicate fun(items: T):boolean 50 | ---@result integer? 51 | function M.index_of(items, predicate) 52 | for i, item in ipairs(items) do 53 | if predicate(item) then 54 | return i 55 | end 56 | end 57 | return nil 58 | end 59 | 60 | 61 | --- Return running processes as a list with { pid, name } tables. 62 | --- 63 | --- Takes an optional `opts` table with the following options: 64 | --- 65 | --- - filter string|fun: A lua pattern or function to filter the processes. 66 | --- If a function the parameter is a table with 67 | --- {pid: integer, name: string} 68 | --- and it must return a boolean. 69 | --- Matches are included. 70 | --- 71 | ---
 72 | --- require("dap.utils").pick_process({ filter = "sway" })
 73 | --- 
74 | --- 75 | ---
 76 | --- require("dap.utils").pick_process({
 77 | ---   filter = function(proc) return vim.endswith(proc.name, "sway") end
 78 | --- })
 79 | --- 
80 | --- 81 | ---@param opts? {filter: string|(fun(proc: dap.utils.Proc): boolean)} 82 | --- 83 | ---@return dap.utils.Proc[] 84 | function M.get_processes(opts) 85 | opts = opts or {} 86 | local is_windows = vim.fn.has('win32') == 1 87 | local separator = is_windows and ',' or ' \\+' 88 | local command = is_windows and {'tasklist', '/nh', '/fo', 'csv'} or {'ps', 'ah', '-U', os.getenv("USER")} 89 | -- output format for `tasklist /nh /fo` csv 90 | -- '"smss.exe","600","Services","0","1,036 K"' 91 | -- output format for `ps ah` 92 | -- " 107021 pts/4 Ss 0:00 /bin/zsh " 93 | local get_pid = function (parts) 94 | if is_windows then 95 | return vim.fn.trim(parts[2], '"') 96 | else 97 | return parts[1] 98 | end 99 | end 100 | 101 | local get_process_name = function (parts) 102 | if is_windows then 103 | return vim.fn.trim(parts[1], '"') 104 | else 105 | return table.concat({unpack(parts, 5)}, ' ') 106 | end 107 | end 108 | 109 | local output = vim.fn.system(command) 110 | local lines = vim.split(output, '\n') 111 | local procs = {} 112 | 113 | local nvim_pid = vim.fn.getpid() 114 | for _, line in pairs(lines) do 115 | if line ~= "" then -- tasklist command outputs additional empty line in the end 116 | local parts = vim.fn.split(vim.fn.trim(line), separator) 117 | local pid, name = get_pid(parts), get_process_name(parts) 118 | pid = tonumber(pid) 119 | if pid and pid ~= nvim_pid then 120 | table.insert(procs, { pid = pid, name = name }) 121 | end 122 | end 123 | end 124 | 125 | if opts.filter then 126 | local filter 127 | if type(opts.filter) == "string" then 128 | filter = function(proc) 129 | return proc.name:find(opts.filter) 130 | end 131 | elseif type(opts.filter) == "function" then 132 | filter = function(proc) 133 | return opts.filter(proc) 134 | end 135 | else 136 | error("opts.filter must be a string or a function") 137 | end 138 | procs = vim.tbl_filter(filter, procs) 139 | end 140 | 141 | return procs 142 | end 143 | 144 | 145 | 146 | 147 | --- Trim a process name to better fit into `columns` 148 | --- 149 | ---@param name string 150 | ---@param columns integer 151 | ---@param wordlimit integer 152 | ---@return string 153 | local function trim_procname(name, columns, wordlimit) 154 | if #name <= columns then 155 | return name 156 | end 157 | 158 | local function trimpart(part, i) 159 | if #part <= wordlimit then 160 | return part 161 | end 162 | -- `/usr/bin/cmd` -> `cmd` 163 | part = part:gsub("(/?[^/]+/)", "") 164 | 165 | -- preserve command name in full length, but trim arguments if they exceed word limit 166 | if i > 1 and #part > wordlimit then 167 | return "‥" .. part:sub(#part - wordlimit) 168 | end 169 | return part 170 | end 171 | 172 | -- proc name can include arguments `foo --bar --baz` 173 | -- trim each element and drop trailing args if still too long 174 | local i = 0 175 | local parts = {} 176 | local len = 0 177 | for word in name:gmatch("[^%s]+") do 178 | i = i + 1 179 | local trimmed = trimpart(word, i) 180 | len = len + #trimmed 181 | if i > 1 and len > columns then 182 | table.insert(parts, "[‥]") 183 | break 184 | else 185 | table.insert(parts, trimmed) 186 | end 187 | end 188 | return i > 0 and table.concat(parts, " ") or trimpart(name, 1) 189 | end 190 | 191 | ---@private 192 | M._trim_procname = trim_procname 193 | 194 | 195 | ---@class dap.utils.Proc 196 | ---@field pid integer 197 | ---@field name string 198 | 199 | ---@class dap.utils.pick_process.Opts 200 | ---@field filter? string|fun(proc: dap.utils.Proc):boolean 201 | ---@field label? fun(proc: dap.utils.Proc): string 202 | ---@field prompt? string 203 | 204 | --- Show a prompt to select a process pid 205 | --- Requires `ps ah -u $USER` on Linux/Mac and `tasklist /nh /fo csv` on windows. 206 | -- 207 | --- Takes an optional `opts` table with the following options: 208 | --- 209 | --- - filter string|fun: A lua pattern or function to filter the processes. 210 | --- If a function the parameter is a table with 211 | --- {pid: integer, name: string} 212 | --- and it must return a boolean. 213 | --- Matches are included. 214 | --- 215 | --- - label fun: A function to generate a custom label for the processes. 216 | --- If not provided, a default label is used. 217 | --- - prompt string: The title/prompt of pick process select. 218 | --- 219 | ---
220 | --- require("dap.utils").pick_process({ filter = "sway" })
221 | --- 
222 | --- 223 | ---
224 | --- require("dap.utils").pick_process({
225 | ---   filter = function(proc) return vim.endswith(proc.name, "sway") end
226 | --- })
227 | --- 
228 | --- 229 | ---
230 | --- require("dap.utils").pick_process({
231 | ---   label = function(proc) return string.format("Process: %s (PID: %d)", proc.name, proc.pid) end
232 | --- })
233 | --- 
234 | --- 235 | ---@param opts? dap.utils.pick_process.Opts 236 | function M.pick_process(opts) 237 | opts = opts or {} 238 | local cols = math.max(14, math.floor(vim.o.columns * 0.7)) 239 | local wordlimit = math.max(10, math.floor(cols / 3)) 240 | local label_fn = opts.label or function(proc) 241 | local name = trim_procname(proc.name, cols, wordlimit) 242 | return string.format("id=%d name=%s", proc.pid, name) 243 | end 244 | local procs = M.get_processes(opts) 245 | local co, ismain = coroutine.running() 246 | local ui = require("dap.ui") 247 | local pick = (co and not ismain) and ui.pick_one or ui.pick_one_sync 248 | local result = pick(procs, opts.prompt or "Select process: ", label_fn) 249 | return result and result.pid or require("dap").ABORT 250 | end 251 | 252 | 253 | ---@param msg string 254 | ---@param log_level? integer 255 | function M.notify(msg, log_level) 256 | if vim.in_fast_event() then 257 | vim.schedule(function() 258 | vim.notify(msg, log_level, {title = 'DAP'}) 259 | end) 260 | else 261 | vim.notify(msg, log_level, {title = 'DAP'}) 262 | end 263 | end 264 | 265 | 266 | ---@generic T 267 | ---@param x T? 268 | ---@param default T 269 | ---@return T 270 | function M.if_nil(x, default) 271 | return x == nil and default or x 272 | end 273 | 274 | 275 | ---@param opts {filter?: string|(fun(name: string):boolean), executables?: boolean} 276 | ---@return string[] 277 | local function get_files(path, opts) 278 | local filter = function(_) return true end 279 | if opts.filter then 280 | if type(opts.filter) == "string" then 281 | filter = function(filepath) 282 | return filepath:find(opts.filter) 283 | end 284 | elseif type(opts.filter) == "function" then 285 | filter = function(filepath) 286 | return opts.filter(filepath) 287 | end 288 | else 289 | error('opts.filter must be a string or a function') 290 | end 291 | end 292 | if opts.executables and vim.fs.dir then 293 | local f = filter 294 | local uv = vim.uv or vim.loop 295 | local user_execute = tonumber("00100", 8) 296 | filter = function(filepath) 297 | if not f(filepath) then 298 | return false 299 | end 300 | local stat = uv.fs_stat(filepath) 301 | return stat and bit.band(stat.mode, user_execute) == user_execute or false 302 | end 303 | end 304 | 305 | if vim.fs.dir then 306 | local files = {} 307 | for name, type in vim.fs.dir(path, { depth = 50 }) do 308 | if type == "file" then 309 | local filepath = vim.fs.joinpath(path, name) 310 | if filter(filepath) then 311 | table.insert(files, filepath) 312 | end 313 | end 314 | end 315 | return files 316 | end 317 | 318 | 319 | local cmd = {"find", path, "-type", "f"} 320 | if opts.executables then 321 | -- The order of options matters! 322 | table.insert(cmd, "-executable") 323 | end 324 | table.insert(cmd, "-follow") 325 | 326 | local output = vim.fn.system(cmd) 327 | return vim.tbl_filter(filter, vim.split(output, '\n')) 328 | end 329 | 330 | 331 | --- Show a prompt to select a file. 332 | --- Returns the path to the selected file. 333 | --- Requires nvim 0.10+ or a `find` executable 334 | --- 335 | --- Takes an optional `opts` table with following options: 336 | --- 337 | --- - filter string|fun: A lua pattern or function to filter the files. 338 | --- If a function the parameter is a string and it 339 | --- must return a boolean. Matches are included. 340 | --- 341 | --- - executables boolean: Show only executables. Defaults to true 342 | --- - path string: Path to search for files. Defaults to cwd 343 | --- 344 | ---
345 | --- require('dap.utils').pick_file({ filter = '.*%.py', executables = true })
346 | --- 
347 | ---@param opts? {filter?: string|(fun(name: string): boolean), executables?: boolean, path?: string} 348 | --- 349 | ---@return thread|string|dap.Abort 350 | function M.pick_file(opts) 351 | opts = opts or {} 352 | local executables = opts.executables == nil and true or opts.executables 353 | local path = opts.path or vim.fn.getcwd() 354 | local files = get_files(path, { 355 | filter = opts.filter, 356 | executables = executables 357 | }) 358 | local prompt = executables and "Select executable: " or "Select file: " 359 | local co, ismain = coroutine.running() 360 | local ui = require("dap.ui") 361 | local pick = (co and not ismain) and ui.pick_one or ui.pick_one_sync 362 | 363 | if not vim.endswith(path, "/") then 364 | path = path .. "/" 365 | end 366 | 367 | ---@param abspath string 368 | ---@return string 369 | local function relpath(abspath) 370 | local _, end_ = abspath:find(path) 371 | return end_ and abspath:sub(end_ + 1) or abspath 372 | end 373 | return pick(files, prompt, relpath) or require("dap").ABORT 374 | end 375 | 376 | 377 | --- Split an argument string on whitespace characters into a list, 378 | --- except if the whitespace is contained within single or double quotes. 379 | --- 380 | --- Leading and trailing whitespace is removed. 381 | --- 382 | --- Examples: 383 | --- 384 | --- ```lua 385 | --- require("dap.utils").splitstr("hello world") 386 | --- {"hello", "world"} 387 | --- ``` 388 | --- 389 | --- ```lua 390 | --- require("dap.utils").splitstr('a "quoted string" is preserved') 391 | --- {"a", "quoted string", "is, "preserved"} 392 | --- ``` 393 | --- 394 | --- Requires nvim 0.10+ 395 | --- 396 | --- @param str string 397 | --- @return string[] 398 | function M.splitstr(str) 399 | local lpeg = vim.lpeg 400 | local P, S, C = lpeg.P, lpeg.S, lpeg.C 401 | 402 | ---@param quotestr string 403 | ---@return vim.lpeg.Pattern 404 | local function qtext(quotestr) 405 | local quote = P(quotestr) 406 | local escaped_quote = P('\\') * quote 407 | return quote * C(((1 - P(quote)) + escaped_quote) ^ 0) * quote 408 | end 409 | str = str:match("^%s*(.*%S)") 410 | if not str or str == "" then 411 | return {} 412 | end 413 | 414 | local space = S(" \t\n\r") ^ 1 415 | local unquoted = C((1 - space) ^ 0) 416 | local element = qtext('"') + qtext("'") + unquoted 417 | local p = lpeg.Ct(element * (space * element) ^ 0) 418 | return lpeg.match(p, str) 419 | end 420 | 421 | 422 | return M 423 | -------------------------------------------------------------------------------- /nvim-dap-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | local _MODREV, _SPECREV = 'scm', '-1' 2 | rockspec_format = "3.0" 3 | package = 'nvim-dap' 4 | version = _MODREV .. _SPECREV 5 | 6 | description = { 7 | summary = 'Debug Adapter Protocol client implementation for Neovim.', 8 | detailed = [[ 9 | nvim-dap allows you to: 10 | 11 | * Launch an application to debug 12 | * Attach to running applications and debug them 13 | * Set breakpoints and step through code 14 | * Inspect the state of the application 15 | ]], 16 | labels = { 17 | 'neovim', 18 | 'plugin', 19 | 'debug-adapter-protocol', 20 | 'debugger', 21 | }, 22 | homepage = 'https://codeberg.org/mfussenegger/nvim-dap', 23 | license = 'GPL-3.0', 24 | } 25 | 26 | dependencies = { 27 | 'lua >= 5.1, < 5.4', 28 | } 29 | 30 | test_dependencies = { 31 | "nlua", 32 | } 33 | 34 | source = { 35 | url = 'git://codeberg.org/mfussenegger/nvim-dap', 36 | } 37 | 38 | build = { 39 | type = 'builtin', 40 | copy_directories = { 41 | 'doc', 42 | 'plugin', 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /plugin/dap.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | if not api.nvim_create_user_command then 3 | return 4 | end 5 | 6 | local cmd = api.nvim_create_user_command 7 | cmd('DapSetLogLevel', 8 | ---@param opts vim.api.keyset.create_user_command.command_args 9 | function(opts) 10 | require('dap').set_log_level(vim.trim(opts.args)) 11 | end, 12 | { 13 | nargs = 1, 14 | complete = function() 15 | return vim.tbl_keys(require('dap.log').levels) 16 | end 17 | } 18 | ) 19 | cmd('DapShowLog', function() require("dap._cmds").show_logs() end, { nargs = 0 }) 20 | cmd('DapContinue', function() require('dap').continue() end, { nargs = 0 }) 21 | cmd('DapToggleBreakpoint', function() require('dap').toggle_breakpoint() end, { nargs = 0 }) 22 | cmd('DapClearBreakpoints', function() require('dap').clear_breakpoints() end, { nargs = 0 }) 23 | cmd('DapToggleRepl', function() require('dap.repl').toggle() end, { nargs = 0 }) 24 | cmd('DapStepOver', function() require('dap').step_over() end, { nargs = 0 }) 25 | cmd('DapStepInto', function() require('dap').step_into() end, { nargs = 0 }) 26 | cmd('DapStepOut', function() require('dap').step_out() end, { nargs = 0 }) 27 | cmd('DapPause', function () require('dap').pause() end, { nargs = 0 }) 28 | cmd('DapTerminate', function() require('dap').terminate() end, { nargs = 0 }) 29 | cmd('DapDisconnect', function() require('dap').disconnect({ terminateDebuggee = false }) end, { nargs = 0 }) 30 | cmd('DapRestartFrame', function() require('dap').restart_frame() end, { nargs = 0 }) 31 | 32 | local function dapnew(args) 33 | return require("dap._cmds").new(args) 34 | end 35 | cmd("DapNew", dapnew, { 36 | nargs = "*", 37 | desc = "Start one or more new debug sessions", 38 | complete = function () 39 | return require("dap._cmds").new_complete() 40 | end 41 | }) 42 | 43 | cmd("DapEval", function(params) 44 | require("dap._cmds").eval(params) 45 | end, { 46 | nargs = 0, 47 | range = "%", 48 | bang = true, 49 | bar = true, 50 | desc = "Create a new window & buffer to evaluate expressions", 51 | }) 52 | 53 | 54 | if api.nvim_create_autocmd then 55 | local launchjson_group = api.nvim_create_augroup('dap-launch.json', { clear = true }) 56 | local pattern = '*/.vscode/launch.json' 57 | api.nvim_create_autocmd('BufNewFile', { 58 | group = launchjson_group, 59 | pattern = pattern, 60 | callback = function(args) 61 | require("dap._cmds").newlaunchjson(args) 62 | end 63 | }) 64 | 65 | api.nvim_create_autocmd("BufReadCmd", { 66 | group = api.nvim_create_augroup("dap-readcmds", { clear = true }), 67 | pattern = "dap-eval://*", 68 | callback = function() 69 | require("dap._cmds").bufread_eval() 70 | end, 71 | }) 72 | end 73 | -------------------------------------------------------------------------------- /spec/bad_adapter.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.exit(10) 4 | -------------------------------------------------------------------------------- /spec/breakpoints_spec.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | 3 | describe('breakpoints', function() 4 | 5 | require('dap') 6 | local breakpoints = require('dap.breakpoints') 7 | after_each(breakpoints.clear) 8 | 9 | it('can set normal breakpoints', function() 10 | breakpoints.set() 11 | local expected = { 12 | [1] = { 13 | { 14 | line = 1, 15 | }, 16 | }, 17 | } 18 | assert.are.same(expected, breakpoints.get()) 19 | breakpoints.set() -- still on the same line, so this replaces the previous one 20 | assert.are.same(expected, breakpoints.get()) 21 | end) 22 | 23 | it('can set a logpoint', function() 24 | breakpoints.set({ log_message = 'xs={xs}' }) 25 | local expected = { 26 | [1] = { 27 | { 28 | line = 1, 29 | logMessage = 'xs={xs}', 30 | }, 31 | }, 32 | } 33 | assert.are.same(expected, breakpoints.get()) 34 | end) 35 | 36 | it('can remove a breakpoint', function() 37 | local lnum = api.nvim_win_get_cursor(0)[1] 38 | breakpoints.toggle({ log_message = 'xs={xs}'}) 39 | local expected = { 40 | [1] = { 41 | { 42 | line = 1, 43 | logMessage = 'xs={xs}', 44 | }, 45 | }, 46 | } 47 | assert.are.same(expected, breakpoints.get()) 48 | breakpoints.remove(api.nvim_get_current_buf(), lnum) 49 | assert.are.same({}, breakpoints.get()) 50 | end) 51 | 52 | it('toggle adds bp if missing, otherwise removes', function() 53 | breakpoints.toggle() 54 | assert.are.same({{{line = 1}}}, breakpoints.get()) 55 | breakpoints.toggle() 56 | assert.are.same({}, breakpoints.get()) 57 | end) 58 | 59 | it('can convert breakpoints to qf_list items', function() 60 | local buf = api.nvim_get_current_buf() 61 | api.nvim_buf_set_lines(buf, 0, -1, true, {'Hello breakpoint'}) 62 | breakpoints.toggle({ condition = 'x > 10' }) 63 | assert.are.same( 64 | { 65 | { 66 | bufnr = 1, 67 | col = 0, 68 | lnum = 1, 69 | text = 'Hello breakpoint, Condition: x > 10' 70 | } 71 | }, 72 | breakpoints.to_qf_list(breakpoints.get()) 73 | ) 74 | 75 | local bps = { 76 | [buf] = { 77 | { 78 | line = 1, 79 | condition = "" 80 | }, 81 | } 82 | } 83 | assert.are.same( 84 | { 85 | { 86 | bufnr = buf, 87 | col = 0, 88 | lnum = 1, 89 | text = "Hello breakpoint" 90 | } 91 | }, 92 | breakpoints.to_qf_list(bps) 93 | ) 94 | end) 95 | end) 96 | -------------------------------------------------------------------------------- /spec/debugpy_spec.lua: -------------------------------------------------------------------------------- 1 | local luassert = require('luassert') 2 | local spy = require('luassert.spy') 3 | local venv_dir = os.tmpname() 4 | local dap = require('dap') 5 | 6 | local function get_num_handles() 7 | local pid = vim.fn.getpid() 8 | local output = vim.fn.system({"lsof", "-p"}, tostring(pid)) 9 | local lines = vim.split(output, "\n", { plain = true }) 10 | return #lines 11 | end 12 | 13 | 14 | describe('dap with debugpy', function() 15 | os.remove(venv_dir) 16 | if vim.fn.executable("uv") == 1 then 17 | os.execute(string.format("uv venv '%s'", venv_dir)) 18 | -- tmpfile could be on tmpfs in which case uv pip spits out hard-copy not-working warnings 19 | -- -> use link-mode=copy 20 | os.execute(string.format("uv --directory '%s' pip install --link-mode=copy debugpy", venv_dir)) 21 | else 22 | os.execute('python -m venv "' .. venv_dir .. '"') 23 | os.execute(venv_dir .. '/bin/python -m pip install debugpy') 24 | end 25 | after_each(function() 26 | dap.terminate() 27 | require('dap.breakpoints').clear() 28 | end) 29 | 30 | it('Basic debugging flow', function() 31 | local breakpoints = require('dap.breakpoints') 32 | dap.adapters.python = { 33 | type = 'executable', 34 | command = venv_dir .. '/bin/python', 35 | args = {'-m', 'debugpy.adapter'}, 36 | options = { 37 | cwd = venv_dir, 38 | } 39 | } 40 | local program = vim.fn.expand('%:p:h') .. '/spec/example.py' 41 | local config = { 42 | type = 'python', 43 | request = 'launch', 44 | name = 'Launch file', 45 | program = program, 46 | dummy_payload = { 47 | cwd = '${workspaceFolder}', 48 | ['key_with_${workspaceFolder}'] = 'value', 49 | numbers = {1, 2, 3, 4}, 50 | strings = {'a', 'b', 'c'}, 51 | } 52 | } 53 | local bp_lnum = 8 54 | local bufnr = vim.fn.bufadd(program) 55 | vim.fn.bufload(bufnr) 56 | breakpoints.set({}, bufnr, bp_lnum) 57 | local events = {} 58 | local dummy_payload = nil 59 | dap.listeners.after.event_initialized['dap.tests'] = function(session) 60 | events.initialized = true 61 | ---@diagnostic disable-next-line: undefined-field 62 | dummy_payload = session.config.dummy_payload 63 | end 64 | dap.listeners.after.setBreakpoints['dap.tests'] = function(_, _, resp) 65 | events.setBreakpoints = resp 66 | end 67 | dap.listeners.after.event_stopped['dap.tests'] = function(session) 68 | vim.wait(1000, function() 69 | return session.stopped_thread_id ~= nil 70 | end) 71 | dap.continue() 72 | events.stopped = true 73 | end 74 | 75 | local num_handles = get_num_handles() 76 | 77 | local launch = spy.on(dap, 'launch') 78 | dap.run(config, { filetype = 'python' }) 79 | vim.wait(1000, function() return dap.session() == nil end, 100) 80 | assert.are.same({ 81 | initialized = true, 82 | setBreakpoints = { 83 | breakpoints = { 84 | { 85 | id = 0, 86 | line = bp_lnum, 87 | source = { 88 | name = 'example.py', 89 | path = program, 90 | }, 91 | verified = true 92 | }, 93 | }, 94 | }, 95 | stopped = true, 96 | }, events) 97 | 98 | -- variable must expand to concrete value 99 | assert(dummy_payload) 100 | assert.are_not.same(dummy_payload.cwd, '${workspaceFolder}') 101 | assert.are.same(dummy_payload.numbers, {1, 2, 3, 4}) 102 | assert.are.same(dummy_payload.strings, {'a', 'b', 'c'}) 103 | assert.are.same(dummy_payload['key_with_' .. vim.fn.getcwd()], 'value') 104 | 105 | -- ensure `called_with` below passes 106 | config.dummy_payload = dummy_payload 107 | 108 | luassert.spy(launch).was.called_with(dap.adapters.python, config, { cwd = venv_dir, filetype = 'python' }) 109 | 110 | dap.terminate() 111 | vim.wait(1000, function() return dap.session() == nil end) 112 | 113 | assert.are.same(num_handles, get_num_handles()) 114 | end) 115 | end) 116 | vim.fn.delete(venv_dir, 'rf') 117 | -------------------------------------------------------------------------------- /spec/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | 5 | 6 | def main(): 7 | cwd = os.getcwd() 8 | print(cwd) 9 | a = 10 10 | b = 30 11 | return print(a + b) 12 | 13 | 14 | if __name__ == '__main__': 15 | main() 16 | -------------------------------------------------------------------------------- /spec/ext_vscode_spec.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: duplicate-set-field 2 | local ui_input = vim.ui.input 3 | local vscode = require('dap.ext.vscode') 4 | describe('dap.ext.vscode', function() 5 | after_each(function() 6 | vim.ui.input = ui_input 7 | end) 8 | 9 | it('can load launch.json file and map adapter type to filetypes', function() 10 | local dap = require('dap') 11 | vscode.load_launchjs('spec/launch.json', { bar = { 'c', 'cpp' } }) 12 | assert.are.same(3, vim.tbl_count(dap.configurations)) 13 | assert.are.same({ { type = 'java', request = 'launch', name = "java test" }, }, dap.configurations.java) 14 | assert.are.same({ { type = 'bar', request = 'attach', name = "bar test" }, }, dap.configurations.c) 15 | assert.are.same({ { type = 'bar', request = 'attach', name = "bar test" }, }, dap.configurations.cpp) 16 | end) 17 | 18 | it('supports promptString input', function() 19 | local prompt 20 | local default 21 | vim.ui.input = function(opts, on_input) 22 | prompt = opts.prompt 23 | default = opts.default 24 | on_input('Fake input') 25 | end 26 | local jsonstr = [[ 27 | { 28 | "configurations": [ 29 | { 30 | "type": "dummy", 31 | "request": "launch", 32 | "name": "Dummy", 33 | "program": "${workspaceFolder}/${input:myInput}" 34 | } 35 | ], 36 | "inputs": [ 37 | { 38 | "id": "myInput", 39 | "type": "promptString", 40 | "description": "Your input", 41 | "default": "the default value" 42 | } 43 | ] 44 | } 45 | ]] 46 | local configs = vscode._load_json(jsonstr) 47 | local ok = false 48 | local result 49 | coroutine.wrap(function() 50 | local conf = configs[1]() 51 | result = conf.program 52 | ok = true 53 | end)() 54 | vim.wait(1000, function() return ok end) 55 | assert.are.same("${workspaceFolder}/Fake input", result) 56 | assert.are.same("Your input: ", prompt) 57 | assert.are.same("the default value", default) 58 | end) 59 | 60 | it('supports pickString input', function() 61 | local options 62 | local opts 63 | local label 64 | vim.ui.select = function(options_, opts_, on_choice) 65 | options = options_ 66 | opts = opts_ 67 | label = opts_.format_item(options_[1]) 68 | on_choice(options_[1]) 69 | end 70 | local jsonstr = [[ 71 | { 72 | "configurations": [ 73 | { 74 | "type": "dummy", 75 | "request": "launch", 76 | "name": "Dummy", 77 | "program": "${workspaceFolder}/${input:my_input}" 78 | } 79 | ], 80 | "inputs": [ 81 | { 82 | "id": "my_input", 83 | "type": "pickString", 84 | "options": ["one", "two", "three"], 85 | "description": "Select input" 86 | } 87 | ] 88 | } 89 | ]] 90 | local configs = vscode._load_json(jsonstr) 91 | local ok = false 92 | local result 93 | coroutine.wrap(function() 94 | local config = configs[1]() 95 | result = config.program 96 | ok = true 97 | end)() 98 | vim.wait(1000, function() return ok end) 99 | assert.are.same(true, ok, "coroutine must finish") 100 | assert.are.same("one", label) 101 | assert.are.same("${workspaceFolder}/one", result) 102 | assert.are.same("Select input", opts.prompt) 103 | assert.are.same({"one", "two", "three"}, options) 104 | end) 105 | 106 | it('inputs can be used in arrays or dicts', function() 107 | vim.fn.input = function(opts) 108 | return opts.default 109 | end 110 | local jsonstr = [[ 111 | { 112 | "configurations": [ 113 | { 114 | "type": "dummy", 115 | "request": "launch", 116 | "name": "Dummy", 117 | "args": ["one", "${input:myInput}", "three"] 118 | } 119 | ], 120 | "inputs": [ 121 | { 122 | "id": "myInput", 123 | "type": "promptString", 124 | "description": "Your input", 125 | "default": "the default value" 126 | } 127 | ] 128 | } 129 | ]] 130 | local config = vscode._load_json(jsonstr)[1] 131 | assert.are.same(3, #config.args) 132 | assert.are.same("one", config.args[1]) 133 | assert.are.same("${input:myInput}", config.args[2]) 134 | assert.are.same("three", config.args[3]) 135 | local ok = false 136 | coroutine.wrap(function() 137 | config = config() 138 | ok = true 139 | end)() 140 | vim.wait(1000, function() return ok end) 141 | assert.are.same("the default value", config.args[2]) 142 | end) 143 | it('can use two inputs within one property', function() 144 | vim.fn.input = function(opts) 145 | return opts.default 146 | end 147 | local jsonstr = [[ 148 | { 149 | "configurations": [ 150 | { 151 | "type": "dummy", 152 | "request": "launch", 153 | "name": "Dummy", 154 | "program": "${input:input1}-${input:input2}" 155 | } 156 | ], 157 | "inputs": [ 158 | { 159 | "id": "input1", 160 | "type": "promptString", 161 | "description": "first input", 162 | "default": "one" 163 | }, 164 | { 165 | "id": "input2", 166 | "type": "promptString", 167 | "description": "second input", 168 | "default": "two" 169 | } 170 | ] 171 | } 172 | ]] 173 | local config = vscode._load_json(jsonstr)[1] 174 | local ok = false 175 | coroutine.wrap(function() 176 | ok, config = true, config() 177 | end)() 178 | vim.wait(1000, function() return ok end) 179 | assert.are.same("one-two", config.program) 180 | end) 181 | 182 | it('supports OS specific properties which are lifted to top-level', function() 183 | if vim.loop.os_uname().sysname == 'Linux' then 184 | local jsonstr = [[ 185 | { 186 | "configurations": [ 187 | { 188 | "type": "dummy", 189 | "request": "launch", 190 | "name": "Dummy", 191 | "linux": { 192 | "foo": "bar" 193 | } 194 | } 195 | ] 196 | } 197 | ]] 198 | local config = vscode._load_json(jsonstr)[1] 199 | assert.are.same("bar", config.foo) 200 | end 201 | end) 202 | 203 | it('supports promptString without default value', function() 204 | local prompt 205 | local default 206 | vim.fn.input = function(opts) 207 | prompt = opts.prompt 208 | default = opts.default 209 | return 'Fake input' 210 | end 211 | local jsonstr = [[ 212 | { 213 | "configurations": [ 214 | { 215 | "type": "dummy", 216 | "request": "launch", 217 | "name": "Dummy", 218 | "program": "${workspaceFolder}/${input:myInput}" 219 | } 220 | ], 221 | "inputs": [ 222 | { 223 | "id": "myInput", 224 | "type": "promptString", 225 | "description": "Your input" 226 | } 227 | ] 228 | } 229 | ]] 230 | local configs = vscode._load_json(jsonstr) 231 | local config 232 | coroutine.wrap(function() 233 | config = configs[1]() 234 | end)() 235 | vim.wait(1000, function() return config ~= nil end) 236 | assert.are.same("${workspaceFolder}/Fake input", config.program) 237 | assert.are.same("Your input: ", prompt) 238 | assert.are.same("", default) 239 | end) 240 | 241 | it('supports pickString with options', function() 242 | local opts 243 | local label 244 | vim.ui.select = function(options_, opts_, on_choice) 245 | opts = opts_ 246 | label = opts_.format_item(options_[1]) 247 | on_choice(options_[1]) 248 | end 249 | local jsonstr = [[ 250 | { 251 | "configurations": [ 252 | { 253 | "type": "dummy", 254 | "request": "launch", 255 | "name": "Dummy", 256 | "program": "${workspaceFolder}/${input:my_input}" 257 | } 258 | ], 259 | "inputs": [ 260 | { 261 | "id": "my_input", 262 | "type": "pickString", 263 | "options": [ 264 | { "label": "First value", "value": "one" }, 265 | { "label": "Second value", "value": "two" } 266 | ], 267 | "description": "Select input" 268 | } 269 | ] 270 | } 271 | ]] 272 | local configs = vscode._load_json(jsonstr) 273 | local config 274 | coroutine.wrap(function() 275 | config = configs[1]() 276 | end)() 277 | vim.wait(1000, function() return config ~= nil end) 278 | assert(config, "coroutine must finish") 279 | assert.are.same("${workspaceFolder}/one", config.program) 280 | assert.are.same("Select input", opts.prompt) 281 | assert.are.same("First value", label) 282 | end) 283 | 284 | it('supports pickString with options, nothing selected', function() 285 | vim.ui.select = function(_, _, on_choice) 286 | on_choice(nil) 287 | end 288 | local jsonstr = [[ 289 | { 290 | "configurations": [ 291 | { 292 | "type": "dummy", 293 | "request": "launch", 294 | "name": "Dummy", 295 | "program": "${workspaceFolder}/${input:my_input}" 296 | } 297 | ], 298 | "inputs": [ 299 | { 300 | "id": "my_input", 301 | "type": "pickString", 302 | "options": [ 303 | { "label": "First value", "value": "one" }, 304 | { "label": "Second value", "value": "two" } 305 | ], 306 | "description": "Select input" 307 | } 308 | ] 309 | } 310 | ]] 311 | local configs = vscode._load_json(jsonstr) 312 | local config 313 | coroutine.wrap(function() 314 | config = configs[1]() 315 | end)() 316 | vim.wait(1000, function() return config ~= nil end) 317 | assert(config, "coroutine must finish") 318 | -- input defaults to '' 319 | assert.are.same("${workspaceFolder}/", config.program) 320 | end) 321 | 322 | it("evaluates input once per config use", function() 323 | local prompt 324 | local default 325 | local calls = 0 326 | vim.fn.input = function(opts) 327 | prompt = opts.prompt 328 | default = opts.default 329 | calls = calls + 1 330 | return 'Fake input' 331 | end 332 | local jsonstr = [[ 333 | { 334 | "configurations": [ 335 | { 336 | "type": "dummy", 337 | "request": "launch", 338 | "name": "Dummy", 339 | "program": "${input:myInput}", 340 | "args": [ 341 | "${input:myInput}", 342 | "foo", 343 | "${input:myInput}" 344 | ] 345 | } 346 | ], 347 | "inputs": [ 348 | { 349 | "id": "myInput", 350 | "type": "promptString", 351 | "description": "Your input" 352 | } 353 | ] 354 | } 355 | ]] 356 | local configs = vscode._load_json(jsonstr) 357 | local config 358 | coroutine.wrap(function() 359 | config = configs[1]() 360 | end)() 361 | vim.wait(1000, function() return config ~= nil end) 362 | 363 | assert.are.same(calls, 1) 364 | assert.are.same("Your input: ", prompt) 365 | assert.are.same("", default) 366 | end) 367 | 368 | it('keeps unsupported input types as is', function() 369 | local jsonstr = [[ 370 | { 371 | "configurations": [ 372 | { 373 | "type": "dummy", 374 | "request": "launch", 375 | "name": "Dummy", 376 | "program": "${input:myCommand}" 377 | } 378 | ], 379 | "inputs": [ 380 | { 381 | "id": "myCommand", 382 | "type": "command", 383 | "command": "shellCommand.execute" 384 | } 385 | ] 386 | } 387 | ]] 388 | local configs = vscode._load_json(jsonstr) 389 | local ok = false 390 | local result 391 | coroutine.wrap(function() 392 | local conf = configs[1]() 393 | result = conf.program 394 | ok = true 395 | end)() 396 | vim.wait(1000, function() return ok end) 397 | assert.are.same("${input:myCommand}", result) 398 | end) 399 | end) 400 | -------------------------------------------------------------------------------- /spec/helpers.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local dap = require("dap") 3 | local assert = require("luassert") 4 | 5 | function M.wait(predicate, msg) 6 | vim.wait(1000, predicate) 7 | local result = predicate() 8 | if type(msg) == "string" then 9 | assert.are_not.same(false, result, msg) 10 | else 11 | assert.are_not.same(false, result, msg and vim.inspect(msg()) or nil) 12 | end 13 | assert.are_not.same(nil, result) 14 | end 15 | 16 | 17 | ---@param command string 18 | ---@return string[] commands received 19 | function M.wait_for_response(server, command) 20 | local function received_command() 21 | for _, response in pairs(server.spy.responses) do 22 | if response.command == command then 23 | return true 24 | end 25 | end 26 | return false 27 | end 28 | local function get_command(x) 29 | return x.command 30 | end 31 | M.wait(received_command, function() 32 | if next(server.spy.responses) then 33 | local responses = vim.tbl_map(get_command, server.spy.responses) 34 | return string.format("Expected `%s` in: %s", command, table.concat(responses, ", ")) 35 | else 36 | return "Server sent no responses, expected: " .. command 37 | end 38 | end) 39 | return vim.tbl_map(get_command, server.spy.responses) 40 | end 41 | 42 | 43 | ---@param conf dap.Configuration 44 | ---@param server table? 45 | ---@return dap.Session 46 | function M.run_and_wait_until_initialized(conf, server) 47 | dap.run(conf) 48 | vim.wait(5000, function() 49 | local session = dap.session() 50 | -- wait for initialize and launch requests 51 | return ( 52 | session ~= nil 53 | and session.initialized == true 54 | and (server == nil or #server.spy.requests == 2) 55 | ) 56 | end, 100) 57 | return assert(dap.session(), "Must have session after run") 58 | end 59 | 60 | return M 61 | -------------------------------------------------------------------------------- /spec/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "java", 6 | "request": "launch", 7 | "name": "java test" 8 | }, 9 | { 10 | "type": "bar", 11 | "request": "attach", 12 | "name": "bar test" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /spec/pipe_spec.lua: -------------------------------------------------------------------------------- 1 | local dap = require('dap') 2 | 3 | local function wait(predicate, msg) 4 | vim.wait(1000, predicate) 5 | local result = predicate() 6 | assert.are_not.same(false, result, msg and vim.inspect(msg()) or nil) 7 | assert.are_not.same(nil, result) 8 | end 9 | 10 | 11 | describe('dap with fake pipe server', function() 12 | local server 13 | before_each(function() 14 | server = require('spec.server').spawn({ new_sock = vim.loop.new_pipe }) 15 | dap.adapters.dummy = server.adapter 16 | end) 17 | after_each(function() 18 | server.stop() 19 | dap.close() 20 | require('dap.breakpoints').clear() 21 | wait(function() return dap.session() == nil end) 22 | end) 23 | it("can connect and terminate", function() 24 | local config = { 25 | type = 'dummy', 26 | request = 'launch', 27 | name = 'Launch file', 28 | } 29 | dap.run(config) 30 | wait(function() 31 | local session = dap.session() 32 | return session and session.initialized and #server.spy.requests == 2 33 | end) 34 | local session = dap.session() 35 | assert.is_not_nil(session) 36 | assert.are.same(1, server.client.num_connected) 37 | dap.terminate() 38 | wait(function() 39 | return ( 40 | dap.session() == nil 41 | and #server.spy.events == 2 42 | and server.spy.events[2].event == "terminated" 43 | and server.client.num_connected == 0 44 | ) 45 | end, function() return server.spy.events end) 46 | assert.is_nil(dap.session()) 47 | assert(session) 48 | assert.is_true(session.closed) 49 | ---@diagnostic disable-next-line: invisible 50 | assert.is_true(session.handle:is_closing()) 51 | assert.are.same(0, server.client.num_connected) 52 | end) 53 | end) 54 | -------------------------------------------------------------------------------- /spec/progress_spec.lua: -------------------------------------------------------------------------------- 1 | describe('progress', function() 2 | local progress = require('dap.progress') 3 | 4 | after_each(progress.reset) 5 | 6 | it('Polling on empty buffer returns nil, report and poll after', function() 7 | assert.are.same(nil, progress.poll_msg()) 8 | assert.are.same(nil, progress.poll_msg()) 9 | 10 | progress.report('hello') 11 | assert.are.same('hello', progress.poll_msg()) 12 | end) 13 | 14 | it('Interleave report and poll', function() 15 | progress.report('one') 16 | progress.report('two') 17 | assert.are.same('one', progress.poll_msg()) 18 | progress.report('three') 19 | assert.are.same('two', progress.poll_msg()) 20 | assert.are.same('three', progress.poll_msg()) 21 | end) 22 | it('Oldest messages are overridden once size limit is reached', function() 23 | for i = 1, 11 do 24 | progress.report(tostring(i)) 25 | end 26 | assert.are.same('2', progress.poll_msg()) 27 | assert.are.same('3', progress.poll_msg()) 28 | progress.report('a') 29 | assert.are.same('4', progress.poll_msg()) 30 | end) 31 | end) 32 | -------------------------------------------------------------------------------- /spec/repl_spec.lua: -------------------------------------------------------------------------------- 1 | local dap = require("dap") 2 | local api = vim.api 3 | local helpers = require("spec.helpers") 4 | 5 | describe('dap.repl', function() 6 | it("append doesn't add newline with newline = false", function() 7 | local repl = require('dap.repl') 8 | local buf = repl.open() 9 | repl.append('foo', nil, { newline = false }) 10 | repl.append('bar', nil, { newline = false }) 11 | repl.append('\nbaz\n', nil, { newline = false }) 12 | 13 | local lines = api.nvim_buf_get_lines(buf, 0, -1, true) 14 | assert.are.same({'foobar', 'baz', ''}, lines) 15 | end) 16 | end) 17 | 18 | 19 | 20 | ---@param replline string 21 | ---@param completion_results dap.CompletionItem[] 22 | local function prepare_session(server, replline, completion_results) 23 | server.client.initialize = function(self, request) 24 | self:send_response(request, { 25 | supportsCompletionsRequest = true, 26 | }) 27 | self:send_event("initialized", {}) 28 | end 29 | server.client.completions = function(self, request) 30 | self:send_response(request, { 31 | targets = completion_results 32 | }) 33 | end 34 | local config = { 35 | type = "dummy", 36 | request = "launch", 37 | name = "Launch file", 38 | } 39 | helpers.run_and_wait_until_initialized(config, server) 40 | local repl = require("dap.repl") 41 | local bufnr, win = repl.open() 42 | api.nvim_set_current_buf(bufnr) 43 | api.nvim_set_current_win(win) 44 | api.nvim_buf_set_lines(bufnr, 0, -1, true, {replline}) 45 | api.nvim_win_set_cursor(win, {1, #replline}) 46 | end 47 | 48 | 49 | local function getcompletion_results(server) 50 | local captured_startcol = nil 51 | local captured_items = nil 52 | 53 | ---@diagnostic disable-next-line, duplicate-set-field: 211 54 | function vim.fn.complete(startcol, items) 55 | captured_startcol = startcol 56 | captured_items = items 57 | end 58 | 59 | local repl = require("dap.repl") 60 | repl.omnifunc(1, "") 61 | 62 | helpers.wait_for_response(server, "completions") 63 | helpers.wait(function() return captured_startcol ~= nil end) 64 | return captured_startcol, captured_items 65 | end 66 | 67 | 68 | describe("dap.repl completion", function() 69 | local server 70 | before_each(function() 71 | server = require("spec.server").spawn() 72 | dap.adapters.dummy = server.adapter 73 | end) 74 | after_each(function() 75 | server.stop() 76 | dap.close() 77 | require('dap.breakpoints').clear() 78 | helpers.wait(function() return dap.session() == nil end, "session should become nil") 79 | end) 80 | it("Uses start position from completion response", function() 81 | prepare_session(server, "dap> com. ", { 82 | { 83 | label = "com.sun.org.apache.xpath", 84 | number = 0, 85 | sortText = "999999183", 86 | start = 0, 87 | text = "sun.org.apache.xpath", 88 | type = "module" 89 | } 90 | }) 91 | 92 | local startcol, items = getcompletion_results(server) 93 | assert.are.same(#"dap> com." + 1, startcol) 94 | local expected_items = { 95 | { 96 | abbr = "com.sun.org.apache.xpath", 97 | dup = 0, 98 | icase = 1, 99 | word = "sun.org.apache.xpath" 100 | } 101 | } 102 | assert.are.same(expected_items, items) 103 | end) 104 | 105 | it("Can handle responses without explicit start column and prefix overlap", function() 106 | prepare_session(server, "dap> info b", { 107 | { 108 | label = "info b", 109 | length = 6, 110 | }, 111 | { 112 | label = "info bookmarks", 113 | length = 14, 114 | }, 115 | { 116 | label = "info breakpoints", 117 | length = 16, 118 | } 119 | }) 120 | 121 | local startcol, items = getcompletion_results(server) 122 | assert.are.same(#"dap> " + 1 , startcol) 123 | local expected_items = { 124 | { 125 | abbr = 'info b', 126 | dup = 0, 127 | icase = 1, 128 | word = 'info b', 129 | }, 130 | { 131 | abbr = 'info bookmarks', 132 | dup = 0, 133 | icase = 1, 134 | word = 'info bookmarks', 135 | }, 136 | { 137 | abbr = 'info breakpoints', 138 | dup = 0, 139 | icase = 1, 140 | word = 'info breakpoints', 141 | } 142 | } 143 | assert.are.same(expected_items, items) 144 | end) 145 | 146 | it("Can handle responses with explicit start column and prefix overlap", function() 147 | prepare_session(server, "dap> `info b", { 148 | { 149 | label = "`info bookmarks", 150 | length = 15, 151 | start = 0, 152 | type = "text" 153 | }, 154 | { 155 | label = "`info breakpoints", 156 | length = 17, 157 | start = 0, 158 | type = "text" 159 | } 160 | }) 161 | 162 | local startcol, items = getcompletion_results(server) 163 | assert.are.same(#"dap> " + 1, startcol) 164 | local expected_items = { 165 | { 166 | abbr = '`info bookmarks', 167 | dup = 0, 168 | icase = 1, 169 | word = '`info bookmarks', 170 | }, 171 | { 172 | abbr = '`info breakpoints', 173 | dup = 0, 174 | icase = 1, 175 | word = '`info breakpoints', 176 | } 177 | } 178 | assert.are.same(expected_items, items) 179 | end) 180 | end) 181 | -------------------------------------------------------------------------------- /spec/run_server.lua: -------------------------------------------------------------------------------- 1 | local server = require('spec.server') 2 | local opts = { 3 | port = _G.DAP_PORT 4 | } 5 | io.stdout:setvbuf("no") 6 | io.stderr:setvbuf("no") 7 | local debug_adapter = server.spawn(opts) 8 | io.stderr:write("Listening on port=" .. debug_adapter.adapter.port .. "\n") 9 | local original_disconnect = debug_adapter.client.disconnect 10 | debug_adapter.client.disconnect = function(self, request) 11 | original_disconnect(self, request) 12 | os.exit(0) 13 | end 14 | vim.loop.run() 15 | vim.loop.walk(vim.loop.close) 16 | vim.loop.run() 17 | -------------------------------------------------------------------------------- /spec/server.lua: -------------------------------------------------------------------------------- 1 | local uv = vim.loop 2 | local rpc = require('dap.rpc') 3 | 4 | local json_decode = vim.json.decode 5 | local json_encode = vim.json.encode 6 | 7 | local M = {} 8 | local Client = {} 9 | 10 | 11 | function Client:send_err_response(request, message, error) 12 | self.seq = request.seq + 1 13 | local payload = { 14 | seq = self.seq, 15 | type = 'response', 16 | command = request.command, 17 | success = false, 18 | request_seq = request.seq, 19 | message = message, 20 | body = { 21 | error = error, 22 | }, 23 | } 24 | if self.socket then 25 | self.socket:write(rpc.msg_with_content_length(json_encode(payload))) 26 | end 27 | table.insert(self.spy.responses, payload) 28 | end 29 | 30 | 31 | function Client:send_response(request, body) 32 | self.seq = request.seq + 1 33 | local payload = { 34 | seq = self.seq, 35 | type = 'response', 36 | command = request.command, 37 | success = true, 38 | request_seq = request.seq, 39 | body = body, 40 | } 41 | if self.socket then 42 | self.socket:write(rpc.msg_with_content_length(json_encode(payload))) 43 | end 44 | table.insert(self.spy.responses, payload) 45 | end 46 | 47 | 48 | function Client:send_event(event, body) 49 | self.seq = self.seq + 1 50 | local payload = { 51 | seq = self.seq, 52 | type = 'event', 53 | event = event, 54 | body = body, 55 | } 56 | self.socket:write(rpc.msg_with_content_length(json_encode(payload))) 57 | table.insert(self.spy.events, payload) 58 | end 59 | 60 | 61 | ---@param command string 62 | ---@param arguments any 63 | function Client:send_request(command, arguments) 64 | self.seq = self.seq + 1 65 | local payload = { 66 | seq = self.seq, 67 | type = "request", 68 | command = command, 69 | arguments = arguments, 70 | } 71 | self.socket:write(rpc.msg_with_content_length(json_encode(payload))) 72 | end 73 | 74 | 75 | function Client:handle_input(body) 76 | local request = json_decode(body) 77 | table.insert(self.spy.requests, request) 78 | local handler = self[request.command] 79 | if handler then 80 | handler(self, request) 81 | else 82 | print('no handler for ' .. request.command) 83 | end 84 | end 85 | 86 | 87 | function Client:initialize(request) 88 | self:send_response(request, {}) 89 | self:send_event('initialized', {}) 90 | end 91 | 92 | 93 | function Client:disconnect(request) 94 | self:send_event('terminated', {}) 95 | self:send_response(request, {}) 96 | end 97 | 98 | 99 | function Client:terminate(request) 100 | self:send_event('terminated', {}) 101 | self:send_response(request, {}) 102 | end 103 | 104 | 105 | function Client:launch(request) 106 | self:send_response(request, {}) 107 | end 108 | 109 | 110 | function M.spawn(opts) 111 | opts = opts or {} 112 | opts.mode = opts.mode or "tcp" 113 | local spy = { 114 | requests = {}, 115 | responses = {}, 116 | events = {}, 117 | } 118 | function spy.clear() 119 | spy.requests = {} 120 | spy.responses = {} 121 | spy.events = {} 122 | end 123 | local adapter 124 | local server 125 | if opts.mode == "tcp" then 126 | server = assert(uv.new_tcp()) 127 | assert(server:bind("127.0.0.1", opts.port or 0), "Must be able to bind to ip:port") 128 | adapter = { 129 | type = "server", 130 | host = "127.0.0.1", 131 | port = server:getsockname().port, 132 | options = { 133 | disconnect_timeout_sec = 0.1 134 | } 135 | } 136 | else 137 | server = assert(uv.new_pipe()) 138 | local pipe = os.tmpname() 139 | os.remove(pipe) 140 | assert(server:bind(pipe), "Must be able to bind to pipe") 141 | adapter = { 142 | type = "pipe", 143 | pipe = pipe, 144 | options = { 145 | disconnect_timeout_sec = 0.1 146 | } 147 | } 148 | end 149 | local client = { 150 | seq = 0, 151 | handlers = {}, 152 | spy = spy, 153 | num_connected = 0, 154 | } 155 | setmetatable(client, {__index = Client}) 156 | server:listen(128, function(err) 157 | assert(not err, err) 158 | client.num_connected = client.num_connected + 1 159 | local socket = assert(opts.mode == "tcp" and uv.new_tcp() or uv.new_pipe()) 160 | client.socket = socket 161 | server:accept(socket) 162 | local function on_chunk(body) 163 | client:handle_input(body) 164 | end 165 | local function on_eof() 166 | client.num_connected = client.num_connected - 1 167 | end 168 | socket:read_start(rpc.create_read_loop(on_chunk, on_eof)) 169 | end) 170 | return { 171 | client = client, 172 | adapter = adapter, 173 | spy = spy, 174 | stop = function() 175 | if opts.mode ~= "tcp" then 176 | pcall(os.remove, adapter.pipe) 177 | end 178 | if client.socket then 179 | client.socket:shutdown(function() 180 | client.socket:close() 181 | client.socket = nil 182 | end) 183 | end 184 | end, 185 | } 186 | end 187 | 188 | 189 | return M 190 | -------------------------------------------------------------------------------- /spec/server_executable_spec.lua: -------------------------------------------------------------------------------- 1 | local helpers = require("spec.helpers") 2 | local wait = helpers.wait 3 | local run_and_wait_until_initialized = helpers.run_and_wait_until_initialized 4 | local uv = vim.uv or vim.loop 5 | 6 | local spec_root = vim.fn.fnamemodify(debug.getinfo(1, "S").source:match("@?(.*/)"), ":h:p") 7 | 8 | local dap = require('dap') 9 | dap.adapters.dummy = { 10 | type = 'server', 11 | port = '${port}', 12 | executable = { 13 | command = vim.v.progpath, 14 | args = { 15 | '-Es', 16 | '-u', 'NONE', 17 | '--headless', 18 | '-c', 'set rtp+=.', 19 | '-c', 'lua DAP_PORT=${port}', 20 | '-c', ('luafile %s/run_server.lua'):format(spec_root) 21 | }, 22 | } 23 | } 24 | 25 | describe('server executable', function() 26 | before_each(function() 27 | end) 28 | after_each(function() 29 | dap.terminate() 30 | vim.wait(100, function() 31 | return dap.session() == nil 32 | end) 33 | assert.are.same(nil, dap.session()) 34 | end) 35 | 36 | it('Starts adapter executable and connects', function() 37 | local config = { 38 | type = 'dummy', 39 | request = 'launch', 40 | name = 'Launch', 41 | } 42 | local log = require("dap.log").create_logger("dap-dummy-stderr.log") 43 | local session = run_and_wait_until_initialized(config) 44 | local adapter = session.adapter --[[@as dap.ServerAdapter]] 45 | assert.are.same(adapter.port, tonumber(adapter.executable.args[8]:match("(%d+)$"))) 46 | assert.are.same(true, session.initialized, "initialized must be true") 47 | 48 | local expected_msg = "Listening on port=" .. adapter.port .. "\n" 49 | log._file:flush() 50 | local f = io.open(log._path, "r") 51 | assert(f) 52 | local content = f:read("*a") 53 | f:close() 54 | assert.are.same(expected_msg, content) 55 | 56 | dap.terminate() 57 | wait(function() return dap.session() == nil end, "Must remove session") 58 | wait(function() return uv.fs_stat(log._path) == nil end) 59 | assert.is_nil(dap.session()) 60 | assert.is_nil(uv.fs_stat(log._path)) 61 | end) 62 | 63 | it('Clears session after closing', function() 64 | local config = { 65 | type = 'dummy', 66 | request = 'launch', 67 | name = 'Launch', 68 | } 69 | local session = run_and_wait_until_initialized(config) 70 | assert.are.same(true, session.initialized, "initialized must be true") 71 | dap.close() 72 | wait(function() return dap.session() == nil end, "Must remove session") 73 | assert.are.same(nil, dap.session()) 74 | end) 75 | end) 76 | -------------------------------------------------------------------------------- /spec/sessions_spec.lua: -------------------------------------------------------------------------------- 1 | local dap = require('dap') 2 | 3 | 4 | local function wait(predicate, msg) 5 | vim.wait(1000, predicate) 6 | local result = predicate() 7 | assert.are_not.same(false, result, msg and vim.inspect(msg()) or nil) 8 | assert.are_not.same(nil, result) 9 | end 10 | 11 | 12 | local function run_and_wait_until_initialized(conf, server) 13 | dap.run(conf) 14 | wait(function() 15 | local session = dap.session() 16 | -- wait for initialize and launch requests 17 | return (session and session.initialized and #server.spy.requests == 2) 18 | end) 19 | return assert(dap.session(), "Must have session after dap.run") 20 | end 21 | 22 | 23 | describe('sessions', function() 24 | local srv1 25 | local srv2 26 | 27 | before_each(function() 28 | srv1 = require('spec.server').spawn() 29 | srv2 = require('spec.server').spawn() 30 | dap.adapters.dummy1 = srv1.adapter 31 | dap.adapters.dummy2 = srv2.adapter 32 | end) 33 | after_each(function() 34 | srv1.stop() 35 | srv2.stop() 36 | dap.terminate() 37 | dap.terminate() 38 | end) 39 | it('can run multiple sessions', function() 40 | local conf1 = { 41 | type = 'dummy1', 42 | request = 'launch', 43 | name = 'Launch file 1', 44 | } 45 | local conf2 = { 46 | type = 'dummy2', 47 | request = 'launch', 48 | name = 'Launch file 2', 49 | } 50 | local s1 = run_and_wait_until_initialized(conf1, srv1) 51 | local s2 = run_and_wait_until_initialized(conf2, srv2) 52 | assert.are.same(2, #dap.sessions()) 53 | assert.are.not_same(s1.id, s2.id) 54 | 55 | dap.terminate() 56 | wait(function() return #dap.sessions() == 1 end, function() return dap.sessions() end) 57 | assert.are.same(true, s2.closed) 58 | assert.are.same(false, s1.closed) 59 | assert.are.same(s1, dap.session()) 60 | 61 | dap.terminate() 62 | wait(function() return #dap.sessions() == 0 end, function() return dap.sessions() end) 63 | assert.are.same(nil, dap.session()) 64 | end) 65 | 66 | it("startDebugging starts a child session", function() 67 | local conf1 = { 68 | type = 'dummy1', 69 | request = 'launch', 70 | name = 'Launch file 1', 71 | } 72 | run_and_wait_until_initialized(conf1, srv1) 73 | srv1.client:send_request("startDebugging", { 74 | request = "launch", 75 | configuration = { 76 | type = "dummy2", 77 | name = "Subprocess" 78 | } 79 | }) 80 | wait( 81 | function() return vim.tbl_count(dap.session().children) == 1 end, 82 | function() return dap.session() end 83 | ) 84 | local _, child = next(dap.session().children) 85 | assert.are.same("Subprocess", child.config.name) 86 | 87 | srv2.stop() 88 | wait(function() return vim.tbl_count(dap.session().children) == 0 end) 89 | assert.are.same({}, dap.session().children) 90 | end) 91 | 92 | it("startDebugging connects to root adapter if type server with executable", function() 93 | local conf1 = { 94 | type = 'dummy1', 95 | request = 'launch', 96 | name = 'Launch file 1', 97 | } 98 | local session = run_and_wait_until_initialized(conf1, srv1) 99 | assert.are.same(1, srv1.client.num_connected) 100 | dap.adapters.dummy2 = { 101 | type = "server", 102 | executable = { 103 | command = "echo", 104 | args = {"not", "used"}, 105 | } 106 | } 107 | srv1.client:send_request("startDebugging", { 108 | request = "launch", 109 | configuration = { 110 | type = "dummy2", 111 | name = "Subprocess" 112 | } 113 | }) 114 | wait( 115 | function() return vim.tbl_count(session.children) == 1 end, 116 | function() return dap.session() end 117 | ) 118 | assert.are.same(2, srv1.client.num_connected) 119 | dap.terminate() 120 | end) 121 | end) 122 | -------------------------------------------------------------------------------- /spec/ui_spec.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | local ui = require('dap.ui') 3 | 4 | describe('ui', function() 5 | describe('layered buf', function() 6 | 7 | -- note that test cases build on each other 8 | local render_item = function(x) return x.label end 9 | local buf = api.nvim_create_buf(true, true) 10 | local layer = ui.layer(buf) 11 | 12 | it('can append items to empty buffer', function() 13 | local items = { 14 | { label = "aa", x = 1 }, 15 | { label = "", x = 3 }, 16 | { label = "dd", x = 4 }, 17 | } 18 | layer.render(items, render_item) 19 | local lines = api.nvim_buf_get_lines(buf, 0, -1, true) 20 | assert.are.same({ 21 | 'aa', 22 | '', 23 | 'dd', 24 | }, lines) 25 | 26 | assert.are.same(3, vim.tbl_count(layer.__marks)) 27 | for i = 1, #items do 28 | assert.are.same(items[i], layer.get(i - 1).item) 29 | end 30 | end) 31 | 32 | it('can append at arbitrary position', function() 33 | layer.render({{ label = "bb", x = 2 },}, render_item, nil, 1) 34 | local lines = api.nvim_buf_get_lines(buf, 0, -1, true) 35 | assert.are.same({ 36 | 'aa', 37 | 'bb', 38 | '', 39 | 'dd', 40 | }, lines) 41 | assert.are.same('aa', layer.get(0).item.label) 42 | assert.are.same('bb', layer.get(1).item.label) 43 | assert.are.same('', layer.get(2).item.label) 44 | end) 45 | 46 | it('can override a region', function() 47 | local items = { 48 | { label = "bbb", x = 22 }, 49 | { label = "bbbb", x = 222 }, 50 | } 51 | layer.render(items, render_item, nil, 1, 2) 52 | local lines = api.nvim_buf_get_lines(buf, 0, -1, true) 53 | assert.are.same({ 54 | 'aa', 55 | 'bbb', 56 | 'bbbb', 57 | '', 58 | 'dd', 59 | }, lines) 60 | assert.are.same('aa', layer.get(0).item.label) 61 | assert.are.same('bbb', layer.get(1).item.label) 62 | assert.are.same('bbbb', layer.get(2).item.label) 63 | assert.are.same('', layer.get(3).item.label) 64 | end) 65 | 66 | it('can append at the end', function() 67 | layer.render({{ label = "e" }}, render_item, nil, nil, nil) 68 | local lines = api.nvim_buf_get_lines(buf, 0, -1, true) 69 | assert.are.same({ 70 | 'aa', 71 | 'bbb', 72 | 'bbbb', 73 | '', 74 | 'dd', 75 | 'e', 76 | }, lines) 77 | assert.are.same('dd', layer.get(4).item.label) 78 | assert.are.same('e', layer.get(5).item.label) 79 | end) 80 | end) 81 | 82 | local opts = { 83 | get_key = function(val) return val.name end, 84 | render_parent = function(val) return val.name end, 85 | has_children = function(val) return val.children end, 86 | get_children = function(val) return val.children end 87 | } 88 | 89 | describe('tree can render a tree structure', function() 90 | local tree = ui.new_tree(opts) 91 | local buf = api.nvim_create_buf(true, true) 92 | local layer = ui.layer(buf) 93 | local d = { name = 'd' } 94 | local c = { name = 'c', children = { d, } } 95 | local b = { name = 'b', children = { c, } } 96 | local a = { name = 'a' } 97 | local root = { 98 | name = 'root', 99 | children = { a, b } 100 | } 101 | local root_copy = vim.deepcopy(root) 102 | tree.render(layer, root) 103 | local lines = api.nvim_buf_get_lines(buf, 0, -1, true) 104 | assert.are.same({ 105 | 'root', 106 | ' a', 107 | ' b', 108 | }, lines) 109 | 110 | it('can expand an element with children', function() 111 | local lnum = 2 112 | local info = layer.get(lnum) 113 | info.context.actions[1].fn(layer, info.item, lnum, info.context) 114 | lines = api.nvim_buf_get_lines(buf, 0, -1, true) 115 | assert.are.same({ 116 | 'root', 117 | ' a', 118 | ' b', 119 | ' c', 120 | }, lines) 121 | 122 | lnum = 3 123 | info = layer.get(lnum) 124 | info.context.actions[1].fn(layer, info.item, lnum, info.context) 125 | lines = api.nvim_buf_get_lines(buf, 0, -1, true) 126 | assert.are.same({ 127 | 'root', 128 | ' a', 129 | ' b', 130 | ' c', 131 | ' d', 132 | }, lines) 133 | end) 134 | 135 | it('can render with new data and previously expanded elements are still expanded', function() 136 | layer.render({}, tostring, nil, 0, -1) 137 | lines = api.nvim_buf_get_lines(buf, 0, -1, true) 138 | assert.are.same({''}, lines) 139 | tree.render(layer, root_copy) 140 | lines = api.nvim_buf_get_lines(buf, 0, -1, true) 141 | assert.are.same({ 142 | 'root', 143 | ' a', 144 | ' b', 145 | ' c', 146 | ' d', 147 | }, lines) 148 | end) 149 | 150 | it('can collapse an expanded item', function() 151 | local lnum = 2 152 | local info = layer.get(lnum) 153 | info.context.actions[1].fn(layer, info.item, lnum, info.context) 154 | lines = api.nvim_buf_get_lines(buf, 0, -1, true) 155 | assert.are.same({ 156 | 'root', 157 | ' a', 158 | ' b', 159 | }, lines) 160 | end) 161 | 162 | it('can re-use a subnode in a different tree', function() 163 | local lnum = 2 164 | local info = layer.get(lnum) 165 | info.context.actions[1].fn(layer, info.item, lnum, info.context) 166 | lines = api.nvim_buf_get_lines(buf, 0, -1, true) 167 | assert.are.same({ 168 | 'root', 169 | ' a', 170 | ' b', 171 | ' c', 172 | }, lines) 173 | layer.render({}, tostring, nil, 0, -1) 174 | local subtree = ui.new_tree(opts) 175 | subtree.render(layer, b) 176 | lines = api.nvim_buf_get_lines(buf, 0, -1, true) 177 | assert.are.same({ 178 | 'b', 179 | ' c', 180 | }, lines) 181 | end) 182 | end) 183 | end) 184 | -------------------------------------------------------------------------------- /spec/utils_spec.lua: -------------------------------------------------------------------------------- 1 | local utils = require('dap.utils') 2 | 3 | describe('utils.index_of', function() 4 | it('returns index of first item where predicate matches', function() 5 | local result = require('dap.utils').index_of( 6 | {'a', 'b', 'c'}, 7 | function(x) return x == 'b' end 8 | ) 9 | assert.are.same(2, result) 10 | end) 11 | end) 12 | 13 | describe('utils.to_dict', function() 14 | it('converts a list to a dictionary', function() 15 | local values = { { k='a', val=1 }, { k='b', val = 2 } } 16 | local result = require('dap.utils').to_dict( 17 | values, 18 | function(x) return x.k end, 19 | function(x) return x.val end 20 | ) 21 | local expected = { 22 | a = 1, 23 | b = 2 24 | } 25 | assert.are.same(expected, result) 26 | end) 27 | 28 | it('supports nil values as argument', function() 29 | local result = require('dap.utils').to_dict(nil, function(x) return x end) 30 | assert.are.same(result, {}) 31 | end) 32 | end) 33 | 34 | 35 | describe('utils.non_empty', function() 36 | it('non_empty returns true on non-empty dicts with numeric keys', function() 37 | local d = { 38 | [20] = 'a', 39 | [30] = 'b', 40 | } 41 | local result = require('dap.utils').non_empty(d) 42 | assert.are.same(true, result) 43 | end) 44 | end) 45 | 46 | describe('utils.fmt_error', function () 47 | it('interpolates message objects with variables', function () 48 | assert.are.equal('Hello, John!', require('dap.utils').fmt_error({ 49 | body = { 50 | error = { 51 | showUser = true, 52 | format = '{greeting}, {name}!', 53 | variables = { 54 | greeting = 'Hello', 55 | name = 'John', 56 | } 57 | } 58 | } 59 | })) 60 | end) 61 | 62 | it('interpolates message objects without variables', function () 63 | assert.are.equal('Hello, John!', require('dap.utils').fmt_error({ 64 | body = { 65 | error = { 66 | showUser = true, 67 | format = 'Hello, John!', 68 | } 69 | } 70 | })) 71 | end) 72 | 73 | it('return message if showUser is false', function () 74 | assert.are.equal('Something went wrong.', require('dap.utils').fmt_error({ 75 | message = 'Something went wrong.', 76 | body = { 77 | error = { 78 | showUser = false, 79 | format = 'Hello, John!', 80 | } 81 | } 82 | })) 83 | end) 84 | 85 | it('can handle response without body part', function() 86 | local result = utils.fmt_error({ 87 | message = 'Bad things happen', 88 | }) 89 | assert.are.same('Bad things happen', result) 90 | end) 91 | end) 92 | 93 | describe('utils.splitstr', function () 94 | if vim.fn.has("nvim-0.10") == 0 then 95 | return 96 | end 97 | it('works with plain string', function () 98 | assert.are.same({"hello", "world"}, utils.splitstr("hello world")) 99 | end) 100 | 101 | it('works extra whitespace', function () 102 | assert.are.same({"hello", "world"}, utils.splitstr('hello world')) 103 | end) 104 | 105 | it('empty quoted', function () 106 | assert.are.same({"hello", "", "world"}, utils.splitstr('hello "" world')) 107 | end) 108 | 109 | it('with double quoted string', function () 110 | assert.are.same({'with', 'double quoted', 'string'}, utils.splitstr('with "double quoted" string')) 111 | end) 112 | 113 | it("with single quoted string", function () 114 | assert.are.same({'with', 'single quoted', 'string'}, utils.splitstr("with 'single quoted' string")) 115 | end) 116 | 117 | it("with unbalanced quote", function () 118 | assert.are.same({"with", "\"single", "quoted", "string"}, utils.splitstr("with \"single quoted string")) 119 | end) 120 | 121 | it("with unbalanced single quoted string", function () 122 | assert.are.same({"with", "'single", "quoted", "string"}, utils.splitstr("with 'single quoted string")) 123 | end) 124 | 125 | it('escaped quote', function () 126 | assert.are.same({'foo', '"bar'}, utils.splitstr('foo \"bar')) 127 | end) 128 | 129 | it("returns empty list for empty strings", function () 130 | assert.are.same({}, utils.splitstr("")) 131 | assert.are.same({}, utils.splitstr(" ")) 132 | end) 133 | it("trims leading and trailing whitespace", function () 134 | assert.are.same({"a"}, utils.splitstr("a ")) 135 | assert.are.same({"a", "b"}, utils.splitstr(" a b ")) 136 | end) 137 | end) 138 | 139 | describe("trim_procname", function() 140 | it("trims long full paths to name", function() 141 | local name = utils._trim_procname("/usr/bin/foobar", 10, 4) 142 | assert.are.same("foobar", name) 143 | end) 144 | 145 | it("drops arguments if there are too many", function() 146 | local name = utils._trim_procname("cmd --one --two --three", 15, 5) 147 | assert.are.same("cmd --one --two [‥]", name) 148 | end) 149 | 150 | it("trims long arguments", function() 151 | local name = utils._trim_procname("foobar --too-long-sorry", 20, 8) 152 | assert.are.same("foobar ‥ong-sorry", name) 153 | end) 154 | end) 155 | -------------------------------------------------------------------------------- /spec/widgets_spec.lua: -------------------------------------------------------------------------------- 1 | local dap = require('dap') 2 | local widgets = require("dap.ui.widgets") 3 | local api = vim.api 4 | local helpers = require("spec.helpers") 5 | 6 | local config = { 7 | type = 'dummy', 8 | request = 'launch', 9 | name = 'Launch file', 10 | } 11 | 12 | 13 | describe("hover widget", function() 14 | 15 | local server 16 | before_each(function() 17 | server = require('spec.server').spawn() 18 | dap.adapters.dummy = server.adapter 19 | end) 20 | after_each(function() 21 | server.stop() 22 | dap.close() 23 | require('dap.breakpoints').clear() 24 | helpers.wait(function() return dap.session() == nil end, "session should become nil") 25 | end) 26 | 27 | it("evaluates expression with hover context", function() 28 | server.client.initialize = function(self, request) 29 | self:send_response(request, { 30 | supportsEvaluateForHovers = true, 31 | }) 32 | self:send_event("initialized", {}) 33 | end 34 | server.client.evaluate = function(self, request) 35 | self:send_response(request, { 36 | result = "2", 37 | variablesReference = 0, 38 | }) 39 | end 40 | helpers.run_and_wait_until_initialized(config, server) 41 | server.spy.clear() 42 | local buf = api.nvim_create_buf(false, true) 43 | api.nvim_buf_set_lines(buf, 0, -1, true, {"foo", "bar"}) 44 | api.nvim_set_current_buf(buf) 45 | api.nvim_win_set_cursor(0, {1, 0}) 46 | widgets.hover("1 + 1") 47 | local commands = helpers.wait_for_response(server, "evaluate") 48 | assert.are.same({"evaluate"}, commands) 49 | assert.are.same("hover", server.spy.requests[1].arguments.context) 50 | assert.are.same("1 + 1", server.spy.requests[1].arguments.expression) 51 | end) 52 | end) 53 | --------------------------------------------------------------------------------