├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── workflow.yaml ├── .gitignore ├── .releaserc.json ├── Dockerfile ├── LICENCE.md ├── README.md ├── doc └── nvim-dap-ui.txt ├── lua └── dapui │ ├── client │ ├── dap_types.lua │ ├── init.lua │ ├── lib.lua │ └── types.lua │ ├── components │ ├── breakpoints.lua │ ├── frames.lua │ ├── hover.lua │ ├── scopes.lua │ ├── threads.lua │ ├── variables.lua │ └── watches.lua │ ├── config │ ├── highlights.lua │ └── init.lua │ ├── controls.lua │ ├── elements │ ├── breakpoints.lua │ ├── console.lua │ ├── hover.lua │ ├── repl.lua │ ├── scopes.lua │ ├── stacks.lua │ └── watches.lua │ ├── init.lua │ ├── render │ ├── canvas.lua │ ├── init.lua │ └── line_hover.lua │ ├── tests │ ├── init.lua │ ├── mocks.lua │ └── util.lua │ ├── util.lua │ └── windows │ ├── float.lua │ ├── init.lua │ └── layout.lua ├── scripts ├── docgen ├── gendocs.lua ├── generate_types ├── style └── test ├── stylua.toml └── tests ├── init.vim ├── minimal_init.lua └── unit ├── config └── init_spec.lua ├── elements ├── breakpoints_spec.lua ├── hover_spec.lua ├── scopes_spec.lua ├── stacks_spec.lua └── watches_spec.lua ├── util_spec.lua └── windows └── layout_spec.lua /.editorconfig: -------------------------------------------------------------------------------- 1 | # see https://github.com/CppCXY/EmmyLuaCodeStyle 2 | [*.lua] 3 | # [basic code reformat option] 4 | # optional space/tab 5 | indent_style = space 6 | # if indent_style is space, this is valid 7 | indent_size = 2 8 | # if indent_style is tab, this is valid 9 | tab_width = 2 10 | # none/single/double 11 | quote_style = double 12 | # keep/remove 13 | call_arg_parentheses = keep 14 | # only support number 15 | continuation_indent_size = 2 16 | # if true, continuation_indent_size for local or assign statement is invalid 17 | # however, if the expression list has cross row expression, it will not be aligned to the first expression 18 | local_assign_continuation_align_to_first_expression = false 19 | # function call expression's args will align to first arg 20 | # however, if the args has cross row arg, it will not be aligned to the first arg 21 | align_call_args = false 22 | # if true, all function define params will align to first param 23 | align_function_define_params = true 24 | # if true, format like this "local t = { 1, 2, 3 }" 25 | keep_one_space_between_table_and_bracket = true 26 | # if indent_style is tab, this option is invalid 27 | align_table_field_to_first_field = false 28 | # if true, ormat like this "local t = 1" 29 | keep_one_space_between_namedef_and_attribute = false 30 | # continous line distance 31 | max_continuous_line_distance = 1 32 | # see document for detail 33 | continuous_assign_statement_align_to_equal_sign = true 34 | # see document for detail 35 | continuous_assign_table_field_align_to_equal_sign = true 36 | # if true, the label loses its current indentation 37 | label_no_indent = false 38 | # if true, there will be no indentation in the do statement 39 | do_statement_no_indent = false 40 | # if true, the conditional expression of the if statement will not be a continuation line indent 41 | if_condition_no_continuation_indent = false 42 | # if true, t[#t+1] will not space wrapper '+' 43 | table_append_expression_no_space = false 44 | # if statement will align like switch case 45 | if_condition_align_with_each_other = false 46 | 47 | long_chain_expression_allow_one_space_after_colon = false 48 | # optional crlf/lf/auto, if it is 'auto', in windows it is crlf other platforms are lf 49 | end_of_line = lf 50 | 51 | # [line layout] 52 | # The following configuration supports three expressions 53 | # minLine:${n} 54 | # keepLine 55 | # KeepLine:${n} 56 | 57 | keep_line_after_if_statement = minLine:0 58 | keep_line_after_do_statement = minLine:0 59 | keep_line_after_while_statement = minLine:0 60 | keep_line_after_repeat_statement = minLine:0 61 | keep_line_after_for_statement = minLine:0 62 | keep_line_after_local_or_assign_statement = keepLine 63 | keep_line_after_function_define_statement = keepLine:1 64 | 65 | # [diagnostic] 66 | # the following is code diagnostic options 67 | enable_check_codestyle = true 68 | # this mean utf8 length 69 | max_line_length = 120 70 | # this will check text end with new line(format always end with new line) 71 | insert_final_newline = true 72 | 73 | # [name style check] 74 | enable_name_style_check = true 75 | # the following is name style check rule 76 | # base option off/camel_case/snake_case/upper_snake_case/pascal_case/same(filename/first_param/'', snake_case/pascal_case/camel_case) 77 | # all option can use '|' represent or 78 | # for example: 79 | # snake_case | upper_snake_case 80 | # same(first_param, snake_case) 81 | # same('m') 82 | local_name_define_style = snake_case 83 | function_param_name_style = snake_case 84 | function_name_define_style = snake_case 85 | local_function_name_define_style = snake_case 86 | table_field_name_define_style = snake_case 87 | global_variable_name_define_style = snake_case|upper_snake_case 88 | module_name_define_style = same('M')|same(filename, snake_case) 89 | require_module_name_style = same(snake_case) 90 | class_name_define_style = same(filename, snake_case) 91 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: rcarriga 4 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yaml: -------------------------------------------------------------------------------- 1 | name: nvim-dap-ui Workflow 2 | on: [push] 3 | jobs: 4 | style: 5 | name: style 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: JohnnyMorganz/stylua-action@v1 10 | with: 11 | token: ${{ secrets.GITHUB_TOKEN }} 12 | version: latest 13 | args: --check lua/ tests/ 14 | tests: 15 | name: tests 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - run: date +%F > todays-date 20 | - name: Restore cache for today's nightly. 21 | uses: actions/cache@v2 22 | with: 23 | path: _neovim 24 | key: ${{ runner.os }}-x64-${{ hashFiles('todays-date') }} 25 | 26 | - name: Prepare dependencies 27 | run: | 28 | git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim 29 | git clone --depth 1 https://github.com/nvim-neotest/nvim-nio ~/.local/share/nvim/site/pack/vendor/start/nvim-nio 30 | git clone --depth 1 https://github.com/mfussenegger/nvim-dap ~/.local/share/nvim/site/pack/vendor/start/nvim-dap 31 | ln -s $(pwd) ~/.local/share/nvim/site/pack/vendor/start 32 | 33 | - name: Run tests 34 | run: | 35 | curl -OL https://raw.githubusercontent.com/norcalli/bot-ci/master/scripts/github-actions-setup.sh 36 | source github-actions-setup.sh nightly-x64 37 | ./scripts/test 38 | 39 | release: 40 | name: release 41 | if: ${{ github.ref == 'refs/heads/master' }} 42 | needs: 43 | - style 44 | - tests 45 | runs-on: ubuntu-20.04 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v2 49 | with: 50 | fetch-depth: 0 51 | - name: Setup Node.js 52 | uses: actions/setup-node@v1 53 | with: 54 | node-version: 20 55 | - name: Release 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | run: npx semantic-release 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | neovim/ 2 | doc/tags 3 | plenary.nvim/ 4 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@semantic-release/commit-analyzer", 4 | "@semantic-release/release-notes-generator", 5 | [ 6 | "@semantic-release/github", 7 | { 8 | "successComment": false 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | ARG NEOVIM_RELEASE=${NEOVIM_RELEASE:-https://github.com/neovim/neovim/releases/download/nightly/nvim-linux64.tar.gz} 3 | FROM ubuntu:21.04 4 | ARG NEOVIM_RELEASE 5 | 6 | RUN apt-get update 7 | RUN apt-get -y install git curl tar gcc g++ make 8 | RUN mkdir /neovim 9 | RUN curl -sL ${NEOVIM_RELEASE} | tar xzf - --strip-components=1 -C "/neovim" 10 | RUN git clone --depth 1 https://github.com/nvim-lua/plenary.nvim 11 | RUN git clone --depth 1 https://github.com/tjdevries/tree-sitter-lua 12 | 13 | WORKDIR tree-sitter-lua 14 | RUN make dist 15 | 16 | RUN mkdir /app 17 | WORKDIR /app 18 | 19 | ENTRYPOINT ["bash", "-c", "PATH=/neovim/bin:${PATH} VIM=/neovim/share/nvim/runtime nvim --headless -c 'set rtp+=. | set rtp+=../plenary.nvim/ | set rtp+=../tree-sitter-lua/ | runtime! plugin/plenary.vim | luafile ./scripts/gendocs.lua' -c 'qa'"] 20 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rónán Carrigan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-dap-ui 2 | 3 | ## Introduction 4 | 5 | A UI for [nvim-dap](https://github.com/mfussenegger/nvim-dap) which provides a 6 | good out of the box configuration. 7 | 8 | ![preview](https://user-images.githubusercontent.com/24252670/191198389-a1321363-c0f1-4ff1-b663-ab1350d2b393.png) 9 | 10 | ## Installation 11 | 12 | Install with your favourite package manager alongside nvim-dap and nvim-nio 13 | 14 | [**dein**](https://github.com/Shougo/dein.vim): 15 | 16 | ```vim 17 | call dein#add("mfussenegger/nvim-dap") 18 | call dein#add("nvim-neotest/nvim-nio") 19 | call dein#add("rcarriga/nvim-dap-ui") 20 | ``` 21 | 22 | [**vim-plug**](https://github.com/junegunn/vim-plug) 23 | 24 | ```vim 25 | Plug 'mfussenegger/nvim-dap' 26 | Plug 'nvim-neotest/nvim-nio' 27 | Plug 'rcarriga/nvim-dap-ui' 28 | ``` 29 | 30 | [**packer.nvim**](https://github.com/wbthomason/packer.nvim) 31 | 32 | ```lua 33 | use { "rcarriga/nvim-dap-ui", requires = {"mfussenegger/nvim-dap", "nvim-neotest/nvim-nio"} } 34 | ``` 35 | 36 | [**lazy.nvim**](https://github.com/folke/lazy.nvim) 37 | 38 | ```lua 39 | { "rcarriga/nvim-dap-ui", dependencies = {"mfussenegger/nvim-dap", "nvim-neotest/nvim-nio"} } 40 | ``` 41 | 42 | It is highly recommended to use [lazydev.nvim](https://github.com/folke/lazydev.nvim) to enable type checking for nvim-dap-ui to get 43 | type checking, documentation and autocompletion for all API functions. 44 | 45 | ```lua 46 | require("lazydev").setup({ 47 | library = { "nvim-dap-ui" }, 48 | }) 49 | ``` 50 | 51 | The default icons use [codicons](https://github.com/microsoft/vscode-codicons). 52 | It's recommended to use this [fork](https://github.com/ChristianChiarulli/neovim-codicons) which fixes alignment issues 53 | for the terminal. If your terminal doesn't support font fallback and you need to have icons included in your font, you can patch it via [Font Patcher](https://github.com/ryanoasis/nerd-fonts#option-8-patch-your-own-font). 54 | There is a simple step by step guide [here](https://github.com/mortepau/codicons.nvim#how-to-patch-fonts). 55 | 56 | ## Configuration 57 | 58 | nvim-dap-ui is built on the idea of "elements". These elements are windows 59 | which provide different features. 60 | 61 | Elements are grouped into layouts which can be placed on any side of the screen. 62 | There can be any number of layouts, containing whichever elements desired. 63 | 64 | Elements can also be displayed temporarily in a floating window. 65 | 66 | Each element has a set of *mappings* for element-specific possible actions, detailed below for each element. 67 | The total set of actions/mappings and their default shortcuts are: 68 | - `edit`: `e` 69 | - `expand`: `` or left click 70 | - `open`: `o` 71 | - `remove`: `d` 72 | - `repl`: `r` 73 | - `toggle`: `t` 74 | 75 | See `:h dapui.setup()` for configuration options and defaults. 76 | 77 | 78 | ### Variable Scopes 79 | 80 | ![image](https://user-images.githubusercontent.com/24252670/126842891-c5175f13-5eb7-4d0a-9dae-620c4d31448a.png) 81 | 82 | Element ID: `scopes` 83 | 84 | Displays the available scopes and variables within them. 85 | 86 | Mappings: 87 | 88 | - `edit`: Edit the value of a variable 89 | - `expand`: Toggle showing any children of variable. 90 | - `repl`: Send variable to REPL 91 | 92 | ### Threads and Stack Frames 93 | 94 | ![image](https://user-images.githubusercontent.com/24252670/126843106-5dce09dc-49d0-4aaa-ba98-fd8f17b31414.png) 95 | 96 | Element ID: `stacks` 97 | 98 | Displays the running threads and their stack frames. 99 | 100 | Mappings: 101 | 102 | - `open`: Jump to a place within the stack frame. 103 | - `toggle`: Toggle displaying [subtle](https://microsoft.github.io/debug-adapter-protocol/specification#Types_StackFrame) frames 104 | 105 | ### Watch Expressions 106 | 107 | ![image](https://user-images.githubusercontent.com/24252670/126843390-4e1575d8-9d7d-4f43-8680-094cfe9eae63.png) 108 | 109 | Element ID: `watches` 110 | 111 | Allows creation of expressions to watch the value of in the context of the 112 | current frame. 113 | This uses a prompt buffer for input. To enter a new expression, just enter 114 | insert mode and you will see a prompt appear. Press enter to submit 115 | 116 | Mappings: 117 | 118 | - `expand`: Toggle showing the children of an expression. 119 | - `remove`: Remove the watched expression. 120 | - `edit`: Edit an expression or set the value of a child variable. 121 | - `repl`: Send expression to REPL 122 | 123 | ### Breakpoints 124 | 125 | ![image](https://user-images.githubusercontent.com/24252670/126843577-361645e4-6265-40eb-86dc-d6607512a15e.png) 126 | 127 | Element ID: `breakpoints` 128 | 129 | List all breakpoints currently set. 130 | 131 | Mappings: 132 | 133 | - `open`: Jump to the location the breakpoint is set 134 | - `toggle`: Enable/disable the selected breakpoint 135 | 136 | ### REPL 137 | 138 | Element ID: `repl` 139 | 140 | The REPL provided by nvim-dap. 141 | 142 | ### Console 143 | 144 | Element ID: `console` 145 | 146 | The console window used by nvim-dap for the integrated terminal. 147 | 148 | ## Usage 149 | 150 | To get started simply call the setup method on startup, optionally providing 151 | custom settings. 152 | 153 | ```lua 154 | require("dapui").setup() 155 | ``` 156 | 157 | You can open, close and toggle the windows with corresponding functions: 158 | 159 | ```lua 160 | require("dapui").open() 161 | require("dapui").close() 162 | require("dapui").toggle() 163 | ``` 164 | 165 | Each of the functions optionally takes either `"sidebar"` or `"tray"` as an 166 | argument to only change the specified component. 167 | 168 | You can use nvim-dap events to open and close the windows automatically (`:help dap-extensions`) 169 | 170 | ```lua 171 | local dap, dapui = require("dap"), require("dapui") 172 | dap.listeners.before.attach.dapui_config = function() 173 | dapui.open() 174 | end 175 | dap.listeners.before.launch.dapui_config = function() 176 | dapui.open() 177 | end 178 | dap.listeners.before.event_terminated.dapui_config = function() 179 | dapui.close() 180 | end 181 | dap.listeners.before.event_exited.dapui_config = function() 182 | dapui.close() 183 | end 184 | ``` 185 | 186 | ### Floating Elements 187 | 188 | For elements that are not opened in the tray or sidebar, you can open them in a 189 | floating window. 190 | 191 | ![image](https://user-images.githubusercontent.com/24252670/126844102-8789effb-4276-4599-afe6-a074b019c38d.png) 192 | 193 | ```lua 194 | require("dapui").float_element(, ) 195 | ``` 196 | 197 | If you do not provide an element ID, you will be queried to select one. 198 | 199 | The optional settings can included the following keys: 200 | 201 | - `width: number` Width of the window 202 | - `height: number` Height of the window 203 | - `enter: boolean` Enter the floating window 204 | - `position: string` Position of floating window. `center` or `nil` 205 | 206 | Call the same function again while the window is open and the cursor will jump 207 | to the floating window. The REPL will automatically jump to the floating 208 | window on open. 209 | 210 | ### Evaluate Expression 211 | 212 | For a one time expression evaluation, you can call a hover window to show a value 213 | 214 | ![image](https://user-images.githubusercontent.com/24252670/126844454-691d691c-4550-46fe-89dc-25e1e9681545.png) 215 | 216 | ```lua 217 | require("dapui").eval() 218 | ``` 219 | 220 | If an expression is not provided it will use the word under the cursor, or if in 221 | visual mode, the currently highlighted text. 222 | You can define a visual mapping like so 223 | 224 | ```vim 225 | vnoremap lua require("dapui").eval() 226 | ``` 227 | 228 | Call the same function again while the window is open to jump to the eval window. 229 | 230 | The same mappings as the variables element apply within the hover window. 231 | -------------------------------------------------------------------------------- /doc/nvim-dap-ui.txt: -------------------------------------------------------------------------------- 1 | nvim-dap-ui.txt* A UI for nvim-dap. 2 | 3 | ============================================================================== 4 | nvim-dap-ui *nvim-dap-ui* 5 | 6 | Setup........................................................|dapui.setup()| 7 | Configuration Options.........................................|dapui.config| 8 | Variable Scopes......................................|dapui.elements.scopes| 9 | Threads and Stack Frames.............................|dapui.elements.stacks| 10 | REPL...................................................|dapui.elements.repl| 11 | Watch Expressions...................................|dapui.elements.watches| 12 | Breakpoints.....................................|dapui.elements.breakpoints| 13 | Console.............................................|dapui.elements.console| 14 | 15 | A UI for nvim-dap which provides a good out of the box configuration. 16 | nvim-dap-ui is built on the idea of "elements". These elements are windows 17 | which provide different features. 18 | Elements are grouped into layouts which can be placed on any side of the 19 | screen. There can be any number of layouts, containing whichever elements 20 | desired. 21 | 22 | Elements can also be displayed temporarily in a floating window. 23 | 24 | See `:h dapui.setup()` for configuration options and defaults 25 | 26 | It is highly recommended to use neodev.nvim to enable type checking for 27 | nvim-dap-ui to get type checking, documentation and autocompletion for 28 | all API functions. 29 | 30 | >lua 31 | require("neodev").setup({ 32 | library = { plugins = { "nvim-dap-ui" }, types = true }, 33 | ... 34 | }) 35 | < 36 | 37 | The default icons use codicons(https://github.com/microsoft/vscode-codicons). 38 | It's recommended to use this fork(https://github.com/ChristianChiarulli/neovim-codicons) 39 | which fixes alignment issues for the terminal. If your terminal doesn't 40 | support font fallback and you need to have icons included in your font, 41 | you can patch it via Font Patcher(https://github.com/ryanoasis/nerd-fonts#option-8-patch-your-own-font). 42 | There is a simple step by step guide here: https://github.com/mortepau/codicons.nvim#how-to-patch-fonts. 43 | 44 | *dapui.setup()* 45 | `setup`({user_config}) 46 | 47 | 48 | Configure nvim-dap-ui 49 | See also ~ 50 | |dapui.Config| 51 | 52 | Default values: 53 | >lua 54 | { 55 | controls = { 56 | element = "repl", 57 | enabled = true, 58 | icons = { 59 | disconnect = "", 60 | pause = "", 61 | play = "", 62 | run_last = "", 63 | step_back = "", 64 | step_into = "", 65 | step_out = "", 66 | step_over = "", 67 | terminate = "" 68 | } 69 | }, 70 | element_mappings = {}, 71 | expand_lines = true, 72 | floating = { 73 | border = "single", 74 | mappings = { 75 | close = { "q", "" } 76 | } 77 | }, 78 | force_buffers = true, 79 | icons = { 80 | collapsed = "", 81 | current_frame = "", 82 | expanded = "" 83 | }, 84 | layouts = { { 85 | elements = { { 86 | id = "scopes", 87 | size = 0.25 88 | }, { 89 | id = "breakpoints", 90 | size = 0.25 91 | }, { 92 | id = "stacks", 93 | size = 0.25 94 | }, { 95 | id = "watches", 96 | size = 0.25 97 | } }, 98 | position = "left", 99 | size = 40 100 | }, { 101 | elements = { { 102 | id = "repl", 103 | size = 0.5 104 | }, { 105 | id = "console", 106 | size = 0.5 107 | } }, 108 | position = "bottom", 109 | size = 10 110 | } }, 111 | mappings = { 112 | edit = "e", 113 | expand = { "", "<2-LeftMouse>" }, 114 | open = "o", 115 | remove = "d", 116 | repl = "r", 117 | toggle = "t" 118 | }, 119 | render = { 120 | indent = 1, 121 | max_value_lines = 100 122 | } 123 | } 124 | < 125 | Parameters~ 126 | {user_config?} `(dapui.Config)` 127 | 128 | Type ~ 129 | `(table)` 130 | 131 | Type ~ 132 | dapui.Element 133 | 134 | *dapui.FloatElementArgs* 135 | Fields~ 136 | {width} `(integer)` Fixed width of window 137 | {height} `(integer)` Fixed height of window 138 | {enter} `(boolean)` Whether or not to enter the window after opening 139 | {title} `(string)` Title of window 140 | {position} `("center")` Position of floating window 141 | 142 | *dapui.float_element()* 143 | `float_element`({elem_name}, {args}) 144 | 145 | Open a floating window containing the desired element. 146 | 147 | If no fixed dimensions are given, the window will expand to fit the contents 148 | of the buffer. 149 | Parameters~ 150 | {elem_name} `(string)` 151 | {args?} `(dapui.FloatElementArgs)` 152 | 153 | *dapui.EvalArgs* 154 | Fields~ 155 | {context} `(string)` Context to use for evalutate request, defaults to 156 | "hover". Hover requests should have no side effects, if you have errors 157 | with evaluation, try changing context to "repl". See the DAP specification 158 | for more details. 159 | {width} `(integer)` Fixed width of window 160 | {height} `(integer)` Fixed height of window 161 | {enter} `(boolean)` Whether or not to enter the window after opening 162 | 163 | *dapui.eval()* 164 | `eval`({expr}, {args}) 165 | 166 | Open a floating window containing the result of evaluting an expression 167 | 168 | If no fixed dimensions are given, the window will expand to fit the contents 169 | of the buffer. 170 | Parameters~ 171 | {expr?} `(string)` Expression to evaluate. If nil, then in normal more the 172 | current word is used, and in visual mode the currently highlighted text. 173 | {args?} `(dapui.EvalArgs)` 174 | 175 | *dapui.update_render()* 176 | `update_render`({update}) 177 | 178 | Update the config.render settings and re-render windows 179 | Parameters~ 180 | {update} `(dapui.Config.render)` Updated settings, from the `render` table of 181 | the config 182 | 183 | *dapui.CloseArgs* 184 | Fields~ 185 | {layout?} `(number)` Index of layout in config 186 | 187 | *dapui.close()* 188 | `close`({args}) 189 | 190 | Close one or all of the window layouts 191 | Parameters~ 192 | {args?} `(dapui.CloseArgs)` 193 | 194 | *dapui.OpenArgs* 195 | Fields~ 196 | {layout?} `(number)` Index of layout in config 197 | {reset?} `(boolean)` Reset windows to original size 198 | 199 | *dapui.open()* 200 | `open`({args}) 201 | 202 | Open one or all of the window layouts 203 | Parameters~ 204 | {args?} `(dapui.OpenArgs)` 205 | 206 | *dapui.ToggleArgs* 207 | Fields~ 208 | {layout?} `(number)` Index of layout in config 209 | {reset?} `(boolean)` Reset windows to original size 210 | 211 | *dapui.toggle()* 212 | `toggle`({args}) 213 | 214 | Toggle one or all of the window layouts. 215 | Parameters~ 216 | {args?} `(dapui.ToggleArgs)` 217 | 218 | dapui.elements *dapui.elements* 219 | 220 | 221 | Access the elements currently registered. See elements corresponding help 222 | tag for API information. 223 | 224 | Fields~ 225 | {hover} `(dapui.elements.hover)` 226 | {breakpoints} `(dapui.elements.breakpoints)` 227 | {repl} `(dapui.elements.repl)` 228 | {scopes} `(dapui.elements.scopes)` 229 | {stack} `(dapui.elements.stacks)` 230 | {watches} `(dapui.elements.watches)` 231 | {console} `(dapui.elements.console)` 232 | 233 | *dapui.Element* 234 | Fields~ 235 | {render} `(fun())` Triggers the element to refresh its buffer. Used when 236 | render settings have changed 237 | {buffer} `(fun(): integer)` Gets the current buffer for the element. The 238 | buffer can change over repeated calls 239 | {float_defaults?} `(fun(): dapui.FloatElementArgs)` Default settings for 240 | floating windows. Useful for element windows which should be larger than 241 | their content 242 | {allow_without_session?} `(boolean)` Allows floating the element when 243 | there is no active debug session 244 | 245 | *dapui.register_element()* 246 | `register_element`({name}, {element}) 247 | 248 | Registers a new element that can be used within layouts or floating windows 249 | Parameters~ 250 | {name} `(string)` Name of the element 251 | {element} `(dapui.Element)` 252 | 253 | 254 | ============================================================================== 255 | dapui.config *dapui.config* 256 | 257 | *dapui.Config* 258 | Fields~ 259 | {icons} `(dapui.Config.icons)` 260 | {mappings} `(table)` Keys to trigger actions in elements 261 | {element_mappings} `(table>)` Per-element overrides of global mappings 262 | {expand_lines} `(boolean)` Expand current line to hover window if larger 263 | than window size 264 | {force_buffers} `(boolean)` Prevents other buffers being loaded into 265 | nvim-dap-ui windows 266 | {layouts} `(dapui.Config.layout[])` Layouts to display elements within. 267 | Layouts are opened in the order defined 268 | {floating} `(dapui.Config.floating)` Floating window specific options 269 | {controls} `(dapui.Config.controls)` Controls configuration 270 | {render} `(dapui.Config.render)` Rendering options which can be updated 271 | after initial setup 272 | {select_window?} `(fun(): integer)` A function which returns a window to be 273 | used for opening buffers such as a stack frame location. 274 | 275 | *dapui.Config.icons* 276 | Fields~ 277 | {expanded} `(string)` 278 | {collapsed} `(string)` 279 | {current_frame} `(string)` 280 | 281 | *dapui.Config.layout* 282 | Fields~ 283 | {elements} `(string[]|dapui.Config.layout.element[])` Elements to display 284 | in this layout 285 | {size} `(number)` Size of the layout in lines/columns 286 | {position} `("left"|"right"|"top"|"bottom")` Which side of editor to open 287 | layout on 288 | 289 | *dapui.Config.layout.element* 290 | Fields~ 291 | {id} `(string)` Element ID 292 | {size} `(number)` Size of the element in lines/columns or as proportion of 293 | total editor size (0-1) 294 | 295 | *dapui.Config.floating* 296 | Fields~ 297 | {max_height?} `(number)` Maximum height of floating window (integer or float 298 | between 0 and 1) 299 | {max_width?} `(number)` Maximum width of floating window (integer or float 300 | between 0 and 1) 301 | {border} `(string|string[])` Border argument supplied to `nvim_open_win` 302 | {mappings} `(table)` Keys to trigger 303 | actions in elements 304 | 305 | *dapui.Config.controls* 306 | Fields~ 307 | {enabled} `(boolean)` Show controls on an element (requires winbar feature) 308 | {element} `(string)` Element to show controls on 309 | {icons} `(dapui.Config.controls.icons)` 310 | 311 | *dapui.Config.controls.icons* 312 | Fields~ 313 | {pause} `(string)` 314 | {play} `(string)` 315 | {step_into} `(string)` 316 | {step_over} `(string)` 317 | {step_out} `(string)` 318 | {step_back} `(string)` 319 | {run_last} `(string)` 320 | {terminate} `(string)` 321 | 322 | *dapui.Config.render* 323 | Fields~ 324 | {indent} `(integer)` Default indentation size 325 | {max_type_length?} `(integer)` Maximum number of characters to allow a type 326 | name to fill before trimming 327 | {max_value_lines?} `(integer)` Maximum number of lines to allow a value to 328 | fill before trimming 329 | {sort_variables?} `(fun(a: dapui.types.Variable, b: dapui.types.Variable):boolean)` Sorting function to determine 330 | render order of variables. 331 | 332 | *dapui.Action* 333 | Alias~ 334 | `dapui.Action` → `"expand"|"open"|"remove"|"edit"|"repl"|"toggle"` 335 | 336 | *dapui.FloatingAction* 337 | Alias~ 338 | `dapui.FloatingAction` → `"close"` 339 | 340 | 341 | ============================================================================== 342 | dapui.elements.scopes *dapui.elements.scopes* 343 | 344 | Displays the available scopes and variables within them. 345 | 346 | Mappings: 347 | - `edit`: Edit the value of a variable 348 | - `expand`: Toggle showing any children of variable. 349 | - `repl`: Send variable to REPL 350 | 351 | 352 | ============================================================================== 353 | dapui.elements.stacks *dapui.elements.stacks* 354 | 355 | Displays the running threads and their stack frames. 356 | 357 | Mappings: 358 | - `open`: Jump to a place within the stack frame. 359 | - `toggle`: Toggle displaying subtle frames 360 | 361 | 362 | ============================================================================== 363 | dapui.elements.repl *dapui.elements.repl* 364 | 365 | The REPL provided by nvim-dap. 366 | 367 | 368 | ============================================================================== 369 | dapui.elements.watches *dapui.elements.watches* 370 | 371 | Allows creation of expressions to watch the value of in the context of the 372 | current frame. 373 | This uses a prompt buffer for input. To enter a new expression, just enter 374 | insert mode and you will see a prompt appear. Press enter to submit 375 | 376 | Mappings: 377 | 378 | - `expand`: Toggle showing the children of an expression. 379 | - `remove`: Remove the watched expression. 380 | - `edit`: Edit an expression or set the value of a child variable. 381 | - `repl`: Send expression to REPL 382 | 383 | *dapui.elements.watches.add()* 384 | `add`({expr}) 385 | 386 | Add a new watch expression 387 | Parameters~ 388 | {expr?} `(string)` 389 | 390 | *dapui.elements.watches.edit()* 391 | `edit`({index}, {new_expr}) 392 | 393 | Change the chosen watch expression 394 | Parameters~ 395 | {index} `(integer)` 396 | {new_expr} `(string)` 397 | 398 | *dapui.elements.watches.remove()* 399 | `remove`({index}) 400 | 401 | Remove the chosen watch expression 402 | 403 | *dapui.elements.watches.get()* 404 | `get`() 405 | 406 | Get the current list of watched expressions 407 | Return~ 408 | `(dapui.elements.watches.Watch[])` 409 | 410 | *dapui.elements.watches.Watch* 411 | Fields~ 412 | {expression} `(string)` 413 | {expanded} `(boolean)` 414 | 415 | *dapui.elements.watches.toggle_expand()* 416 | `toggle_expand`({index}) 417 | 418 | Toggle the expanded state of the chosen watch expression 419 | Parameters~ 420 | {index} `(integer)` 421 | 422 | 423 | ============================================================================== 424 | dapui.elements.breakpoints *dapui.elements.breakpoints* 425 | 426 | Lists all breakpoints currently set. 427 | 428 | Mappings: 429 | - `open`: Jump to the location the breakpoint is set 430 | - `toggle`: Enable/disable the selected breakpoint 431 | - `remove`: Remove breakpoint. Only works on enabled breakpoints. 432 | 433 | 434 | ============================================================================== 435 | dapui.elements.console *dapui.elements.console* 436 | 437 | The console window used by nvim-dap for the integrated terminal. 438 | 439 | 440 | vim:tw=78:ts=8:noet:ft=help:norl: 441 | -------------------------------------------------------------------------------- /lua/dapui/client/dap_types.lua: -------------------------------------------------------------------------------- 1 | ---nvim-dap internal representation of a breakpoint 2 | ---@class dapui.types.DAPBreakpoint 3 | ---@field line integer 4 | ---@field condition? string 5 | ---@field logMessage? string 6 | ---@field hitCondition? string 7 | ---@field state dapui.types.DAPBreakpointState 8 | 9 | ---@class dapui.types.DAPBreakpointState 10 | ---@field verified boolean If true, the breakpoint could be set (but not necessarily at the desired location). 11 | ---@field message? string A message about the state of the breakpoint. This is shown to the user and can be used to explain why a breakpoint could not be verified. 12 | -------------------------------------------------------------------------------- /lua/dapui/client/init.lua: -------------------------------------------------------------------------------- 1 | local dap = require("dap") 2 | local nio = require("nio") 3 | local util = require("dapui.util") 4 | local types = require("dapui.client.types") 5 | 6 | ---@alias dap.Session Session 7 | 8 | ---@class dapui.DAPClient 9 | ---@field request dapui.DAPRequestsClient 10 | ---@field listen dapui.DAPEventListenerClient 11 | ---@field session? dapui.SessionProxy 12 | ---@field lib dapui.DAPClientLib 13 | ---@field breakpoints dapui.BreakpointsProxy 14 | local DAPUIClient = {} 15 | 16 | ---@class dapui.SessionProxy 17 | ---@field current_frame? dapui.types.StackFrame 18 | ---@field _frame_set fun(frame: dapui.types.StackFrame) 19 | ---@field stopped_thread_id integer 20 | ---@field capabilities dapui.types.Capabilities 21 | ---@field threads table 22 | 23 | local proxied_session_keys = {} 24 | for _, key in ipairs({ 25 | "current_frame", 26 | "_frame_set", 27 | "stopped_thread_id", 28 | "capabilities", 29 | "threads", 30 | }) do 31 | proxied_session_keys[key] = true 32 | end 33 | 34 | ---@return dapui.SessionProxy 35 | local function create_session_proxy(session) 36 | return setmetatable({}, { 37 | __index = function(_, key) 38 | if not proxied_session_keys[key] then 39 | return nil 40 | end 41 | local value = session[key] 42 | if type(value) == "function" then 43 | return function(...) 44 | return value(session, ...) 45 | end 46 | end 47 | return value 48 | end, 49 | }) 50 | end 51 | 52 | ---@class dapui.client.BreakpointArgs{ 53 | ---@field condition? string 54 | ---@field hit_condition? string 55 | ---@field log_message? string 56 | 57 | ---@class dapui.BreakpointsProxy 58 | ---@field get fun(): table 59 | ---@field get_buf fun(bufnr: integer): dapui.types.DAPBreakpoint[] 60 | ---@field toggle fun(bufnr: integer, line: integer, args: dapui.client.BreakpointArgs) 61 | ---@field remove fun(bufnr: integer, line: integer) 62 | 63 | ---@return dapui.BreakpointsProxy 64 | local function create_breakpoints_proxy(breakpoints, session_factory) 65 | local proxy = {} 66 | local function refresh(bufnr) 67 | local bps = breakpoints.get(bufnr) 68 | local session = session_factory() 69 | if session then 70 | session:set_breakpoints(bps) 71 | end 72 | end 73 | 74 | proxy.get = function() 75 | return breakpoints.get() 76 | end 77 | proxy.get_buf = function(bufnr) 78 | return breakpoints.get(bufnr) 79 | end 80 | proxy.toggle = function(bufnr, line, args) 81 | breakpoints.toggle(args, bufnr, line) 82 | refresh(bufnr) 83 | end 84 | proxy.remove = function(bufnr, line) 85 | breakpoints.remove(bufnr, line) 86 | refresh(bufnr) 87 | end 88 | return proxy 89 | end 90 | 91 | local Error = function(err, args) 92 | local err_tbl = vim.tbl_extend("keep", err, args or {}) 93 | err_tbl.traceback = debug.traceback("test", 2) 94 | return setmetatable(err_tbl, { 95 | __tostring = function() 96 | local formatted = util.format_error(err) 97 | local message = ("DAP error: %s"):format(formatted) 98 | for name, value in pairs(args) do 99 | message = message 100 | .. ("\n%s: %s"):format(name, type(value) ~= "table" and value or vim.inspect(value)) 101 | end 102 | message = message .. "\n" .. err_tbl.traceback 103 | return message 104 | end, 105 | }) 106 | end 107 | 108 | ---@param session_factory fun(): dap.Session 109 | ---@return dapui.DAPClient 110 | local function create_client(session_factory, breakpoints) 111 | breakpoints = breakpoints or require("dap.breakpoints") 112 | local request_seqs = {} 113 | local async_request = nio.wrap(function(command, args, cb) 114 | local session = session_factory() 115 | request_seqs[session] = request_seqs[session] or {} 116 | request_seqs[session][session.seq] = true 117 | session:request(command, args, function(...) 118 | request_seqs[session][session.seq] = nil 119 | cb(...) 120 | end) 121 | end, 3) 122 | 123 | local request = setmetatable({}, { 124 | __index = function(_, command) 125 | return function(args) 126 | local start = vim.loop.now() 127 | local err, body = async_request(command, args) 128 | local diff = vim.loop.now() - start 129 | if err then 130 | error(Error(err, { command = command, args = args })) 131 | elseif body.error then 132 | error(Error(body.err, { command = command, args = args })) 133 | end 134 | return body 135 | end 136 | end, 137 | }) 138 | 139 | local listener_prefix = "DAPClient" .. tostring(vim.loop.now()) 140 | local listener_count = 0 141 | local listener_ids = {} 142 | local listen = setmetatable({}, { 143 | __index = function(_, event) 144 | return function(listener, opts) 145 | opts = opts or {} 146 | local listeners 147 | if opts.before then 148 | listeners = dap.listeners.before 149 | else 150 | listeners = dap.listeners.after 151 | end 152 | local listener_id = listener_prefix .. tostring(listener_count) 153 | listener_count = listener_count + 1 154 | local is_event = not types.request[event] 155 | local key = is_event and "event_" .. event or event 156 | listener_ids[#listener_ids + 1] = { key, listener_id } 157 | 158 | local wrap = function(inner) 159 | listeners[key][listener_id] = function(_, ...) 160 | if inner(...) then 161 | listeners[key][listener_id] = nil 162 | end 163 | end 164 | end 165 | 166 | if is_event then 167 | wrap(listener) 168 | else 169 | wrap(function(err, body, req, req_seq) 170 | if (request_seqs[session_factory()] or {})[req_seq] then 171 | return 172 | end 173 | return listener({ error = err, response = body, request = req }) 174 | end) 175 | end 176 | end 177 | end, 178 | }) 179 | 180 | local client = setmetatable({ 181 | breakpoints = create_breakpoints_proxy(breakpoints, session_factory), 182 | request = request, 183 | listen = listen, 184 | shutdown = function() 185 | for _, listener in ipairs(listener_ids) do 186 | dap.listeners.before[listener[1]][listener[2]] = nil 187 | dap.listeners.after[listener[1]][listener[2]] = nil 188 | end 189 | end, 190 | }, { 191 | __index = function(_, key) 192 | if key == "session" then 193 | local session = session_factory() 194 | if not session then 195 | return nil 196 | end 197 | return create_session_proxy(session) 198 | end 199 | end, 200 | }) 201 | client.lib = require("dapui.client.lib")(client) 202 | return client 203 | end 204 | 205 | return create_client 206 | -------------------------------------------------------------------------------- /lua/dapui/client/lib.lua: -------------------------------------------------------------------------------- 1 | local util = require("dapui.util") 2 | local nio = require("nio") 3 | 4 | ---@param client dapui.DAPClient 5 | return function(client) 6 | ---@class dapui.DAPClientLib 7 | local client_lib = {} 8 | 9 | ---@param frame dapui.types.StackFrame 10 | ---@param set_frame boolean Set the current frame of session to given frame 11 | function client_lib.jump_to_frame(frame, set_frame) 12 | local opened = (function() 13 | local line = frame.line 14 | local column = frame.column 15 | local source = frame.source 16 | if not source then 17 | return 18 | end 19 | 20 | if (source.sourceReference or 0) > 0 then 21 | local buf = nio.api.nvim_create_buf(false, true) 22 | local response = client.request.source({ sourceReference = source.sourceReference }) 23 | if not response.content then 24 | util.notify("No source available for frame", vim.log.levels.WARN) 25 | return 26 | end 27 | nio.api.nvim_buf_set_lines(buf, 0, 0, true, vim.split(response.content, "\n")) 28 | nio.api.nvim_buf_set_option(buf, "bufhidden", "delete") 29 | nio.api.nvim_buf_set_option(buf, "modifiable", false) 30 | return util.open_buf(buf, line, column) 31 | end 32 | 33 | if not source.path or not vim.uv.fs_stat(source.path) then 34 | util.notify("No source available for frame", vim.log.levels.WARN) 35 | return 36 | end 37 | 38 | local path = source.path 39 | 40 | if not column or column == 0 then 41 | column = 1 42 | end 43 | 44 | local bufnr = vim.uri_to_bufnr( 45 | util.is_uri(path) and path or vim.uri_from_fname(vim.fn.fnamemodify(path, ":p")) 46 | ) 47 | nio.fn.bufload(bufnr) 48 | return util.open_buf(bufnr, line, column) 49 | end)() 50 | if opened and set_frame then 51 | client.session._frame_set(frame) 52 | end 53 | end 54 | 55 | ---@param variable dapui.types.Variable 56 | function client_lib.set_variable(container_ref, variable, value) 57 | local ok, err = pcall(function() 58 | if client.session.capabilities.supportsSetExpression and variable.evaluateName then 59 | local frame_id = client.session.current_frame and client.session.current_frame.id 60 | client.request.setExpression({ 61 | expression = variable.evaluateName, 62 | value = value, 63 | frameId = frame_id, 64 | }) 65 | elseif client.session.capabilities.supportsSetVariable and container_ref then 66 | client.request.setVariable({ 67 | variablesReference = container_ref, 68 | name = variable.name, 69 | value = value, 70 | }) 71 | else 72 | util.notify( 73 | "Debug server doesn't support setting " .. (variable.evaluateName or variable.name), 74 | vim.log.levels.WARN 75 | ) 76 | end 77 | end) 78 | if not ok then 79 | util.notify(util.format_error(err)) 80 | end 81 | end 82 | 83 | local stop_count = 0 84 | client.listen.stopped(function() 85 | stop_count = stop_count + 1 86 | end) 87 | client.listen.initialized(function() 88 | stop_count = 0 89 | end) 90 | 91 | ---@return integer: The number of times the debugger has stopped 92 | function client_lib.step_number() 93 | return stop_count 94 | end 95 | 96 | return client_lib 97 | end 98 | -------------------------------------------------------------------------------- /lua/dapui/components/breakpoints.lua: -------------------------------------------------------------------------------- 1 | local config = require("dapui.config") 2 | local nio = require("nio") 3 | local util = require("dapui.util") 4 | 5 | ---@param client dapui.DAPClient 6 | return function(client, send_ready) 7 | local _disabled_breakpoints = {} 8 | 9 | for _, event in ipairs({ 10 | "setBreakpoints", 11 | "setFunctionBreakpoints", 12 | "setInstructionBreakpoints", 13 | "setDataBreakpoints", 14 | "stackTrace", 15 | "terminated", 16 | "exited", 17 | "disconnect", 18 | }) do 19 | client.listen[event](send_ready) 20 | end 21 | 22 | ---@param bp dapui.types.DAPBreakpoint 23 | local function _toggle(bufnr, bp) 24 | client.breakpoints.toggle(bufnr, bp.line, { 25 | condition = bp.condition, 26 | hit_condition = bp.hitCondition, 27 | log_message = bp.logMessage, 28 | }) 29 | 30 | local buffer_breakpoints = client.breakpoints.get_buf(bufnr) 31 | local enabled = false 32 | for _, buf_bp in ipairs(buffer_breakpoints) do 33 | if buf_bp.line == bp.line then 34 | enabled = true 35 | break 36 | end 37 | end 38 | 39 | if not _disabled_breakpoints[bufnr] then 40 | _disabled_breakpoints[bufnr] = {} 41 | end 42 | 43 | if not enabled then 44 | bp.enabled = false 45 | _disabled_breakpoints[bufnr][bp.line] = bp 46 | else 47 | _disabled_breakpoints[bufnr][bp.line] = nil 48 | end 49 | send_ready() 50 | end 51 | 52 | local function _get_breakpoints() 53 | ---@type table 54 | local bps = client.breakpoints.get() 55 | local merged_breakpoints = {} 56 | local buffers = {} 57 | for buf, _ in pairs(bps) do 58 | buffers[buf] = true 59 | end 60 | for bufnr, _ in pairs(_disabled_breakpoints) do 61 | buffers[bufnr] = true 62 | end 63 | for bufnr, _ in pairs(buffers) do 64 | local buf_points = bps[bufnr] or {} 65 | for _, bp in ipairs(buf_points) do 66 | bp.enabled = true 67 | if _disabled_breakpoints[bufnr] then 68 | _disabled_breakpoints[bufnr][bp.line] = nil 69 | end 70 | end 71 | merged_breakpoints[bufnr] = buf_points 72 | for _, bp in pairs(_disabled_breakpoints[bufnr] or {}) do 73 | table.insert(merged_breakpoints[bufnr], bp) 74 | end 75 | table.sort(merged_breakpoints[bufnr], function(a, b) 76 | return a.line < b.line 77 | end) 78 | end 79 | local sorted = {} 80 | for buffer, breakpoints in pairs(merged_breakpoints) do 81 | sorted[#sorted + 1] = { 82 | name = nio.api.nvim_buf_get_name(buffer), 83 | buffer = buffer, 84 | breakpoints = breakpoints, 85 | } 86 | end 87 | table.sort(sorted, function(a, b) 88 | return a.name < b.name 89 | end) 90 | return sorted 91 | end 92 | 93 | return { 94 | ---@param canvas dapui.Canvas 95 | render = function(canvas) 96 | local current_frame = client.session and client.session.current_frame 97 | local current_line = 0 98 | local current_file = "" 99 | if current_frame and current_frame.source and current_frame.source.path then 100 | current_file = nio.fn.bufname(current_frame.source.path) 101 | current_line = current_frame.line 102 | end 103 | local indent = config.render.indent 104 | for _, data in ipairs(_get_breakpoints()) do 105 | local buffer, bufname = data.buffer, data.name 106 | ---@type dapui.types.DAPBreakpoint[] 107 | local breakpoints = data.breakpoints 108 | local name = util.pretty_name(bufname) 109 | canvas:write(name, { group = "DapUIBreakpointsPath" }) 110 | canvas:write(":\n") 111 | 112 | for _, bp in ipairs(breakpoints) do 113 | local text = vim.api.nvim_buf_get_lines(buffer, bp.line - 1, bp.line, false) 114 | local jump_to_bp = util.partial( 115 | client.lib.jump_to_frame, 116 | { line = bp.line, column = 0, source = { path = bufname } } 117 | ) 118 | if vim.tbl_count(text) ~= 0 then 119 | canvas:add_mapping("open", jump_to_bp) 120 | canvas:add_mapping("remove", function() 121 | client.breakpoints.remove(buffer, bp.line) 122 | send_ready() 123 | end) 124 | canvas:add_mapping("toggle", function() 125 | _toggle(buffer, bp) 126 | end) 127 | canvas:write(string.rep(" ", indent)) 128 | local group 129 | if _disabled_breakpoints[buffer] and _disabled_breakpoints[buffer][bp.line] then 130 | group = "DapUIBreakpointsDisabledLine" 131 | elseif bp.line == current_line and name == current_file then 132 | group = "DapUIBreakpointsCurrentLine" 133 | else 134 | group = "DapUIBreakpointsLine" 135 | end 136 | canvas:write(tostring(bp.line), { group = group }) 137 | canvas:write(" " .. vim.trim(text[1]) .. "\n") 138 | 139 | local info_indent = indent + #tostring(bp.line) + 1 140 | local whitespace = string.rep(" ", info_indent) 141 | 142 | local function add_info(message, data) 143 | canvas:add_mapping("open", jump_to_bp) 144 | canvas:write(whitespace) 145 | canvas:write(message, { group = "DapUIBreakpointsInfo" }) 146 | canvas:write(" " .. data .. "\n") 147 | end 148 | 149 | if bp.logMessage then 150 | add_info("Log Message:", bp.logMessage) 151 | end 152 | if bp.condition then 153 | add_info("Condition:", bp.condition) 154 | end 155 | if bp.hitCondition then 156 | add_info("Hit Condition:", bp.hitCondition) 157 | end 158 | end 159 | end 160 | canvas:write("\n") 161 | end 162 | if canvas:length() > 1 then 163 | canvas:remove_line() 164 | canvas:remove_line() 165 | else 166 | canvas:write("") 167 | end 168 | end, 169 | } 170 | end 171 | -------------------------------------------------------------------------------- /lua/dapui/components/frames.lua: -------------------------------------------------------------------------------- 1 | local config = require("dapui.config") 2 | local util = require("dapui.util") 3 | 4 | ---@param client dapui.DAPClient 5 | return function(client, send_ready) 6 | client.listen.scopes(send_ready) 7 | client.listen.terminated(send_ready) 8 | client.listen.exited(send_ready) 9 | client.listen.disconnect(send_ready) 10 | 11 | return { 12 | ---@async 13 | ---@param canvas dapui.Canvas 14 | render = function(canvas, thread_id, show_subtle, indent) 15 | if not client.session then 16 | return 17 | end 18 | 19 | local current_frame_id = nil 20 | 21 | local threads = client.session.threads 22 | 23 | if not threads or not threads[thread_id] then 24 | return 25 | end 26 | 27 | local frames = threads[thread_id].frames 28 | if not frames then 29 | local success, response = pcall(client.request.stackTrace, { threadId = thread_id }) 30 | frames = success and response.stackFrames 31 | end 32 | if not frames then 33 | return 34 | end 35 | 36 | if not show_subtle then 37 | frames = vim.tbl_filter(function(frame) 38 | return frame.presentationHint ~= "subtle" 39 | end, frames) 40 | end 41 | 42 | if client.session then 43 | current_frame_id = client.session.current_frame and client.session.current_frame.id 44 | end 45 | 46 | for _, frame in ipairs(frames) do 47 | local is_current = frame.id == current_frame_id 48 | canvas:write(string.rep(" ", is_current and (indent - 1) or indent)) 49 | 50 | if is_current then 51 | canvas:write(config.icons.current_frame .. " ") 52 | end 53 | 54 | canvas:write( 55 | frame.name, 56 | { group = frame.id == current_frame_id and "DapUICurrentFrameName" or "DapUIFrameName" } 57 | ) 58 | canvas:write(" ") 59 | 60 | if frame.source ~= nil then 61 | local file_name = frame.source.name or frame.source.path or "" 62 | local source_name = util.pretty_name(file_name) 63 | canvas:write(source_name, { group = "DapUISource" }) 64 | end 65 | 66 | if frame.line ~= nil then 67 | canvas:write(":") 68 | canvas:write(frame.line, { group = "DapUILineNumber" }) 69 | end 70 | canvas:add_mapping("open", util.partial(client.lib.jump_to_frame, frame, true)) 71 | canvas:write("\n") 72 | end 73 | 74 | canvas:remove_line() 75 | end, 76 | } 77 | end 78 | -------------------------------------------------------------------------------- /lua/dapui/components/hover.lua: -------------------------------------------------------------------------------- 1 | local config = require("dapui.config") 2 | local util = require("dapui.util") 3 | 4 | ---@class Hover 5 | ---@field expression string 6 | ---@field expanded boolean 7 | ---@field var_component Variables 8 | ---@field mode "set" | nil 9 | local Hover = {} 10 | 11 | ---@param client dapui.DAPClient 12 | return function(client, send_ready) 13 | ---@return Hover 14 | local expression 15 | local expr_context = "hover" 16 | local expanded = false 17 | local render_vars = require("dapui.components.variables")(client, send_ready) 18 | local prompt_func 19 | 20 | return { 21 | set_expression = function(new_expr, context) 22 | expression = new_expr 23 | expr_context = context or "hover" 24 | send_ready() 25 | end, 26 | ---@param canvas dapui.Canvas 27 | render = function(canvas) 28 | local frame = client.session and client.session.current_frame 29 | if not frame then 30 | return 31 | end 32 | if not expression then 33 | return 34 | end 35 | 36 | if prompt_func then 37 | canvas:set_prompt("> ", prompt_func, { fill = expression }) 38 | end 39 | 40 | local success, hover_expr = pcall( 41 | client.request.evaluate, 42 | { expression = expression, context = expr_context, frameId = frame.id } 43 | ) 44 | 45 | local var_ref = success and hover_expr.variablesReference 46 | 47 | local prefix 48 | if not success or hover_expr.variablesReference > 0 then 49 | prefix = config.icons[expanded and "expanded" or "collapsed"] .. " " 50 | canvas:write(prefix, { group = success and "DapUIDecoration" or "DapUIWatchesError" }) 51 | end 52 | 53 | canvas:write(expression) 54 | 55 | local val_start = 0 56 | local value 57 | if not success then 58 | canvas:write(": ") 59 | val_start = canvas:line_width() 60 | --- Fails formatting if it isn't a DAP error 61 | value = util.format_error(hover_expr) or error(hover_expr) 62 | elseif hover_expr then 63 | local eval_type = util.render_type(hover_expr.type) 64 | if #eval_type > 0 then 65 | canvas:write(" ") 66 | canvas:write(eval_type, { group = "DapUIType" }) 67 | end 68 | canvas:write(" = ") 69 | val_start = canvas:line_width() 70 | value = hover_expr.result 71 | else 72 | return 73 | end 74 | for _, line in ipairs(util.format_value(val_start, value)) do 75 | canvas:write(line, { group = "DapUIValue" }) 76 | if success then 77 | canvas:add_mapping("expand", function() 78 | expanded = not expanded 79 | send_ready() 80 | end) 81 | canvas:add_mapping("repl", util.partial(util.send_to_repl, expression)) 82 | end 83 | canvas:add_mapping("edit", function() 84 | prompt_func = function(new_expr) 85 | expression = new_expr 86 | prompt_func = prompt_func 87 | send_ready() 88 | end 89 | send_ready() 90 | end) 91 | canvas:write("\n") 92 | end 93 | 94 | if expanded and var_ref then 95 | render_vars.render(canvas, expression, var_ref, config.render.indent) 96 | end 97 | canvas:remove_line() 98 | end, 99 | } 100 | end 101 | -------------------------------------------------------------------------------- /lua/dapui/components/scopes.lua: -------------------------------------------------------------------------------- 1 | local config = require("dapui.config") 2 | ---@param client dapui.DAPClient 3 | return function(client, send_ready) 4 | local render_vars = require("dapui.components.variables")(client, send_ready) 5 | local closed_scopes = {} 6 | 7 | ---@param scope dapui.types.Scope 8 | local function scope_prefix(scope) 9 | if scope.indexedVariables == 0 then 10 | return " " 11 | end 12 | return config.icons[closed_scopes[scope.name] and "collapsed" or "expanded"] 13 | end 14 | 15 | ---@type dapui.types.Scope[] | nil 16 | local _scopes 17 | client.listen.scopes(function(args) 18 | if args.response then 19 | _scopes = args.response.scopes 20 | -- when new scopes are parsed, automatically disable the scopes that are too expensive to render 21 | for _, scope in ipairs(_scopes) do 22 | if scope.expensive then 23 | closed_scopes[scope.name] = true 24 | end 25 | end 26 | end 27 | send_ready() 28 | end) 29 | local on_exit = function() 30 | _scopes = nil 31 | send_ready() 32 | end 33 | client.listen.terminated(on_exit) 34 | client.listen.exited(on_exit) 35 | client.listen.disconnect(on_exit) 36 | 37 | return { 38 | ---@param canvas dapui.Canvas 39 | render = function(canvas) 40 | -- In case scopes are wiped during render 41 | local scopes = _scopes 42 | if not scopes then 43 | return 44 | end 45 | for i, scope in pairs(scopes) do 46 | canvas:add_mapping("expand", function() 47 | closed_scopes[scope.name] = not closed_scopes[scope.name] 48 | send_ready() 49 | end) 50 | 51 | canvas:write({ 52 | { scope_prefix(scope), group = "DapUIDecoration" }, 53 | " ", 54 | { scope.name, group = "DapUIScope" }, 55 | { ":\n" }, 56 | }) 57 | 58 | -- only render expanded scopes to save resources 59 | if not closed_scopes[scope.name] then 60 | render_vars.render(canvas, scope.name, scope.variablesReference, config.render.indent) 61 | end 62 | 63 | if i < #scopes then 64 | canvas:write("\n") 65 | end 66 | end 67 | 68 | canvas:remove_line() 69 | end, 70 | } 71 | end 72 | -------------------------------------------------------------------------------- /lua/dapui/components/threads.lua: -------------------------------------------------------------------------------- 1 | local config = require("dapui.config") 2 | local frame_renderer = require("dapui.components.frames") 3 | 4 | ---@param client dapui.DAPClient 5 | ---@param send_ready function 6 | return function(client, send_ready) 7 | ---@type dapui.types.Thread[] | nil 8 | local _threads = nil 9 | 10 | client.listen.threads(function(args) 11 | _threads = args.response.threads 12 | end) 13 | client.listen.scopes(function() 14 | send_ready() 15 | end) 16 | 17 | local on_exit = function() 18 | _threads = nil 19 | send_ready() 20 | end 21 | client.listen.terminated(on_exit) 22 | client.listen.exited(on_exit) 23 | client.listen.disconnect(on_exit) 24 | 25 | local render_frames = frame_renderer(client, send_ready) 26 | local subtle_threads = {} 27 | return { 28 | ---@param canvas dapui.Canvas 29 | render = function(canvas, indent) 30 | -- In case threads are wiped during render 31 | local threads = _threads 32 | local session = client.session 33 | if not threads or not session then 34 | return 35 | end 36 | 37 | indent = indent or 0 38 | 39 | ---@param thread dapui.types.Thread 40 | local function render_thread(thread, match_group) 41 | local first_line = canvas:length() 42 | 43 | canvas:write({ { thread.name, group = match_group }, ":\n" }) 44 | 45 | render_frames.render( 46 | canvas, 47 | thread.id, 48 | subtle_threads[thread.id] or false, 49 | indent + config.render.indent 50 | ) 51 | 52 | local last_line = canvas:length() 53 | 54 | for line = first_line, last_line, 1 do 55 | canvas:add_mapping("toggle", function() 56 | subtle_threads[thread.id] = not subtle_threads[thread.id] 57 | send_ready() 58 | end, { line = line }) 59 | end 60 | 61 | canvas:write("\n\n") 62 | end 63 | 64 | local stopped_thread_id = session.stopped_thread_id 65 | 66 | for _, thread in pairs(threads) do 67 | if thread.id == stopped_thread_id then 68 | render_thread(thread, "DapUIStoppedThread") 69 | end 70 | end 71 | for _, thread in pairs(threads) do 72 | if thread.id ~= stopped_thread_id then 73 | render_thread(thread, "DapUIThread") 74 | end 75 | end 76 | 77 | -- canvas:remove_line() 78 | -- canvas:remove_line() 79 | end, 80 | } 81 | end 82 | -------------------------------------------------------------------------------- /lua/dapui/components/variables.lua: -------------------------------------------------------------------------------- 1 | local config = require("dapui.config") 2 | local util = require("dapui.util") 3 | local partial = util.partial 4 | local nio = require("nio") 5 | 6 | ---@class Variables 7 | ---@field frame_expanded_children table 8 | ---@field child_components table 9 | ---@field var_to_set table | nil 10 | ---@field mode "set" | nil 11 | ---@field rendered_step integer | nil 12 | ---@field rendered_vars table[] | nil 13 | local Variables = {} 14 | 15 | ---@param client dapui.DAPClient 16 | ---@param send_ready function 17 | return function(client, send_ready) 18 | local expanded_children = {} 19 | 20 | ---@type fun(value: string) | nil 21 | local prompt_func 22 | ---@type string | nil 23 | local prompt_fill 24 | ---@type table 25 | local rendered_vars = {} 26 | 27 | local function reference_prefix(path, variable) 28 | if variable.variablesReference == 0 then 29 | return " " 30 | end 31 | return config.icons[expanded_children[path] and "expanded" or "collapsed"] 32 | end 33 | 34 | ---@param path string 35 | local function path_changed(path, value) 36 | return rendered_vars[path] and rendered_vars[path] ~= value 37 | end 38 | 39 | ---@param canvas dapui.Canvas 40 | ---@param parent_path string 41 | ---@param parent_ref integer 42 | ---@param indent integer 43 | local function render(canvas, parent_path, parent_ref, indent) 44 | if not canvas.prompt and prompt_func then 45 | canvas:set_prompt("> ", prompt_func, { fill = prompt_fill }) 46 | end 47 | indent = indent or 0 48 | local success, var_data = pcall(client.request.variables, { variablesReference = parent_ref }) 49 | local variables = success and var_data.variables or {} 50 | if config.render.sort_variables then 51 | table.sort(variables, config.render.sort_variables) 52 | end 53 | for _, variable in pairs(variables) do 54 | local var_path = parent_path .. "." .. variable.name 55 | 56 | canvas:write({ 57 | string.rep(" ", indent), 58 | { reference_prefix(var_path, variable), group = "DapUIDecoration" }, 59 | " ", 60 | { variable.name, group = "DapUIVariable" }, 61 | }) 62 | 63 | local var_type = util.render_type(variable.type) 64 | if #var_type > 0 then 65 | canvas:write({ " ", { var_type, group = "DapUIType" } }) 66 | end 67 | 68 | local var_group 69 | if path_changed(var_path, variable.value) then 70 | var_group = "DapUIModifiedValue" 71 | else 72 | var_group = "DapUIValue" 73 | end 74 | rendered_vars[var_path] = variable.value 75 | local function add_var_line(line) 76 | if variable.variablesReference > 0 then 77 | canvas:add_mapping("expand", function() 78 | expanded_children[var_path] = not expanded_children[var_path] 79 | send_ready() 80 | end) 81 | if variable.evaluateName then 82 | canvas:add_mapping("repl", partial(util.send_to_repl, variable.evaluateName)) 83 | end 84 | end 85 | canvas:add_mapping("edit", function() 86 | prompt_func = function(new_value) 87 | nio.run(function() 88 | prompt_func = nil 89 | prompt_fill = nil 90 | client.lib.set_variable(parent_ref, variable, new_value) 91 | send_ready() 92 | end) 93 | end 94 | prompt_fill = variable.value 95 | send_ready() 96 | end) 97 | canvas:write(line .. "\n", { group = var_group }) 98 | end 99 | 100 | if #(variable.value or "") > 0 then 101 | canvas:write(" = ") 102 | local value_start = #canvas.lines[canvas:length()] 103 | local value = variable.value 104 | 105 | for _, line in ipairs(util.format_value(value_start, value)) do 106 | add_var_line(line) 107 | end 108 | else 109 | add_var_line(variable.value) 110 | end 111 | 112 | if expanded_children[var_path] and variable.variablesReference ~= 0 then 113 | render(canvas, var_path, variable.variablesReference, indent + config.render.indent) 114 | end 115 | end 116 | end 117 | 118 | return { 119 | render = render, 120 | } 121 | end 122 | -------------------------------------------------------------------------------- /lua/dapui/components/watches.lua: -------------------------------------------------------------------------------- 1 | local config = require("dapui.config") 2 | local util = require("dapui.util") 3 | 4 | local partial = util.partial 5 | 6 | ---@class dapui.watches.Watch 7 | ---@field expression string 8 | ---@field expanded boolean 9 | 10 | ---@param client dapui.DAPClient 11 | return function(client, send_ready) 12 | local running = false 13 | client.listen.scopes(function() 14 | running = true 15 | send_ready() 16 | end) 17 | local on_exit = function() 18 | running = false 19 | send_ready() 20 | end 21 | 22 | client.listen.terminated(on_exit) 23 | client.listen.exited(on_exit) 24 | client.listen.disconnect(on_exit) 25 | 26 | ---@type dapui.watches.Watch[] 27 | local watches = {} 28 | local edit_index = nil 29 | local rendered_exprs = {} 30 | local rendered_step = client.lib.step_number() 31 | local render_vars = require("dapui.components.variables")(client, send_ready) 32 | 33 | local function add_watch(value) 34 | if #value > 0 then 35 | watches[#watches + 1] = { 36 | expression = value, 37 | expanded = false, 38 | } 39 | send_ready() 40 | end 41 | end 42 | 43 | local function edit_expr(new_value, index) 44 | index = index or edit_index 45 | edit_index = nil 46 | if #new_value > 0 then 47 | watches[index].expression = new_value 48 | end 49 | send_ready() 50 | end 51 | 52 | local function remove_expr(expr_i) 53 | table.remove(watches, expr_i) 54 | send_ready() 55 | end 56 | 57 | local function toggle_expression(expr_i) 58 | watches[expr_i].expanded = not watches[expr_i].expanded 59 | send_ready() 60 | end 61 | 62 | return { 63 | add = add_watch, 64 | edit = edit_expr, 65 | remove = remove_expr, 66 | get = function() 67 | return vim.deepcopy(watches) 68 | end, 69 | expand = toggle_expression, 70 | ---@param canvas dapui.Canvas 71 | render = function(canvas) 72 | if not edit_index then 73 | canvas:set_prompt("> ", add_watch) 74 | else 75 | canvas:set_prompt("> ", edit_expr, { fill = watches[edit_index].expression }) 76 | end 77 | 78 | if vim.tbl_count(watches) == 0 then 79 | canvas:write("No Expressions\n", { group = "DapUIWatchesEmpty" }) 80 | return 81 | end 82 | local frame_id = client.session 83 | and client.session.current_frame 84 | and client.session.current_frame.id 85 | local step = client.lib.step_number() 86 | for i, watch in pairs(watches) do 87 | local success, evaluated 88 | if running then 89 | success, evaluated = pcall( 90 | client.request.evaluate, 91 | { context = "watch", expression = watch.expression, frameId = frame_id } 92 | ) 93 | else 94 | success, evaluated = false, { message = "No active session" } 95 | end 96 | local prefix = config.icons[watch.expanded and "expanded" or "collapsed"] 97 | 98 | canvas:write({ 99 | { prefix, group = success and "DapUIWatchesValue" or "DapUIWatchesError" }, 100 | " " .. watch.expression, 101 | }) 102 | 103 | local value = "" 104 | if not success then 105 | watch.expanded = false 106 | canvas:write(": ") 107 | value = util.format_error(evaluated) 108 | else 109 | local eval_type = util.render_type(evaluated.type) 110 | if #eval_type > 0 then 111 | canvas:write({ " ", { eval_type, group = "DapUIType" } }) 112 | end 113 | canvas:write(" = ") 114 | value = evaluated.result 115 | end 116 | local val_start = canvas:line_width() 117 | local var_group 118 | 119 | if not success or rendered_exprs[i] == evaluated.result then 120 | var_group = "DapUIValue" 121 | else 122 | var_group = "DapUIModifiedValue" 123 | end 124 | 125 | for _, line in ipairs(util.format_value(val_start, value)) do 126 | canvas:write(line, { group = var_group }) 127 | canvas:add_mapping("remove", partial(remove_expr, i)) 128 | canvas:add_mapping("edit", function() 129 | edit_index = i 130 | send_ready() 131 | end) 132 | if success then 133 | canvas:add_mapping("expand", partial(toggle_expression, i)) 134 | canvas:add_mapping("repl", partial(util.send_to_repl, watch.expression)) 135 | end 136 | canvas:write("\n") 137 | end 138 | 139 | local var_ref = success and evaluated.variablesReference or 0 140 | if watch.expanded and var_ref > 0 then 141 | render_vars.render(canvas, watch.expression, var_ref, config.render.indent) 142 | end 143 | if rendered_step ~= step then 144 | rendered_exprs[i] = evaluated.result 145 | end 146 | end 147 | if rendered_step ~= step then 148 | rendered_step = step 149 | end 150 | end, 151 | } 152 | end 153 | -------------------------------------------------------------------------------- /lua/dapui/config/highlights.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local control_hl_groups = { 4 | "DapUINormal", 5 | "DapUIPlayPause", 6 | "DapUIRestart", 7 | "DapUIStop", 8 | "DapUIUnavailable", 9 | "DapUIStepOver", 10 | "DapUIStepInto", 11 | "DapUIStepBack", 12 | "DapUIStepOut", 13 | } 14 | 15 | function M.setup() 16 | vim.cmd([[ 17 | hi default link DapUINormal Normal 18 | hi default link DapUIVariable Normal 19 | hi default link DapUIScope Identifier 20 | hi default link DapUIType Type 21 | hi default link DapUIValue Normal 22 | hi default link DapUIModifiedValue Function 23 | hi default link DapUIDecoration Identifier 24 | hi default link DapUIThread Identifier 25 | hi default link DapUIStoppedThread Function 26 | hi default link DapUIFrameName Normal 27 | hi default link DapUISource Define 28 | hi default link DapUILineNumber LineNr 29 | hi default link DapUIFloatNormal NormalFloat 30 | hi default link DapUIFloatBorder Identifier 31 | hi default link DapUIWatchesEmpty PreProc 32 | hi default link DapUIWatchesValue Statement 33 | hi default link DapUIWatchesError PreProc 34 | hi default link DapUIBreakpointsPath Identifier 35 | hi default link DapUIBreakpointsInfo Statement 36 | hi default link DapUIBreakpointsCurrentLine CursorLineNr 37 | hi default link DapUIBreakpointsLine DapUILineNumber 38 | hi default link DapUIBreakpointsDisabledLine Comment 39 | hi default link DapUICurrentFrameName DapUIBreakpointsCurrentLine 40 | hi default link DapUIStepOver Label 41 | hi default link DapUIStepInto Label 42 | hi default link DapUIStepBack Label 43 | hi default link DapUIStepOut Label 44 | hi default link DapUIStop PreProc 45 | hi default link DapUIPlayPause Repeat 46 | hi default link DapUIRestart Repeat 47 | hi default link DapUIUnavailable Comment 48 | hi default link DapUIWinSelect Special 49 | hi default link DapUIEndofBuffer EndofBuffer 50 | ]]) 51 | 52 | ---gets the argument highlight group information, using the newer `nvim_get_hl` if available 53 | ---@param highlight string highlight group 54 | ---@return table hl highlight information 55 | local function get_highlight(highlight) 56 | local ok, hl 57 | if vim.fn.has("nvim-0.9") == 1 then 58 | ok, hl = pcall(vim.api.nvim_get_hl, 0, { name = highlight }) 59 | if not ok then -- highlight group is invalid 60 | return vim.empty_dict() 61 | end 62 | else 63 | ok, hl = pcall(vim.api.nvim_get_hl_by_name, highlight, true) 64 | if not ok or hl[true] then -- highlight group is invalid or cleared 65 | return vim.empty_dict() 66 | end 67 | -- change `nvim_get_hl_by_name` output into `nvim_get_hl` output format 68 | hl.bg = hl.background 69 | hl.fg = hl.foreground 70 | end 71 | return hl 72 | end 73 | 74 | -- Generate *NC variants of the control highlight groups 75 | if vim.fn.has("nvim-0.8") == 1 then 76 | local bg = get_highlight("WinBar").bg 77 | local bgNC = get_highlight("WinBarNC").bg 78 | 79 | for _, hl_group in pairs(control_hl_groups) do 80 | local gui = get_highlight(hl_group) 81 | -- if highlight group is cleared or invalid, skip 82 | if not vim.tbl_isempty(gui) then 83 | gui.default = true 84 | if gui.bg ~= bg then 85 | gui.bg = bg 86 | vim.api.nvim_set_hl(0, hl_group, gui) 87 | end 88 | gui.bg = bgNC 89 | vim.api.nvim_set_hl(0, hl_group .. "NC", gui) 90 | end 91 | end 92 | else 93 | for _, hl_group in pairs(control_hl_groups) do 94 | vim.cmd(string.format("hi default link %sNC %s", hl_group, hl_group)) 95 | end 96 | end 97 | end 98 | 99 | vim.cmd([[ 100 | augroup DAPUIRefreshHighlights 101 | autocmd! 102 | autocmd ColorScheme * lua require('dapui.config.highlights').setup() 103 | augroup END 104 | ]]) 105 | 106 | return M 107 | -------------------------------------------------------------------------------- /lua/dapui/config/init.lua: -------------------------------------------------------------------------------- 1 | local dapui = {} 2 | 3 | ---@tag dapui.config 4 | ---@toc_entry Configuration Options 5 | 6 | ---@class dapui.Config 7 | ---@field icons dapui.Config.icons 8 | ---@field mappings table Keys to trigger actions in elements 9 | ---@field element_mappings table> Per-element overrides of global mappings 10 | ---@field expand_lines boolean Expand current line to hover window if larger 11 | --- than window size 12 | ---@field force_buffers boolean Prevents other buffers being loaded into 13 | --- nvim-dap-ui windows 14 | ---@field layouts dapui.Config.layout[] Layouts to display elements within. 15 | --- Layouts are opened in the order defined 16 | ---@field floating dapui.Config.floating Floating window specific options 17 | ---@field controls dapui.Config.controls Controls configuration 18 | ---@field render dapui.Config.render Rendering options which can be updated 19 | --- after initial setup 20 | ---@field select_window? fun(): integer A function which returns a window to be 21 | --- used for opening buffers such as a stack frame location. 22 | 23 | ---@class dapui.Config.icons 24 | ---@field expanded string 25 | ---@field collapsed string 26 | ---@field current_frame string 27 | 28 | ---@class dapui.Config.layout 29 | ---@field elements string[]|dapui.Config.layout.element[] Elements to display 30 | --- in this layout 31 | ---@field size number Size of the layout in lines/columns 32 | ---@field position "left"|"right"|"top"|"bottom" Which side of editor to open 33 | --- layout on 34 | 35 | ---@class dapui.Config.layout.element 36 | ---@field id string Element ID 37 | ---@field size number Size of the element in lines/columns or as proportion of 38 | --- total editor size (0-1) 39 | 40 | ---@class dapui.Config.floating 41 | ---@field max_height? number Maximum height of floating window (integer or float 42 | --- between 0 and 1) 43 | ---@field max_width? number Maximum width of floating window (integer or float 44 | --- between 0 and 1) 45 | ---@field border string|string[] Border argument supplied to `nvim_open_win` 46 | ---@field mappings table Keys to trigger 47 | --- actions in elements 48 | 49 | ---@class dapui.Config.controls 50 | ---@field enabled boolean Show controls on an element (requires winbar feature) 51 | ---@field element string Element to show controls on 52 | ---@field icons dapui.Config.controls.icons 53 | 54 | ---@class dapui.Config.controls.icons 55 | ---@field pause string 56 | ---@field play string 57 | ---@field step_into string 58 | ---@field step_over string 59 | ---@field step_out string 60 | ---@field step_back string 61 | ---@field run_last string 62 | ---@field terminate string 63 | 64 | ---@class dapui.Config.render 65 | ---@field indent integer Default indentation size 66 | ---@field max_type_length? integer Maximum number of characters to allow a type 67 | --- name to fill before trimming 68 | ---@field max_value_lines? integer Maximum number of lines to allow a value to 69 | --- fill before trimming 70 | ---@field sort_variables? fun(a: dapui.types.Variable, b: dapui.types.Variable):boolean Sorting function to determine 71 | --- render order of variables. 72 | 73 | ---@alias dapui.Action "expand"|"open"|"remove"|"edit"|"repl"|"toggle" 74 | 75 | ---@alias dapui.FloatingAction "close" 76 | 77 | ---@type dapui.Config 78 | ---@nodoc 79 | local default_config = { 80 | icons = { expanded = "", collapsed = "", current_frame = "" }, 81 | mappings = { 82 | -- Use a table to apply multiple mappings 83 | expand = { "", "<2-LeftMouse>" }, 84 | open = "o", 85 | remove = "d", 86 | edit = "e", 87 | repl = "r", 88 | toggle = "t", 89 | }, 90 | element_mappings = {}, 91 | expand_lines = vim.fn.has("nvim-0.7") == 1, 92 | force_buffers = true, 93 | layouts = { 94 | { 95 | -- You can change the order of elements in the sidebar 96 | elements = { 97 | -- Provide IDs as strings or tables with "id" and "size" keys 98 | { 99 | id = "scopes", 100 | size = 0.25, -- Can be float or integer > 1 101 | }, 102 | { id = "breakpoints", size = 0.25 }, 103 | { id = "stacks", size = 0.25 }, 104 | { id = "watches", size = 0.25 }, 105 | }, 106 | size = 40, 107 | position = "left", -- Can be "left" or "right" 108 | }, 109 | { 110 | elements = { 111 | "repl", 112 | "console", 113 | }, 114 | size = 10, 115 | position = "bottom", -- Can be "bottom" or "top" 116 | }, 117 | }, 118 | floating = { 119 | max_height = nil, 120 | max_width = nil, 121 | border = "single", 122 | mappings = { 123 | ["close"] = { "q", "" }, 124 | }, 125 | }, 126 | controls = { 127 | enabled = vim.fn.exists("+winbar") == 1, 128 | element = "repl", 129 | icons = { 130 | pause = "", 131 | play = "", 132 | step_into = "", 133 | step_over = "", 134 | step_out = "", 135 | step_back = "", 136 | run_last = "", 137 | terminate = "", 138 | disconnect = "", 139 | }, 140 | }, 141 | render = { 142 | max_type_length = nil, -- Can be integer or nil. 143 | max_value_lines = 100, -- Can be integer or nil. 144 | indent = 1, 145 | }, 146 | } 147 | 148 | local user_config = default_config 149 | 150 | local function fill_elements(area) 151 | area = vim.deepcopy(area) 152 | local filled = {} 153 | if vim.fn.has("nvim-0.11") == 1 then 154 | vim.validate("size", area.size, "number") 155 | vim.validate("elements", area.elements, "table") 156 | vim.validate("position", area.position, "string") 157 | else 158 | vim.validate({ 159 | size = { area.size, "number" }, 160 | elements = { area.elements, "table" }, 161 | position = { area.position, "string" }, 162 | }) 163 | end 164 | for i, element in ipairs(area.elements) do 165 | if type(element) == "string" then 166 | filled[i] = { id = element, size = 1 / #area.elements } 167 | else 168 | filled[i] = element 169 | end 170 | end 171 | area.elements = filled 172 | return area 173 | end 174 | 175 | local function fill_mappings(mappings) 176 | local filled = {} 177 | for action, keys in pairs(mappings) do 178 | filled[action] = type(keys) == "table" and keys or { keys } 179 | end 180 | return filled 181 | end 182 | 183 | ---@class dapui.config : dapui.Config 184 | ---@nodoc 185 | dapui.config = {} 186 | 187 | function dapui.config.setup(config) 188 | config = config or {} 189 | local filled = vim.tbl_deep_extend("keep", config, default_config) 190 | 191 | if config.layouts then 192 | filled.layouts = config.layouts 193 | end 194 | filled.mappings = fill_mappings(filled.mappings) 195 | 196 | local element_mappings = {} 197 | for elem, mappings in pairs(filled.element_mappings) do 198 | element_mappings[elem] = fill_mappings(mappings) 199 | end 200 | 201 | filled.element_mappings = element_mappings 202 | filled.floating.mappings = fill_mappings(filled.floating.mappings) 203 | for i, layout in ipairs(filled.layouts) do 204 | filled.layouts[i] = fill_elements(layout) 205 | end 206 | 207 | user_config = filled 208 | require("dapui.config.highlights").setup() 209 | end 210 | 211 | function dapui.config._format_default() 212 | local lines = { "Default values:", ">lua" } 213 | for line in vim.gsplit(vim.inspect(default_config), "\n", true) do 214 | table.insert(lines, " " .. line) 215 | end 216 | table.insert(lines, "<") 217 | return lines 218 | end 219 | 220 | ---@param update dapui.Config.render 221 | ---@nodoc 222 | function dapui.config.update_render(update) 223 | user_config.render = vim.tbl_deep_extend("keep", update, user_config.render) 224 | end 225 | 226 | function dapui.config.element_mapping(element) 227 | return vim.tbl_extend("keep", user_config.element_mappings[element] or {}, user_config.mappings) 228 | end 229 | 230 | setmetatable(dapui.config, { 231 | __index = function(_, key) 232 | return user_config[key] 233 | end, 234 | }) 235 | 236 | dapui.config.setup() 237 | 238 | return dapui.config 239 | -------------------------------------------------------------------------------- /lua/dapui/controls.lua: -------------------------------------------------------------------------------- 1 | local dap = require("dap") 2 | local config = require("dapui.config") 3 | 4 | local M = {} 5 | 6 | local controls_active = false 7 | M.refresh_control_panel = function() end 8 | 9 | function M.enable_controls(element) 10 | if controls_active then 11 | return 12 | end 13 | controls_active = true 14 | local buffer = element.buffer() 15 | 16 | local group = vim.api.nvim_create_augroup("DAPUIControls", {}) 17 | local win = vim.fn.bufwinid(buffer) 18 | 19 | M.refresh_control_panel = function() 20 | if win then 21 | local is_current = win == vim.fn.win_getid() 22 | if not pcall(vim.api.nvim_win_set_option, win, "winbar", M.controls(is_current)) then 23 | win = nil 24 | end 25 | vim.cmd("redrawstatus!") 26 | end 27 | end 28 | 29 | local list_id = "dapui_controls" 30 | local events = { 31 | "continue", 32 | "terminate", 33 | "restart", 34 | "disconnect", 35 | "event_terminated", 36 | "disconnect", 37 | "event_exited", 38 | "event_stopped", 39 | "threads", 40 | "event_continued", 41 | } 42 | for _, event in ipairs(events) do 43 | dap.listeners.after[event][list_id] = M.refresh_control_panel 44 | end 45 | 46 | vim.api.nvim_create_autocmd("BufWinEnter", { 47 | buffer = buffer, 48 | group = group, 49 | callback = function(opts) 50 | if win then 51 | return 52 | end 53 | 54 | win = vim.fn.bufwinid(opts.buf) 55 | if win == -1 then 56 | win = nil 57 | return 58 | end 59 | M.refresh_control_panel() 60 | vim.api.nvim_create_autocmd({ "WinClosed", "BufWinLeave" }, { 61 | group = group, 62 | buffer = buffer, 63 | callback = function() 64 | if win and not vim.api.nvim_win_is_valid(win) then 65 | win = nil 66 | end 67 | end, 68 | }) 69 | end, 70 | }) 71 | -- If original buffer is deleted, this will get newest element buffer 72 | vim.api.nvim_create_autocmd("BufWipeout", { 73 | buffer = buffer, 74 | group = group, 75 | callback = vim.schedule_wrap(function() 76 | controls_active = false 77 | M.enable_controls(element) 78 | end), 79 | }) 80 | 81 | vim.api.nvim_create_autocmd("WinEnter", { 82 | buffer = buffer, 83 | group = group, 84 | callback = function() 85 | local winbar = M.controls(true) 86 | vim.api.nvim_win_set_option(vim.api.nvim_get_current_win(), "winbar", winbar) 87 | end, 88 | }) 89 | vim.api.nvim_create_autocmd("WinLeave", { 90 | buffer = buffer, 91 | group = group, 92 | callback = function() 93 | local winbar = M.controls(false) 94 | vim.api.nvim_win_set_option(vim.api.nvim_get_current_win(), "winbar", winbar) 95 | end, 96 | }) 97 | end 98 | 99 | _G._dapui = { 100 | play = function() 101 | local session = dap.session() 102 | if not session or session.stopped_thread_id then 103 | dap.continue() 104 | else 105 | dap.pause() 106 | end 107 | end, 108 | } 109 | 110 | setmetatable(_dapui, { 111 | __index = function(_, key) 112 | return function() 113 | return dap[key]() 114 | end 115 | end, 116 | }) 117 | function M.controls(is_active) 118 | local session = dap.session() 119 | 120 | local running = (session and not session.stopped_thread_id) 121 | 122 | local avail_hl = function(group, allow_running) 123 | if not session or (not allow_running and running) then 124 | return is_active and "DapUIUnavailable" or "DapUIUnavailableNC" 125 | end 126 | return group 127 | end 128 | 129 | local icons = config.controls.icons 130 | local elems = { 131 | { 132 | func = "play", 133 | icon = running and icons.pause or icons.play, 134 | hl = is_active and "DapUIPlayPause" or "DapUIPlayPauseNC", 135 | }, 136 | { func = "step_into", hl = avail_hl(is_active and "DapUIStepInto" or "DapUIStepIntoNC") }, 137 | { func = "step_over", hl = avail_hl(is_active and "DapUIStepOver" or "DapUIStepOverNC") }, 138 | { func = "step_out", hl = avail_hl(is_active and "DapUIStepOut" or "DapUIStepOutNC") }, 139 | { func = "step_back", hl = avail_hl(is_active and "DapUIStepBack" or "DapUIStepBackNC") }, 140 | { func = "run_last", hl = is_active and "DapUIRestart" or "DapUIRestartNC" }, 141 | { func = "terminate", hl = avail_hl(is_active and "DapUIStop" or "DapUIStopNC", true) }, 142 | { func = "disconnect", hl = avail_hl(is_active and "DapUIStop" or "DapUIStopNC", true) }, 143 | } 144 | local bar = "" 145 | for _, elem in ipairs(elems) do 146 | bar = bar 147 | .. (" %%#%s#%%0@v:lua._dapui.%s@%s%%#0#"):format( 148 | elem.hl, 149 | elem.func, 150 | elem.icon or icons[elem.func] 151 | ) 152 | end 153 | return bar 154 | end 155 | 156 | return M 157 | -------------------------------------------------------------------------------- /lua/dapui/elements/breakpoints.lua: -------------------------------------------------------------------------------- 1 | local config = require("dapui.config") 2 | local Canvas = require("dapui.render.canvas") 3 | local util = require("dapui.util") 4 | 5 | ---@param client dapui.DAPClient 6 | ---@nodoc 7 | return function(client) 8 | local dapui = { elements = {} } 9 | 10 | ---@class dapui.elements.breakpoints 11 | ---@toc_entry Breakpoints 12 | ---@text 13 | --- Lists all breakpoints currently set. 14 | --- 15 | --- Mappings: 16 | --- - `open`: Jump to the location the breakpoint is set 17 | --- - `toggle`: Enable/disable the selected breakpoint 18 | dapui.elements.breakpoints = { 19 | allow_without_session = true, 20 | } 21 | 22 | local send_ready = util.create_render_loop(function() 23 | dapui.elements.breakpoints.render() 24 | end) 25 | 26 | local breakpoints = require("dapui.components.breakpoints")(client, send_ready) 27 | 28 | ---@nodoc 29 | function dapui.elements.breakpoints.render() 30 | local canvas = Canvas.new() 31 | breakpoints.render(canvas) 32 | canvas:render_buffer(dapui.elements.breakpoints.buffer(), config.element_mapping("breakpoints")) 33 | end 34 | 35 | ---@nodoc 36 | dapui.elements.breakpoints.buffer = util.create_buffer("DAP Breakpoints", { 37 | filetype = "dapui_breakpoints", 38 | }) 39 | 40 | return dapui.elements.breakpoints 41 | end 42 | -------------------------------------------------------------------------------- /lua/dapui/elements/console.lua: -------------------------------------------------------------------------------- 1 | local nio = require("nio") 2 | local dap = require("dap") 3 | local util = require("dapui.util") 4 | 5 | return function() 6 | local dapui = { elements = {} } 7 | 8 | ---@class dapui.elements.console 9 | ---@toc_entry Console 10 | ---@text 11 | --- The console window used by nvim-dap for the integrated terminal. 12 | dapui.elements.console = {} 13 | 14 | local console_buf = -1 15 | local autoscroll = true 16 | ---@nodoc 17 | local function get_buf() 18 | if nio.api.nvim_buf_is_valid(console_buf) then 19 | return console_buf 20 | end 21 | console_buf = util.create_buffer("DAP Console", { filetype = "dapui_console" })() 22 | if vim.fn.has("nvim-0.7") == 1 then 23 | vim.keymap.set("n", "G", function() 24 | autoscroll = true 25 | vim.cmd("normal! G") 26 | end, { silent = true, buffer = console_buf }) 27 | nio.api.nvim_create_autocmd({ "InsertEnter", "CursorMoved" }, { 28 | group = nio.api.nvim_create_augroup("dap-repl-au", { clear = true }), 29 | buffer = console_buf, 30 | callback = function() 31 | local active_buf = nio.api.nvim_win_get_buf(0) 32 | if active_buf == console_buf then 33 | local lnum = nio.api.nvim_win_get_cursor(0)[1] 34 | autoscroll = lnum == nio.api.nvim_buf_line_count(console_buf) 35 | end 36 | end, 37 | }) 38 | nio.api.nvim_buf_attach(console_buf, false, { 39 | on_lines = function(_, _, _, _, _, _) 40 | local active_buf = nio.api.nvim_win_get_buf(0) 41 | 42 | if autoscroll and vim.fn.mode() == "n" and active_buf == console_buf then 43 | vim.cmd("normal! G") 44 | end 45 | end, 46 | }) 47 | end 48 | return console_buf 49 | end 50 | 51 | dap.defaults.fallback.terminal_win_cmd = get_buf 52 | 53 | function dapui.elements.console.render() end 54 | 55 | function dapui.elements.console.buffer() 56 | return get_buf() 57 | end 58 | 59 | function dapui.elements.console.float_defaults() 60 | return { width = 80, height = 20, enter = true } 61 | end 62 | 63 | return dapui.elements.console 64 | end 65 | -------------------------------------------------------------------------------- /lua/dapui/elements/hover.lua: -------------------------------------------------------------------------------- 1 | local config = require("dapui.config") 2 | local util = require("dapui.util") 3 | local Canvas = require("dapui.render.canvas") 4 | 5 | return function(client) 6 | local dapui = { elements = {} } 7 | 8 | ---@class dapui.elements.hover 9 | dapui.elements.hover = {} 10 | 11 | local send_ready = util.create_render_loop(function() 12 | dapui.elements.hover.render() 13 | end) 14 | 15 | local hover = require("dapui.components.hover")(client, send_ready) 16 | 17 | ---@nodoc 18 | function dapui.elements.hover.render() 19 | local canvas = Canvas.new() 20 | hover.render(canvas) 21 | canvas:render_buffer(dapui.elements.hover.buffer(), config.element_mapping("hover")) 22 | end 23 | 24 | ---@nodoc 25 | dapui.elements.hover.buffer = util.create_buffer("DAP Hover", { 26 | filetype = "dapui_hover", 27 | }) 28 | 29 | ---Set the expression for the hover window 30 | ---@param expression string 31 | function dapui.elements.hover.set_expression(expression, context) 32 | hover.set_expression(expression, context) 33 | end 34 | 35 | return dapui.elements.hover 36 | end 37 | -------------------------------------------------------------------------------- /lua/dapui/elements/repl.lua: -------------------------------------------------------------------------------- 1 | local nio = require("nio") 2 | local dap = require("dap") 3 | 4 | return function() 5 | local dapui = { elements = {} } 6 | 7 | ---@class dapui.elements.repl 8 | ---@toc_entry REPL 9 | ---@text 10 | --- The REPL provided by nvim-dap. 11 | dapui.elements.repl = {} 12 | 13 | ---@nodoc 14 | local function get_buffer() 15 | -- TODO: All of this is a hack because of an error with indentline when buffer 16 | -- is opened in a window so have to manually find the window that was opened. 17 | local all_wins = nio.api.nvim_list_wins() 18 | local open_wins = {} 19 | for _, win in pairs(all_wins) do 20 | open_wins[win] = true 21 | end 22 | pcall(dap.repl.open, {}) 23 | 24 | local buf = nio.fn.bufnr("dap-repl") 25 | 26 | for _, win in ipairs(nio.api.nvim_list_wins()) do 27 | if not open_wins[win] then 28 | pcall(nio.api.nvim_win_close, win, true) 29 | break 30 | end 31 | end 32 | return buf 33 | end 34 | 35 | local buf 36 | ---@nodoc 37 | function dapui.elements.repl.render() end 38 | 39 | ---@nodoc 40 | function dapui.elements.repl.buffer() 41 | if not nio.api.nvim_buf_is_valid(buf or -1) then 42 | buf = get_buffer() 43 | end 44 | return buf 45 | end 46 | 47 | ---@nodoc 48 | function dapui.elements.repl.float_defaults() 49 | return { width = 80, height = 20, enter = true } 50 | end 51 | 52 | return dapui.elements.repl 53 | end 54 | -------------------------------------------------------------------------------- /lua/dapui/elements/scopes.lua: -------------------------------------------------------------------------------- 1 | local config = require("dapui.config") 2 | local util = require("dapui.util") 3 | local Canvas = require("dapui.render.canvas") 4 | 5 | return function(client) 6 | local dapui = { elements = {} } 7 | 8 | ---@class dapui.elements.scopes 9 | ---@toc_entry Variable Scopes 10 | ---@text 11 | --- Displays the available scopes and variables within them. 12 | --- 13 | --- Mappings: 14 | --- - `edit`: Edit the value of a variable 15 | --- - `expand`: Toggle showing any children of variable. 16 | --- - `repl`: Send variable to REPL 17 | dapui.elements.scopes = {} 18 | 19 | local send_ready = util.create_render_loop(function() 20 | dapui.elements.scopes.render() 21 | end) 22 | 23 | local scopes = require("dapui.components.scopes")(client, send_ready) 24 | 25 | ---@nodoc 26 | function dapui.elements.scopes.render() 27 | local canvas = Canvas.new() 28 | scopes.render(canvas) 29 | canvas:render_buffer(dapui.elements.scopes.buffer(), config.element_mapping("scopes")) 30 | end 31 | 32 | ---@nodoc 33 | dapui.elements.scopes.buffer = util.create_buffer("DAP Scopes", { 34 | filetype = "dapui_scopes", 35 | }) 36 | 37 | return dapui.elements.scopes 38 | end 39 | -------------------------------------------------------------------------------- /lua/dapui/elements/stacks.lua: -------------------------------------------------------------------------------- 1 | local config = require("dapui.config") 2 | local Canvas = require("dapui.render.canvas") 3 | local util = require("dapui.util") 4 | 5 | return function(client) 6 | local dapui = { elements = {} } 7 | 8 | ---@class dapui.elements.stacks 9 | ---@toc_entry Threads and Stack Frames 10 | ---@text 11 | --- Displays the running threads and their stack frames. 12 | --- 13 | --- Mappings: 14 | --- - `open`: Jump to a place within the stack frame. 15 | --- - `toggle`: Toggle displaying subtle frames 16 | dapui.elements.stacks = {} 17 | 18 | local send_ready = util.create_render_loop(function() 19 | dapui.elements.stacks.render() 20 | end) 21 | 22 | local threads = require("dapui.components.threads")(client, send_ready) 23 | 24 | ---@nodoc 25 | function dapui.elements.stacks.render() 26 | local canvas = Canvas.new() 27 | threads.render(canvas) 28 | canvas:render_buffer(dapui.elements.stacks.buffer(), config.element_mapping("stacks")) 29 | end 30 | 31 | ---@nodoc 32 | dapui.elements.stacks.buffer = util.create_buffer("DAP Stacks", { 33 | filetype = "dapui_stacks", 34 | }) 35 | 36 | return dapui.elements.stacks 37 | end 38 | -------------------------------------------------------------------------------- /lua/dapui/elements/watches.lua: -------------------------------------------------------------------------------- 1 | local util = require("dapui.util") 2 | local config = require("dapui.config") 3 | local Canvas = require("dapui.render.canvas") 4 | 5 | return function(client) 6 | local dapui = { elements = {} } 7 | 8 | ---@class dapui.elements.watches 9 | ---@toc_entry Watch Expressions 10 | ---@text 11 | --- Allows creation of expressions to watch the value of in the context of the 12 | --- current frame. 13 | --- This uses a prompt buffer for input. To enter a new expression, just enter 14 | --- insert mode and you will see a prompt appear. Press enter to submit 15 | --- 16 | --- Mappings: 17 | --- 18 | --- - `expand`: Toggle showing the children of an expression. 19 | --- - `remove`: Remove the watched expression. 20 | --- - `edit`: Edit an expression or set the value of a child variable. 21 | --- - `repl`: Send expression to REPL 22 | dapui.elements.watches = { 23 | allow_without_session = true, 24 | } 25 | 26 | local send_ready = util.create_render_loop(function() 27 | dapui.elements.watches.render() 28 | end) 29 | 30 | local watches = require("dapui.components.watches")(client, send_ready) 31 | 32 | --- Add a new watch expression 33 | ---@param expr? string 34 | function dapui.elements.watches.add(expr) 35 | if not expr then 36 | expr = util.get_current_expr() 37 | end 38 | watches.add(expr) 39 | end 40 | 41 | --- Change the chosen watch expression 42 | ---@param index integer 43 | ---@param new_expr string 44 | function dapui.elements.watches.edit(index, new_expr) 45 | watches.edit(new_expr, index) 46 | end 47 | 48 | --- Remove the chosen watch expression 49 | function dapui.elements.watches.remove(index) 50 | watches.remove(index) 51 | end 52 | 53 | --- Get the current list of watched expressions 54 | ---@return dapui.elements.watches.Watch[] 55 | function dapui.elements.watches.get() 56 | return watches.get() 57 | end 58 | 59 | ---@class dapui.elements.watches.Watch 60 | ---@field expression string 61 | ---@field expanded boolean 62 | 63 | --- Toggle the expanded state of the chosen watch expression 64 | ---@param index integer 65 | function dapui.elements.watches.toggle_expand(index) 66 | watches.expand(index) 67 | end 68 | 69 | ---@nodoc 70 | function dapui.elements.watches.render() 71 | local canvas = Canvas.new() 72 | watches.render(canvas) 73 | canvas:render_buffer(dapui.elements.watches.buffer(), config.element_mapping("watches")) 74 | end 75 | 76 | ---@nodoc 77 | dapui.elements.watches.buffer = util.create_buffer("DAP Watches", { 78 | filetype = "dapui_watches", 79 | omnifunc = "v:lua.require'dap'.omnifunc", 80 | }) 81 | 82 | return dapui.elements.watches 83 | end 84 | -------------------------------------------------------------------------------- /lua/dapui/init.lua: -------------------------------------------------------------------------------- 1 | ---@tag nvim-dap-ui 2 | 3 | ---@toc 4 | ---@text 5 | --- A UI for nvim-dap which provides a good out of the box configuration. 6 | --- nvim-dap-ui is built on the idea of "elements". These elements are windows 7 | --- which provide different features. 8 | --- Elements are grouped into layouts which can be placed on any side of the 9 | --- screen. There can be any number of layouts, containing whichever elements 10 | --- desired. 11 | --- 12 | --- Elements can also be displayed temporarily in a floating window. 13 | --- 14 | --- See `:h dapui.setup()` for configuration options and defaults 15 | --- 16 | --- It is highly recommended to use neodev.nvim to enable type checking for 17 | --- nvim-dap-ui to get type checking, documentation and autocompletion for 18 | --- all API functions. 19 | --- 20 | --- ```lua 21 | --- require("neodev").setup({ 22 | --- library = { plugins = { "nvim-dap-ui" }, types = true }, 23 | --- ... 24 | --- }) 25 | --- ``` 26 | --- 27 | --- The default icons use codicons(https://github.com/microsoft/vscode-codicons). 28 | --- It's recommended to use this fork(https://github.com/ChristianChiarulli/neovim-codicons) 29 | --- which fixes alignment issues for the terminal. If your terminal doesn't 30 | --- support font fallback and you need to have icons included in your font, 31 | --- you can patch it via Font Patcher(https://github.com/ryanoasis/nerd-fonts#option-8-patch-your-own-font). 32 | --- There is a simple step by step guide here: https://github.com/mortepau/codicons.nvim#how-to-patch-fonts. 33 | 34 | local success, _ = pcall(require, "nio") 35 | if not success then 36 | error( 37 | "nvim-dap-ui requires nvim-nio to be installed. Install from https://github.com/nvim-neotest/nvim-nio" 38 | ) 39 | end 40 | 41 | local dap = require("dap") 42 | 43 | ---@class dapui 44 | ---@nodoc 45 | local dapui = {} 46 | 47 | local windows = require("dapui.windows") 48 | local config = require("dapui.config") 49 | local util = require("dapui.util") 50 | local nio = require("nio") 51 | local controls = require("dapui.controls") 52 | 53 | ---@type table 54 | ---@nodoc 55 | local elements = {} 56 | 57 | local open_float = nil 58 | 59 | local function query_elem_name() 60 | local entries = {} 61 | for name, _ in pairs(elements) do 62 | if name ~= "hover" then 63 | entries[#entries + 1] = name 64 | end 65 | end 66 | return nio.ui.select(entries, { 67 | prompt = "Select an element:", 68 | format_item = function(entry) 69 | return entry:sub(1, 1):upper() .. entry:sub(2) 70 | end, 71 | }) 72 | end 73 | 74 | ---@toc_entry Setup 75 | ---@text 76 | --- Configure nvim-dap-ui 77 | ---@seealso |dapui.Config| 78 | --- 79 | ---@eval return require('dapui.config')._format_default() 80 | ---@param user_config? dapui.Config 81 | function dapui.setup(user_config) 82 | util.stop_render_tasks() 83 | 84 | config.setup(user_config) 85 | 86 | local client = require("dapui.client")(dap.session) 87 | 88 | ---@type table 89 | for _, module in pairs({ 90 | "breakpoints", 91 | "repl", 92 | "scopes", 93 | "stacks", 94 | "watches", 95 | "hover", 96 | "console", 97 | }) do 98 | local existing_elem = elements[module] 99 | if existing_elem then 100 | local buffer = existing_elem.buffer() 101 | if vim.api.nvim_buf_is_valid(buffer) then 102 | vim.api.nvim_buf_delete(buffer, { force = true }) 103 | end 104 | end 105 | ---@type dapui.Element 106 | local elem = require("dapui.elements." .. module)(client) 107 | 108 | elements[module] = elem 109 | end 110 | 111 | local element_buffers = {} 112 | for name, elem in pairs(elements) do 113 | element_buffers[name] = elem.buffer 114 | end 115 | windows.setup(element_buffers) 116 | end 117 | 118 | ---@class dapui.FloatElementArgs 119 | ---@field width integer Fixed width of window 120 | ---@field height integer Fixed height of window 121 | ---@field enter boolean Whether or not to enter the window after opening 122 | ---@field title string Title of window 123 | ---@field position "center" Position of floating window 124 | 125 | --- Open a floating window containing the desired element. 126 | --- 127 | --- If no fixed dimensions are given, the window will expand to fit the contents 128 | --- of the buffer. 129 | ---@param elem_name string 130 | ---@param args? dapui.FloatElementArgs 131 | function dapui.float_element(elem_name, args) 132 | nio.run(function() 133 | elem_name = elem_name or query_elem_name() 134 | if not elem_name then 135 | return 136 | end 137 | local elem = elements[elem_name] 138 | if not elem then 139 | util.notify("No such element: " .. elem_name, vim.log.levels.ERROR) 140 | return 141 | end 142 | if not elem.allow_without_session and not dap.session() then 143 | util.notify("No active debug session", vim.log.levels.WARN) 144 | return 145 | end 146 | if open_float then 147 | return open_float:jump_to() 148 | end 149 | local line_no = nio.fn.screenrow() 150 | local col_no = nio.fn.screencol() 151 | local position = { line = line_no, col = col_no } 152 | elem.render() 153 | args = vim.tbl_deep_extend( 154 | "keep", 155 | args or {}, 156 | elem.float_defaults and elem.float_defaults() or {}, 157 | { title = elem_name } 158 | ) 159 | nio.scheduler() 160 | open_float = require("dapui.windows").open_float(elem_name, elem, position, args) 161 | if open_float then 162 | open_float:listen("close", function() 163 | open_float = nil 164 | end) 165 | end 166 | end) 167 | end 168 | 169 | local prev_expr = nil 170 | 171 | ---@class dapui.EvalArgs 172 | ---@field context string Context to use for evalutate request, defaults to 173 | --- "hover". Hover requests should have no side effects, if you have errors 174 | --- with evaluation, try changing context to "repl". See the DAP specification 175 | --- for more details. 176 | ---@field width integer Fixed width of window 177 | ---@field height integer Fixed height of window 178 | ---@field enter boolean Whether or not to enter the window after opening 179 | 180 | --- Open a floating window containing the result of evaluting an expression 181 | --- 182 | --- If no fixed dimensions are given, the window will expand to fit the contents 183 | --- of the buffer. 184 | ---@param expr? string Expression to evaluate. If nil, then in normal more the 185 | --- current word is used, and in visual mode the currently highlighted text. 186 | ---@param args? dapui.EvalArgs 187 | function dapui.eval(expr, args) 188 | nio.run(function() 189 | if not dap.session() then 190 | util.notify("No active debug session", vim.log.levels.WARN) 191 | return 192 | end 193 | args = args or {} 194 | if not expr then 195 | expr = util.get_current_expr() 196 | end 197 | if open_float then 198 | if prev_expr == expr then 199 | open_float:jump_to() 200 | return 201 | else 202 | open_float:close() 203 | end 204 | end 205 | prev_expr = expr 206 | local elem = dapui.elements.hover 207 | elem.set_expression(expr, args.context) 208 | local win_pos = nio.api.nvim_win_get_position(0) 209 | local position = { 210 | line = win_pos[1] + nio.fn.winline(), 211 | col = win_pos[2] + nio.fn.wincol() - 1, 212 | } 213 | open_float = require("dapui.windows").open_float("hover", elem, position, args) 214 | if open_float then 215 | open_float:listen("close", function() 216 | open_float = nil 217 | end) 218 | end 219 | end) 220 | end 221 | 222 | --- Update the config.render settings and re-render windows 223 | ---@param update dapui.Config.render Updated settings, from the `render` table of 224 | --- the config 225 | function dapui.update_render(update) 226 | config.update_render(update) 227 | nio.run(function() 228 | for _, elem in pairs(elements) do 229 | elem.render() 230 | end 231 | end) 232 | end 233 | 234 | local function keep_cmdheight(cb) 235 | local cmd_height = vim.o.cmdheight 236 | 237 | cb() 238 | 239 | vim.o.cmdheight = cmd_height 240 | end 241 | 242 | ---@class dapui.CloseArgs 243 | ---@field layout? number Index of layout in config 244 | 245 | --- Close one or all of the window layouts 246 | ---@param args? dapui.CloseArgs 247 | function dapui.close(args) 248 | keep_cmdheight(function() 249 | args = args or {} 250 | if type(args) == "number" then 251 | args = { layout = args } 252 | end 253 | local layout = args.layout 254 | 255 | for _, win_layout in ipairs(windows.layouts) do 256 | win_layout:update_sizes() 257 | end 258 | for i, win_layout in ipairs(windows.layouts) do 259 | if not layout or layout == i then 260 | win_layout:close() 261 | end 262 | end 263 | end) 264 | end 265 | 266 | ---@generic T 267 | ---@param list T[] 268 | ---@return fun(): number, T 269 | ---@nodoc 270 | local function reverse(list) 271 | local i = #list + 1 272 | return function() 273 | i = i - 1 274 | if i <= 0 then 275 | return nil 276 | end 277 | return i, list[i] 278 | end 279 | end 280 | 281 | ---@class dapui.OpenArgs 282 | ---@field layout? number Index of layout in config 283 | ---@field reset? boolean Reset windows to original size 284 | 285 | --- Open one or all of the window layouts 286 | ---@param args? dapui.OpenArgs 287 | function dapui.open(args) 288 | keep_cmdheight(function() 289 | args = args or {} 290 | if type(args) == "number" then 291 | args = { layout = args } 292 | end 293 | local layout = args.layout 294 | 295 | for _, win_layout in ipairs(windows.layouts) do 296 | win_layout:update_sizes() 297 | end 298 | local closed = {} 299 | if layout then 300 | for i = 1, (layout and layout - 1) or #windows.layouts, 1 do 301 | if windows.layouts[i]:is_open() then 302 | closed[#closed + 1] = i 303 | windows.layouts[i]:close() 304 | end 305 | end 306 | end 307 | 308 | for i, win_layout in reverse(windows.layouts) do 309 | if not layout or layout == i then 310 | win_layout:open() 311 | end 312 | end 313 | 314 | if #closed > 0 then 315 | for _, i in ipairs(closed) do 316 | windows.layouts[i]:open() 317 | end 318 | end 319 | 320 | for _, win_layout in ipairs(windows.layouts) do 321 | win_layout:resize(args) 322 | end 323 | end) 324 | dapui.update_render({}) 325 | if config.controls.enabled and config.controls.element ~= "" then 326 | controls.enable_controls(elements[config.controls.element]) 327 | end 328 | controls.refresh_control_panel() 329 | end 330 | 331 | ---@class dapui.ToggleArgs 332 | ---@field layout? number Index of layout in config 333 | ---@field reset? boolean Reset windows to original size 334 | 335 | --- Toggle one or all of the window layouts. 336 | ---@param args? dapui.ToggleArgs 337 | function dapui.toggle(args) 338 | keep_cmdheight(function() 339 | args = args or {} 340 | if type(args) == "number" then 341 | args = { layout = args } 342 | end 343 | local layout = args.layout 344 | 345 | for _, win_layout in reverse(windows.layouts) do 346 | win_layout:update_sizes() 347 | end 348 | 349 | local closed = {} 350 | if layout then 351 | for i = 1, (layout and layout - 1) or #windows.layouts, 1 do 352 | if windows.layouts[i]:is_open() then 353 | closed[#closed + 1] = i 354 | windows.layouts[i]:close() 355 | end 356 | end 357 | end 358 | 359 | for i, win_layout in reverse(windows.layouts) do 360 | if not layout or layout == i then 361 | win_layout:toggle() 362 | end 363 | end 364 | 365 | for _, i in reverse(closed) do 366 | windows.layouts[i]:open() 367 | end 368 | 369 | for _, win_layout in ipairs(windows.layouts) do 370 | win_layout:resize(args) 371 | end 372 | end) 373 | dapui.update_render({}) 374 | if config.controls.enabled and config.controls.element ~= "" then 375 | controls.enable_controls(elements[config.controls.element]) 376 | end 377 | controls.refresh_control_panel() 378 | end 379 | 380 | ---@text 381 | --- Access the elements currently registered. See elements corresponding help 382 | --- tag for API information. 383 | --- 384 | ---@class dapui.elements 385 | ---@field hover dapui.elements.hover 386 | ---@field breakpoints dapui.elements.breakpoints 387 | ---@field repl dapui.elements.repl 388 | ---@field scopes dapui.elements.scopes 389 | ---@field stack dapui.elements.stacks 390 | ---@field watches dapui.elements.watches 391 | ---@field console dapui.elements.console 392 | dapui.elements = setmetatable({}, { 393 | __newindex = function() 394 | error("Elements should be registered instead of adding them to the elements table") 395 | end, 396 | __index = function(_, key) 397 | return elements[key] 398 | end, 399 | }) 400 | 401 | ---@class dapui.Element 402 | ---@field render fun() Triggers the element to refresh its buffer. Used when 403 | --- render settings have changed 404 | ---@field buffer fun(): integer Gets the current buffer for the element. The 405 | --- buffer can change over repeated calls 406 | ---@field float_defaults? fun(): dapui.FloatElementArgs Default settings for 407 | --- floating windows. Useful for element windows which should be larger than 408 | --- their content 409 | ---@field allow_without_session boolean Allows floating the element when 410 | --- there is no active debug session 411 | 412 | --- Registers a new element that can be used within layouts or floating windows 413 | ---@param name string Name of the element 414 | ---@param element dapui.Element 415 | function dapui.register_element(name, element) 416 | if elements[name] then 417 | error("Element " .. name .. " already exists") 418 | end 419 | elements[name] = element 420 | windows.register_element(name, element) 421 | nio.run(function() 422 | element.render() 423 | end) 424 | end 425 | 426 | return dapui 427 | -------------------------------------------------------------------------------- /lua/dapui/render/canvas.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local api = vim.api 4 | 5 | local util = require("dapui.util") 6 | local config = require("dapui.config") 7 | M.namespace = api.nvim_create_namespace("dapui") 8 | 9 | ---@class dapui.Canvas 10 | ---@field lines table 11 | ---@field matches table 12 | ---@field mappings table 13 | ---@field prompt table 14 | ---@field valid boolean 15 | ---@field expand_lines boolean 16 | local Canvas = {} 17 | 18 | ---@type dapui.Action[] 19 | local all_actions = { "expand", "open", "remove", "edit", "repl", "toggle" } 20 | 21 | ---@return dapui.Canvas 22 | function Canvas:new() 23 | local mappings = {} 24 | for _, action in pairs(all_actions) do 25 | mappings[action] = {} 26 | end 27 | local canvas = { 28 | lines = { "" }, 29 | matches = {}, 30 | mappings = mappings, 31 | prompt = nil, 32 | valid = true, 33 | expand_lines = config.expand_lines, 34 | } 35 | setmetatable(canvas, self) 36 | self.__index = self 37 | return canvas 38 | end 39 | 40 | function Canvas:write(text, opts) 41 | if type(text) == "table" then 42 | for _, line in pairs(text) do 43 | if type(line) == "table" then 44 | self:write(line[1], line) 45 | else 46 | self:write(line) 47 | end 48 | end 49 | return 50 | end 51 | 52 | if type(text) ~= "string" then 53 | text = tostring(text) 54 | end 55 | opts = opts or {} 56 | local lines = vim.split(text, "[\r]?\n", { plain = false, trimempty = false }) 57 | if #self.lines == 0 then 58 | self.lines = { "" } 59 | end 60 | for i, line in ipairs(lines) do 61 | local cur_line = self.lines[#self.lines] 62 | self.lines[#self.lines] = cur_line .. line 63 | if opts.group and #line > 0 then 64 | self.matches[#self.matches + 1] = { opts.group, { #self.lines, #cur_line + 1, #line } } 65 | end 66 | if i < #lines then 67 | table.insert(self.lines, "") 68 | end 69 | end 70 | end 71 | 72 | function Canvas:line_width(line) 73 | line = line or self:length() 74 | return #(self.lines[line] or "") 75 | end 76 | 77 | --- Remove the last line from state 78 | function Canvas:remove_line() 79 | self.lines[#self.lines] = nil 80 | end 81 | 82 | function Canvas:reset() 83 | self.lines = {} 84 | self.matches = {} 85 | for _, action in pairs(vim.tbl_keys(self.mappings)) do 86 | self.mappings[action] = {} 87 | end 88 | end 89 | 90 | ---Add a mapping for a specific line 91 | ---@param action dapui.Action 92 | ---@param callback function Callback for when mapping is used 93 | ---@param opts? table Optional extra arguments 94 | -- Extra arguments currently accepts: 95 | -- `line` Line to map to, defaults to last in state 96 | function Canvas:add_mapping(action, callback, opts) 97 | opts = opts or {} 98 | local line = opts.line or self:length() 99 | if line == 0 then 100 | line = 1 101 | end 102 | self.mappings[action][line] = self.mappings[action][line] or {} 103 | self.mappings[action][line][#self.mappings[action][line] + 1] = callback 104 | end 105 | 106 | function Canvas:set_prompt(text, callback, opts) 107 | opts = opts or {} 108 | self.prompt = { text = text, callback = callback, fill = opts.fill, enter = opts.enter or false } 109 | end 110 | 111 | ---Get the number of lines in state 112 | function Canvas:length() 113 | return #self.lines 114 | end 115 | 116 | ---Get the length of the longest line in state 117 | function Canvas:width() 118 | local width = 0 119 | for _, line in pairs(self.lines) do 120 | width = width < #line and #line or width 121 | end 122 | return width 123 | end 124 | 125 | function Canvas:set_expand_lines(value) 126 | self.expand_lines = value 127 | end 128 | 129 | ---Apply a render.canvas to a buffer 130 | ---@param buffer number 131 | function Canvas:render_buffer(buffer, action_keys) 132 | local success, _ = pcall(api.nvim_buf_set_option, buffer, "modifiable", true) 133 | if not success then 134 | return false 135 | end 136 | 137 | for action, line_callbacks in pairs(self.mappings) do 138 | util.apply_mapping(action_keys[action], function(line) 139 | line = line or vim.fn.line(".") 140 | local callbacks = line_callbacks[line] 141 | if not callbacks then 142 | util.notify("No " .. action .. " action for current line", vim.log.levels.INFO) 143 | return 144 | end 145 | for _, callback in pairs(callbacks) do 146 | callback() 147 | end 148 | end, buffer, action) 149 | end 150 | 151 | local lines = self.lines 152 | local matches = self.matches 153 | api.nvim_buf_clear_namespace(buffer, M.namespace, 0, -1) 154 | api.nvim_buf_set_lines(buffer, 0, #lines, false, lines) 155 | local last_line = vim.fn.getbufinfo(buffer)[1].linecount 156 | if last_line > #lines then 157 | api.nvim_buf_set_lines(buffer, #lines, last_line, false, {}) 158 | end 159 | for _, match in pairs(matches) do 160 | local pos = match[2] 161 | pcall( 162 | api.nvim_buf_set_extmark, 163 | buffer, 164 | M.namespace, 165 | pos[1] - 1, 166 | (pos[2] or 1) - 1, 167 | { end_col = pos[3] and (pos[2] + pos[3] - 1), hl_group = match[1] } 168 | ) 169 | end 170 | if self.expand_lines then 171 | local group = api.nvim_create_augroup( 172 | "DAPUIExpandLongLinesFor" .. vim.fn.bufname(buffer):gsub("DAP ", ""), 173 | { clear = true } 174 | ) 175 | api.nvim_create_autocmd({ "CursorMoved", "WinScrolled" }, { 176 | buffer = buffer, 177 | group = group, 178 | callback = function() 179 | vim.schedule(require("dapui.render.line_hover").show) 180 | end, 181 | }) 182 | end 183 | if self.prompt then 184 | api.nvim_buf_set_option(buffer, "buftype", "prompt") 185 | vim.fn.prompt_setprompt(buffer, self.prompt.text) 186 | vim.fn.prompt_setcallback(buffer, function(value) 187 | vim.cmd("stopinsert") 188 | self.prompt.callback(value) 189 | end) 190 | if self.prompt.fill then 191 | api.nvim_buf_set_lines(buffer, -1, -1, true, { "> " .. self.prompt.fill }) 192 | if api.nvim_get_current_buf() == buffer then 193 | api.nvim_input("A") 194 | end 195 | end 196 | api.nvim_buf_set_option(buffer, "modified", false) 197 | local group = api.nvim_create_augroup("DAPUIPromptSetUnmodified" .. buffer, {}) 198 | api.nvim_create_autocmd({ "ExitPre" }, { 199 | buffer = buffer, 200 | group = group, 201 | callback = function() 202 | api.nvim_buf_set_option(buffer, "modified", false) 203 | end, 204 | }) 205 | else 206 | api.nvim_buf_set_option(buffer, "modifiable", false) 207 | api.nvim_buf_set_option(buffer, "buftype", "nofile") 208 | end 209 | return true 210 | end 211 | 212 | --- @return dapui.Canvas 213 | function M.new() 214 | return Canvas:new() 215 | end 216 | 217 | return M 218 | -------------------------------------------------------------------------------- /lua/dapui/render/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local canvas = require("dapui.render.canvas") 4 | 5 | M.new_canvas = canvas.new 6 | 7 | return M 8 | -------------------------------------------------------------------------------- /lua/dapui/render/line_hover.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local api = vim.api 4 | local namespace = api.nvim_create_namespace("dapui") 5 | 6 | local buf_wins = {} 7 | 8 | local function create_buffer(content) 9 | local buf_nr = api.nvim_create_buf(false, true) 10 | vim.fn.setbufline(buf_nr, 1, content) 11 | api.nvim_buf_set_option(buf_nr, "bufhidden", "wipe") 12 | api.nvim_buf_set_option(buf_nr, "modified", false) 13 | 14 | return buf_nr 15 | end 16 | 17 | local function auto_close(win_id, buf_id, orig_line, orig_text) 18 | if not api.nvim_win_is_valid(win_id) then 19 | return 20 | end 21 | local group = api.nvim_create_augroup("DAPUILongLineExpand" .. buf_id, { clear = true }) 22 | api.nvim_create_autocmd({ "WinEnter", "TabClosed", "CursorMoved", "WinScrolled" }, { 23 | callback = function() 24 | if not api.nvim_win_is_valid(win_id) then 25 | return 26 | end 27 | local cur_line = vim.fn.line(".") 28 | if 29 | api.nvim_get_current_buf() == buf_id 30 | and orig_line == cur_line 31 | and vim.api.nvim_buf_get_lines(buf_id, cur_line - 1, cur_line, false)[1] == orig_text 32 | then 33 | auto_close(win_id, buf_id, orig_line) 34 | return 35 | end 36 | buf_wins[vim.api.nvim_get_current_buf()] = nil 37 | local ok, error = pcall(api.nvim_win_close, win_id, true) 38 | if not ok then 39 | require("dapui.util").notify(error, vim.log.levels.DEBUG) 40 | end 41 | end, 42 | once = true, 43 | group = group, 44 | }) 45 | end 46 | 47 | function M.show() 48 | local buffer = api.nvim_get_current_buf() 49 | if api.nvim_win_get_config(0).relative ~= "" then 50 | return 51 | end 52 | 53 | local orig_line, orig_col = unpack(api.nvim_win_get_cursor(0)) 54 | orig_line = orig_line - 1 55 | 56 | local line_content = vim.fn.getline("."):sub(orig_col + 1) 57 | local content_width = vim.str_utfindex(line_content) 58 | 59 | if vim.fn.screencol() + content_width > vim.opt.columns:get() then 60 | orig_col = 0 61 | line_content = vim.fn.getline(".") 62 | content_width = vim.str_utfindex(line_content) 63 | end 64 | 65 | if 66 | content_width <= 0 67 | or content_width 68 | < vim.fn.winwidth(0) - vim.fn.getwininfo(vim.api.nvim_get_current_win())[1].textoff - orig_col - 1 69 | then 70 | return 71 | end 72 | 73 | if content_width <= 0 then 74 | return 75 | end 76 | 77 | local extmarks = api.nvim_buf_get_extmarks( 78 | buffer, 79 | namespace, 80 | { orig_line, 0 }, 81 | { orig_line, -1 }, 82 | { details = true } 83 | ) 84 | 85 | local win_opts = { 86 | relative = "cursor", 87 | width = content_width, 88 | height = 1, 89 | style = "minimal", 90 | border = "none", 91 | row = 0, 92 | col = 0, 93 | } 94 | 95 | local window_id = buf_wins[buffer] 96 | local hover_buf 97 | if window_id and not api.nvim_win_is_valid(window_id) then 98 | buf_wins[buffer] = nil 99 | window_id = nil 100 | end 101 | -- Use existing window to prevent flickering 102 | if window_id then 103 | window_id = buf_wins[buffer] 104 | hover_buf = api.nvim_win_get_buf(window_id) 105 | api.nvim_win_set_config(window_id, win_opts) 106 | api.nvim_buf_set_lines(hover_buf, 0, -1, false, { line_content }) 107 | else 108 | hover_buf = create_buffer(line_content) 109 | win_opts.noautocmd = true 110 | window_id = api.nvim_open_win(hover_buf, false, win_opts) 111 | buf_wins[buffer] = window_id 112 | 113 | api.nvim_win_call(window_id, function() 114 | vim.opt.winhighlight:append({ NormalFloat = "Normal" }) 115 | end) 116 | end 117 | 118 | for _, mark in ipairs(extmarks) do 119 | local _, _, col, details = unpack(mark) 120 | if not details.end_col or details.end_col > orig_col then 121 | details.end_row = 0 122 | details.ns_id = nil 123 | details.end_col = details.end_col and (details.end_col - orig_col) 124 | col = math.max(col, orig_col) 125 | local ok, error = 126 | pcall(api.nvim_buf_set_extmark, hover_buf, namespace, 0, col - orig_col, details) 127 | if not ok then 128 | require("dapui.util").notify(error, vim.log.levels.DEBUG) 129 | end 130 | end 131 | end 132 | 133 | auto_close( 134 | window_id, 135 | buffer, 136 | orig_line, 137 | api.nvim_buf_get_lines(buffer, orig_line - 1, orig_line, false)[1] 138 | ) 139 | end 140 | 141 | return M 142 | -------------------------------------------------------------------------------- /lua/dapui/tests/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | M.mocks = require("dapui.tests.mocks") 4 | 5 | M.namespace = require("dapui.render.canvas").namespace 6 | 7 | M.bootstrap = function() 8 | assert:add_formatter(vim.inspect) 9 | 10 | A = function(...) 11 | local obj = select("#", ...) == 1 and select(1, ...) or { ... } 12 | local s = type(obj) == "string" and obj or vim.inspect(obj) 13 | if vim.in_fast_event() then 14 | vim.schedule(function() 15 | print(s) 16 | end) 17 | else 18 | print(s) 19 | end 20 | end 21 | end 22 | 23 | M.util = require("dapui.tests.util") 24 | 25 | return M 26 | -------------------------------------------------------------------------------- /lua/dapui/tests/mocks.lua: -------------------------------------------------------------------------------- 1 | local dap = require("dap") 2 | local Client = require("dapui.client") 3 | 4 | local M = {} 5 | 6 | ---@class dapui.tests.mocks.ScopesArgs 7 | ---@field scopes table 8 | 9 | ---@param args dapui.tests.mocks.ScopesArgs 10 | function M.scopes(args) 11 | ---@param request_args dapui.types.ScopesArguments 12 | ---@return dapui.types.ScopesResponse 13 | return function(request_args) 14 | local scopes = args.scopes[request_args.frameId] 15 | assert(scopes, "No scopes found for frameId " .. request_args.frameId) 16 | return { 17 | scopes = scopes, 18 | } 19 | end 20 | end 21 | 22 | ---@class dapui.tests.mocks.EvaluateArgs 23 | ---@field expressions table 24 | function M.evaluate(args) 25 | ---@param request_args dapui.types.EvaluateArguments 26 | ---@return dapui.types.EvaluateResponse 27 | return function(request_args) 28 | local result = args.expressions[request_args.expression] 29 | assert(result, "No expression found for " .. request_args.expression) 30 | if type(result) == "string" then 31 | return { 32 | result = result, 33 | variablesReference = 0, 34 | } 35 | end 36 | result.variablesReference = result.variablesReference or 0 37 | return result 38 | end 39 | end 40 | 41 | ---@class dapui.tests.mocks.VariablesArgs 42 | ---@field variables table 43 | 44 | ---@param args dapui.tests.mocks.VariablesArgs 45 | function M.variables(args) 46 | ---@param request_args dapui.types.VariablesArguments 47 | ---@return dapui.types.VariablesResponse 48 | return function(request_args) 49 | local variables = args.variables[request_args.variablesReference] 50 | assert(variables, "No variables for variablesReference: " .. request_args.variablesReference) 51 | return { 52 | variables = variables, 53 | } 54 | end 55 | end 56 | 57 | ---@class dapui.tests.mocks.ThreadsArgs 58 | ---@field threads dapui.types.Thread[] 59 | 60 | ---@param args dapui.tests.mocks.ThreadsArgs 61 | function M.threads(args) 62 | ---@return dapui.types.ThreadsResponse 63 | return function() 64 | return { 65 | threads = args.threads, 66 | } 67 | end 68 | end 69 | 70 | ---@class dapui.tests.mocks.StackTracesArgs 71 | ---@field stack_traces table 72 | 73 | ---@param args dapui.tests.mocks.StackTracesArgs 74 | function M.stack_traces(args) 75 | ---@param request_args dapui.types.StackTraceArguments 76 | ---@return dapui.types.StackTraceResponse 77 | return function(request_args) 78 | local stack_frames = args.stack_traces[request_args.threadId] 79 | assert(stack_frames, "No stack frames for threadId: " .. request_args.threadId) 80 | return { 81 | stackFrames = stack_frames, 82 | } 83 | end 84 | end 85 | 86 | ---@class dapui.tests.mocks.ClientArgs 87 | ---@field requests dapui.DAPRequestsClient 88 | ---@field current_frame? dapui.types.StackFrame 89 | ---@field stopped_thread_id? integer 90 | 91 | ---@param args? dapui.tests.mocks.ClientArgs 92 | ---@return dapui.DAPClient 93 | function M.client(args) 94 | args = args or { requests = {} } 95 | local session 96 | session = { 97 | seq = 0, 98 | stopped_thread_id = args.stopped_thread_id, 99 | current_frame = args.current_frame, 100 | set_breakpoints = function() end, 101 | 102 | request = function(_, command, request_args, callback) 103 | session.seq = session.seq + 1 104 | if not args.requests[command] then 105 | error("No request handler for " .. command) 106 | end 107 | local response = args.requests[command](request_args) 108 | for _, c in pairs(dap.listeners.before[command]) do 109 | c(session, nil, response, request_args) 110 | end 111 | callback(nil, response, session.seq) 112 | for _, c in pairs(dap.listeners.after[command]) do 113 | c(session, nil, response, request_args) 114 | end 115 | end, 116 | } 117 | 118 | ---@type table 119 | local breakpoints = {} 120 | 121 | return Client(function() 122 | return session 123 | end, { 124 | get = function(bufnr) 125 | if bufnr then 126 | return breakpoints[bufnr] 127 | end 128 | return breakpoints 129 | end, 130 | ---@param bp_args dapui.client.BreakpointArgs 131 | toggle = function(bp_args, bufnr, line) 132 | local buf_bps = breakpoints[bufnr] or {} 133 | for i, bp in ipairs(buf_bps) do 134 | if bp.line == line then 135 | table.remove(buf_bps, i) 136 | return 137 | end 138 | end 139 | 140 | ---@type dapui.types.DAPBreakpoint 141 | buf_bps[#buf_bps + 1] = { 142 | condition = bp_args.condition, 143 | hitCondition = bp_args.hit_condition, 144 | line = line, 145 | logMessage = bp_args.log_message, 146 | } 147 | breakpoints[bufnr] = buf_bps 148 | end, 149 | }) 150 | end 151 | 152 | return M 153 | -------------------------------------------------------------------------------- /lua/dapui/tests/util.lua: -------------------------------------------------------------------------------- 1 | local nio = require("nio") 2 | local namespace = require("dapui.render.canvas").namespace 3 | 4 | local M = {} 5 | 6 | function M.get_highlights(bufnr) 7 | local formatted = {} 8 | local extmarks = nio.api.nvim_buf_get_extmarks(bufnr, namespace, 0, -1, { details = true }) 9 | for _, extmark in ipairs(extmarks) do 10 | local _, start_row, start_col, details = unpack(extmark) 11 | table.insert(formatted, { 12 | details.hl_group, 13 | start_row, 14 | start_col, 15 | details.end_row, 16 | details.end_col, 17 | }) 18 | end 19 | return formatted 20 | end 21 | 22 | ---@class dapui.tests.util.Mapping 23 | ---@field buffer integer 24 | ---@field callback? function 25 | ---@field expr integer 26 | ---@field lhs string 27 | ---@field lhsraw string 28 | ---@field lnum integer 29 | ---@field mode string 30 | ---@field noremap integer 31 | ---@field nowait integer 32 | ---@field script integer 33 | ---@field sid integer 34 | ---@field silent integer 35 | 36 | ---@param buf integer 37 | ---@return table Per-key mappings in the buffer 38 | function M.get_mappings(buf) 39 | ---@type dapui.tests.util.Mapping[] 40 | local raw_mappings = vim.api.nvim_buf_get_keymap(buf, "n") 41 | local mappings = {} 42 | for _, mapping in ipairs(raw_mappings) do 43 | mappings[mapping.lhs] = mapping.callback 44 | end 45 | return mappings 46 | end 47 | 48 | return M 49 | -------------------------------------------------------------------------------- /lua/dapui/util.lua: -------------------------------------------------------------------------------- 1 | local config = require("dapui.config") 2 | local nio = require("nio") 3 | 4 | local M = {} 5 | 6 | local api = nio.api 7 | 8 | local render_tasks = {} 9 | 10 | function M.stop_render_tasks() 11 | for _, task in ipairs(render_tasks) do 12 | task.cancel() 13 | end 14 | render_tasks = {} 15 | end 16 | 17 | ---@return function 18 | function M.create_render_loop(render) 19 | local render_event = nio.control.event() 20 | 21 | render_tasks[#render_tasks + 1] = nio.run(function() 22 | while true do 23 | render_event.wait() 24 | render_event.clear() 25 | xpcall(render, function(msg) 26 | local traceback = debug.traceback(msg, 1) 27 | M.notify(("Rendering failed: %s"):format(traceback), vim.log.levels.WARN) 28 | end) 29 | nio.sleep(10) 30 | end 31 | end) 32 | 33 | return function() 34 | render_event.set() 35 | end 36 | end 37 | 38 | function M.get_current_expr() 39 | if nio.fn.mode() == "v" then 40 | local start = nio.fn.getpos("v") 41 | local finish = nio.fn.getpos(".") 42 | local lines = M.get_selection(start, finish) 43 | return table.concat(lines, "\n") 44 | end 45 | return nio.fn.expand("") 46 | end 47 | 48 | function M.create_buffer(name, options) 49 | local buf 50 | return function() 51 | if not buf then 52 | buf = name ~= "" and nio.fn.bufnr(name) or -1 53 | end 54 | if nio.api.nvim_buf_is_valid(buf) then 55 | return buf 56 | end 57 | buf = nio.api.nvim_create_buf(true, true) 58 | options = vim.tbl_extend("keep", options or {}, { 59 | modifiable = false, 60 | buflisted = false, 61 | }) 62 | nio.api.nvim_buf_set_name(buf, name) 63 | for opt, value in pairs(options) do 64 | nio.api.nvim_buf_set_option(buf, opt, value) 65 | end 66 | return buf 67 | end 68 | end 69 | 70 | function M.round(num) 71 | if num < math.floor(num) + 0.5 then 72 | return math.floor(num) 73 | else 74 | return math.ceil(num) 75 | end 76 | end 77 | 78 | function M.notify(msg, level, opts) 79 | return vim.schedule_wrap(vim.notify)( 80 | msg, 81 | level or vim.log.levels.INFO, 82 | vim.tbl_extend("keep", opts or {}, { 83 | title = "nvim-dap-ui", 84 | icon = "", 85 | on_open = function(win) 86 | vim.api.nvim_buf_set_option(vim.api.nvim_win_get_buf(win), "filetype", "markdown") 87 | end, 88 | }) 89 | ) 90 | end 91 | 92 | function M.is_uri(path) 93 | local scheme = path:match("^([a-z]+)://.*") 94 | if scheme then 95 | return true 96 | else 97 | return false 98 | end 99 | end 100 | 101 | local function set_opts(win, opts) 102 | for opt, value in pairs(opts) do 103 | api.nvim_win_set_option(win, opt, value) 104 | end 105 | end 106 | 107 | function M.select_win() 108 | if config.select_window then 109 | return config.select_window() 110 | end 111 | local windows = vim.tbl_filter(function(win) 112 | if api.nvim_win_get_config(win).relative ~= "" then 113 | return false 114 | end 115 | local buf = api.nvim_win_get_buf(win) 116 | return api.nvim_buf_get_option(buf, "buftype") == "" 117 | end, api.nvim_tabpage_list_wins(0)) 118 | 119 | if #windows < 2 then 120 | return windows[1] 121 | end 122 | 123 | local overwritten_opts = {} 124 | local laststatus = vim.o.laststatus 125 | vim.o.laststatus = 2 126 | 127 | for i, win in ipairs(windows) do 128 | overwritten_opts[win] = { 129 | statusline = api.nvim_win_get_option(win, "statusline"), 130 | winhl = api.nvim_win_get_option(win, "winhl"), 131 | } 132 | set_opts(win, { 133 | statusline = "%=" .. string.char(64 + i) .. "%=", 134 | winhl = ("StatusLine:%s,StatusLineNC:%s"):format("DapUIWinSelect", "DapUIWinSelect"), 135 | }) 136 | end 137 | 138 | vim.cmd("redrawstatus!") 139 | local index, char 140 | local ESC, CTRL_C = 27, 22 141 | print("Select window: ") 142 | pcall(function() 143 | while char ~= ESC and char ~= CTRL_C and not windows[index] do 144 | char = vim.fn.getchar() 145 | if type(char) == "number" then 146 | if char >= 65 and char <= 90 then 147 | -- Upper to lower case 148 | char = char + 32 149 | end 150 | index = char - 96 151 | end 152 | end 153 | end) 154 | 155 | for win, opts in pairs(overwritten_opts) do 156 | pcall(set_opts, win, opts) 157 | end 158 | 159 | vim.o.laststatus = laststatus 160 | vim.cmd("normal! :") 161 | 162 | return windows[index] 163 | end 164 | 165 | function M.open_buf(bufnr, line, column) 166 | local function set_win_pos(win) 167 | if line then 168 | api.nvim_win_set_cursor(win, { line, column }) 169 | end 170 | pcall(api.nvim_set_current_win, win) 171 | end 172 | 173 | for _, win in pairs(api.nvim_tabpage_list_wins(0)) do 174 | if api.nvim_win_get_buf(win) == bufnr then 175 | set_win_pos(win) 176 | return true 177 | end 178 | end 179 | 180 | local success, win = pcall(M.select_win) 181 | if not success or not win then 182 | return false 183 | end 184 | api.nvim_win_set_buf(win, bufnr) 185 | set_win_pos(win) 186 | return true 187 | end 188 | 189 | function M.get_selection(start, finish) 190 | local start_line, start_col = start[2], start[3] 191 | local finish_line, finish_col = finish[2], finish[3] 192 | 193 | if start_line > finish_line or (start_line == finish_line and start_col > finish_col) then 194 | start_line, start_col, finish_line, finish_col = finish_line, finish_col, start_line, start_col 195 | end 196 | 197 | local lines = vim.fn.getline(start_line, finish_line) 198 | if #lines == 0 then 199 | return 200 | end 201 | lines[#lines] = string.sub(lines[#lines], 1, finish_col) 202 | lines[1] = string.sub(lines[1], start_col) 203 | return lines 204 | end 205 | 206 | function M.apply_mapping(mappings, func, buffer, label) 207 | for _, key in pairs(mappings) do 208 | if type(func) ~= "string" then 209 | vim.api.nvim_buf_set_keymap( 210 | buffer, 211 | "n", 212 | key, 213 | "", 214 | { noremap = true, callback = func, nowait = true, desc = label } 215 | ) 216 | else 217 | vim.api.nvim_buf_set_keymap( 218 | buffer, 219 | "n", 220 | key, 221 | func, 222 | { noremap = true, nowait = true, desc = label } 223 | ) 224 | end 225 | end 226 | end 227 | 228 | function M.pretty_name(path) 229 | if M.is_uri(path) then 230 | path = vim.uri_to_fname(path) 231 | end 232 | return vim.fn.fnamemodify(path, ":t") 233 | end 234 | 235 | function M.format_error(error) 236 | if vim.tbl_isempty(error.body or {}) then 237 | return error.message 238 | end 239 | if not error.body.error then 240 | return error.body.message 241 | end 242 | local formatted = error.body.error.format 243 | for name, val in pairs(error.body.error.variables or {}) do 244 | formatted = string.gsub(formatted, "{" .. name .. "}", val) 245 | end 246 | return formatted 247 | end 248 | 249 | function M.partial(func, ...) 250 | local args = { ... } 251 | return function(...) 252 | local final = vim.list_extend(args, { ... }) 253 | return func(unpack(final)) 254 | end 255 | end 256 | 257 | function M.send_to_repl(expression) 258 | local repl_win = vim.fn.bufwinid("\\[dap-repl") -- incomplete bracket to allow e.g. '[dap-repl-2]' 259 | if repl_win == -1 then 260 | M.float_element("repl") 261 | repl_win = vim.fn.bufwinid("\\[dap-repl\\]") 262 | end 263 | api.nvim_set_current_win(repl_win) 264 | vim.cmd("normal i" .. expression) 265 | end 266 | 267 | function M.float_element(elem_name) 268 | local line_no = vim.fn.screenrow() 269 | local col_no = vim.fn.screencol() 270 | local position = { line = line_no, col = col_no } 271 | local elem = require("dapui.elements." .. elem_name) 272 | if type(elem) == "function" then elem = elem() end 273 | return require("dapui.windows").open_float(elem_name, elem, position, elem.settings or {}) 274 | end 275 | 276 | function M.render_type(maybe_type) 277 | if not maybe_type then 278 | return "" 279 | end 280 | local max_length = config.render.max_type_length 281 | if not max_length or max_length == -1 then 282 | return maybe_type 283 | end 284 | if max_length == 0 then 285 | return "" 286 | end 287 | if vim.str_utfindex(maybe_type) <= max_length then 288 | return maybe_type 289 | end 290 | 291 | local byte_length = vim.str_byteindex(maybe_type, max_length) 292 | return string.sub(maybe_type, 1, byte_length) .. "..." 293 | end 294 | 295 | ---@param value_start integer 296 | ---@param value string 297 | ---@return string[] 298 | function M.format_value(value_start, value) 299 | local formatted = {} 300 | local max_lines = config.render.max_value_lines 301 | local i = 0 302 | --- Use gsplit instead of split because adapters can returns very long values 303 | --- and we want to avoid creating thousands of substrings that we won't use. 304 | for line in vim.gsplit(value, "\n") do 305 | i = i + 1 306 | 307 | if max_lines and i > max_lines then 308 | local line_count = 1 309 | for _ in value:gmatch("\n") do 310 | line_count = line_count + 1 311 | end 312 | 313 | formatted[i - 1] = formatted[i - 1] .. ((" ... [%s more lines]"):format(line_count - i + 1)) 314 | break 315 | end 316 | if i > 1 then 317 | line = string.rep(" ", value_start - 2) .. line 318 | end 319 | formatted[i] = line 320 | end 321 | return formatted 322 | end 323 | 324 | function M.tbl_flatten(t) 325 | return vim.fn.has("nvim-0.10") == 1 and vim.iter(t):flatten(math.huge):totable() 326 | or vim.tbl_flatten(t) 327 | end 328 | 329 | return M 330 | -------------------------------------------------------------------------------- /lua/dapui/windows/float.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | local api = vim.api 3 | local config = require("dapui.config") 4 | 5 | local Float = { win_id = nil, listeners = { close = {} }, position = {} } 6 | 7 | local function create_opts(content_width, content_height, position, title) 8 | local line_no = position.line 9 | local col_no = position.col 10 | 11 | local vert_anchor = "N" 12 | local hor_anchor = "W" 13 | 14 | local max_height = config.floating.max_height or vim.o.lines 15 | local max_width = config.floating.max_width or vim.o.columns 16 | local border = config.floating.border 17 | if 0 < max_height and max_height < 1 then 18 | max_height = math.floor(vim.o.lines * max_height) 19 | end 20 | if 0 < max_width and max_width < 1 then 21 | max_width = math.floor(vim.o.columns * max_width) 22 | end 23 | local height = math.min(content_height, max_height - 2) 24 | local width = math.min(content_width, max_width - 2) 25 | 26 | local row = line_no + math.min(0, vim.o.lines - (height + line_no + 3)) 27 | local col = col_no + math.min(0, vim.o.columns - (width + col_no + 3)) 28 | 29 | return { 30 | relative = "editor", 31 | row = row, 32 | col = col, 33 | anchor = vert_anchor .. hor_anchor, 34 | width = width, 35 | height = height, 36 | style = "minimal", 37 | border = border, 38 | title = title, 39 | title_pos = title and "center", 40 | } 41 | end 42 | 43 | function Float:new(win_id, position) 44 | local win = {} 45 | setmetatable(win, self) 46 | self.__index = self 47 | win.win_id = win_id 48 | win.position = position 49 | return win 50 | end 51 | 52 | function Float:listen(event, callback) 53 | self.listeners[event][#self.listeners[event] + 1] = callback 54 | end 55 | 56 | function Float:resize(width, height, position) 57 | if position == nil then 58 | position = self.position 59 | end 60 | local opts = create_opts(width, height, position) 61 | api.nvim_win_set_config(self.win_id, opts) 62 | end 63 | 64 | function Float:get_buf() 65 | local pass, win = pcall(api.nvim_win_get_buf, self.win_id) 66 | if not pass then 67 | return -1 68 | end 69 | return win 70 | end 71 | 72 | function Float:jump_to() 73 | if vim.fn.mode(true) ~= "n" then 74 | vim.cmd([[call feedkeys("\\", "n")]]) 75 | end 76 | api.nvim_set_current_win(self.win_id) 77 | end 78 | 79 | function Float:close(force) 80 | if not force and api.nvim_get_current_win() == self.win_id then 81 | return false 82 | end 83 | local buf = self:get_buf() 84 | pcall(api.nvim_win_close, self.win_id, true) 85 | for _, listener in pairs(self.listeners.close) do 86 | listener({ buffer = buf }) 87 | end 88 | return true 89 | end 90 | 91 | -- settings: 92 | -- Required: 93 | -- height 94 | -- width 95 | -- Optional: 96 | -- buffer 97 | -- position 98 | -- title 99 | function M.open_float(settings) 100 | local line_no = vim.fn.screenrow() 101 | local col_no = vim.fn.screencol() 102 | local position = settings.position or { line = line_no, col = col_no } 103 | local opts = create_opts(settings.width, settings.height, position, settings.title) 104 | local content_buffer = settings.buffer or api.nvim_create_buf(false, true) 105 | local content_window = api.nvim_open_win(content_buffer, false, opts) 106 | 107 | local output_win_id = api.nvim_win_get_number(content_window) 108 | vim.fn.setwinvar(output_win_id, "&winhl", "Normal:DapUIFloatNormal,FloatBorder:DapUIFloatBorder") 109 | vim.api.nvim_win_set_option(content_window, "wrap", false) 110 | 111 | return Float:new(content_window, position) 112 | end 113 | 114 | return M 115 | -------------------------------------------------------------------------------- /lua/dapui/windows/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local nio = require("nio") 4 | local api = vim.api 5 | local util = require("dapui.util") 6 | local config = require("dapui.config") 7 | local WindowLayout = require("dapui.windows.layout") 8 | 9 | local float_windows = {} 10 | 11 | ---@type dapui.WindowLayout[] 12 | M.layouts = {} 13 | 14 | local registered_elements = {} 15 | 16 | local function horizontal_layout(height, position, win_configs, buffers) 17 | local open_cmd = position == "top" and "topleft" or "botright" 18 | 19 | local function open_tray_win(index) 20 | vim.cmd(index == 1 and open_cmd .. " " .. " split" or "vsplit") 21 | return buffers[index] 22 | end 23 | 24 | local win_states = {} 25 | for _, conf in ipairs(win_configs) do 26 | win_states[#win_states + 1] = vim.tbl_extend("force", conf, { init_size = conf.size }) 27 | end 28 | 29 | return WindowLayout({ 30 | layout_type = "horizontal", 31 | area_state = { size = height, init_size = height }, 32 | win_states = win_states, 33 | get_win_size = api.nvim_win_get_width, 34 | get_area_size = api.nvim_win_get_height, 35 | set_win_size = api.nvim_win_set_width, 36 | set_area_size = api.nvim_win_set_height, 37 | open_index = open_tray_win, 38 | }) 39 | end 40 | 41 | local function vertical_layout(width, position, win_configs, buffers) 42 | local open_cmd = position == "left" and "topleft" or "botright" 43 | local function open_side_win(index) 44 | vim.cmd(index == 1 and open_cmd .. " " .. "vsplit" or "split") 45 | return buffers[index] 46 | end 47 | 48 | local win_states = {} 49 | for _, conf in ipairs(win_configs) do 50 | win_states[#win_states + 1] = vim.tbl_extend("force", conf, { init_size = conf.size }) 51 | end 52 | 53 | return WindowLayout({ 54 | layout_type = "vertical", 55 | area_state = { size = width, init_size = width }, 56 | win_states = win_states, 57 | get_win_size = api.nvim_win_get_height, 58 | get_area_size = api.nvim_win_get_width, 59 | set_area_size = api.nvim_win_set_width, 60 | set_win_size = api.nvim_win_set_height, 61 | open_index = open_side_win, 62 | }) 63 | end 64 | 65 | function M.area_layout(size, position, win_configs, buffers) 66 | local win_states = vim.deepcopy(win_configs) 67 | local layout_func 68 | if position == "top" or position == "bottom" then 69 | layout_func = horizontal_layout 70 | else 71 | layout_func = vertical_layout 72 | end 73 | return layout_func(size, position, win_states, buffers) 74 | end 75 | 76 | local function force_buffers(keep_current) 77 | for _, layout in ipairs(M.layouts) do 78 | layout:force_buffers(keep_current) 79 | end 80 | end 81 | 82 | ---@param element_buffers table 83 | function M.setup(element_buffers) 84 | local dummy_buf = util.create_buffer("", {}) 85 | for _, layout in ipairs(M.layouts) do 86 | layout:close() 87 | end 88 | local layout_configs = config.layouts 89 | M.layouts = {} 90 | for i, layout in ipairs(layout_configs) do 91 | local buffers = {} 92 | for index, win_config in ipairs(layout.elements) do 93 | buffers[index] = element_buffers[win_config.id] 94 | or function() 95 | local elem = registered_elements[win_config.id] 96 | if not elem then 97 | return dummy_buf() 98 | end 99 | return elem.buffer() 100 | end 101 | end 102 | M.layouts[i] = M.area_layout(layout.size, layout.position, layout.elements, buffers) 103 | end 104 | if config.force_buffers then 105 | local group = api.nvim_create_augroup("DapuiWindowsSetup", {}) 106 | api.nvim_create_autocmd({ "BufWinEnter", "BufWinLeave" }, { 107 | callback = function() 108 | force_buffers(false) 109 | end, 110 | group = group, 111 | }) 112 | end 113 | end 114 | 115 | function M.register_element(name, elem) 116 | registered_elements[name] = elem 117 | force_buffers(false) 118 | end 119 | 120 | ---@param element dapui.Element 121 | function M.open_float(name, element, position, settings) 122 | if float_windows[name] then 123 | float_windows[name]:jump_to() 124 | return float_windows[name] 125 | end 126 | local buf = element.buffer() 127 | if type(settings) == "function" then 128 | settings = settings() 129 | end 130 | local float_win = require("dapui.windows.float").open_float({ 131 | height = settings.height or 1, 132 | width = settings.width or 1, 133 | position = position, 134 | buffer = buf, 135 | title = settings.title, 136 | }) 137 | 138 | local resize = function() 139 | local width = settings.width 140 | local height = settings.height 141 | 142 | if not width or not height then 143 | local lines = nio.api.nvim_buf_get_lines(buf, 0, -1, false) 144 | if not width then 145 | width = 0 146 | for _, line in ipairs(lines) do 147 | width = math.max(width, vim.str_utfindex(line)) 148 | end 149 | end 150 | 151 | if not height then 152 | height = #lines 153 | end 154 | end 155 | 156 | if settings.position == "center" then 157 | local screen_w = vim.opt.columns:get() 158 | local screen_h = vim.opt.lines:get() - vim.opt.cmdheight:get() 159 | position.line = (screen_h - height) / 2 160 | position.col = (screen_w - width) / 2 161 | end 162 | 163 | if width <= 0 or height <= 0 then 164 | return 165 | end 166 | float_win:resize(width, height, position) 167 | end 168 | 169 | nio.api.nvim_buf_attach(buf, true, { 170 | on_lines = function() 171 | if not vim.api.nvim_win_is_valid(float_win.win_id) then 172 | return true 173 | end 174 | resize() 175 | end, 176 | }) 177 | -- In case render doesn't trigger on_lines 178 | resize() 179 | 180 | util.apply_mapping(config.floating.mappings["close"], "q", buf) 181 | local close_cmd = "lua require('dapui.windows').close_float('" .. name .. "')" 182 | vim.cmd("au WinEnter,CursorMoved * ++once " .. close_cmd) 183 | vim.cmd("au WinClosed " .. float_win.win_id .. " ++once " .. close_cmd) 184 | float_windows[name] = float_win 185 | if settings.enter then 186 | float_win:jump_to() 187 | end 188 | return float_win 189 | end 190 | 191 | function M.close_float(element_name) 192 | if float_windows[element_name] == nil then 193 | return 194 | end 195 | local win = float_windows[element_name] 196 | local closed = win:close(false) 197 | if not closed then 198 | local close_cmd = "lua require('dapui.windows').close_float('" .. element_name .. "')" 199 | vim.cmd("au WinEnter * ++once " .. close_cmd) 200 | vim.cmd("au WinClosed " .. win.win_id .. " ++once " .. close_cmd) 201 | else 202 | float_windows[element_name] = nil 203 | end 204 | end 205 | 206 | return M 207 | -------------------------------------------------------------------------------- /lua/dapui/windows/layout.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | local util = require("dapui.util") 3 | 4 | ---@class dapui.WinState 5 | ---@field id string 6 | ---@field size number 7 | ---@field init_size number 8 | 9 | ---@class dapui.AreaState 10 | ---@field init_size number 11 | ---@field size number 12 | 13 | ---@class dapui.WindowLayout 14 | ---@field opened_wins integer[] 15 | ---@field win_bufs table 16 | ---@field win_states table 17 | ---@field area_state dapui.AreaState 18 | ---@field layout_type "horizontal" | "vertical" 19 | -- 20 | ---@field open_index fun(index: number): fun(): integer 21 | ---@field get_win_size fun(win_id: integer): integer 22 | ---@field get_area_size fun(win_id: integer): integer 23 | ---@field set_win_size fun(win_id: integer, size: integer) 24 | ---@field set_area_size fun(win_id: integer, size: integer) 25 | local WindowLayout = {} 26 | 27 | function WindowLayout:open() 28 | if self:is_open() then 29 | return 30 | end 31 | local cur_win = api.nvim_get_current_win() 32 | for i, _ in pairs(self.win_states) do 33 | local get_buffer = self.open_index(i) 34 | local win_id = api.nvim_get_current_win() 35 | api.nvim_set_current_buf(get_buffer()) 36 | self.opened_wins[i] = win_id 37 | self:_init_win_settings(win_id) 38 | self.win_bufs[win_id] = get_buffer 39 | end 40 | self:resize() 41 | -- Fails if cur win was floating that closed 42 | pcall(api.nvim_set_current_win, cur_win) 43 | end 44 | 45 | function WindowLayout:force_buffers(keep_current) 46 | local curwin = api.nvim_get_current_win() 47 | for win, get_buffer in pairs(self.win_bufs) do 48 | local bufnr = get_buffer() 49 | local valid, curbuf = pcall(api.nvim_win_get_buf, win) 50 | if valid and curbuf ~= bufnr then 51 | if api.nvim_buf_is_loaded(bufnr) and api.nvim_buf_is_valid(bufnr) then 52 | -- pcall necessary to avoid erroring with `mark not set` although no mark are set 53 | -- this avoid other issues 54 | pcall(api.nvim_win_set_buf, win, bufnr) 55 | end 56 | if keep_current and curwin == win then 57 | util.open_buf(curbuf) 58 | end 59 | end 60 | end 61 | end 62 | 63 | function WindowLayout:_total_size() 64 | local total_size = 0 65 | for _, open_win in ipairs(self.opened_wins) do 66 | local success, win_size = pcall(self.get_win_size, open_win) 67 | total_size = total_size + (success and win_size or 0) 68 | end 69 | return total_size 70 | end 71 | 72 | function WindowLayout:_area_size() 73 | for _, win in ipairs(self.opened_wins) do 74 | local success, area_size = pcall(self.get_area_size, win) 75 | if success then 76 | return area_size 77 | end 78 | end 79 | return 0 80 | end 81 | 82 | function WindowLayout:resize(opts) 83 | opts = opts or {} 84 | if opts.reset then 85 | self.area_state.size = self.area_state.init_size 86 | end 87 | if not self:is_open() then 88 | return 89 | end 90 | 91 | -- Detecting whether self.area_state.size is a float or int 92 | if self.area_state.size < 1 then 93 | if self.layout_type == "vertical" then 94 | local left = 1 95 | local right = vim.opt.columns:get() 96 | self.area_state.size = math.floor((right - left) * self.area_state.size) 97 | elseif self.layout_type == "horizontal" then 98 | local top = vim.opt.tabline:get() == "" and 0 or 1 99 | local bottom = vim.opt.lines:get() - (vim.opt.laststatus:get() > 0 and 2 or 1) 100 | self.area_state.size = math.floor((bottom - top) * self.area_state.size) 101 | else 102 | error("Unknown layout type") 103 | end 104 | end 105 | 106 | self.set_area_size(self.opened_wins[1], self.area_state.size) 107 | local total_size = self:_total_size() 108 | for i, win_state in pairs(self.win_states) do 109 | local win_size = opts.reset and win_state.init_size or win_state.size or 1 110 | win_size = util.round(win_size * total_size) 111 | if win_size == 0 then 112 | win_size = 1 113 | end 114 | self.set_win_size(self.opened_wins[i], win_size) 115 | end 116 | end 117 | 118 | function WindowLayout:update_sizes() 119 | if not self:is_open() then 120 | return 121 | end 122 | local area_size = self:_area_size() 123 | if area_size == 0 then 124 | return 125 | end 126 | self.area_state.size = area_size 127 | local total_size = self:_total_size() 128 | for i, win_state in ipairs(self.win_states) do 129 | local win = self.opened_wins[i] 130 | local win_exists, _ = pcall(api.nvim_win_get_buf, win) 131 | if win_exists then 132 | local success, current_size = pcall(self.get_win_size, self.opened_wins[i]) 133 | if success then 134 | win_state.size = current_size / total_size 135 | end 136 | end 137 | end 138 | end 139 | 140 | function WindowLayout:close() 141 | local current_win = api.nvim_get_current_win() 142 | for _, win in pairs(self.opened_wins) do 143 | local win_exists = api.nvim_win_is_valid(win) 144 | 145 | if win_exists then 146 | if win == current_win then 147 | vim.cmd("stopinsert") -- Prompt buffers act poorly when closed in insert mode, see #33 148 | end 149 | pcall(api.nvim_win_close, win, true) 150 | end 151 | end 152 | self.opened_wins = {} 153 | end 154 | 155 | ---@return boolean 156 | function WindowLayout:is_open() 157 | for _, win in ipairs(self.opened_wins) do 158 | if pcall(vim.api.nvim_win_get_number, win) then 159 | return true 160 | end 161 | end 162 | return false 163 | end 164 | 165 | function WindowLayout:toggle() 166 | if self:is_open() then 167 | self:close() 168 | else 169 | self:open() 170 | end 171 | end 172 | 173 | function WindowLayout:_init_win_settings(win) 174 | local win_settings = { 175 | list = false, 176 | relativenumber = false, 177 | number = false, 178 | winfixwidth = true, 179 | winfixheight = true, 180 | wrap = false, 181 | signcolumn = "auto", 182 | spell = false, 183 | } 184 | for key, val in pairs(win_settings) do 185 | api.nvim_win_set_option(win, key, val) 186 | end 187 | api.nvim_win_call(win, function() 188 | vim.opt.winhighlight:append({ Normal = "DapUINormal", EndOfBuffer = "DapUIEndOfBuffer" }) 189 | end) 190 | end 191 | 192 | function WindowLayout:new(layout) 193 | layout.opened_wins = {} 194 | layout.win_bufs = {} 195 | setmetatable(layout, self) 196 | self.__index = self 197 | return layout 198 | end 199 | 200 | ---@return dapui.WindowLayout 201 | return function(layout) 202 | return WindowLayout:new(layout) 203 | end 204 | -------------------------------------------------------------------------------- /scripts/docgen: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | nvim --headless -c "luafile ./scripts/gendocs.lua" -c 'qa' 4 | -------------------------------------------------------------------------------- /scripts/gendocs.lua: -------------------------------------------------------------------------------- 1 | -- TODO: A lot of this is private code from minidoc, which could be removed if made public 2 | 3 | local util = require("dapui.util") 4 | local minidoc = require("mini.doc") 5 | 6 | local H = {} 7 | --stylua: ignore start 8 | H.pattern_sets = { 9 | -- Patterns for working with afterlines. At the moment deliberately crafted 10 | -- to work only on first line without indent. 11 | 12 | -- Determine if line is a function definition. Captures function name and 13 | -- arguments. For reference see '2.5.9 – Function Definitions' in Lua manual. 14 | afterline_fundef = { 15 | "%s*function%s+(%S-)(%b())", -- Regular definition 16 | "^local%s+function%s+(%S-)(%b())", -- Local definition 17 | "^(%S+)%s*=%s*function(%b())", -- Regular assignment 18 | "^local%s+(%S+)%s*=%s*function(%b())", -- Local assignment 19 | }, 20 | 21 | -- Determine if line is a general assignment 22 | afterline_assign = { 23 | "^(%S-)%s*=", -- General assignment 24 | "^local%s+(%S-)%s*=", -- Local assignment 25 | }, 26 | 27 | -- Patterns to work with type descriptions 28 | -- (see https://github.com/sumneko/lua-language-server/wiki/EmmyLua-Annotations#types-and-type) 29 | types = { 30 | "table%b<>", 31 | "fun%b(): %S+", "fun%b()", "async fun%b(): %S+", "async fun%b()", 32 | "nil", "any", "boolean", "string", "number", "integer", "function", "table", "thread", "userdata", "lightuserdata", 33 | "%.%.%.", 34 | "%S+", 35 | 36 | }, 37 | } 38 | 39 | 40 | H.apply_config = function(config) 41 | MiniDoc.config = config 42 | end 43 | 44 | H.is_disabled = function() 45 | return vim.g.minidoc_disable == true or vim.b.minidoc_disable == true 46 | end 47 | 48 | H.get_config = function(config) 49 | return vim.tbl_deep_extend("force", MiniDoc.config, vim.b.minidoc_config or {}, config or {}) 50 | end 51 | 52 | -- Work with project specific script ========================================== 53 | H.execute_project_script = function(input, output, config) 54 | -- Don't process script if there are more than one active `generate` calls 55 | if H.generate_is_active then 56 | return 57 | end 58 | 59 | -- Don't process script if at least one argument is not default 60 | if not (input == nil and output == nil and config == nil) then 61 | return 62 | end 63 | 64 | -- Store information 65 | local global_config_cache = vim.deepcopy(MiniDoc.config) 66 | local local_config_cache = vim.b.minidoc_config 67 | 68 | -- Pass information to a possible `generate()` call inside script 69 | H.generate_is_active = true 70 | H.generate_recent_output = nil 71 | 72 | -- Execute script 73 | local success = pcall(vim.cmd, "luafile " .. H.get_config(config).script_path) 74 | 75 | -- Restore information 76 | MiniDoc.config = global_config_cache 77 | vim.b.minidoc_config = local_config_cache 78 | H.generate_is_active = nil 79 | 80 | return success 81 | end 82 | 83 | -- Default documentation targets ---------------------------------------------- 84 | H.default_input = function() 85 | -- Search in current and recursively in other directories for files with 86 | -- 'lua' extension 87 | local res = {} 88 | for _, dir_glob in ipairs({ ".", "lua/**", "after/**", "colors/**" }) do 89 | local files = vim.fn.globpath(dir_glob, "*.lua", false, true) 90 | 91 | -- Use full paths 92 | files = vim.tbl_map(function(x) 93 | return vim.fn.fnamemodify(x, ":p") 94 | end, files) 95 | 96 | -- Put 'init.lua' first among files from same directory 97 | table.sort(files, function(a, b) 98 | if vim.fn.fnamemodify(a, ":h") == vim.fn.fnamemodify(b, ":h") then 99 | if vim.fn.fnamemodify(a, ":t") == "init.lua" then 100 | return true 101 | end 102 | if vim.fn.fnamemodify(b, ":t") == "init.lua" then 103 | return false 104 | end 105 | end 106 | 107 | return a < b 108 | end) 109 | table.insert(res, files) 110 | end 111 | 112 | return util.tbl_flatten(res) 113 | end 114 | 115 | H.default_output = function() 116 | local cur_dir = vim.fn.fnamemodify(vim.loop.cwd(), ":t:r") 117 | return ("doc/%s.txt"):format(cur_dir) 118 | end 119 | 120 | -- Parsing -------------------------------------------------------------------- 121 | H.lines_to_block_arr = function(lines, config) 122 | local matched_prev, matched_cur 123 | 124 | local res = {} 125 | local block_raw = { annotation = {}, section_id = {}, afterlines = {}, line_begin = 1 } 126 | 127 | for i, l in ipairs(lines) do 128 | local from, to, section_id = config.annotation_extractor(l) 129 | matched_prev, matched_cur = matched_cur, from ~= nil 130 | 131 | if matched_cur then 132 | if not matched_prev then 133 | -- Finish current block 134 | block_raw.line_end = i - 1 135 | table.insert(res, H.raw_block_to_block(block_raw, config)) 136 | 137 | -- Start new block 138 | block_raw = { annotation = {}, section_id = {}, afterlines = {}, line_begin = i } 139 | end 140 | 141 | -- Add annotation line without matched annotation pattern 142 | table.insert(block_raw.annotation, ("%s%s"):format(l:sub(0, from - 1), l:sub(to + 1))) 143 | 144 | -- Add section id (it is empty string in case of no section id capture) 145 | table.insert(block_raw.section_id, section_id or "") 146 | else 147 | -- Add afterline 148 | table.insert(block_raw.afterlines, l) 149 | end 150 | end 151 | block_raw.line_end = #lines 152 | table.insert(res, H.raw_block_to_block(block_raw, config)) 153 | 154 | return res 155 | end 156 | 157 | -- Raw block structure is an intermediate step added for convenience. It is 158 | -- a table with the following keys: 159 | -- - `annotation` - lines (after removing matched annotation pattern) that were 160 | -- parsed as annotation. 161 | -- - `section_id` - array with length equal to `annotation` length with strings 162 | -- captured as section id. Empty string of no section id was captured. 163 | -- - Everything else is used as block info (like `afterlines`, etc.). 164 | H.raw_block_to_block = function(block_raw, config) 165 | if #block_raw.annotation == 0 and #block_raw.afterlines == 0 then 166 | return nil 167 | end 168 | 169 | local block = H.new_struct("block", { 170 | afterlines = block_raw.afterlines, 171 | line_begin = block_raw.line_begin, 172 | line_end = block_raw.line_end, 173 | }) 174 | local block_begin = block.info.line_begin 175 | 176 | -- Parse raw block annotation lines from top to bottom. New section starts 177 | -- when section id is detected in that line. 178 | local section_cur = H.new_struct( 179 | "section", 180 | { id = config.default_section_id, line_begin = block_begin } 181 | ) 182 | 183 | for i, annotation_line in ipairs(block_raw.annotation) do 184 | local id = block_raw.section_id[i] 185 | if id ~= "" then 186 | -- Finish current section 187 | if #section_cur > 0 then 188 | section_cur.info.line_end = block_begin + i - 2 189 | block:insert(section_cur) 190 | end 191 | 192 | -- Start new section 193 | section_cur = H.new_struct("section", { id = id, line_begin = block_begin + i - 1 }) 194 | end 195 | 196 | section_cur:insert(annotation_line) 197 | end 198 | 199 | if #section_cur > 0 then 200 | section_cur.info.line_end = block_begin + #block_raw.annotation - 1 201 | block:insert(section_cur) 202 | end 203 | 204 | return block 205 | end 206 | 207 | -- Hooks ---------------------------------------------------------------------- 208 | H.apply_structure_hooks = function(doc, hooks) 209 | for _, file in ipairs(doc) do 210 | for _, block in ipairs(file) do 211 | hooks.block_pre(block) 212 | 213 | for _, section in ipairs(block) do 214 | hooks.section_pre(section) 215 | 216 | local hook = hooks.sections[section.info.id] 217 | if hook ~= nil then 218 | hook(section) 219 | end 220 | 221 | hooks.section_post(section) 222 | end 223 | 224 | hooks.block_post(block) 225 | end 226 | 227 | hooks.file(file) 228 | end 229 | 230 | hooks.doc(doc) 231 | end 232 | 233 | H.alias_register = function(s) 234 | if #s == 0 then 235 | return 236 | end 237 | 238 | -- Remove first word (with bits of surrounding whitespace) while capturing it 239 | local alias_name 240 | s[1] = s[1]:gsub("%s*(%S+) ?", function(x) 241 | alias_name = x 242 | return "" 243 | end, 1) 244 | if alias_name == nil then 245 | return 246 | end 247 | 248 | MiniDoc.current.aliases = MiniDoc.current.aliases or {} 249 | MiniDoc.current.aliases[alias_name] = table.concat(s, "\n") 250 | end 251 | 252 | H.alias_replace = function(s) 253 | if MiniDoc.current.aliases == nil then 254 | return 255 | end 256 | 257 | for i, _ in ipairs(s) do 258 | for alias_name, alias_desc in pairs(MiniDoc.current.aliases) do 259 | -- Escape special characters. This is done here and not while registering 260 | -- alias to allow user to refer to aliases by its original name. 261 | -- Store escaped words in separate variables because `vim.pesc()` returns 262 | -- two values which might conflict if outputs are used as arguments. 263 | local name_escaped = vim.pesc(alias_name) 264 | local desc_escaped = vim.pesc(alias_desc) 265 | s[i] = s[i]:gsub(name_escaped, desc_escaped) 266 | end 267 | end 268 | end 269 | 270 | H.toc_register = function(s) 271 | MiniDoc.current.toc = MiniDoc.current.toc or {} 272 | table.insert(MiniDoc.current.toc, s) 273 | end 274 | 275 | H.toc_insert = function(s) 276 | if MiniDoc.current.toc == nil then 277 | return 278 | end 279 | 280 | -- Render table of contents 281 | local toc_lines = {} 282 | for _, toc_entry in ipairs(MiniDoc.current.toc) do 283 | local _, tag_section = toc_entry.parent:has_descendant(function(x) 284 | return type(x) == "table" and x.type == "section" and x.info.id == "@tag" 285 | end) 286 | tag_section = tag_section or {} 287 | 288 | local lines = {} 289 | for i = 1, math.max(#toc_entry, #tag_section) do 290 | local left = toc_entry[i] or "" 291 | -- Use tag refernce instead of tag enclosure 292 | local right = string.match(tag_section[i], "%*.*%*"):gsub("%*", "|") 293 | -- local right = vim.trim((tag_section[i] or ""):gsub("%*", "|")) 294 | -- Add visual line only at first entry (while not adding trailing space) 295 | local filler = i == 1 and "." or (right == "" and "" or " ") 296 | -- Make padding of 2 spaces at both left and right 297 | local n_filler = math.max(74 - H.visual_text_width(left) - H.visual_text_width(right), 3) 298 | table.insert(lines, (" %s%s%s"):format(left, filler:rep(n_filler), right)) 299 | end 300 | 301 | table.insert(toc_lines, lines) 302 | 303 | -- Don't show `toc_entry` lines in output 304 | toc_entry:clear_lines() 305 | end 306 | 307 | for _, l in ipairs(util.tbl_flatten(toc_lines)) do 308 | s:insert(l) 309 | end 310 | end 311 | 312 | H.add_section_heading = function(s, heading) 313 | if #s == 0 or s.type ~= "section" then 314 | return 315 | end 316 | 317 | -- Add heading 318 | s:insert(1, ("%s~"):format(heading)) 319 | end 320 | 321 | H.enclose_var_name = function(s) 322 | if #s == 0 or s.type ~= "section" then 323 | return 324 | end 325 | 326 | s[1] = s[1]:gsub("(%S+)", "{%1}", 1) 327 | end 328 | 329 | ---@param init number Start of searching for first "type-like" string. It is 330 | --- needed to not detect type early. Like in `@param a_function function`. 331 | ---@private 332 | H.enclose_type = function(s, enclosure, init) 333 | if #s == 0 or s.type ~= "section" then 334 | return 335 | end 336 | enclosure = enclosure or "`%(%1%)`" 337 | init = init or 1 338 | 339 | local cur_type = H.match_first_pattern(s[1], H.pattern_sets["types"], init) 340 | if #cur_type == 0 then 341 | return 342 | end 343 | 344 | -- Add `%S*` to front and back of found pattern to support their combination 345 | -- with `|`. Also allows using `[]` and `?` prefixes. 346 | local type_pattern = ("(%%S*%s%%S*)"):format(vim.pesc(cur_type[1])) 347 | 348 | -- Avoid replacing possible match before `init` 349 | local l_start = s[1]:sub(1, init - 1) 350 | local l_end = s[1]:sub(init):gsub(type_pattern, enclosure, 1) 351 | s[1] = ("%s%s"):format(l_start, l_end) 352 | end 353 | 354 | -- Infer data from afterlines ------------------------------------------------- 355 | H.infer_header = function(b) 356 | local has_signature = b:has_descendant(function(x) 357 | return type(x) == "table" and x.type == "section" and x.info.id == "@signature" 358 | end) 359 | local has_tag = b:has_descendant(function(x) 360 | return type(x) == "table" and x.type == "section" and x.info.id == "@tag" 361 | end) 362 | 363 | if has_signature and has_tag then 364 | return 365 | end 366 | 367 | local l_all = table.concat(b.info.afterlines, " ") 368 | local tag, signature 369 | 370 | -- Try function definition 371 | local fun_capture = H.match_first_pattern(l_all, H.pattern_sets["afterline_fundef"]) 372 | if #fun_capture > 0 then 373 | tag = tag or ("%s()"):format(fun_capture[1]) 374 | signature = signature or ("%s%s"):format(fun_capture[1], fun_capture[2]) 375 | end 376 | 377 | -- Try general assignment 378 | local assign_capture = H.match_first_pattern(l_all, H.pattern_sets["afterline_assign"]) 379 | if #assign_capture > 0 then 380 | tag = tag or assign_capture[1] 381 | signature = signature or assign_capture[1] 382 | end 383 | 384 | if tag ~= nil then 385 | -- First insert signature (so that it will appear after tag section) 386 | if not has_signature then 387 | b:insert(1, H.as_struct({ signature }, "section", { id = "@signature" })) 388 | end 389 | 390 | -- Insert tag 391 | if not has_tag then 392 | b:insert(1, H.as_struct({ tag }, "section", { id = "@tag" })) 393 | end 394 | end 395 | end 396 | 397 | function H.is_module(name) 398 | if string.find(name, "%(") then 399 | return false 400 | end 401 | if string.find(name, "[A-Z]") then 402 | return false 403 | end 404 | return true 405 | end 406 | 407 | H.format_signature = function(line) 408 | -- Try capture function signature 409 | local name, args = line:match("(%S-)(%b())") 410 | 411 | 412 | -- Otherwise pick first word 413 | name = name or line:match("(%S+)") 414 | if not args and H.is_module(name) then 415 | return "" 416 | end 417 | local name_elems = vim.split(name, ".", { plain = true }) 418 | name = name_elems[#name_elems] 419 | 420 | if not name then 421 | return "" 422 | end 423 | 424 | -- Tidy arguments 425 | if args and args ~= "()" then 426 | local arg_parts = vim.split(args:sub(2, -2), ",") 427 | local arg_list = {} 428 | for _, a in ipairs(arg_parts) do 429 | -- Enclose argument in `{}` while controlling whitespace 430 | table.insert(arg_list, ("{%s}"):format(vim.trim(a))) 431 | end 432 | args = ("(%s)"):format(table.concat(arg_list, ", ")) 433 | end 434 | 435 | return ("`%s`%s"):format(name, args or "") 436 | end 437 | 438 | -- Work with structures ------------------------------------------------------- 439 | -- Constructor 440 | H.new_struct = function(struct_type, info) 441 | local output = { 442 | info = info or {}, 443 | type = struct_type, 444 | } 445 | 446 | output.insert = function(self, index, child) 447 | -- Allow both `x:insert(child)` and `x:insert(1, child)` 448 | if child == nil then 449 | child, index = index, #self + 1 450 | end 451 | 452 | if type(child) == "table" then 453 | child.parent = self 454 | child.parent_index = index 455 | end 456 | 457 | table.insert(self, index, child) 458 | 459 | H.sync_parent_index(self) 460 | end 461 | 462 | output.remove = function(self, index) 463 | index = index or #self 464 | table.remove(self, index) 465 | 466 | H.sync_parent_index(self) 467 | end 468 | 469 | output.has_descendant = function(self, predicate) 470 | local bool_res, descendant = false, nil 471 | H.apply_recursively(function(x) 472 | if not bool_res and predicate(x) then 473 | bool_res = true 474 | descendant = x 475 | end 476 | end, self) 477 | return bool_res, descendant 478 | end 479 | 480 | output.has_lines = function(self) 481 | return self:has_descendant(function(x) 482 | return type(x) == "string" 483 | end) 484 | end 485 | 486 | output.clear_lines = function(self) 487 | for i, x in ipairs(self) do 488 | if type(x) == "string" then 489 | self[i] = nil 490 | else 491 | x:clear_lines() 492 | end 493 | end 494 | end 495 | 496 | return output 497 | end 498 | 499 | H.sync_parent_index = function(x) 500 | for i, _ in ipairs(x) do 501 | if type(x[i]) == "table" then 502 | x[i].parent_index = i 503 | end 504 | end 505 | return x 506 | end 507 | 508 | -- Converter (this ensures that children have proper parent-related data) 509 | H.as_struct = function(array, struct_type, info) 510 | -- Make default info `info` for cases when structure is created manually 511 | local default_info = ({ 512 | section = { id = "@text", line_begin = -1, line_end = -1 }, 513 | block = { afterlines = {}, line_begin = -1, line_end = -1 }, 514 | file = { path = "" }, 515 | doc = { input = {}, output = "", config = H.get_config() }, 516 | })[struct_type] 517 | info = vim.tbl_deep_extend("force", default_info, info or {}) 518 | 519 | local res = H.new_struct(struct_type, info) 520 | for _, x in ipairs(array) do 521 | res:insert(x) 522 | end 523 | return res 524 | end 525 | 526 | -- Work with text ------------------------------------------------------------- 527 | H.ensure_indent = function(text, n_indent_target) 528 | local lines = vim.split(text, "\n") 529 | local n_indent, n_indent_cur = math.huge, math.huge 530 | 531 | -- Find number of characters in indent 532 | for _, l in ipairs(lines) do 533 | -- Update lines indent: minimum of all indents except empty lines 534 | if n_indent > 0 then 535 | _, n_indent_cur = l:find("^%s*") 536 | -- Condition "current n-indent equals line length" detects empty line 537 | if (n_indent_cur < n_indent) and (n_indent_cur < l:len()) then 538 | n_indent = n_indent_cur 539 | end 540 | end 541 | end 542 | 543 | -- Ensure indent 544 | local indent = string.rep(" ", n_indent_target) 545 | for i, l in ipairs(lines) do 546 | if l ~= "" then 547 | lines[i] = indent .. l:sub(n_indent + 1) 548 | end 549 | end 550 | 551 | return table.concat(lines, "\n") 552 | end 553 | 554 | H.align_text = function(text, width, direction) 555 | if type(text) ~= "string" then 556 | return 557 | end 558 | text = vim.trim(text) 559 | width = width or 78 560 | direction = direction or "left" 561 | 562 | -- Don't do anything if aligning left or line is a whitespace 563 | if direction == "left" or text:find("^%s*$") then 564 | return text 565 | end 566 | 567 | local n_left = math.max(0, 78 - H.visual_text_width(text)) 568 | if direction == "center" then 569 | n_left = math.floor(0.5 * n_left) 570 | end 571 | 572 | return (" "):rep(n_left) .. text 573 | end 574 | 575 | H.visual_text_width = function(text) 576 | -- Ignore concealed characters (usually "invisible" in 'help' filetype) 577 | local _, n_concealed_chars = text:gsub("([*|`])", "%1") 578 | return vim.fn.strdisplaywidth(text) - n_concealed_chars 579 | end 580 | 581 | --- Return earliest match among many patterns 582 | --- 583 | --- Logic here is to test among several patterns. If several got a match, 584 | --- return one with earliest match. 585 | --- 586 | ---@private 587 | H.match_first_pattern = function(text, pattern_set, init) 588 | local start_tbl = vim.tbl_map(function(pattern) 589 | return text:find(pattern, init) or math.huge 590 | end, pattern_set) 591 | 592 | local min_start, min_id = math.huge, nil 593 | for id, st in ipairs(start_tbl) do 594 | if st < min_start then 595 | min_start, min_id = st, id 596 | end 597 | end 598 | 599 | if min_id == nil then 600 | return {} 601 | end 602 | return { text:match(pattern_set[min_id], init) } 603 | end 604 | 605 | -- Utilities ------------------------------------------------------------------ 606 | H.apply_recursively = function(f, x, used) 607 | used = used or {} 608 | if used[x] then 609 | return 610 | end 611 | f(x) 612 | used[x] = true 613 | 614 | if type(x) == "table" then 615 | for _, t in ipairs(x) do 616 | H.apply_recursively(f, t, used) 617 | end 618 | end 619 | end 620 | 621 | H.collect_strings = function(x) 622 | local res = {} 623 | H.apply_recursively(function(y) 624 | if type(y) == "string" then 625 | -- Allow `\n` in strings 626 | table.insert(res, vim.split(y, "\n")) 627 | end 628 | end, x) 629 | -- Flatten to only have strings and not table of strings (from `vim.split`) 630 | return util.tbl_flatten(res) 631 | end 632 | 633 | H.file_read = function(path) 634 | local file = assert(io.open(path)) 635 | local contents = file:read("*all") 636 | file:close() 637 | 638 | return vim.split(contents, "\n") 639 | end 640 | 641 | H.file_write = function(path, lines) 642 | -- Ensure target directory exists 643 | local dir = vim.fn.fnamemodify(path, ":h") 644 | vim.fn.mkdir(dir, "p") 645 | 646 | -- Write to file 647 | vim.fn.writefile(lines, path, "b") 648 | end 649 | 650 | H.full_path = function(path) 651 | return vim.fn.resolve(vim.fn.fnamemodify(path, ":p")) 652 | end 653 | 654 | H.message = function(msg) 655 | vim.cmd("echomsg " .. vim.inspect("(mini.doc) " .. msg)) 656 | end 657 | 658 | minidoc.setup({}) 659 | minidoc.generate( 660 | { 661 | "./lua/dapui/init.lua", 662 | "./lua/dapui/config/init.lua", 663 | "./lua/dapui/elements/scopes.lua", 664 | "./lua/dapui/elements/stacks.lua", 665 | "./lua/dapui/elements/repl.lua", 666 | "./lua/dapui/elements/watches.lua", 667 | "./lua/dapui/elements/breakpoints.lua", 668 | "./lua/dapui/elements/console.lua", 669 | }, 670 | nil, 671 | { 672 | annotation_extractor = function(l) return string.find(l, "%s*%-%-%-(%S*) ?") end, 673 | 674 | hooks = vim.tbl_extend("force", minidoc.default_hooks, { 675 | block_pre = function(b) 676 | -- Infer metadata based on afterlines 677 | if b:has_lines() and #b.info.afterlines > 0 then H.infer_header(b) end 678 | end, 679 | 680 | block_post = function(b) 681 | if not b:has_lines() then return end 682 | 683 | local found_param, found_field = false, false 684 | local n_tag_sections = 0 685 | H.apply_recursively(function(x) 686 | if not (type(x) == "table" and x.type == "section") then return end 687 | 688 | -- Add headings before first occurence of a section which type usually 689 | -- appear several times 690 | if not found_param and x.info.id == "@param" then 691 | H.add_section_heading(x, "Parameters") 692 | found_param = true 693 | end 694 | if not found_field and x.info.id == "@field" then 695 | H.add_section_heading(x, "Fields") 696 | found_field = true 697 | end 698 | 699 | if x.info.id == "@tag" then 700 | local text = x[1] 701 | local tag = string.match(text, "%*.*%*") 702 | local prefix = (string.sub(tag, 2, #tag - 1)) 703 | if not H.is_module(prefix) then 704 | prefix = "" 705 | end 706 | local n_filler = math.max(78 - H.visual_text_width(prefix) - H.visual_text_width(tag), 3) 707 | local line = ("%s%s%s"):format(prefix, (" "):rep(n_filler), tag) 708 | x:remove(1) 709 | x:insert(1, line) 710 | x.parent:remove(x.parent_index) 711 | n_tag_sections = n_tag_sections + 1 712 | x.parent:insert(n_tag_sections, x) 713 | end 714 | end, b) 715 | 716 | -- b:insert(1, H.as_struct({ string.rep('=', 78) }, 'section')) 717 | b:insert(H.as_struct({ "" }, "section")) 718 | end, 719 | 720 | 721 | doc = function(d) 722 | -- Render table of contents 723 | H.apply_recursively(function(x) 724 | if not (type(x) == "table" and x.type == "section" and x.info.id == "@toc") then return end 725 | H.toc_insert(x) 726 | end, d) 727 | 728 | -- Insert modeline 729 | d:insert( 730 | H.as_struct( 731 | { H.as_struct({ H.as_struct({ " vim:tw=78:ts=8:noet:ft=help:norl:" }, "section") }, "block") }, 732 | "file" 733 | ) 734 | ) 735 | end, 736 | section_post = function(section) 737 | for i, line in ipairs(section) do 738 | if type(line) == "string" then 739 | if string.find(line, "^```") then 740 | string.gsub(line, "```(.*)", function(lang) 741 | section[i] = lang == "" and "<" or (">%s"):format(lang) 742 | end) 743 | end 744 | end 745 | end 746 | end, 747 | sections = { 748 | ["@generic"] = function(s) 749 | s:remove(1) 750 | end, 751 | ["@field"] = function(s) 752 | -- H.mark_optional(s) 753 | if string.find(s[1], "^private ") then 754 | s:remove(1) 755 | return 756 | end 757 | H.enclose_var_name(s) 758 | H.enclose_type(s, "`%(%1%)`", s[1]:find("%s")) 759 | end, 760 | ["@alias"] = function(s) 761 | local name = s[1]:match("%s*(%S*)") 762 | local alias = s[1]:match("%s(.*)$") 763 | s[1] = ("`%s` → `%s`"):format(name, alias) 764 | H.add_section_heading(s, "Alias") 765 | s:insert(1, H.as_struct({ ("*%s*"):format(name) }, "section", { id = "@tag" })) 766 | end, 767 | 768 | ["@param"] = function(s) 769 | H.enclose_var_name(s) 770 | H.enclose_type(s, "`%(%1%)`", s[1]:find("%s")) 771 | end, 772 | ["@return"] = function(s) 773 | H.enclose_type(s, "`%(%1%)`", 1) 774 | H.add_section_heading(s, "Return") 775 | end, 776 | ["@nodoc"] = function(s) s.parent:clear_lines() end, 777 | ["@class"] = function(s) 778 | H.enclose_var_name(s) 779 | -- Add heading 780 | local line = s[1] 781 | s:remove(1) 782 | local class_name = string.match(line, "%{(.*)%}") 783 | local inherits = string.match(line, ": (.*)") 784 | if inherits then 785 | s:insert(1, ("Inherits: `%s`"):format(inherits)) 786 | s:insert(2, "") 787 | end 788 | s:insert(1, H.as_struct({ ("*%s*"):format(class_name) }, "section", { id = "@tag" })) 789 | end, 790 | 791 | ["@signature"] = function(s) 792 | s[1] = H.format_signature(s[1]) 793 | if s[1] ~= "" then 794 | table.insert(s, "") 795 | end 796 | end, 797 | 798 | }, 799 | 800 | file = function(f) 801 | if not f:has_lines() then 802 | return 803 | end 804 | 805 | if f.info.path ~= "./lua/dapui/init.lua" then 806 | f:insert(1, H.as_struct({ H.as_struct({ string.rep("=", 78) }, "section") }, "block")) 807 | f:insert(H.as_struct({ H.as_struct({ "" }, "section") }, "block")) 808 | else 809 | f:insert( 810 | 1, 811 | H.as_struct( 812 | { 813 | H.as_struct( 814 | { "nvim-dap-ui.txt* A UI for nvim-dap." }, 815 | "section" 816 | ), 817 | }, 818 | "block" 819 | ) 820 | ) 821 | f:insert(2, H.as_struct({ H.as_struct({ "" }, "section") }, "block")) 822 | f:insert(3, H.as_struct({ H.as_struct({ string.rep("=", 78) }, "section") }, "block")) 823 | f:insert(H.as_struct({ H.as_struct({ "" }, "section") }, "block")) 824 | end 825 | end, 826 | }), 827 | } 828 | ) 829 | -------------------------------------------------------------------------------- /scripts/generate_types: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.10 2 | 3 | import importlib.machinery 4 | import importlib.util 5 | import inspect as i 6 | import json 7 | import logging 8 | import re 9 | import tempfile as tf 10 | from datetime import datetime 11 | from enum import Enum 12 | from pathlib import Path 13 | from typing import Any, Union, get_args, get_origin 14 | 15 | import datamodel_code_generator as d 16 | import requests 17 | from pydantic import BaseModel, Field, create_model 18 | from pydantic.fields import ModelField 19 | 20 | logging.basicConfig(level=logging.INFO) 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | # Name conversion patterns 25 | pat1 = re.compile("(.)([A-Z][a-z]+)") 26 | pat2 = re.compile("([a-z0-9])([A-Z])") 27 | 28 | 29 | def camel_to_snake(name): 30 | name = pat1.sub(r"\1_\2", name) 31 | return pat2.sub(r"\1_\2", name).lower() 32 | 33 | 34 | class FieldSummary(BaseModel): 35 | type_name: str 36 | type: type[BaseModel] 37 | 38 | 39 | class ModelSummary(BaseModel): 40 | type: type[BaseModel] 41 | type_name: str 42 | fields: dict[str, FieldSummary] 43 | description: str | None 44 | 45 | 46 | class DAPTypesGenerator: 47 | def __init__(self) -> None: 48 | self.known_types = set() 49 | self.create_module() 50 | 51 | def create_module(self): 52 | protocol_path = Path("/tmp/dap-protocol.json") 53 | if not protocol_path.exists(): 54 | logger.info("Downloading protocol") 55 | protocol_path.write_bytes( 56 | requests.get( 57 | "https://raw.githubusercontent.com/microsoft/debug-adapter-protocol/gh-pages/debugAdapterProtocol.json" 58 | ).content 59 | ) 60 | 61 | self.schema = json.loads(protocol_path.read_bytes().decode()) 62 | _, output_path = tf.mkstemp() 63 | output_path = Path(output_path) 64 | 65 | d.generate( 66 | input_=protocol_path, output=output_path, use_schema_description=True 67 | ) 68 | with open(output_path) as f: 69 | data = f.readlines() 70 | # Remove the update_forward_refs calls which cause errors 71 | while "update_forward_refs" in data[-1]: 72 | data.pop() 73 | 74 | with open(output_path, "w") as f: 75 | f.writelines(data) 76 | with open("model.py", "w") as f: 77 | f.writelines(data) 78 | 79 | # Import mymodule 80 | loader = importlib.machinery.SourceFileLoader("models", str(output_path)) 81 | spec = importlib.util.spec_from_loader("models", loader) 82 | models_module = importlib.util.module_from_spec(spec) 83 | loader.exec_module(models_module) 84 | self.models = models_module 85 | 86 | def is_model(self, t) -> bool: 87 | return i.isclass(t) and issubclass(t, BaseModel) 88 | 89 | def extract_summary( 90 | self, 91 | model_cls: type[BaseModel], 92 | ) -> ModelSummary: 93 | fields = {} 94 | for field_name, field in model_cls.__fields__.items(): 95 | if field and self.is_model(field.outer_type_): 96 | fields[field_name] = FieldSummary( 97 | type=field.type_, 98 | type_name=self.type_name(field.type_), 99 | ) 100 | return ModelSummary( 101 | type=model_cls, 102 | type_name=self.type_name(model_cls), 103 | fields=fields, 104 | description=self.class_doc(model_cls), 105 | ) 106 | 107 | def type_name(self, t) -> str: 108 | if self.is_model(t): 109 | return f"dapui.types.{t.__name__}" 110 | if t is int: 111 | return "integer" 112 | if t is float: 113 | return "number" 114 | if t is str: 115 | return "string" 116 | if t is bool: 117 | return "boolean" 118 | if i.isclass(t) and issubclass(t, Enum): 119 | return "|".join(f'"{member.value}"' for member in t.__members__.values()) 120 | if get_origin(t) is Union: 121 | # Account for Optional with ? on field 122 | return " | ".join( 123 | self.type_name(a) for a in get_args(t) if a is not type(None) 124 | ) 125 | if get_origin(t) is list: 126 | return f"{self.type_name(get_args(t)[0])}[]" 127 | if t is list: 128 | return f"any[]" 129 | if get_origin(t) is dict: 130 | return f"table<{self.type_name(get_args(t)[0])},{self.type_name(get_args(t)[1])}>" 131 | if t is dict: 132 | return f"table" 133 | if t is Any: 134 | return "any" 135 | if t is type(None): 136 | return "nil" 137 | raise Exception(f"Unknown type {t}") 138 | 139 | def safe_name(self, name: str) -> str: 140 | if name == "goto": 141 | return "goto_" 142 | return name 143 | 144 | def prepare_doc(self, doc: str, multiline: bool): 145 | lines = doc.strip().splitlines() 146 | if len(lines) == 1: 147 | return lines[0] 148 | if multiline: 149 | return f"{lines[0]}\n" + "\n".join( 150 | f"--- {line.strip()}" for line in lines[1:] 151 | ) 152 | return " ".join(line.strip() for line in lines) 153 | 154 | def field_annotation(self, field: ModelField) -> str: 155 | description = field.field_info.description and self.prepare_doc( 156 | field.field_info.description, multiline=False 157 | ) 158 | return f"---@field {field.name}{'?' if not field.required else ''} {self.type_name(field.outer_type_)} {description or ''}" 159 | 160 | def class_doc(self, model_cls: type[BaseModel]) -> str | None: 161 | if model_cls.__doc__: 162 | return model_cls.__doc__ 163 | cls_schema = self.schema["definitions"].get(model_cls.__name__) 164 | if not cls_schema: 165 | return 166 | if sub_types := cls_schema.get("allOf"): 167 | for definition in sub_types: 168 | if sub_desc := definition.get("description"): 169 | return sub_desc 170 | 171 | def create_class(self, t, class_name: str | None = None) -> list[str]: 172 | sub_classes = [] 173 | lines = [] 174 | t_name = class_name or self.type_name(t) 175 | if self.is_model(t) and t_name not in self.known_types: 176 | self.known_types.add(t_name) 177 | for field in t.__fields__.values(): 178 | if field_sub_classes := self.create_class(field.outer_type_): 179 | sub_classes += field_sub_classes 180 | sub_classes.append("") 181 | lines.append(self.field_annotation(field)) 182 | class_doc = self.class_doc(t) 183 | class_prefix = ( 184 | [f"--- {self.prepare_doc(class_doc, multiline=True)}"] 185 | if class_doc 186 | else [] 187 | ) 188 | class_prefix.append(f"---@class {t_name}") 189 | lines = [ 190 | *class_prefix, 191 | *lines, 192 | ] 193 | else: 194 | try: 195 | sub_classes = [ 196 | line 197 | for sub_type in get_args(t) 198 | for line in self.create_class(sub_type) 199 | ] 200 | except TypeError: 201 | ... 202 | return [ 203 | *sub_classes, 204 | *lines, 205 | ] 206 | 207 | def create_request( 208 | self, request_cls: type[BaseModel], response_cls: type[BaseModel] 209 | ) -> list[str]: 210 | (command,) = list(request_cls.__fields__["command"].outer_type_.__members__) 211 | 212 | request_summary = self.extract_summary(request_cls) 213 | types = [] 214 | signature = [] 215 | if request_summary.description: 216 | signature.append( 217 | f"--- {self.prepare_doc(request_summary.description, multiline=True)}" 218 | ) 219 | signature.append("---@async") 220 | if arg_summary := request_summary.fields.get("arguments"): 221 | types = self.create_class(arg_summary.type) 222 | signature += [ 223 | f"---@param args {arg_summary.type_name}", 224 | f"function DAPUIRequestsClient.{self.safe_name(command)}(args) end", 225 | ] 226 | else: 227 | signature += [ 228 | f"function DAPUIRequestsClient.{self.safe_name(command)}() end", 229 | ] 230 | 231 | response_summary = self.extract_summary(response_cls) 232 | if body_summary := response_summary.fields.get("body"): 233 | types.append("") 234 | types += self.create_class(body_summary.type, response_summary.type_name) 235 | signature.insert(-1, f"---@return {response_summary.type_name}") 236 | 237 | x = [ 238 | *types, 239 | "", 240 | *signature, 241 | "", 242 | "", 243 | ] 244 | return x 245 | 246 | def create_event_listener(self, event_cls: type[BaseModel]) -> list[str]: 247 | (event,) = list(event_cls.__fields__["event"].outer_type_.__members__) 248 | event_summary = self.extract_summary(event_cls) 249 | types = [] 250 | signature = [] 251 | if event_summary.description: 252 | signature.append( 253 | f"--- {self.prepare_doc(event_summary.description, multiline=True)}" 254 | ) 255 | if body_summary := event_summary.fields.get("body"): 256 | body_class_name = f"{event_summary.type_name}Args" 257 | types = self.create_class(body_summary.type, body_class_name) 258 | signature.append(f"---@param listener fun(args: {body_class_name})") 259 | else: 260 | signature.append(f"---@param listener fun()") 261 | 262 | signature.append(f"---@param opts? dapui.client.ListenerOpts") 263 | x = [ 264 | *types, 265 | "", 266 | *signature, 267 | f"function DAPUIEventListenerClient.{self.safe_name(event)}(listener, opts) end", 268 | "", 269 | "", 270 | ] 271 | return x 272 | 273 | def create_request_listener( 274 | self, request_cls: type[BaseModel], response_cls: type[BaseModel] 275 | ) -> list[str]: 276 | (command,) = list(request_cls.__fields__["command"].outer_type_.__members__) 277 | 278 | request_summary = self.extract_summary(request_cls) 279 | response_summary = self.extract_summary(response_cls) 280 | 281 | listener_args = {} 282 | if args := request_summary.fields.get("arguments"): 283 | listener_args["request"] = args.type 284 | listener_args["error"] = dict | None 285 | if args := response_summary.fields.get("body"): 286 | listener_args["response"] = create_model( 287 | response_cls.__name__, __base__=args.type 288 | ) 289 | 290 | args_summary = self.extract_summary( 291 | create_model( 292 | f"{command}RequestListenerArgs", 293 | **{name: (t, Field()) for name, t in listener_args.items()}, 294 | ) 295 | ) 296 | types = self.create_class(args_summary.type) 297 | return [ 298 | *types, 299 | "", 300 | f"---@param listener fun(args: {args_summary.type_name}): boolean | nil", 301 | f"---@param opts? dapui.client.ListenerOpts", 302 | f"function DAPUIEventListenerClient.{self.safe_name(command)}(listener, opts) end", 303 | "", 304 | "", 305 | ] 306 | 307 | PREFIX = """ 308 | ---@class dapui.DAPRequestsClient 309 | local DAPUIRequestsClient = {} 310 | 311 | ---@class dapui.DAPEventListenerClient 312 | local DAPUIEventListenerClient = {} 313 | 314 | ---@class dapui.client.ListenerOpts 315 | ---@field before boolean Run before event/request is processed by nvim-dap 316 | """ 317 | 318 | def generate_types(self) -> str: 319 | output = f"--- Generated on {datetime.utcnow()}\n" 320 | output += self.PREFIX 321 | 322 | member_names = dict(i.getmembers(self.models)) 323 | for _, member in i.getmembers(self.models, self.is_model): 324 | member.update_forward_refs(**member_names) 325 | for name, request in i.getmembers( 326 | self.models, 327 | lambda x: x is not self.models.Request 328 | and i.isclass(x) 329 | and issubclass(x, self.models.Request), 330 | ): 331 | try: 332 | output += "\n".join( 333 | self.create_request( 334 | request, 335 | getattr(self.models, name.replace("Request", "Response")), 336 | ) 337 | ) 338 | output += "\n".join( 339 | self.create_request_listener( 340 | request, 341 | getattr(self.models, name.replace("Request", "Response")), 342 | ) 343 | ) 344 | except Exception as e: 345 | logger.exception(f"Failed to create {name}: {e}") 346 | 347 | for name, event in i.getmembers( 348 | self.models, 349 | lambda x: x is not self.models.Event 350 | and i.isclass(x) 351 | and issubclass(x, self.models.Event), 352 | ): 353 | try: 354 | output += "\n".join(self.create_event_listener(event)) 355 | except Exception as e: 356 | logger.exception(f"Failed to create {name}: {e}") 357 | output += "\nreturn { request = DAPUIRequestsClient, listen = DAPUIEventListenerClient }" 358 | return output 359 | 360 | 361 | generator = DAPTypesGenerator() 362 | logger.info("Generating types") 363 | 364 | types = generator.generate_types() 365 | 366 | logger.info("Outputting types") 367 | print(types) 368 | -------------------------------------------------------------------------------- /scripts/style: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | stylua lua tests 4 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | tempfile=".test_output.tmp" 3 | 4 | if [[ -n $1 ]]; then 5 | nvim --headless --noplugin -u tests/init.vim -c "PlenaryBustedFile $1" | tee "${tempfile}" 6 | else 7 | nvim --headless --noplugin -u tests/init.vim -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/init.vim'}" | tee "${tempfile}" 8 | fi 9 | 10 | # Plenary doesn't emit exit code 1 when tests have errors during setup 11 | errors=$(sed 's/\x1b\[[0-9;]*m//g' "${tempfile}" | awk '/(Errors|Failed) :/ {print $3}' | grep -v '0') 12 | 13 | rm "${tempfile}" 14 | 15 | if [[ -n $errors ]]; then 16 | echo "Tests failed" 17 | exit 1 18 | fi 19 | 20 | exit 0 21 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 100 2 | line_endings = "Unix" 3 | indent_type = "Spaces" 4 | indent_width = 2 5 | quote_style = "AutoPreferDouble" 6 | -------------------------------------------------------------------------------- /tests/init.vim: -------------------------------------------------------------------------------- 1 | set rtp+=. 2 | set rtp+=../plenary.nvim 3 | set rtp+=../nvim-dap 4 | set rtp+=../nvim-nio 5 | runtime! plugin/plenary.vim 6 | -------------------------------------------------------------------------------- /tests/minimal_init.lua: -------------------------------------------------------------------------------- 1 | local lazypath = vim.fn.stdpath("data") .. "/lazy" 2 | vim.notify = print 3 | vim.opt.rtp:append(".") 4 | vim.opt.rtp:append(lazypath .. "/nvim-dap") 5 | vim.opt.rtp:append(lazypath .. "/nvim-nio") 6 | 7 | local home = os.getenv("HOME") 8 | vim.opt.rtp:append(home .. "/Dev/nvim-nio") 9 | 10 | vim.opt.swapfile = false 11 | vim.cmd("runtime! plugin/plenary.vim") 12 | A = function(...) 13 | print(vim.inspect(...)) 14 | end 15 | -------------------------------------------------------------------------------- /tests/unit/config/init_spec.lua: -------------------------------------------------------------------------------- 1 | local config = require("dapui.config") 2 | 3 | describe("checking setup function", function() 4 | it("allows nil config", function() 5 | config.setup() 6 | assert.equal(config.icons.expanded, "") 7 | end) 8 | 9 | it("allows empty config", function() 10 | config.setup({}) 11 | assert.equal(config.icons.expanded, "") 12 | end) 13 | 14 | it("allows overriding values", function() 15 | config.setup({ icons = { expanded = "X" } }) 16 | assert.equal(config.icons.expanded, "X") 17 | end) 18 | 19 | it("fills mappings", function() 20 | config.setup({ mappings = { edit = "e" } }) 21 | assert.same({ "e" }, config.mappings.edit) 22 | end) 23 | 24 | it("fills elements", function() 25 | config.setup({ layouts = { { size = 10, position = "left", elements = { "scopes" } } } }) 26 | assert.same({ { id = "scopes", size = 1 } }, config.layouts[1].elements) 27 | end) 28 | 29 | it("fills elements with proportional size", function() 30 | config.setup({ 31 | layouts = { { size = 10, position = "left", elements = { "scopes", "stacks" } } }, 32 | }) 33 | assert.same( 34 | { { id = "scopes", size = 0.5 }, { id = "stacks", size = 0.5 } }, 35 | config.layouts[1].elements 36 | ) 37 | end) 38 | end) 39 | -------------------------------------------------------------------------------- /tests/unit/elements/breakpoints_spec.lua: -------------------------------------------------------------------------------- 1 | local nio = require("nio") 2 | local Breakpoints = require("dapui.elements.breakpoints") 3 | local a = nio.tests 4 | local tests = require("dapui.tests") 5 | tests.bootstrap() 6 | local mocks = tests.mocks 7 | 8 | describe("breakpoints element", function() 9 | local client, breakpoints, buf 10 | local init_bps = { 11 | test_a = { 12 | lines = { 13 | "line_a_1", 14 | "line_a_2", 15 | "line_a_3", 16 | }, 17 | bps = { 18 | [1] = {}, 19 | [3] = { condition = "a + 3 == 3" }, 20 | }, 21 | }, 22 | test_b = { 23 | lines = { 24 | "line_b_1", 25 | "line_b_2", 26 | "line_b_3", 27 | }, 28 | bps = { 29 | [2] = { log_message = "here" }, 30 | }, 31 | }, 32 | } 33 | a.before_each(function() 34 | client = mocks.client({ 35 | current_frame = { 36 | id = 1, 37 | line = 1, 38 | source = { 39 | path = "test_a", 40 | }, 41 | }, 42 | }) 43 | for path, data in pairs(init_bps) do 44 | local path_buf = vim.api.nvim_create_buf(true, true) 45 | nio.api.nvim_buf_set_name(path_buf, path) 46 | nio.api.nvim_buf_set_lines(path_buf, 0, -1, false, data.lines) 47 | for line, bp in pairs(data.bps) do 48 | client.breakpoints.toggle(path_buf, line, bp) 49 | end 50 | end 51 | breakpoints = Breakpoints(client) 52 | breakpoints.render() 53 | buf = breakpoints.buffer() 54 | end) 55 | 56 | after_each(function() 57 | pcall(vim.api.nvim_buf_delete, buf, { force = true }) 58 | for path, _ in pairs(init_bps) do 59 | local path_buf = vim.fn.bufnr(path) 60 | pcall(vim.api.nvim_buf_delete, path_buf, { force = true }) 61 | end 62 | breakpoints = nil 63 | end) 64 | 65 | a.it("renders lines", function() 66 | local lines = nio.api.nvim_buf_get_lines(buf, 0, -1, false) 67 | assert.same({ 68 | "test_a:", 69 | " 1 line_a_1", 70 | " 3 line_a_3", 71 | " Condition: a + 3 == 3", 72 | "", 73 | "test_b:", 74 | " 2 line_b_2", 75 | " Log Message: here", 76 | }, lines) 77 | end) 78 | 79 | a.it("renders highlights", function() 80 | local highlights = tests.util.get_highlights(buf) 81 | assert.same({ 82 | { "DapUIBreakpointsPath", 0, 0, 0, 6 }, 83 | { "DapUIBreakpointsCurrentLine", 1, 1, 1, 2 }, 84 | { "DapUIBreakpointsLine", 2, 1, 2, 2 }, 85 | { "DapUIBreakpointsInfo", 3, 3, 3, 13 }, 86 | { "DapUIBreakpointsPath", 5, 0, 5, 6 }, 87 | { "DapUIBreakpointsLine", 6, 1, 6, 2 }, 88 | { "DapUIBreakpointsInfo", 7, 3, 7, 15 }, 89 | }, highlights) 90 | end) 91 | 92 | a.it("renders highlights with toggled breakpoint", function() 93 | local keymaps = tests.util.get_mappings(buf) 94 | keymaps.t(3) 95 | local highlights = tests.util.get_highlights(buf) 96 | assert.same({ 97 | { "DapUIBreakpointsPath", 0, 0, 0, 6 }, 98 | { "DapUIBreakpointsCurrentLine", 1, 1, 1, 2 }, 99 | { "DapUIBreakpointsDisabledLine", 2, 1, 2, 2 }, 100 | { "DapUIBreakpointsInfo", 3, 3, 3, 13 }, 101 | { "DapUIBreakpointsPath", 5, 0, 5, 6 }, 102 | { "DapUIBreakpointsLine", 6, 1, 6, 2 }, 103 | { "DapUIBreakpointsInfo", 7, 3, 7, 15 }, 104 | }, highlights) 105 | end) 106 | end) 107 | -------------------------------------------------------------------------------- /tests/unit/elements/hover_spec.lua: -------------------------------------------------------------------------------- 1 | local nio = require("nio") 2 | local a = nio.tests 3 | local Hover = require("dapui.elements.hover") 4 | local tests = require("dapui.tests") 5 | tests.bootstrap() 6 | local mocks = tests.mocks 7 | 8 | describe("hover element", function() 9 | ---@type dapui.elements.hover 10 | local hover 11 | local client, buf 12 | a.before_each(function() 13 | client = mocks.client({ 14 | current_frame = { 15 | id = 1, 16 | }, 17 | requests = { 18 | evaluate = mocks.evaluate({ 19 | expressions = { 20 | a = "'a value'", 21 | ["b - 1"] = { result = "1", type = "number" }, 22 | c = { result = "{ d = 1 }", type = "table", variablesReference = 1 }, 23 | }, 24 | }), 25 | variables = mocks.variables({ 26 | variables = { 27 | [1] = { 28 | { 29 | name = "d", 30 | value = "1", 31 | type = "number", 32 | variablesReference = 0, 33 | }, 34 | }, 35 | }, 36 | }), 37 | }, 38 | }) 39 | hover = Hover(client) 40 | buf = hover.buffer() 41 | end) 42 | after_each(function() 43 | pcall(vim.api.nvim_buf_delete, buf, { force = true }) 44 | hover = nil 45 | end) 46 | a.it("renders lines", function() 47 | hover.set_expression("a") 48 | nio.sleep(10) 49 | local lines = nio.api.nvim_buf_get_lines(buf, 0, -1, false) 50 | assert.same({ "a = 'a value'" }, lines) 51 | end) 52 | a.it("renders lines after expression update", function() 53 | hover.set_expression("a") 54 | hover.set_expression("b - 1") 55 | nio.sleep(10) 56 | local lines = nio.api.nvim_buf_get_lines(buf, 0, -1, false) 57 | assert.same({ "b - 1 number = 1" }, lines) 58 | end) 59 | 60 | a.it("renders lines with expandable expression", function() 61 | hover.set_expression("c") 62 | nio.sleep(10) 63 | local lines = nio.api.nvim_buf_get_lines(buf, 0, -1, false) 64 | assert.same({ " c table = { d = 1 }" }, lines) 65 | end) 66 | 67 | a.it("renders highlights with expandable expression", function() 68 | hover.set_expression("c") 69 | nio.sleep(10) 70 | local formatted = tests.util.get_highlights(buf) 71 | assert.same({ 72 | { "DapUIDecoration", 0, 0, 0, 4 }, 73 | { "DapUIType", 0, 6, 0, 11 }, 74 | { "DapUIValue", 0, 14, 0, 23 }, 75 | }, formatted) 76 | end) 77 | 78 | describe("with expanded variables", function() 79 | a.it("renders expanded lines", function() 80 | hover.set_expression("c") 81 | nio.sleep(10) 82 | local keymaps = tests.util.get_mappings(hover.buffer()) 83 | keymaps[""](1) 84 | nio.sleep(10) 85 | 86 | local lines = nio.api.nvim_buf_get_lines(buf, 0, -1, false) 87 | assert.same({ " c table = { d = 1 }", " d number = 1" }, lines) 88 | end) 89 | a.it("renders expanded highlights", function() 90 | hover.set_expression("c") 91 | nio.sleep(10) 92 | local keymaps = tests.util.get_mappings(hover.buffer()) 93 | keymaps[""](1) 94 | nio.sleep(10) 95 | 96 | local formatted = tests.util.get_highlights(buf) 97 | assert.same({ 98 | { "DapUIDecoration", 0, 0, 0, 4 }, 99 | { "DapUIType", 0, 6, 0, 11 }, 100 | { "DapUIValue", 0, 14, 0, 23 }, 101 | { "DapUIDecoration", 1, 1, 1, 2 }, 102 | { "DapUIVariable", 1, 3, 1, 4 }, 103 | { "DapUIType", 1, 5, 1, 11 }, 104 | { "DapUIValue", 1, 14, 1, 15 }, 105 | }, formatted) 106 | end) 107 | end) 108 | end) 109 | -------------------------------------------------------------------------------- /tests/unit/elements/scopes_spec.lua: -------------------------------------------------------------------------------- 1 | local nio = require("nio") 2 | local a = nio.tests 3 | local Scopes = require("dapui.elements.scopes") 4 | local tests = require("dapui.tests") 5 | tests.bootstrap() 6 | local mocks = tests.mocks 7 | 8 | describe("scopes element", function() 9 | ---@type dapui.DAPClient 10 | local client 11 | local scopes, buf 12 | a.before_each(function() 13 | client = mocks.client({ 14 | current_frame = { 15 | id = 1, 16 | }, 17 | requests = { 18 | scopes = mocks.scopes({ 19 | scopes = { 20 | [1] = { 21 | { 22 | name = "Locals", 23 | variablesReference = 1, 24 | }, 25 | { 26 | name = "Globals", 27 | variablesReference = 2, 28 | }, 29 | }, 30 | }, 31 | }), 32 | variables = mocks.variables({ 33 | variables = { 34 | [1] = { 35 | { 36 | name = "a", 37 | value = "1", 38 | type = "number", 39 | variablesReference = 0, 40 | }, 41 | { 42 | name = "b", 43 | value = "2", 44 | type = "number", 45 | variablesReference = 3, 46 | }, 47 | }, 48 | [2] = { 49 | { 50 | name = "CONST_A", 51 | value = "true", 52 | type = "boolean", 53 | variablesReference = 0, 54 | }, 55 | }, 56 | [3] = { 57 | { 58 | name = "c", 59 | value = "'3'", 60 | type = "string", 61 | variablesReference = 0, 62 | }, 63 | }, 64 | }, 65 | }), 66 | }, 67 | }) 68 | scopes = Scopes(client) 69 | buf = scopes.buffer() 70 | client.request.scopes({ frameId = 1 }) 71 | end) 72 | after_each(function() 73 | pcall(vim.api.nvim_buf_delete, buf, { force = true }) 74 | scopes = nil 75 | end) 76 | a.it("renders initial lines", function() 77 | local lines = nio.api.nvim_buf_get_lines(buf, 0, -1, false) 78 | assert.same({ 79 | " Locals:", 80 | " a number = 1", 81 | "  b number = 2", 82 | "", 83 | " Globals:", 84 | " CONST_A boolean = true", 85 | }, lines) 86 | end) 87 | 88 | a.it("renders initial highlights", function() 89 | local formatted = tests.util.get_highlights(buf) 90 | assert.same({ 91 | { "DapUIDecoration", 0, 0, 0, 3 }, 92 | { "DapUIScope", 0, 4, 0, 10 }, 93 | { "DapUIDecoration", 1, 1, 1, 2 }, 94 | { "DapUIVariable", 1, 3, 1, 4 }, 95 | { "DapUIType", 1, 5, 1, 11 }, 96 | { "DapUIValue", 1, 14, 1, 15 }, 97 | { "DapUIDecoration", 2, 1, 2, 4 }, 98 | { "DapUIVariable", 2, 5, 2, 6 }, 99 | { "DapUIType", 2, 7, 2, 13 }, 100 | { "DapUIValue", 2, 16, 2, 17 }, 101 | { "DapUIDecoration", 4, 0, 4, 3 }, 102 | { "DapUIScope", 4, 4, 4, 11 }, 103 | { "DapUIDecoration", 5, 1, 5, 2 }, 104 | { "DapUIVariable", 5, 3, 5, 10 }, 105 | { "DapUIType", 5, 11, 5, 18 }, 106 | { "DapUIValue", 5, 21, 5, 25 }, 107 | }, formatted) 108 | end) 109 | 110 | describe("with expanded variables", function() 111 | a.it("renders expanded lines", function() 112 | local keymaps = tests.util.get_mappings(scopes.buffer()) 113 | keymaps[""](3) 114 | nio.sleep(10) 115 | local lines = nio.api.nvim_buf_get_lines(buf, 0, -1, false) 116 | assert.same({ 117 | " Locals:", 118 | " a number = 1", 119 | "  b number = 2", 120 | " c string = '3'", 121 | "", 122 | " Globals:", 123 | " CONST_A boolean = true", 124 | }, lines) 125 | end) 126 | a.it("renders expanded highlights", function() 127 | local keymaps = tests.util.get_mappings(scopes.buffer()) 128 | keymaps[""](3) 129 | nio.sleep(10) 130 | local formatted = tests.util.get_highlights(buf) 131 | assert.same({ 132 | { "DapUIDecoration", 0, 0, 0, 3 }, 133 | { "DapUIScope", 0, 4, 0, 10 }, 134 | { "DapUIDecoration", 1, 1, 1, 2 }, 135 | { "DapUIVariable", 1, 3, 1, 4 }, 136 | { "DapUIType", 1, 5, 1, 11 }, 137 | { "DapUIValue", 1, 14, 1, 15 }, 138 | { "DapUIDecoration", 2, 1, 2, 4 }, 139 | { "DapUIVariable", 2, 5, 2, 6 }, 140 | { "DapUIType", 2, 7, 2, 13 }, 141 | { "DapUIValue", 2, 16, 2, 17 }, 142 | { "DapUIDecoration", 3, 2, 3, 3 }, 143 | { "DapUIVariable", 3, 4, 3, 5 }, 144 | { "DapUIType", 3, 6, 3, 12 }, 145 | { "DapUIValue", 3, 15, 3, 18 }, 146 | { "DapUIDecoration", 5, 0, 5, 3 }, 147 | { "DapUIScope", 5, 4, 5, 11 }, 148 | { "DapUIDecoration", 6, 1, 6, 2 }, 149 | { "DapUIVariable", 6, 3, 6, 10 }, 150 | { "DapUIType", 6, 11, 6, 18 }, 151 | { "DapUIValue", 6, 21, 6, 25 }, 152 | }, formatted) 153 | end) 154 | end) 155 | end) 156 | -------------------------------------------------------------------------------- /tests/unit/elements/stacks_spec.lua: -------------------------------------------------------------------------------- 1 | local nio = require("nio") 2 | local a = nio.tests 3 | local Stacks = require("dapui.elements.stacks") 4 | local tests = require("dapui.tests") 5 | tests.bootstrap() 6 | local mocks = tests.mocks 7 | 8 | describe("stacks element", function() 9 | local client, stacks, buf 10 | a.before_each(function() 11 | client = mocks.client({ 12 | current_frame = { 13 | id = 1, 14 | }, 15 | requests = { 16 | scopes = mocks.scopes({ 17 | scopes = { 18 | [1] = { 19 | { 20 | name = "Locals", 21 | variablesReference = 1, 22 | }, 23 | { 24 | name = "Globals", 25 | variablesReference = 2, 26 | }, 27 | }, 28 | }, 29 | }), 30 | threads = mocks.threads({ 31 | threads = { 32 | { 33 | id = 1, 34 | name = "Thread 1", 35 | }, 36 | { 37 | id = 2, 38 | name = "Thread 2", 39 | }, 40 | }, 41 | }), 42 | stackTrace = mocks.stack_traces({ 43 | stack_traces = { 44 | [1] = { 45 | { 46 | id = 1, 47 | name = "stack_frame_1", 48 | source = { 49 | name = "file_1", 50 | }, 51 | line = 1, 52 | }, 53 | { 54 | id = 2, 55 | name = "stack_frame_2", 56 | source = { 57 | name = "file_2", 58 | }, 59 | line = 2, 60 | presentationHint = "subtle", 61 | }, 62 | }, 63 | [2] = { 64 | { 65 | id = 3, 66 | name = "stack_frame_3", 67 | source = { 68 | name = "file_3", 69 | }, 70 | line = 3, 71 | }, 72 | { 73 | id = 4, 74 | name = "stack_frame_4", 75 | source = { 76 | name = "file_4", 77 | }, 78 | line = 4, 79 | }, 80 | }, 81 | }, 82 | }), 83 | }, 84 | }) 85 | stacks = Stacks(client) 86 | client.request.threads() 87 | client.request.scopes({ frameId = 1 }) 88 | buf = stacks.buffer() 89 | nio.sleep(10) 90 | end) 91 | after_each(function() 92 | pcall(vim.api.nvim_buf_delete, buf, { force = true }) 93 | stacks = nil 94 | client.shutdown() 95 | end) 96 | a.it("renders initial lines", function() 97 | stacks.render() 98 | local lines = nio.api.nvim_buf_get_lines(buf, 0, -1, false) 99 | assert.same({ 100 | "Thread 1:", 101 | " stack_frame_1 file_1:1", 102 | "", 103 | "Thread 2:", 104 | " stack_frame_3 file_3:3", 105 | " stack_frame_4 file_4:4", 106 | "", 107 | "", 108 | }, lines) 109 | end) 110 | 111 | a.it("renders initial highlights", function() 112 | stacks.render() 113 | local formatted = tests.util.get_highlights(buf) 114 | assert.same({ 115 | { "DapUIThread", 0, 0, 0, 8 }, 116 | { "DapUICurrentFrameName", 1, 4, 1, 17 }, 117 | { "DapUISource", 1, 18, 1, 24 }, 118 | { "DapUILineNumber", 1, 25, 1, 26 }, 119 | { "DapUIThread", 3, 0, 3, 8 }, 120 | { "DapUIFrameName", 4, 1, 4, 14 }, 121 | { "DapUISource", 4, 15, 4, 21 }, 122 | { "DapUILineNumber", 4, 22, 4, 23 }, 123 | { "DapUIFrameName", 5, 1, 5, 14 }, 124 | { "DapUISource", 5, 15, 5, 21 }, 125 | { "DapUILineNumber", 5, 22, 5, 23 }, 126 | }, formatted) 127 | end) 128 | 129 | describe("with subtle frames shown", function() 130 | a.it("renders expanded lines", function() 131 | stacks.render() 132 | local keymaps = tests.util.get_mappings(stacks.buffer()) 133 | keymaps["t"](1) 134 | nio.sleep(10) 135 | local lines = nio.api.nvim_buf_get_lines(buf, 0, -1, false) 136 | assert.same({ 137 | "Thread 1:", 138 | " stack_frame_1 file_1:1", 139 | " stack_frame_2 file_2:2", 140 | "", 141 | "Thread 2:", 142 | " stack_frame_3 file_3:3", 143 | " stack_frame_4 file_4:4", 144 | "", 145 | "", 146 | }, lines) 147 | end) 148 | a.it("renders expanded highlights", function() 149 | stacks.render() 150 | local keymaps = tests.util.get_mappings(stacks.buffer()) 151 | keymaps["t"](1) 152 | stacks.render() 153 | local formatted = tests.util.get_highlights(buf) 154 | assert.same({ 155 | { "DapUIThread", 0, 0, 0, 8 }, 156 | { "DapUICurrentFrameName", 1, 4, 1, 17 }, 157 | { "DapUISource", 1, 18, 1, 24 }, 158 | { "DapUILineNumber", 1, 25, 1, 26 }, 159 | { "DapUIFrameName", 2, 1, 2, 14 }, 160 | { "DapUISource", 2, 15, 2, 21 }, 161 | { "DapUILineNumber", 2, 22, 2, 23 }, 162 | { "DapUIThread", 4, 0, 4, 8 }, 163 | { "DapUIFrameName", 5, 1, 5, 14 }, 164 | { "DapUISource", 5, 15, 5, 21 }, 165 | { "DapUILineNumber", 5, 22, 5, 23 }, 166 | { "DapUIFrameName", 6, 1, 6, 14 }, 167 | { "DapUISource", 6, 15, 6, 21 }, 168 | { "DapUILineNumber", 6, 22, 6, 23 }, 169 | }, formatted) 170 | end) 171 | end) 172 | end) 173 | -------------------------------------------------------------------------------- /tests/unit/elements/watches_spec.lua: -------------------------------------------------------------------------------- 1 | local nio = require("nio") 2 | local a = nio.tests 3 | local Watches = require("dapui.elements.watches") 4 | local tests = require("dapui.tests") 5 | tests.bootstrap() 6 | local mocks = tests.mocks 7 | 8 | describe("watches element", function() 9 | ---@type dapui.elements.watches 10 | local watches 11 | local client, buf 12 | a.before_each(function() 13 | client = mocks.client({ 14 | current_frame = { 15 | id = 1, 16 | }, 17 | requests = { 18 | scopes = mocks.scopes({ 19 | scopes = { [1] = {} }, 20 | }), 21 | evaluate = mocks.evaluate({ 22 | expressions = { 23 | a = "'a value'", 24 | ["b - 1"] = { result = "1", type = "number" }, 25 | c = { result = "{ d = 1 }", type = "table", variablesReference = 1 }, 26 | }, 27 | }), 28 | variables = mocks.variables({ 29 | variables = { 30 | [1] = { 31 | { 32 | name = "d", 33 | value = "1", 34 | type = "number", 35 | variablesReference = 0, 36 | }, 37 | }, 38 | }, 39 | }), 40 | }, 41 | }) 42 | client.request.scopes({ frameId = 1 }) 43 | watches = Watches(client) 44 | buf = watches.buffer() 45 | watches.render() 46 | end) 47 | after_each(function() 48 | pcall(vim.api.nvim_buf_delete, buf, { force = true }) 49 | watches = nil 50 | end) 51 | describe("with no expressions", function() 52 | a.it("renders no expressions lines", function() 53 | local lines = nio.api.nvim_buf_get_lines(buf, 0, -1, false) 54 | assert.same({ "No Expressions", "" }, lines) 55 | end) 56 | a.it("renders lines after expression update", function() 57 | local highlights = tests.util.get_highlights(buf) 58 | assert.same({ { "DapUIWatchesEmpty", 0, 0, 0, 14 } }, highlights) 59 | end) 60 | end) 61 | 62 | a.it("renders lines with expressions", function() 63 | watches.add("a") 64 | watches.add("b - 1") 65 | watches.add("c") 66 | nio.sleep(10) 67 | local lines = nio.api.nvim_buf_get_lines(buf, 0, -1, false) 68 | assert.same( 69 | { " a = 'a value'", " b - 1 number = 1", " c table = { d = 1 }", "" }, 70 | lines 71 | ) 72 | end) 73 | 74 | a.it("renders highlights with expressions", function() 75 | watches.add("a") 76 | watches.add("b - 1") 77 | watches.add("c") 78 | nio.sleep(10) 79 | local highlights = tests.util.get_highlights(buf) 80 | assert.same({ 81 | { "DapUIWatchesValue", 0, 0, 0, 3 }, 82 | { "DapUIModifiedValue", 0, 8, 0, 17 }, 83 | { "DapUIWatchesValue", 1, 0, 1, 3 }, 84 | { "DapUIType", 1, 10, 1, 16 }, 85 | { "DapUIModifiedValue", 1, 19, 1, 20 }, 86 | { "DapUIWatchesValue", 2, 0, 2, 3 }, 87 | { "DapUIType", 2, 6, 2, 11 }, 88 | { "DapUIModifiedValue", 2, 14, 2, 23 }, 89 | }, highlights) 90 | end) 91 | 92 | describe("with expanded variables", function() 93 | a.it("renders expanded lines", function() 94 | watches.add("c") 95 | watches.toggle_expand(1) 96 | nio.sleep(10) 97 | 98 | local lines = nio.api.nvim_buf_get_lines(buf, 0, -1, false) 99 | assert.same({ " c table = { d = 1 }", " d number = 1", "" }, lines) 100 | end) 101 | a.it("renders expanded highlights", function() 102 | watches.add("c") 103 | watches.toggle_expand(1) 104 | nio.sleep(10) 105 | 106 | local highlights = tests.util.get_highlights(buf) 107 | assert.same({ 108 | { "DapUIWatchesValue", 0, 0, 0, 3 }, 109 | { "DapUIType", 0, 6, 0, 11 }, 110 | { "DapUIModifiedValue", 0, 14, 0, 23 }, 111 | { "DapUIDecoration", 1, 1, 1, 2 }, 112 | { "DapUIVariable", 1, 3, 1, 4 }, 113 | { "DapUIType", 1, 5, 1, 11 }, 114 | { "DapUIValue", 1, 14, 1, 15 }, 115 | }, highlights) 116 | end) 117 | end) 118 | end) 119 | -------------------------------------------------------------------------------- /tests/unit/util_spec.lua: -------------------------------------------------------------------------------- 1 | local util = require("dapui.util") 2 | 3 | describe("checking is_uri", function() 4 | it("returns true on uri", function() 5 | assert(util.is_uri("file://myfile")) 6 | end) 7 | 8 | it("returns false on non-uri", function() 9 | assert(not util.is_uri("/myfile")) 10 | end) 11 | end) 12 | 13 | describe("checking pretty name", function() 14 | it("converts a path", function() 15 | local uri = "/home/file.py" 16 | local result = util.pretty_name(uri) 17 | assert.equals(result, "file.py") 18 | end) 19 | 20 | it("converts a uri", function() 21 | local uri = "file:///home/file.py" 22 | local result = util.pretty_name(uri) 23 | assert.equals(result, "file.py") 24 | end) 25 | end) 26 | 27 | describe("checking format_error", function() 28 | it("formats variables", function() 29 | local error = { 30 | body = { 31 | error = { 32 | format = 'Unable to eval expression: "{e}"', 33 | variables = { e = "could not find symbol value for a" }, 34 | }, 35 | }, 36 | } 37 | local expected = 'Unable to eval expression: "could not find symbol value for a"' 38 | local result = util.format_error(error) 39 | assert.equals(expected, result) 40 | end) 41 | it("returns message", function() 42 | local error = { message = "Couldn't evaluate expression 'a'" } 43 | local expected = "Couldn't evaluate expression 'a'" 44 | local result = util.format_error(error) 45 | assert.equals(expected, result) 46 | end) 47 | it("returns message within body", function() 48 | local error = { body = { message = "Couldn't evaluate expression 'a'" } } 49 | local expected = "Couldn't evaluate expression 'a'" 50 | local result = util.format_error(error) 51 | assert.equals(expected, result) 52 | end) 53 | end) 54 | 55 | describe("checking partial", function() 56 | it("supplies preloaded args", function() 57 | local f = function(a, b) 58 | assert.equals(a, 1) 59 | assert.equals(b, 2) 60 | end 61 | local g = util.partial(f, 1, 2) 62 | g() 63 | end) 64 | 65 | it("supplies new args", function() 66 | local f = function(a, b) 67 | assert.equals(a, 1) 68 | assert.equals(b, 2) 69 | end 70 | local g = util.partial(f, 1) 71 | g(2) 72 | end) 73 | end) 74 | 75 | describe("checking round", function() 76 | it("rounds down", function() 77 | assert.equal(0, util.round(0.1)) 78 | end) 79 | it("rounds up", function() 80 | assert.equal(1, util.round(0.5)) 81 | end) 82 | end) 83 | -------------------------------------------------------------------------------- /tests/unit/windows/layout_spec.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | local util = require("dapui.util") 3 | local windows = require("dapui.windows") 4 | 5 | describe("checking window layout", function() 6 | local layout 7 | local win_configs = { 8 | { id = "a", size = 0.6 }, 9 | { id = "b", size = 0.2 }, 10 | { id = "c", size = 0.2 }, 11 | } 12 | local buffers 13 | 14 | before_each(function() 15 | buffers = {} 16 | for index, win_conf in ipairs(win_configs) do 17 | local buf = api.nvim_create_buf(true, true) 18 | api.nvim_buf_set_name(buf, win_conf.id) 19 | buffers[index] = function() 20 | return buf 21 | end 22 | end 23 | 24 | layout = windows.area_layout(10, "right", win_configs, buffers) 25 | layout:open() 26 | end) 27 | 28 | after_each(function() 29 | for _, get_buf in ipairs(buffers) do 30 | vim.api.nvim_buf_delete(get_buf(), { force = true }) 31 | end 32 | layout:close() 33 | end) 34 | 35 | it("opens all windows", function() 36 | for _, win_config in pairs(win_configs) do 37 | assert.Not.equal(-1, vim.fn.bufwinnr(win_config.id)) 38 | end 39 | end) 40 | 41 | it("sizes area correctly", function() 42 | assert.equal(10, api.nvim_win_get_width(vim.fn.bufwinid(win_configs[1].id))) 43 | end) 44 | 45 | it("sizes windows correctly", function() 46 | local total_size = 0 47 | local heights = {} 48 | for _, win_config in pairs(win_configs) do 49 | local win = vim.fn.bufwinid(win_config.id) 50 | heights[win_config.id] = api.nvim_win_get_height(win) 51 | total_size = total_size + heights[win_config.id] 52 | end 53 | 54 | for i, win_config in ipairs(win_configs) do 55 | assert.equal(util.round(total_size * win_configs[i].size), heights[win_config.id]) 56 | end 57 | end) 58 | 59 | it("closes all windows", function() 60 | layout:close() 61 | for _, win_config in pairs(win_configs) do 62 | assert.equal(-1, vim.fn.bufwinid(win_config.id)) 63 | end 64 | end) 65 | 66 | it("retains sizes on close", function() 67 | local total_size = 0 68 | local heights = {} 69 | vim.api.nvim_win_set_height(vim.fn.bufwinid(win_configs[1].id), 1) 70 | vim.api.nvim_win_set_width(vim.fn.bufwinid(win_configs[1].id), 20) 71 | for _, win_config in pairs(win_configs) do 72 | local win = vim.fn.bufwinid(win_config.id) 73 | heights[win_config.id] = api.nvim_win_get_height(win) 74 | total_size = total_size + heights[win_config.id] 75 | end 76 | layout:update_sizes() 77 | layout:close() 78 | layout:open() 79 | assert.equal(20, api.nvim_win_get_width(vim.fn.bufwinid(win_configs[1].id))) 80 | for _, win_config in pairs(win_configs) do 81 | local win = vim.fn.bufwinid(win_config.id) 82 | assert.equal(heights[win_config.id], api.nvim_win_get_height(win)) 83 | end 84 | end) 85 | end) 86 | --------------------------------------------------------------------------------