├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml └── workflows │ ├── format.yml │ ├── release.yml │ └── website.yml ├── .gitignore ├── .stylua.toml ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── .markdownlint.yaml ├── .npmrc ├── .prettierrc ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src │ ├── app.css │ ├── app.d.ts │ ├── app.html │ ├── lib │ │ ├── components │ │ │ ├── Header.svelte │ │ │ ├── Sidebar.svelte │ │ │ └── custom │ │ │ │ ├── img.svelte │ │ │ │ └── index.ts │ │ ├── config.ts │ │ └── types.ts │ ├── mdsvex.svelte │ ├── posts │ │ ├── acknowlegdgements.md │ │ ├── api.md │ │ ├── automatic-toggle.md │ │ ├── basics.md │ │ ├── commands.md │ │ ├── configuration.md │ │ ├── control-bar.md │ │ ├── faq.md │ │ ├── filetypes-autocmds.md │ │ ├── hide-terminal.md │ │ ├── highlight-groups.md │ │ ├── home.md │ │ ├── keymaps.md │ │ └── known-issues.md │ └── routes │ │ ├── +error.svelte │ │ ├── +layout.svelte │ │ ├── +layout.ts │ │ ├── +page.server.ts │ │ ├── [slug] │ │ ├── +page.svelte │ │ └── +page.ts │ │ └── api │ │ └── posts │ │ └── +server.ts ├── static │ └── fonts │ │ └── ZedMonoNF.woff2 ├── svelte.config.js ├── tsconfig.json └── vite.config.ts ├── lua ├── dap-view.lua └── dap-view │ ├── actions.lua │ ├── autocmds.lua │ ├── breakpoints │ ├── actions.lua │ ├── util │ │ ├── extmarks.lua │ │ └── treesitter.lua │ ├── vendor │ │ └── init.lua │ └── view.lua │ ├── complete.lua │ ├── config.lua │ ├── events.lua │ ├── exceptions │ ├── actions.lua │ ├── init.lua │ └── view.lua │ ├── globals.lua │ ├── guard.lua │ ├── highlight.lua │ ├── options │ ├── autocmd.lua │ ├── controls.lua │ └── winbar.lua │ ├── scopes │ └── view.lua │ ├── setup │ ├── init.lua │ └── validate │ │ ├── help.lua │ │ ├── init.lua │ │ ├── util.lua │ │ ├── winbar.lua │ │ └── windows.lua │ ├── state.lua │ ├── term │ ├── init.lua │ └── options.lua │ ├── threads │ ├── actions.lua │ ├── init.lua │ └── view.lua │ ├── tree │ └── traversal.lua │ ├── types.lua │ ├── util │ ├── exprs.lua │ ├── hl.lua │ ├── init.lua │ └── statusline.lua │ ├── views │ ├── init.lua │ ├── keymaps │ │ ├── docs.lua │ │ ├── init.lua │ │ └── util.lua │ ├── options.lua │ ├── util.lua │ └── windows │ │ ├── init.lua │ │ └── switchbuf.lua │ └── watches │ ├── actions.lua │ ├── eval.lua │ ├── set.lua │ └── view.lua └── plugin └── dap-view.lua /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: igorlfs 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug/issue 3 | title: "bug: " 4 | labels: [bug] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | **Before** reporting an issue, make sure to read the documentation and search existing issues. Usage questions such as ***"How do I...?"*** belong in Discussions and will be closed. 10 | - type: checkboxes 11 | attributes: 12 | label: Did you check docs and existing issues? 13 | description: Make sure you checked all of the below before submitting an issue 14 | options: 15 | - label: I have read all the plugin docs 16 | required: true 17 | - label: I have searched the existing issues 18 | required: true 19 | - label: I have searched the existing issues of plugins related to this issue 20 | required: true 21 | - type: input 22 | attributes: 23 | label: "Neovim version (nvim -v)" 24 | placeholder: "0.8.0 commit db1b0ee3b30f" 25 | validations: 26 | required: true 27 | - type: input 28 | attributes: 29 | label: "Operating system/version" 30 | placeholder: "MacOS 11.5" 31 | validations: 32 | required: true 33 | - type: textarea 34 | attributes: 35 | label: Describe the bug 36 | description: A clear and concise description of what the bug is. Please include any related errors you see in Neovim. 37 | validations: 38 | required: true 39 | - type: textarea 40 | attributes: 41 | label: Steps To Reproduce 42 | description: Steps to reproduce the behavior. 43 | placeholder: | 44 | 1. 45 | 2. 46 | 3. 47 | validations: 48 | required: true 49 | - type: textarea 50 | attributes: 51 | label: Expected Behavior 52 | description: A concise description of what you expected to happen. 53 | validations: 54 | required: true 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature 3 | title: "feature: " 4 | labels: [enhancement] 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Did you check the docs? 9 | description: Make sure you read all the docs before submitting a feature request 10 | options: 11 | - label: I have read all the docs 12 | required: true 13 | - type: textarea 14 | validations: 15 | required: true 16 | attributes: 17 | label: Is your feature request related to a problem? Please describe. 18 | description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 19 | - type: textarea 20 | validations: 21 | required: true 22 | attributes: 23 | label: Describe the solution you'd like 24 | description: A clear and concise description of what you want to happen. 25 | - type: textarea 26 | validations: 27 | required: true 28 | attributes: 29 | label: Describe alternatives you've considered 30 | description: A clear and concise description of any alternative solutions or features you've considered. 31 | - type: textarea 32 | validations: 33 | required: false 34 | attributes: 35 | label: Additional context 36 | description: Add any other context or screenshots about the feature request here. 37 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | --- 2 | on: 3 | push: 4 | branches-ignore: 5 | - "gh-pages" 6 | pull_request: 7 | 8 | name: format 9 | 10 | jobs: 11 | stylua: 12 | name: stylua 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: JohnnyMorganz/stylua-action@v3 17 | with: 18 | version: latest 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | args: --color always --check lua 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release To GitHub 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | 13 | jobs: 14 | release: 15 | name: Release To GitHub 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: googleapis/release-please-action@v4 19 | with: 20 | token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} 21 | release-type: simple 22 | -------------------------------------------------------------------------------- /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | --- 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | 8 | name: Publish Website 9 | 10 | jobs: 11 | publish: 12 | name: Publish Website 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 22 20 | - name: Install dependencies 21 | run: npm install 22 | working-directory: ./docs 23 | - name: Build site 24 | run: npm run build 25 | working-directory: ./docs 26 | - name: Deploy with gh-pages 27 | run: | 28 | git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git 29 | npx gh-pages -d build --dotfiles --nojekyll -u "github-actions-bot " 30 | working-directory: ./docs 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/plenary.nvim 2 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 110 2 | line_endings = "Unix" 3 | indent_type = "Spaces" 4 | indent_width = 4 5 | quote_style = "AutoPreferDouble" 6 | no_call_parentheses = false 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ellison 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-view 2 | 3 | > minimalistic [nvim-dap-ui](https://github.com/rcarriga/nvim-dap-ui) alternative 4 | 5 | 6 | 7 | ## Installation 8 | 9 | > [!WARNING] 10 | > **Requires neovim 0.11+** 11 | 12 | ### Via lazy.nvim 13 | 14 | ```lua 15 | return { 16 | { 17 | "igorlfs/nvim-dap-view", 18 | ---@module 'dap-view' 19 | ---@type dapview.Config 20 | opts = {}, 21 | }, 22 | } 23 | ``` 24 | 25 | ## Features 26 | 27 | - Watch expressions 28 | - Manipulate breakpoints 29 | - Navigate in the call stack 30 | - Convenient wrapper around `nvim-dap` widgets (scopes) + REPL 31 | 32 | All of that in a unified, unintrusive window. 33 | 34 | ## Documentation 35 | 36 | Visit the full documentation on the [website](https://igorlfs.github.io/nvim-dap-view/home). 37 | 38 | ## Contributing 39 | 40 | You can contribute in many ways: 41 | 42 | - If you have any questions, create a [discussion](https://github.com/igorlfs/nvim-dap-view/discussions/new/choose). 43 | - If something isn't working, create a [bug report](https://github.com/igorlfs/nvim-dap-view/issues/new?template=bug_report.yml). 44 | - If you have an idea, file a [feature request](https://github.com/igorlfs/nvim-dap-view/issues/new?template=feature_request.yml). You can also go ahead and implement it yourself with a [PR](https://github.com/igorlfs/nvim-dap-view/compare). 45 | - If you have some spare bucks, consider [sponsoring](https://github.com/sponsors/igorlfs). 46 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | -------------------------------------------------------------------------------- /docs/.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | MD007: 2 | indent: 4 3 | -------------------------------------------------------------------------------- /docs/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /docs/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "printWidth": 100, 5 | "tabWidth": 4 6 | } 7 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dap-view-docs", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "deploy": "gh-pages -d build --dotfiles --nojekyll" 12 | }, 13 | "devDependencies": { 14 | "@sveltejs/adapter-static": "^3.0.8", 15 | "@sveltejs/kit": "^2.21.0", 16 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 17 | "@tailwindcss/typography": "^0.5.16", 18 | "gh-pages": "^6.3.0", 19 | "svelte": "^5.30.1", 20 | "svelte-check": "^4.2.1", 21 | "tailwindcss": "4.0.6", 22 | "tslib": "^2.8.1", 23 | "typescript": "^5.8.3", 24 | "vite": "^6.3.5" 25 | }, 26 | "type": "module", 27 | "dependencies": { 28 | "@tailwindcss/vite": "^4.1.7", 29 | "mdsvex": "^0.11.2", 30 | "rehype-autolink-headings": "^7.1.0", 31 | "rehype-slug": "^6.0.0", 32 | "remark-footnotes": "~2.0.0", 33 | "remark-unwrap-images": "^4.0.1", 34 | "shiki": "^2.5.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | ignoredBuiltDependencies: 2 | - '@tailwindcss/oxide' 3 | - esbuild 4 | -------------------------------------------------------------------------------- /docs/src/app.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap'); 2 | @import url("https://www.nerdfonts.com/assets/css/webfont.css"); 3 | 4 | @import 'tailwindcss'; 5 | 6 | @plugin '@tailwindcss/typography'; 7 | 8 | @theme { 9 | --color-*: initial; 10 | --color-primary: #b4befe; 11 | --color-accent: #89b4fa; 12 | --color-text: #cdd6f4; 13 | --color-subtext: #a6adc8; 14 | --color-lavender: #b4befe; 15 | --color-base: #1e1e2e; 16 | --color-mantle: #181825; 17 | --color-crust: #11111b; 18 | --color-blue: #89b4fa; 19 | 20 | --font-sans: Roboto; 21 | --font-mono: ZedMonoNF; 22 | } 23 | 24 | @font-face { 25 | font-family: ZedMonoNF; 26 | src: url("/fonts/ZedMonoNF.woff2") format("woff2"); 27 | } 28 | 29 | :root { 30 | --primary-color: #89b4fa; 31 | --accent-color: #b4befe; 32 | --dark-background-color: #11111b; 33 | --text-color: #cdd6f4; 34 | } 35 | 36 | .prose { 37 | color: var(--text-color); 38 | } 39 | 40 | .prose :is(h1, h2, h3, h4, h5, h6) { 41 | margin-top: 12px; 42 | margin-bottom: 8px; 43 | text-align: justify; 44 | text-justify: inter-word; 45 | } 46 | 47 | .prose p:not(:is(h2, h3, h4, h5, h6) + p) { 48 | margin-top: 20px; 49 | } 50 | 51 | .prose p { 52 | text-align: justify; 53 | text-justify: inter-word; 54 | } 55 | 56 | .prose code { 57 | background: var(--dark-background-color); 58 | color: var(--text-color); 59 | border-radius: 6px; 60 | } 61 | 62 | .prose pre { 63 | background: var(--dark-background-color) !important; 64 | } 65 | 66 | .prose strong { 67 | color: var(--text-color) !important; 68 | font-weight: 900; 69 | } 70 | 71 | .prose hr { 72 | border-color: var(--dark-background-color); 73 | } 74 | 75 | .prose blockquote { 76 | color: var(--accent-color) 77 | } 78 | 79 | .prose li { 80 | margin: 0px; 81 | } 82 | 83 | .prose th { 84 | color: var(--accent-color); 85 | } 86 | 87 | .prose tr { 88 | border: none 89 | } 90 | 91 | .prose ::marker { 92 | color: var(--accent-color) 93 | } 94 | 95 | .prose thead { 96 | border: none none solid none; 97 | border-color: var(--dark-background-color) 98 | } 99 | 100 | a:link { 101 | font-weight: bold; 102 | color: var(--accent-color); 103 | text-decoration: none; 104 | font-weight: 900; 105 | } 106 | 107 | a:hover { 108 | font-weight: bold; 109 | color: var(--primary-color); 110 | font-weight: 900; 111 | } 112 | 113 | .prose img { 114 | display: block; 115 | margin-left: auto; 116 | margin-right: auto; 117 | } 118 | -------------------------------------------------------------------------------- /docs/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /docs/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %sveltekit.head% 9 | 10 | 11 | 12 |
%sveltekit.body%
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/src/lib/components/Header.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /docs/src/lib/components/Sidebar.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 |
22 |
23 |
24 | {#each topLevelPosts as post} 25 | 30 | {/each} 31 |
32 |
33 | {#each postsGroupedByCategory as { posts, category, collapsed }, i} 34 |
35 | 45 |
46 | {#if !collapsed} 47 |
48 | {#each posts as post} 49 | 54 | {/each} 55 |
56 | {/if} 57 | {/each} 58 |
59 |
60 |
61 |
62 | -------------------------------------------------------------------------------- /docs/src/lib/components/custom/img.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/src/lib/components/custom/index.ts: -------------------------------------------------------------------------------- 1 | import img from './img.svelte'; 2 | export { img }; 3 | -------------------------------------------------------------------------------- /docs/src/lib/config.ts: -------------------------------------------------------------------------------- 1 | export const title = 'NVIM DAP View' 2 | -------------------------------------------------------------------------------- /docs/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from "svelte"; 2 | 3 | export const CATEGORIES = ['Recipes'] as const; 4 | type Category = (typeof CATEGORIES)[number]; 5 | 6 | export type PostMetadata = { 7 | title: string; 8 | slug: string; 9 | category?: Category 10 | hidden?: boolean 11 | }; 12 | 13 | export type Post = { default: Component; metadata: PostMetadata } 14 | -------------------------------------------------------------------------------- /docs/src/mdsvex.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | {@render children()} 12 | -------------------------------------------------------------------------------- /docs/src/posts/acknowlegdgements.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Acknowledgements 3 | --- 4 | 5 | - [nvim-dap-ui](https://github.com/rcarriga/nvim-dap-ui) is obviously a huge inspiration! 6 | - Code to inject treesitter highlights into line is taken from [quicker.nvim](https://github.com/stevearc/quicker.nvim) 7 | - [lucaSartore](https://github.com/lucaSartore/nvim-dap-exception-breakpoints) for the inspiration for handling exceptions breakpoint 8 | - [Kulala](https://github.com/mistweaverco/kulala.nvim) for the creative usage of neovim's `'winbar'` to handle multiple "views" 9 | - [blink.cmp](https://github.com/Saghen/blink.cmp/blob/main/lua/blink/cmp/config/utils.lua) for the config validation (which is partially taken from a PR to [indent-blankline](https://github.com/lukas-reineke/indent-blankline.nvim/pull/934/files#diff-09ebcaa8c75cd1e92d25640e377ab261cfecaf8351c9689173fd36c2d0c23d94R16)) 10 | -------------------------------------------------------------------------------- /docs/src/posts/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: API 3 | --- 4 | 5 | The API offer the same functionality as the [commands](commands). 6 | 7 | ```lua 8 | require("dap-view").open() 9 | ``` 10 | 11 | Opens both `nvim-dap-view` windows[^1]: views + console. 12 | 13 | --- 14 | 15 | ```lua 16 | ---@param hide_terminal? boolean 17 | require("dap-view").close() 18 | ``` 19 | 20 | Closes the views window. Can also hide the terminal window if specified. 21 | 22 | --- 23 | 24 | ```lua 25 | ---@param hide_terminal? boolean 26 | require("dap-view").toggle() 27 | ``` 28 | 29 | Calls `require("dap-view").open()` if there's no views window. Else, behaves like `require("dap-view").close()` 30 | 31 | --- 32 | 33 | ```lua 34 | ---@param expr? string 35 | require("dap-view").add_expr() 36 | ``` 37 | 38 | In normal mode, adds the expression under the cursor to the watch list (see [caveats](faq#dapviewwatch-isnt-adding-the-whole-variable)). In visual mode, adds the selection to the watch list. If `expr` is specified, adds the expression directly. 39 | 40 | --- 41 | 42 | ```lua 43 | ---@param view "breakpoints" | "exceptions" | "watches" | "repl" | "threads" | "console" | "scopes" 44 | require("dap-view").jump_to_view(view) 45 | ``` 46 | 47 | Shows a given view and jumps to its window. 48 | 49 | --- 50 | 51 | ```lua 52 | ---@param view "breakpoints" | "exceptions" | "watches" | "repl" | "threads" | "console" | "scopes" 53 | require("dap-view").show_view(view) 54 | ``` 55 | 56 | Shows a given view. If the specified view is already the current one, jumps to its window. 57 | 58 | [^1]: In the current tab. May close the views window in another tab. 59 | -------------------------------------------------------------------------------- /docs/src/posts/automatic-toggle.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Automatic Toggle 3 | category: Recipes 4 | --- 5 | 6 | If you find yourself constantly toggling `nvim-dap-view` once a session starts, and then closing it when the session ends, you might want to add the following snippet to your configuration, to do that automatically: 7 | 8 | ```lua 9 | local dap, dv = require("dap"), require("dap-view") 10 | dap.listeners.before.attach["dap-view-config"] = function() 11 | dv.open() 12 | end 13 | dap.listeners.before.launch["dap-view-config"] = function() 14 | dv.open() 15 | end 16 | dap.listeners.before.event_terminated["dap-view-config"] = function() 17 | dv.close() 18 | end 19 | dap.listeners.before.event_exited["dap-view-config"] = function() 20 | dv.close() 21 | end 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/src/posts/basics.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basics 3 | hidden: true 4 | --- 5 | 6 | Getting started with `nvim-dap` is easier than it sounds! This guide aims to explain all you need to know. 7 | 8 | First things first: what is the DAP? Much like the LSP, the DAP is a protocol created to solve a scalability problem: it used to be the case that each text editor had to have a custom integration for each debugger they wanted to support. That means handling the communication was a nightmare: each debugger has its own way of defining breakpoints, or evaluating expressions and whatnot. What DAP brings to the table is a standardization for this communication, massively simplifying the implementation. 9 | 10 | To accomplish its goal, the DAP introduces the concept of **debug adapters**: programs that make debuggers comply with the protocol (in fact, many debuggers actually support the protocol natively, such as `gdb`). The first step (after installing the plugin) to setup `nvim-dap` is choosing an adapter, which will depend on the language you're using. You can install an adapter with your system's package manager (or, most likely, using [mason.nvim](https://github.com/mason-org/mason.nvim)). To give some concrete examples, this guides picks `codelldb`: [a powerful adapter](https://github.com/vadimcn/codelldb) which can be used for C, C++ and Rust. Under the hood, `codelldb` (the adapter) uses `lldb` (the debugger). To configure `codelldb` (or any adapter, for that matter) refer to `nvim-dap`'s [wiki](https://codeberg.org/mfussenegger/nvim-dap/wiki/Debug-Adapter-installation). There, we can find a [snippet](https://codeberg.org/mfussenegger/nvim-dap/wiki/C-C---Rust-(via--codelldb)#1-11-0-and-later) to define `codelldb`: 11 | 12 | ```lua 13 | require("dap").adapters.codelldb = { 14 | type = "executable", 15 | command = "codelldb", -- or if not in $PATH: "/absolute/path/to/codelldb" 16 | 17 | -- On windows you may have to uncomment this: 18 | -- detached = false, 19 | } 20 | ``` 21 | 22 | Fantastic! Now we have to define a way for the adapter to actually connect with the code we're trying to debug. That's what's known as a "configuration". `nvim-dap` offers it's own way of defining configurations, but my recommendation is to use a `.vscode/launch.json` file, which `nvim-dap` supports natively, out of the box. The main advantage of this approach is that your colleagues will be able to use the configuration as well! 23 | 24 | The configuration, however, can be a bit tricky. There are some base options, but most adapters introduce their own flags. Again, you can use the `nvim-dap` [wiki](https://codeberg.org/mfussenegger/nvim-dap/wiki/Debug-Adapter-installation) as reference, but you may need to also check your adapter's documentation as well. Here's a basic configuration to launch a C++ program using `codelldb`: 25 | 26 | ```jsonc 27 | { 28 | "version": "0.2.0", 29 | "configurations": [ 30 | { 31 | // Base options 32 | "name": "Launch My Cool Project", // Actually not that important, could be anything 33 | "type": "codelldb", // Must match the adapter we defined earlier 34 | "request": "launch", // Could be either launch or attach 35 | 36 | // Adapter specific options 37 | "program": "${workspaceFolder}/path/to/your/binary" // Path for the program that will be launched 38 | // In spite of not being a base option, "program" is fairly common for launch requests 39 | } 40 | ] 41 | } 42 | ``` 43 | 44 | Great! The tricky part is over! Now all you have to do is configure `nvim-dap` like any other plugin: get to know the commands and define some keybindings (take a look at [my config](https://github.com/igorlfs/dotfiles/blob/main/nvim/.config/nvim/lua/plugins/bare/nvim-dap.lua) if you need inspiration). Refer to `:h dap-user-commands` to learn what you can do. 45 | 46 | The last step is to test your setup. Remember to compile your program with debug symbols if necessary[^1]. Create a breakpoint with `:DapToggleBreakpoint` (or using your custom keymap) and then start a debugging session with `:DapContinue`. If all goes well, the execution will be stopped when hitting the line with the breakpoint. 47 | 48 | Hooray! Now you can start tweaking `nvim-dap-view`! 🎉 49 | 50 | [^1]: The path of the binary with debug symbols must match the path of the `program` defined in the configuration. 51 | -------------------------------------------------------------------------------- /docs/src/posts/commands.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Commands 3 | --- 4 | 5 | Commands offer the same functionality as the [API](api). 6 | 7 | ## `DapViewOpen` 8 | 9 | Opens both `nvim-dap-view` windows[^1]: views + console. 10 | 11 | ## `DapViewClose` 12 | 13 | Closes the views window. Accepts a bang (i.e., `DapViewClose!`) to also hide the terminal window. 14 | 15 | ## `DapViewToggle` 16 | 17 | Behaves like `DapViewOpen` if there's no views window. Else behaves like `DapViewClose` (also accepts a bang to behave like `DapViewClose!`). 18 | 19 | ## `DapViewWatch` 20 | 21 | In normal mode, adds the expression under the cursor to the watch list (see [caveats](faq#dapviewwatch-isnt-adding-the-whole-variable)). In visual mode, adds the selection to the watch list. Also accepts adding an expression directly (i.e., `:DapViewWatch foo + bar`), which takes precedence. 22 | 23 | ## `DapViewJump [view]` 24 | 25 | Shows a given view and jumps to its window. For instance, to jump to the REPL, you can use `:DapViewJump repl`. 26 | 27 | ## `DapViewShow [view]` 28 | 29 | Shows a given view. If the specified view is already the current one, jumps to its window. 30 | 31 | [^1]: In the current tab. May close the views window in another tab. 32 | -------------------------------------------------------------------------------- /docs/src/posts/configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Configuration 3 | --- 4 | 5 | ## Defaults 6 | 7 | These are the default options from `nvim-dap-view`. You can use them as reference. You don't have to copy-paste them! 8 | 9 | ```lua 10 | return { 11 | winbar = { 12 | show = true, 13 | -- You can add a "console" section to merge the terminal with the other views 14 | sections = { "watches", "scopes", "exceptions", "breakpoints", "threads", "repl" }, 15 | -- Must be one of the sections declared above 16 | default_section = "watches", 17 | headers = { 18 | breakpoints = "Breakpoints [B]", 19 | scopes = "Scopes [S]", 20 | exceptions = "Exceptions [E]", 21 | watches = "Watches [W]", 22 | threads = "Threads [T]", 23 | repl = "REPL [R]", 24 | console = "Console [C]", 25 | }, 26 | controls = { 27 | enabled = false, 28 | position = "right", 29 | buttons = { 30 | "play", 31 | "step_into", 32 | "step_over", 33 | "step_out", 34 | "step_back", 35 | "run_last", 36 | "terminate", 37 | "disconnect", 38 | }, 39 | custom_buttons = {}, 40 | icons = { 41 | pause = "", 42 | play = "", 43 | step_into = "", 44 | step_over = "", 45 | step_out = "", 46 | step_back = "", 47 | run_last = "", 48 | terminate = "", 49 | disconnect = "", 50 | }, 51 | }, 52 | }, 53 | windows = { 54 | height = 12, 55 | position = "below", 56 | terminal = { 57 | position = "left", 58 | width = 0.5, 59 | -- List of debug adapters for which the terminal should be ALWAYS hidden 60 | hide = {}, 61 | -- Hide the terminal when starting a new session 62 | start_hidden = false, 63 | }, 64 | }, 65 | help = { 66 | border = nil, 67 | }, 68 | -- Controls how to jump when selecting a breakpoint or navigating the stack 69 | switchbuf = "usetab,newtab", 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/src/posts/control-bar.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Control Bar 3 | --- 4 | 5 | The control bar is disabled by default. It can be enabled by setting `winbar.controls.enable`. 6 | 7 | control bar 8 | 9 | ## Options 10 | 11 | Default options are listed below. Remeber, you don't have to copy-paste them! 12 | 13 | ```lua 14 | return { 15 | winbar = { 16 | controls = { 17 | enabled = false, 18 | position = "right", 19 | buttons = { 20 | "play", 21 | "step_into", 22 | "step_over", 23 | "step_out", 24 | "step_back", 25 | "run_last", 26 | "terminate", 27 | "disconnect", 28 | }, 29 | custom_buttons = {}, 30 | icons = { 31 | pause = "", 32 | play = "", 33 | step_into = "", 34 | step_over = "", 35 | step_out = "", 36 | step_back = "", 37 | run_last = "", 38 | terminate = "", 39 | disconnect = "", 40 | }, 41 | }, 42 | }, 43 | } 44 | ``` 45 | 46 | ## Custom Buttons 47 | 48 | `nvim-dap-view` provides some default buttons for the control bar, but you can also add your own. To do that, you can use the `controls.custom_buttons` table to declare your new button and then add it at the position you want in the `buttons` list. 49 | 50 | A custom button has 2 methods: 51 | 52 | 1. `render` returning a string used to display the button (typically an emoji or a NerdFont glyph wrapped in an highlight group) 53 | 2. `action` a function that will be executed when the button is clicked. The function receives 3 arguments: 54 | - `clicks` the number of clicks 55 | - `button` the button clicked (`l`, `r`, `m`) 56 | - `modifiers` a string with the modifiers pressed (`c` for `control`, `s` for `shift`, `a` for `alt` and `m` for `meta`) 57 | 58 | See the `@ N` section in `:help statusline` for the complete specifications of a click handler. 59 | 60 | ### Example 61 | 62 | An example adding 2 buttons: 63 | 64 | - `fun`: the most basic button possible, just prints "🎊" when clicked 65 | - `term_restart`: an hybrid button that acts as a stop/restart button. If the stop button is triggered by anything else than a single left click (middle click, right click, double click or click with a modifier), it will disconnect the session instead. 66 | 67 | ```lua 68 | return { 69 | winbar = { 70 | controls = { 71 | enabled = true, 72 | buttons = { "play", "step_into", "step_over", "step_out", "term_restart", "fun" }, 73 | custom_buttons = { 74 | fun = { 75 | render = function() 76 | return "🎉" 77 | end, 78 | action = function() 79 | vim.print("🎊") 80 | end, 81 | }, 82 | -- Stop/Restart button 83 | -- Double click, middle click or click with a modifier disconnect instead of stop 84 | term_restart = { 85 | render = function() 86 | local session = require("dap").session() 87 | local group = session and "ControlTerminate" or "ControlRunLast" 88 | local icon = session and "" or "" 89 | return "%#NvimDapView" .. group .. "#" .. icon .. "%*" 90 | end, 91 | action = function(clicks, button, modifiers) 92 | local dap = require("dap") 93 | local alt = clicks > 1 or button ~= "l" or modifiers:gsub(" ", "") ~= "" 94 | if not dap.session() then 95 | dap.run_last() 96 | elseif alt then 97 | dap.disconnect() 98 | else 99 | dap.terminate() 100 | end 101 | end, 102 | }, 103 | }, 104 | }, 105 | }, 106 | } 107 | ``` 108 | -------------------------------------------------------------------------------- /docs/src/posts/faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: FAQ 3 | --- 4 | 5 | ## Why add `nvim-dap-view` as a dependency for `nvim-dap`? 6 | 7 | By default, when launching a new session, `nvim-dap`'s terminal window takes half the screen. As a saner default, `nvim-dap-view` hijacks the terminal window (even if not invoked), making the split take only 12 lines (a setting which is of course, configurable). See [this](https://github.com/rcarriga/nvim-dap-ui/issues/407) related issue (from `nvim-dap-ui`). Of course, this workaround only works if `nvim-dap-view` is loaded when a session starts. 8 | 9 | ```lua 10 | -- Your nvim-dap config 11 | return { 12 | { 13 | "mfussenegger/nvim-dap", 14 | dependencies = { 15 | { "igorlfs/nvim-dap-view", opts = {} }, 16 | ..., 17 | }, 18 | ..., 19 | }, 20 | } 21 | ``` 22 | 23 | ## `DapViewWatch` isn't adding the whole variable 24 | 25 | In normal mode, `:DapViewWatch` expands the `` under the cursor (see `:h `). By default, this setting works really well for C-like languages, but it can be cumbersome for other languages. To handle that, you can tweak the value for the `'iskeyword'` option (see `:h 'iskeyword'`). For instance, with PHP, you can use `set iskeyword+=$`. 26 | 27 | ## How to control which window will be used when jumping to a breakpoint or a call in the stack? 28 | 29 | `nvim-dap-view` ships its own `switchbuf` (see `:h 'switchbuf'`), which supports a subset of neovim's built-in options: `newtab`, `useopen`, `usetab` and `uselast`. You can customize it with: 30 | 31 | ```lua 32 | return { 33 | switchbuf = "useopen", 34 | } 35 | ``` 36 | 37 | You can use a commas to define fallback behavior (e.g., `"useopen,newtab"` creates a new tab if the buffer is not found). 38 | 39 | ## `nvim-dap` is overriding one of the `nvim-dap-view`'s windows on stop 40 | 41 | When `windows.terminal.position` is `right` the views window may be used to display the current frame, because `nvim-dap` has its own internal `switchbuf` setting (see `:h 'switchbuf'`), which defaults to the global `switchbuf` option. A common solution is to set `nvim-dap`'s `switchbuf` to another value. For instance: 42 | 43 | ```lua 44 | -- Don't change focus if the window is visible, otherwise jump to the first window (from any tab) containing the buffer 45 | -- If no window contains the buffer, create a new tab 46 | require("dap").defaults.fallback.switchbuf = "usevisible,usetab,newtab" -- See :h dap-defaults to learn more 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/src/posts/filetypes-autocmds.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Filetypes & Autocommands 3 | --- 4 | 5 | `nvim-dap-view` sets the following filetypes: 6 | 7 | | Buffer | Filetype | 8 | | ------------------------------------------------- | ------------- | 9 | | Breakpoints, Exceptions, Scopes, Threads, Watches | dap-view | 10 | | Terminal | dap-view-term | 11 | | Help | dap-view-help | 12 | 13 | They can be used to override buffer and window options set by `nvim-dap-view`. 14 | 15 | If the REPL is enabled, the `dap-repl` filetype (which is set by `nvim-dap`) is also used. **If you wish to consistently override the plugin's behavior, be sure to also include the `dap-repl` filetype** in your autocommand. 16 | 17 | ## Example autocommand 18 | 19 | Map q to quit in `nvim-dap-view` filetypes: 20 | 21 | ```lua 22 | vim.api.nvim_create_autocmd({ "FileType" }, { 23 | pattern = { "dap-view", "dap-view-term", "dap-repl" }, -- dap-repl is set by `nvim-dap` 24 | callback = function(evt) 25 | vim.keymap.set("n", "q", "q", { buffer = evt.buf }) 26 | end, 27 | }) 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/src/posts/hide-terminal.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hide Terminal 3 | category: Recipes 4 | --- 5 | 6 | Some debug adapters don't use the integrated terminal (console). To avoid having a completely useless window lying around, you can hide the terminal for them. To achieve that, add the following snippet to your `nvim-dap-view` setup: 7 | 8 | ```lua 9 | return { 10 | windows = { 11 | terminal = { 12 | -- Use the actual names for the adapters you want to hide 13 | hide = { "go" }, -- `go` is known to not use the terminal. 14 | }, 15 | }, 16 | } 17 | ``` 18 | 19 | ## Anchoring 20 | 21 | In some scenarios, it's useful to use another window as if it was `nvim-dap-view`'s terminal. One such scenario is when using the `delve` adapter for Go (more specifically, coupled with an `attach` request): the window with the terminal that launched `dlv` can act as if it was the `nvim-dap-view`'s terminal window. By doing that, `nvim-dap-view`'s main window will "follow" `delve`'s window (i.e., `nvim-dap-view`'s main window will open by the side of `delve`'s window). Watch this [video](https://github.com/user-attachments/assets/5dce4b3d-fc01-4be6-9a72-b0f969e34b14) for context. 22 | 23 | To achieve that, in addition to hidding the terminal for `delve` (see above), you have to create your own `anchor` function that returns a window number (or `nil`). If `nil` is returned, there's a fallback to the default behavior. Here's a simple function you can use: 24 | 25 | ```lua 26 | return { 27 | windows = { 28 | anchor = function() 29 | -- Anchor to the first terminal window found in the current tab 30 | -- Tweak according to your needs 31 | local windows = vim.api.nvim_tabpage_list_wins(0) 32 | 33 | for _, win in ipairs(windows) do 34 | local bufnr = vim.api.nvim_win_get_buf(win) 35 | if vim.bo[bufnr].buftype == "terminal" then 36 | return win 37 | end 38 | end 39 | end, 40 | }, 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/src/posts/highlight-groups.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Highlight Groups 3 | --- 4 | 5 | `nvim-dap-view` defines 29 highlight groups linked to (somewhat) reasonable defaults, but they may look odd with your colorscheme. If the links aren't defined, no highlighting will be applied. To fix that, you have to manually define the highlight groups (see `:h nvim_set_hl()`). Consider contributing to your colorscheme by sending a PR to add support to `nvim-dap-view`! 6 | 7 | | Highlight Group | Default Link | 8 | | ------------------------------------ | ------------------------- | 9 | | `NvimDapViewBoolean` | Boolean | 10 | | `NvimDapViewControlDisconnect` | DapBreakpoint | 11 | | `NvimDapViewControlNC` | Comment | 12 | | `NvimDapViewControlPause` | Boolean | 13 | | `NvimDapViewControlPlay` | Keyword | 14 | | `NvimDapViewControlRunLast` | Keyword | 15 | | `NvimDapViewControlStepBack` | Function | 16 | | `NvimDapViewControlStepInto` | Function | 17 | | `NvimDapViewControlStepOut` | Function | 18 | | `NvimDapViewControlStepOver` | Function | 19 | | `NvimDapViewControlTerminate` | DapBreakpoint | 20 | | `NvimDapViewExceptionFilterDisabled` | DiagnosticError | 21 | | `NvimDapViewExceptionFilterEnabled` | DiagnosticOk | 22 | | `NvimDapViewFileName` | qfFileName | 23 | | `NvimDapViewFloat` | Float | 24 | | `NvimDapViewFunction` | Function | 25 | | `NvimDapViewLineNumber` | qfLineNr | 26 | | `NvimDapViewMissingData` | DapBreakpoint | 27 | | `NvimDapViewNumber` | Number | 28 | | `NvimDapViewSeparator` | Comment | 29 | | `NvimDapViewString` | String | 30 | | `NvimDapViewTabSelected` | TabLineSel | 31 | | `NvimDapViewTab` | TabLine | 32 | | `NvimDapViewThreadError` | DiagnosticError | 33 | | `NvimDapViewThreadStopped` | Conditional | 34 | | `NvimDapViewThread` | Tag | 35 | | `NvimDapViewWatchError` | DiagnosticError | 36 | | `NvimDapViewWatchExpr` | Identifier | 37 | | `NvimDapViewWatchUpdated` | DiagnosticVirtualTextWarn | 38 | -------------------------------------------------------------------------------- /docs/src/posts/home.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: NVIM DAP View 3 | --- 4 | 5 | > Modern, minimalistic debugging UI for neovim 6 | 7 | 12 | 13 | ## Installation 14 | 15 | **Requires neovim 0.11+** 16 | 17 | A nerd font is a soft requirement. 18 | 19 | ### Via lazy.nvim 20 | 21 | ```lua 22 | return { 23 | { 24 | "igorlfs/nvim-dap-view", 25 | ---@module 'dap-view' 26 | ---@type dapview.Config 27 | opts = {}, 28 | }, 29 | } 30 | ``` 31 | 32 | For a better experience, consider adding `nvim-dap-view` **as a dependency** for `nvim-dap`. [Why?](faq#why-add-nvim-dap-view-as-a-dependency-for-nvim-dap) 33 | 34 | If you're using a plugin that overrides the `'winbar'` option, make sure to disable it for `nvim-dap-view` [buffers](filetypes-autocmds). 35 | 36 | ## Features 37 | 38 | The plugin provides 7 "views" (aka "sections") that (mostly) share the same window (so there's clutter) 39 | 40 | ### Watches view 41 | 42 | watches view 43 | 44 | Shows a list of user defined expressions, that are evaluated by debug adapter 45 | 46 | - Basic CRUD operations for expressions 47 | - Expand, collapse, copy or set the value of expressions and variables 48 | 49 | ### Threads view 50 | 51 | threads view 52 | 53 | List all threads and their stack traces 54 | 55 | - Jump to a function in the call stack, switching the context to that call 56 | - Toggle `subtle` (hidden) frames 57 | 58 | ### Breakpoints view 59 | 60 | breakpoints view 61 | 62 | List all breakpoints with full syntax highlighting, including treesitter and semantic tokens 63 | 64 | ### Exceptions view 65 | 66 | exceptions view 67 | 68 | Control when the debugger should stop, outside of regular breakpoints (e.g., whenever an exception is thrown) 69 | 70 | ### Scopes view 71 | 72 | scopes view 73 | 74 | Use the scopes widget provided by nvim-dap 75 | 76 | ### REPL view 77 | 78 | repl view 79 | 80 | Use REPL provided by nvim-dap 81 | 82 | ### Console view 83 | 84 | You can also interact with the console (terminal), which is also provided by `nvim-dap`. By default, the console has its own window, but it can be configured to be shown with the other views. See details on the [config](configuration) page. 85 | 86 | The console's default height is resized to match your `nvim-dap-view` configuration. You can also either completely [hide](hide-terminal) it (if it's not being used at all, which is the case for some debug adapters) or hide it only during session initialization (which is nice when debugging tests, for instance). 87 | 88 | ### Control bar 89 | 90 | `nvim-dap-view` also provides a "non view" component: the control bar, which exposes some clickable buttons to control your session. It's disabled by default. See details on how to enable and configure it [here](control-bar). 91 | 92 | control bar 93 | 94 | ## Usage 95 | 96 | Learn about `nvim-dap-view`'s [commands](commands) and [keymaps](keymaps) to get started. For a more advanced setup, see the `Recipes` section in the sidebar. If, on the other hand, you need help getting started with `nvim-dap`, you can read the [guide](basics). 97 | 98 | ## Customization 99 | 100 | `nvim-dap-view` is fully customizable! Visit the [config](configuration) page to learn what you can do. 101 | -------------------------------------------------------------------------------- /docs/src/posts/keymaps.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Keymaps 3 | --- 4 | 5 | Each view has its own keymaps, listed below. At any time (from within `nvim-dap-view`'s main window) you can use `g?` to show a window that lists all of them. 6 | 7 | | Key | Action | 8 | | ------ | -------------------------------------------- | 9 | | **Threads** | 10 | | `` | Jump to a frame | 11 | | `t` | Toggle subtle frames | 12 | | **Scopes** | 13 | | `` | Expand or collapse a variable | 14 | | `o` | Trigger actions | 15 | | **Breakpoints** | 16 | | `` | Jump to a breakpoint | 17 | | `d` | Delete a breakpoint | 18 | | **Watches** | 19 | | `` | Expand or collapse a variable | 20 | | `i` | Insert an expression | 21 | | `d` | Delete an expression | 22 | | `e` | Edit an expression | 23 | | `c` | Copy the value of an expression or variable | 24 | | `s` | Set the value of an expression or variable | 25 | | **Exceptions** | 26 | | `` | Toggle filter | 27 | 28 | `nvim-dap-view` doesn't define any keybindings outside its own buffers. 29 | -------------------------------------------------------------------------------- /docs/src/posts/known-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Known Issues 3 | --- 4 | 5 | ## Limitations with the breakpoints view 6 | 7 | - Doesn't show breakpoint conditions 8 | - Isn't updated when there's no active session 9 | 10 | These are limitations with the current API from `nvim-dap`. A new API is [planned](https://github.com/mfussenegger/nvim-dap/issues/1388). 11 | -------------------------------------------------------------------------------- /docs/src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 |

{page.status}

8 | 9 |

{page.error?.message}

10 |
11 |
12 | -------------------------------------------------------------------------------- /docs/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |
11 |
12 |
13 | 14 |
15 |
16 | {@render children()} 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /docs/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /docs/src/routes/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect, type Load } from '@sveltejs/kit'; 2 | 3 | export const load: Load = async () => { 4 | redirect(301, 'home') 5 | }; 6 | -------------------------------------------------------------------------------- /docs/src/routes/[slug]/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | {metadata.title} 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 |

28 | {metadata.title} 29 |

30 |
31 | 32 |
33 |
34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /docs/src/routes/[slug]/+page.ts: -------------------------------------------------------------------------------- 1 | import type { Post, PostMetadata } from '$lib/types'; 2 | import { error } from '@sveltejs/kit'; 3 | import type { Load } from '@sveltejs/kit'; 4 | 5 | export const load: Load = async ({ params, fetch }) => { 6 | try { 7 | const post: Post = await import(`../../posts/${params.slug}.md`); 8 | 9 | const response = await fetch('api/posts'); 10 | const posts: PostMetadata[] = await response.json(); 11 | 12 | return { posts, post }; 13 | } catch { 14 | error(404, `Could not find ${params.slug}`); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /docs/src/routes/api/posts/+server.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | 3 | import type { PostMetadata } from '$lib/types'; 4 | import { json } from '@sveltejs/kit'; 5 | 6 | async function getPosts() { 7 | let posts: PostMetadata[] = []; 8 | 9 | const paths = import.meta.glob('/src/posts/*.md', { eager: true }); 10 | 11 | for (const path in paths) { 12 | const file = paths[path]; 13 | const slug = path.split('/').at(-1)?.replace('.md', ''); 14 | 15 | if (file && typeof file === 'object' && 'metadata' in file && slug) { 16 | const metadata = file.metadata as Omit; 17 | const post = { ...metadata, slug } satisfies PostMetadata; 18 | posts.push(post); 19 | } 20 | } 21 | 22 | return posts; 23 | } 24 | 25 | export async function GET() { 26 | const posts = await getPosts(); 27 | return json(posts); 28 | } 29 | -------------------------------------------------------------------------------- /docs/static/fonts/ZedMonoNF.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igorlfs/nvim-dap-view/25d8ef1a815946fd4563215ff57e2fc127fce68c/docs/static/fonts/ZedMonoNF.woff2 -------------------------------------------------------------------------------- /docs/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | import { mdsvex, escapeSvelte } from 'mdsvex'; 4 | import { getSingletonHighlighter } from 'shiki'; 5 | import remarkUnwrapImages from 'remark-unwrap-images'; 6 | import remarkFootNotes from 'remark-footnotes'; 7 | import rehypeAutolinkHeadings from 'rehype-autolink-headings'; 8 | import rehypeSlug from 'rehype-slug'; 9 | 10 | /** @type {import('mdsvex').MdsvexOptions} */ 11 | const mdsvexOptions = { 12 | extensions: ['.md'], 13 | layout: { 14 | _: './src/mdsvex.svelte' 15 | }, 16 | remarkPlugins: [remarkFootNotes, remarkUnwrapImages], 17 | rehypePlugins: [ 18 | rehypeSlug, 19 | [ 20 | rehypeAutolinkHeadings, 21 | { 22 | behavior: 'wrap' 23 | } 24 | ] 25 | ], 26 | highlight: { 27 | highlighter: async (code, lang = 'text') => { 28 | const highlighter = await getSingletonHighlighter({ 29 | themes: ['catppuccin-mocha'], 30 | langs: ['javascript', 'typescript', 'lua', 'jsonc'] 31 | }); 32 | await highlighter.loadLanguage('javascript', 'typescript', 'lua', 'jsonc'); 33 | const html = escapeSvelte( 34 | highlighter.codeToHtml(code, { lang, theme: 'catppuccin-mocha' }) 35 | ); 36 | return `{@html \`${html}\` }`; 37 | } 38 | } 39 | }; 40 | 41 | /** @type {import('@sveltejs/kit').Config} */ 42 | const config = { 43 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 44 | // for more information about preprocessors 45 | extensions: ['.svelte', '.md'], 46 | preprocess: [vitePreprocess(), mdsvex(mdsvexOptions)], 47 | 48 | kit: { 49 | adapter: adapter(), 50 | paths: { 51 | base: process.env.NODE_ENV === 'production' ? '/igorlfs.github.io/nvim-dap-view' : '' 52 | } 53 | } 54 | }; 55 | 56 | export default config; 57 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import tailwindcss from '@tailwindcss/vite'; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [sveltekit(), tailwindcss()], 7 | }); 8 | -------------------------------------------------------------------------------- /lua/dap-view.lua: -------------------------------------------------------------------------------- 1 | require("dap-view.highlight") 2 | require("dap-view.autocmds") 3 | -- Connect hooks to listen to DAP events 4 | require("dap-view.events") 5 | 6 | local actions = require("dap-view.actions") 7 | 8 | local M = {} 9 | 10 | ---@param config dapview.Config? 11 | M.setup = function(config) 12 | require("dap-view.setup").setup(config) 13 | end 14 | 15 | M.open = function() 16 | actions.open() 17 | end 18 | 19 | ---@param hide_terminal? boolean 20 | M.close = function(hide_terminal) 21 | actions.close(hide_terminal) 22 | end 23 | 24 | ---@param hide_terminal? boolean 25 | M.toggle = function(hide_terminal) 26 | actions.toggle(hide_terminal) 27 | end 28 | 29 | ---@param expr? string 30 | M.add_expr = function(expr) 31 | actions.add_expr(expr) 32 | end 33 | 34 | ---@param view dapview.SectionType 35 | M.jump_to_view = function(view) 36 | actions.jump_to_view(view) 37 | end 38 | 39 | ---@param view dapview.SectionType 40 | M.show_view = function(view) 41 | actions.show_view(view) 42 | end 43 | 44 | return M 45 | -------------------------------------------------------------------------------- /lua/dap-view/actions.lua: -------------------------------------------------------------------------------- 1 | local winbar = require("dap-view.options.winbar") 2 | local setup = require("dap-view.setup") 3 | local autocmd = require("dap-view.options.autocmd") 4 | local term = require("dap-view.term.init") 5 | local state = require("dap-view.state") 6 | local globals = require("dap-view.globals") 7 | 8 | local M = {} 9 | 10 | local api = vim.api 11 | 12 | ---@param hide_terminal? boolean 13 | M.toggle = function(hide_terminal) 14 | if state.winnr and api.nvim_win_is_valid(state.winnr) then 15 | M.close(hide_terminal) 16 | else 17 | M.open() 18 | end 19 | end 20 | 21 | ---@param hide_terminal? boolean 22 | M.close = function(hide_terminal) 23 | if state.winnr and api.nvim_win_is_valid(state.winnr) then 24 | api.nvim_win_close(state.winnr, true) 25 | end 26 | state.winnr = nil 27 | if state.bufnr and api.nvim_buf_is_valid(state.bufnr) then 28 | api.nvim_buf_delete(state.bufnr, { force = true }) 29 | end 30 | state.bufnr = nil 31 | if hide_terminal then 32 | term.hide_term_buf_win() 33 | end 34 | end 35 | 36 | M.open = function() 37 | M.close() 38 | 39 | local bufnr = api.nvim_create_buf(false, false) 40 | 41 | assert(bufnr ~= 0, "Failed to create nvim-dap-view buffer") 42 | 43 | state.bufnr = bufnr 44 | 45 | api.nvim_buf_set_name(bufnr, globals.MAIN_BUF_NAME) 46 | 47 | local separate_term_win = not vim.tbl_contains(setup.config.winbar.sections, "console") 48 | local term_winnr = separate_term_win and term.open_term_buf_win() 49 | 50 | local is_term_win_valid = term_winnr and api.nvim_win_is_valid(term_winnr) 51 | 52 | local windows_config = setup.config.windows 53 | 54 | local term_position = require("dap-view.util").inverted_directions[windows_config.terminal.position] 55 | 56 | local anchor_win = windows_config.anchor and windows_config.anchor() 57 | local is_anchor_win_valid = anchor_win and api.nvim_win_is_valid(anchor_win) 58 | 59 | local winnr = api.nvim_open_win(bufnr, false, { 60 | split = (is_anchor_win_valid or is_term_win_valid) and term_position or windows_config.position, 61 | win = is_anchor_win_valid and anchor_win or is_term_win_valid and term_winnr or -1, 62 | height = windows_config.height < 1 and math.floor(vim.go.lines * windows_config.height) 63 | or windows_config.height, 64 | }) 65 | 66 | assert(winnr ~= 0, "Failed to create nvim-dap-view window") 67 | 68 | state.winnr = winnr 69 | 70 | require("dap-view.views.options").set_options() 71 | require("dap-view.views.keymaps").set_keymaps() 72 | 73 | state.current_section = state.current_section or setup.config.winbar.default_section 74 | 75 | winbar.set_winbar_action_keymaps() 76 | winbar.show_content(state.current_section) 77 | 78 | -- Clean up states dap-view buffer is wiped out 79 | autocmd.quit_buf_autocmd(state.bufnr, function() 80 | -- The buffer is already being wiped out, so prevent close() from doing it again. 81 | state.bufnr = nil 82 | M.close() 83 | end) 84 | end 85 | 86 | ---@param expr? string 87 | M.add_expr = function(expr) 88 | local final_expr = expr or require("dap-view.util.exprs").get_current_expr() 89 | if require("dap-view.watches.actions").add_watch_expr(final_expr) then 90 | require("dap-view.views").switch_to_view("watches") 91 | end 92 | end 93 | 94 | ---@param view dapview.SectionType 95 | M.jump_to_view = function(view) 96 | if not vim.tbl_contains(setup.config.winbar.sections, view) then 97 | vim.notify("Can't jump to unconfigured view: " .. view) 98 | return 99 | end 100 | if state.bufnr and state.winnr and api.nvim_win_is_valid(state.winnr) then 101 | api.nvim_set_current_win(state.winnr) 102 | winbar.show_content(view) 103 | else 104 | vim.notify("Can't jump to view: couldn't find the window") 105 | end 106 | end 107 | 108 | ---@param view dapview.SectionType 109 | M.show_view = function(view) 110 | if not vim.tbl_contains(setup.config.winbar.sections, view) then 111 | vim.notify("Can't show unconfigured view: " .. view) 112 | return 113 | end 114 | if state.current_section == view then 115 | M.jump_to_view(view) 116 | elseif state.bufnr and state.winnr and api.nvim_win_is_valid(state.winnr) then 117 | winbar.show_content(view) 118 | else 119 | vim.notify("Can't show view: couldn't find the window") 120 | end 121 | end 122 | 123 | return M 124 | -------------------------------------------------------------------------------- /lua/dap-view/autocmds.lua: -------------------------------------------------------------------------------- 1 | local state = require("dap-view.state") 2 | local globals = require("dap-view.globals") 3 | local winbar = require("dap-view.options.winbar") 4 | 5 | local api = vim.api 6 | 7 | api.nvim_create_autocmd("TabEnter", { 8 | callback = function() 9 | local winnr = nil 10 | local term_winnr = nil 11 | 12 | for _, win in ipairs(api.nvim_tabpage_list_wins(0)) do 13 | local bufnr = api.nvim_win_get_buf(win) 14 | local ft = vim.bo[bufnr].filetype 15 | 16 | if ft == "dap-view" then 17 | if winnr == nil then 18 | winnr = win 19 | end 20 | end 21 | 22 | if ft == "dap-view-term" then 23 | if state.current_section == "console" then 24 | if winnr == nil then 25 | winnr = win 26 | end 27 | elseif term_winnr == nil then 28 | term_winnr = win 29 | end 30 | end 31 | 32 | if ft == "dap-repl" and state.current_section == "repl" then 33 | if winnr == nil then 34 | winnr = win 35 | end 36 | end 37 | end 38 | 39 | state.winnr = winnr 40 | state.term_winnr = term_winnr 41 | 42 | if winnr ~= nil then 43 | winbar.show_content(state.current_section) 44 | end 45 | end, 46 | }) 47 | 48 | api.nvim_create_autocmd("BufEnter", { 49 | callback = function(args) 50 | local buf = args.buf 51 | local win = vim.fn.bufwinid(buf) 52 | local ft = vim.bo[buf].filetype 53 | 54 | -- Reset the winnr if the buffer changed 55 | -- 56 | -- We can't use winfixbuf since the window shares many buffers (REPL, Console) 57 | -- 58 | -- Therefore, it's possible to switch to a (regular) buffer (any ft) while keeping the status of state.winnr 59 | -- 60 | -- While it's unlikely users do that very often, such change occurs when the switchbuf is triggerd as "newtab" 61 | -- For some reason, the new tab starts with the "dap-view" ft, which causes the winbar to appear on the regular buffer 62 | if state.winnr == win then 63 | if not vim.tbl_contains({ "dap-view", "dap-view-term", "dap-repl" }, ft) then 64 | state.winnr = nil 65 | end 66 | end 67 | -- For good measure, also handle term_winr 68 | if state.term_winnr == win and ft ~= "dap-view-term" then 69 | state.term_winnr = nil 70 | end 71 | end, 72 | }) 73 | 74 | api.nvim_create_autocmd("CursorMoved", { 75 | pattern = globals.MAIN_BUF_NAME, 76 | callback = function() 77 | state.cur_pos[state.current_section] = api.nvim_win_get_cursor(state.winnr)[1] 78 | end, 79 | }) 80 | -------------------------------------------------------------------------------- /lua/dap-view/breakpoints/actions.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | -- From https://github.com/ibhagwan/fzf-lua/blob/9a1f4b6f9e37d6fad6730301af58c29b00d363f8/lua/fzf-lua/actions.lua#L1079-L1101 4 | M.remove_breakpoint = function() 5 | local dap_breakpoints = require("dap.breakpoints") 6 | 7 | local bufnr, line_num = require("dap-view.views.util").get_bufnr("^(.-)|(%d+)|") 8 | 9 | dap_breakpoints.remove(bufnr, line_num) 10 | 11 | local session = require("dap").session() 12 | if session and bufnr then 13 | local breapoints = dap_breakpoints.get() 14 | -- If all BPs were removed from a buffer we must clear the buffer by sending an empty table in the bufnr index 15 | breapoints[bufnr] = breapoints[bufnr] or {} 16 | session:set_breakpoints(breapoints) 17 | end 18 | end 19 | 20 | return M 21 | -------------------------------------------------------------------------------- /lua/dap-view/breakpoints/util/extmarks.lua: -------------------------------------------------------------------------------- 1 | local globals = require("dap-view.globals") 2 | local state = require("dap-view.state") 3 | 4 | local M = {} 5 | 6 | local api = vim.api 7 | 8 | ---@param src_bufnr integer 9 | ---@param src_row integer 10 | ---@param target_row integer 11 | ---@param col_offset integer 12 | M.copy_extmarks = function(src_bufnr, src_row, target_row, col_offset) 13 | local extmarks = api.nvim_buf_get_extmarks( 14 | src_bufnr, 15 | -1, 16 | { src_row, 0 }, 17 | { src_row + 1, 0 }, 18 | { details = true, type = "highlight" } 19 | ) 20 | 21 | for _, extmark in ipairs(extmarks) do 22 | local namespace = extmark[1] 23 | local col = extmark[3] 24 | local opts = extmark[4] 25 | 26 | if opts and opts.end_col then 27 | opts.end_col = opts.end_col + col_offset 28 | end 29 | 30 | if opts then 31 | api.nvim_buf_set_extmark( 32 | state.bufnr, 33 | opts.ns_id or globals.NAMESPACE, 34 | target_row, 35 | col + col_offset, 36 | { 37 | id = namespace, 38 | end_col = opts.end_col, 39 | priority = opts.priority, 40 | hl_group = opts.hl_group, 41 | right_gravity = opts.right_gravity, 42 | hl_eol = opts.hl_eol, 43 | virt_text = opts.virt_text, 44 | virt_text_pos = opts.virt_text_pos, 45 | virt_text_win_col = opts.virt_text_win_col, 46 | hl_mode = opts.hl_mode, 47 | line_hl_group = opts.line_hl_group, 48 | spell = opts.spell, 49 | url = opts.url, 50 | } 51 | ) 52 | end 53 | end 54 | end 55 | 56 | return M 57 | -------------------------------------------------------------------------------- /lua/dap-view/breakpoints/util/treesitter.lua: -------------------------------------------------------------------------------- 1 | local globals = require("dap-view.globals") 2 | local state = require("dap-view.state") 3 | 4 | local M = {} 5 | 6 | local api = vim.api 7 | 8 | ---@param src_bufnr integer 9 | ---@param src_row integer 10 | ---@param target_row integer 11 | ---@param col_offset integer 12 | --- See https://github.com/stevearc/quicker.nvim/blob/049d66534d3de5920663ee1b8dd0096d70f55a67/lua/quicker/highlight.lua#L164 13 | M.copy_highlights = function(src_bufnr, src_row, target_row, col_offset) 14 | local filetype = vim.filetype.match({ buf = src_bufnr }) 15 | 16 | if not filetype then 17 | return 18 | end 19 | 20 | local lang = vim.treesitter.language.get_lang(filetype) 21 | 22 | if not lang then 23 | return 24 | end 25 | 26 | local line = api.nvim_buf_get_lines(src_bufnr, src_row, src_row + 1, false) 27 | local text = table.concat(line, "\n") 28 | 29 | local has_parser, parser = pcall(vim.treesitter.get_string_parser, text, lang) 30 | 31 | if not has_parser then 32 | return 33 | end 34 | 35 | local root = parser:parse(true)[1]:root() 36 | local query = vim.treesitter.query.get(lang, "highlights") 37 | 38 | if not query then 39 | return 40 | end 41 | 42 | local highlights = {} 43 | for capture, node, metadata in query:iter_captures(root, text) do 44 | if capture == nil then 45 | break 46 | end 47 | 48 | local range = vim.treesitter.get_range(node, text, metadata[capture]) 49 | local start_row, start_col, _, end_row, end_col, _ = unpack(range) 50 | local capture_name = query.captures[capture] 51 | 52 | local hl = string.format("@%s.%s", capture_name, lang) 53 | 54 | if end_row > start_row then 55 | end_col = -1 56 | end 57 | 58 | table.insert(highlights, { start_col + col_offset, end_col + col_offset, hl }) 59 | end 60 | 61 | for _, hl in ipairs(highlights) do 62 | local start_col, end_col, hl_group = hl[1], hl[2], hl[3] 63 | api.nvim_buf_set_extmark(state.bufnr, globals.NAMESPACE, target_row, start_col, { 64 | hl_group = hl_group, 65 | end_col = end_col, 66 | priority = 100, 67 | strict = false, 68 | }) 69 | end 70 | end 71 | 72 | return M 73 | -------------------------------------------------------------------------------- /lua/dap-view/breakpoints/vendor/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local NVIM_DAP_NAMESPACE = "dap_breakpoints" 4 | 5 | ---@class SignDict 6 | ---@field group string 7 | ---@field id integer 8 | ---@field lnum integer 9 | ---@field name string 10 | ---@field priority integer 11 | 12 | ---@class PlacedSigns 13 | ---@field bufnr integer 14 | ---@field signs SignDict 15 | 16 | ---@param bufnr? integer 17 | ---@return PlacedSigns 18 | local function get_breakpoint_signs(bufnr) 19 | if bufnr then 20 | return vim.fn.sign_getplaced(bufnr, { group = NVIM_DAP_NAMESPACE }) 21 | end 22 | 23 | local bufs_with_signs = vim.fn.sign_getplaced() 24 | 25 | local result = {} 26 | 27 | for _, buf_signs in ipairs(bufs_with_signs) do 28 | buf_signs = vim.fn.sign_getplaced(buf_signs.bufnr, { group = NVIM_DAP_NAMESPACE })[1] 29 | 30 | if #buf_signs.signs > 0 then 31 | table.insert(result, buf_signs) 32 | end 33 | end 34 | 35 | return result 36 | end 37 | 38 | ---@param bufnr? integer 39 | ---@return table 40 | function M.get(bufnr) 41 | local signs = get_breakpoint_signs(bufnr) 42 | 43 | if #signs == 0 then 44 | return {} 45 | end 46 | 47 | local result = {} 48 | 49 | for _, buf_breakpoint_signs in pairs(signs) do 50 | local breakpoints = {} 51 | local buf = buf_breakpoint_signs.bufnr 52 | 53 | result[buf] = breakpoints 54 | 55 | for _, breakpoint_sign in pairs(buf_breakpoint_signs.signs) do 56 | table.insert(breakpoints, { 57 | lnum = breakpoint_sign.lnum, 58 | }) 59 | end 60 | end 61 | 62 | return result 63 | end 64 | 65 | return M 66 | -------------------------------------------------------------------------------- /lua/dap-view/breakpoints/view.lua: -------------------------------------------------------------------------------- 1 | local state = require("dap-view.state") 2 | local vendor = require("dap-view.breakpoints.vendor") 3 | local extmarks = require("dap-view.breakpoints.util.extmarks") 4 | local treesitter = require("dap-view.breakpoints.util.treesitter") 5 | local views = require("dap-view.views") 6 | local hl = require("dap-view.util.hl") 7 | 8 | local M = {} 9 | 10 | local api = vim.api 11 | 12 | M.show = function() 13 | if state.bufnr and state.winnr then 14 | local breakpoints = vendor.get() 15 | 16 | local line = 0 17 | 18 | if views.cleanup_view(vim.tbl_isempty(breakpoints), "No breakpoints") then 19 | return 20 | end 21 | 22 | for buf, buf_entries in pairs(breakpoints) do 23 | local filename = api.nvim_buf_get_name(buf) 24 | local relative_path = vim.fn.fnamemodify(filename, ":.") 25 | 26 | for _, entry in pairs(buf_entries) do 27 | local buf_lines = api.nvim_buf_get_lines(buf, entry.lnum - 1, entry.lnum, true) 28 | local text = table.concat(buf_lines, "\n") 29 | 30 | local content = { relative_path .. "|" .. entry.lnum .. "|" .. text } 31 | 32 | api.nvim_buf_set_lines(state.bufnr, line, line, false, content) 33 | 34 | local col_offset = #relative_path + #tostring(entry.lnum) + 2 35 | 36 | treesitter.copy_highlights(buf, entry.lnum - 1, line, col_offset) 37 | extmarks.copy_extmarks(buf, entry.lnum - 1, line, col_offset) 38 | 39 | hl.highlight_file_name_and_line_number(line, #relative_path, #tostring(entry.lnum)) 40 | 41 | line = line + 1 42 | end 43 | end 44 | 45 | -- Clear previous content 46 | api.nvim_buf_set_lines(state.bufnr, line, -1, true, {}) 47 | end 48 | end 49 | 50 | return M 51 | -------------------------------------------------------------------------------- /lua/dap-view/complete.lua: -------------------------------------------------------------------------------- 1 | local setup = require("dap-view.setup") 2 | 3 | local M = {} 4 | 5 | ---@param arg_lead string 6 | ---@return string[] 7 | M.complete_sections = function(arg_lead) 8 | local sections = setup.config.winbar.sections 9 | return vim.iter(sections) 10 | :filter(function(section) 11 | return section:find(arg_lead or "") == 1 12 | end) 13 | :totable() 14 | end 15 | 16 | return M 17 | -------------------------------------------------------------------------------- /lua/dap-view/config.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---@alias dapview.SectionType "breakpoints" | "exceptions" | "watches" | "repl" | "threads" | "console" | "scopes" 4 | 5 | ---@alias dapview.CustomButton string 6 | ---@alias dapview.DefaultButton "play" | "step_into" | "step_over" | "step_out" | "step_back" | "run_last" | "terminate" | "disconnect" 7 | ---@alias dapview.Button dapview.CustomButton | dapview.DefaultButton 8 | 9 | ---@class dapview.TerminalConfig 10 | ---@field hide string[] Hide the terminal for listed adapters. 11 | ---@field position 'right' | 'left' | 'above' | 'below' 12 | ---@field width number If > 1 number of columns, else percentage the terminal window should use 13 | ---@field start_hidden boolean Don't show the terminal window when starting a new session 14 | 15 | ---@class dapview.WindowsConfig 16 | ---@field height integer If > 1 number of lines, else percentage the windows should use 17 | ---@field position 'right' | 'left' | 'above' | 'below' 18 | ---@field anchor? fun(): integer? Function that returns a window number for the main nvim-dap-view window to follow 19 | ---@field terminal dapview.TerminalConfig 20 | 21 | ---@class dapview.WinbarHeaders 22 | ---@field breakpoints string 23 | ---@field scopes string 24 | ---@field exceptions string 25 | ---@field watches string 26 | ---@field threads string 27 | ---@field repl string 28 | ---@field console string 29 | 30 | ---@class dapview.ControlsIcons 31 | ---@field pause string 32 | ---@field play string 33 | ---@field step_into string 34 | ---@field step_over string 35 | ---@field step_out string 36 | ---@field step_back string 37 | ---@field run_last string 38 | ---@field terminate string 39 | ---@field disconnect string 40 | 41 | ---@class dapview.ControlButton 42 | ---@field render fun(): string Render the button (highlight and icon). 43 | ---@field action fun(clicks: integer, button: string, modifiers: string): nil Click handler. See `:help statusline`. 44 | 45 | ---@class dapview.ControlsConfig 46 | ---@field enabled boolean 47 | ---@field position 'left' | 'right' 48 | ---@field buttons dapview.Button[] Buttons to show in the controls section. 49 | ---@field custom_buttons table Custom buttons to show in the controls section. 50 | ---@field icons dapview.ControlsIcons Icons for each button. 51 | 52 | ---@class dapview.WinbarConfig 53 | ---@field sections dapview.SectionType[] 54 | ---@field default_section dapview.SectionType 55 | ---@field show boolean 56 | ---@field headers dapview.WinbarHeaders Header label for each section. 57 | ---@field controls dapview.ControlsConfig 58 | 59 | ---@class dapview.HelpConfig 60 | ---@field border? string|string[] Override `winborder` 61 | 62 | ---@class (exact) dapview.ConfigStrict 63 | ---@field winbar dapview.WinbarConfig 64 | ---@field windows dapview.WindowsConfig 65 | ---@field help dapview.HelpConfig 66 | ---@field switchbuf string Control how to jump when selecting a breakpoint or a call in the stack 67 | 68 | ---@type dapview.ConfigStrict 69 | M.config = { 70 | winbar = { 71 | show = true, 72 | sections = { "watches", "scopes", "exceptions", "breakpoints", "threads", "repl" }, 73 | default_section = "watches", 74 | headers = { 75 | breakpoints = "Breakpoints [B]", 76 | scopes = "Scopes [S]", 77 | exceptions = "Exceptions [E]", 78 | watches = "Watches [W]", 79 | threads = "Threads [T]", 80 | repl = "REPL [R]", 81 | console = "Console [C]", 82 | }, 83 | controls = { 84 | enabled = false, 85 | position = "right", 86 | buttons = { 87 | "play", 88 | "step_into", 89 | "step_over", 90 | "step_out", 91 | "step_back", 92 | "run_last", 93 | "terminate", 94 | "disconnect", 95 | }, 96 | custom_buttons = {}, 97 | icons = { 98 | pause = "", 99 | play = "", 100 | step_into = "", 101 | step_over = "", 102 | step_out = "", 103 | step_back = "", 104 | run_last = "", 105 | terminate = "", 106 | disconnect = "", 107 | }, 108 | }, 109 | }, 110 | windows = { 111 | height = 12, 112 | position = "below", 113 | terminal = { 114 | position = "left", 115 | hide = {}, 116 | width = 0.5, 117 | start_hidden = false, 118 | }, 119 | }, 120 | help = { 121 | border = nil, 122 | }, 123 | switchbuf = "usetab,newtab", 124 | } 125 | 126 | return M 127 | -------------------------------------------------------------------------------- /lua/dap-view/events.lua: -------------------------------------------------------------------------------- 1 | local dap = require("dap") 2 | 3 | local state = require("dap-view.state") 4 | local breakpoints = require("dap-view.breakpoints.view") 5 | local scopes = require("dap-view.scopes.view") 6 | local threads = require("dap-view.threads.view") 7 | local exceptions = require("dap-view.exceptions.view") 8 | local term = require("dap-view.term.init") 9 | local eval = require("dap-view.watches.eval") 10 | local setup = require("dap-view.setup") 11 | local winbar = require("dap-view.options.winbar") 12 | 13 | local SUBSCRIPTION_ID = "dap-view" 14 | 15 | dap.listeners.before.initialize[SUBSCRIPTION_ID] = function(session, _) 16 | local adapter = session.config.type 17 | -- When initializing a new session, there might a leftover terminal buffer 18 | -- Usually, this wouldn't be a problem, but it can cause inconsistencies when starting a session that 19 | -- 20 | -- (A) Doesn't use the terminal, after a session that does 21 | -- The problem here is that the terminal could be used if it was left open from the earlier session 22 | -- 23 | -- (B) Uses the terminal, after a session that doesn't 24 | -- The terminal wouldn't show up, since it's hidden 25 | -- 26 | -- To handle these scenarios, we have to delete the terminal buffer 27 | -- However, if we always close the terminal, dap-view will be shifted very quickly (if open), 28 | -- causing a flickering effect. 29 | -- 30 | -- To address that, we only delete the terminal buffer if the new session has a different adapter 31 | -- (which should cover most scenarios where the flickering would occur) 32 | -- 33 | -- However, do not try to delete the buffer on the first session, 34 | -- as it conflicts with bootstrapping the terminal window. 35 | -- See: https://github.com/igorlfs/nvim-dap-view/issues/18 36 | if state.last_active_adapter and state.last_active_adapter ~= adapter then 37 | term.force_delete_term_buf() 38 | end 39 | state.last_active_adapter = adapter 40 | 41 | term.setup_term_win_cmd() 42 | 43 | local separate_term_win = not vim.tbl_contains(setup.config.winbar.sections, "console") 44 | if not setup.config.windows.terminal.start_hidden and separate_term_win then 45 | term.open_term_buf_win() 46 | end 47 | end 48 | 49 | dap.listeners.after.setBreakpoints[SUBSCRIPTION_ID] = function() 50 | if state.current_section == "breakpoints" then 51 | breakpoints.show() 52 | end 53 | end 54 | 55 | dap.listeners.after.scopes[SUBSCRIPTION_ID] = function() 56 | -- nvim-dap needs a buffer to operate 57 | if state.current_section == "scopes" and state.bufnr then 58 | scopes.refresh() 59 | end 60 | 61 | -- Avoid race conditions by not using `event_stopped` 62 | require("dap-view.threads").get_threads() 63 | for expr, _ in pairs(state.watched_expressions) do 64 | eval.eval_expr(expr) 65 | end 66 | end 67 | 68 | dap.listeners.after.variables[SUBSCRIPTION_ID] = function() 69 | if state.current_section == "watches" then 70 | require("dap-view.views").switch_to_view("watches") 71 | end 72 | end 73 | 74 | dap.listeners.after.stackTrace[SUBSCRIPTION_ID] = function() 75 | if state.current_section == "threads" then 76 | threads.show() 77 | end 78 | end 79 | 80 | dap.listeners.after.setExpression[SUBSCRIPTION_ID] = function() 81 | eval.reeval() 82 | end 83 | 84 | dap.listeners.after.setVariable[SUBSCRIPTION_ID] = function() 85 | eval.reeval() 86 | end 87 | 88 | dap.listeners.after.initialize[SUBSCRIPTION_ID] = function(session, _) 89 | state.exceptions_options = vim.iter(session.capabilities.exceptionBreakpointFilters or {}) 90 | :map(function(filter) 91 | return { enabled = filter.default, exception_filter = filter } 92 | end) 93 | :totable() 94 | -- Remove applied filters from view when initializing a new session 95 | -- Since we don't store the applied filters between sessions 96 | -- (i.e., we always override with the defaults from the adapter on a new session) 97 | -- Therefore, the exceptions view could look outdated 98 | -- 99 | -- Also, we can't just update the filters at this stage (after the initialize request) 100 | -- due to how the initialization works: setExceptionBreakpoints happens after initialize 101 | -- (with the default configuration) 102 | -- See https://microsoft.github.io/debug-adapter-protocol/specification#Events_Initialized 103 | if state.current_section == "exceptions" then 104 | exceptions.show() 105 | end 106 | end 107 | 108 | dap.listeners.after.event_terminated[SUBSCRIPTION_ID] = function() 109 | -- Refresh threads view on exit to avoid showing outdated trace 110 | if state.current_section == "threads" then 111 | threads.show() 112 | end 113 | 114 | winbar.redraw_controls() 115 | end 116 | 117 | --- Refresh winbar on dap session state change events not having a dedicated event handler 118 | local events = { 119 | "continue", 120 | "disconnect", 121 | "event_exited", 122 | "event_stopped", 123 | "restart", 124 | "threads", 125 | } 126 | 127 | for _, event in ipairs(events) do 128 | dap.listeners.after[event][SUBSCRIPTION_ID] = winbar.redraw_controls 129 | end 130 | -------------------------------------------------------------------------------- /lua/dap-view/exceptions/actions.lua: -------------------------------------------------------------------------------- 1 | local exceptions = require("dap-view.exceptions") 2 | local state = require("dap-view.state") 3 | local hl = require("dap-view.util.hl") 4 | 5 | local M = {} 6 | 7 | local api = vim.api 8 | 9 | M.toggle_exception_filter = function() 10 | local cursor_line = api.nvim_win_get_cursor(state.winnr)[1] 11 | 12 | local current_option = state.exceptions_options[cursor_line] 13 | 14 | current_option.enabled = not current_option.enabled 15 | 16 | local icon = current_option.enabled and "" or "" 17 | 18 | local content = " " .. icon .. " " .. current_option.exception_filter.label 19 | 20 | api.nvim_buf_set_lines(state.bufnr, cursor_line - 1, cursor_line, false, { content }) 21 | 22 | local hl_type = current_option.enabled and "Enabled" or "Disabled" 23 | hl.hl_range("ExceptionFilter" .. hl_type, { cursor_line - 1, 0 }, { cursor_line - 1, 4 }) 24 | 25 | exceptions.update_exception_breakpoints_filters() 26 | end 27 | 28 | return M 29 | -------------------------------------------------------------------------------- /lua/dap-view/exceptions/init.lua: -------------------------------------------------------------------------------- 1 | local dap = require("dap") 2 | 3 | local state = require("dap-view.state") 4 | 5 | local M = {} 6 | 7 | M.update_exception_breakpoints_filters = function() 8 | if vim.tbl_isempty(state.exceptions_options) then 9 | return 10 | end 11 | 12 | local filters = vim.iter(state.exceptions_options) 13 | :filter(function(x) 14 | return x.enabled 15 | end) 16 | :map(function(x) 17 | return x.exception_filter.filter 18 | end) 19 | :totable() 20 | 21 | dap.set_exception_breakpoints(filters) 22 | end 23 | 24 | return M 25 | -------------------------------------------------------------------------------- /lua/dap-view/exceptions/view.lua: -------------------------------------------------------------------------------- 1 | local dap = require("dap") 2 | 3 | local state = require("dap-view.state") 4 | local views = require("dap-view.views") 5 | local hl = require("dap-view.util.hl") 6 | 7 | local M = {} 8 | 9 | local api = vim.api 10 | 11 | M.show = function() 12 | if state.bufnr and state.winnr then 13 | if views.cleanup_view(not dap.session(), "No active session") then 14 | return 15 | end 16 | 17 | if 18 | views.cleanup_view(vim.tbl_isempty(state.exceptions_options), "Not supported by debug adapter") 19 | then 20 | return 21 | end 22 | 23 | local content = vim.iter(state.exceptions_options) 24 | :map(function(opt) 25 | local icon = opt.enabled and "" or "" 26 | return " " .. icon .. " " .. opt.exception_filter.label 27 | end) 28 | :totable() 29 | 30 | api.nvim_buf_set_lines(state.bufnr, 0, -1, false, content) 31 | 32 | for i, opt in ipairs(state.exceptions_options) do 33 | local hl_type = opt.enabled and "Enabled" or "Disabled" 34 | hl.hl_range("ExceptionFilter" .. hl_type, { i - 1, 0 }, { i - 1, 4 }) 35 | end 36 | end 37 | end 38 | 39 | return M 40 | -------------------------------------------------------------------------------- /lua/dap-view/globals.lua: -------------------------------------------------------------------------------- 1 | return { 2 | MAIN_BUF_NAME = "dap-view://main", 3 | NAMESPACE = vim.api.nvim_create_namespace("dap-view"), 4 | HL_PREFIX = "NvimDapView", 5 | } 6 | -------------------------------------------------------------------------------- /lua/dap-view/guard.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | M.expect_session = function() 4 | local session = require("dap").session() 5 | 6 | if not session then 7 | vim.notify("No active session") 8 | end 9 | 10 | return session and true or false 11 | end 12 | 13 | return M 14 | -------------------------------------------------------------------------------- /lua/dap-view/highlight.lua: -------------------------------------------------------------------------------- 1 | local globals = require("dap-view.globals") 2 | 3 | local api = vim.api 4 | 5 | ---@param name string 6 | ---@param link string 7 | local hl_create = function(name, link) 8 | api.nvim_set_hl(0, globals.HL_PREFIX .. name, { link = link }) 9 | end 10 | 11 | local define_base_links = function() 12 | hl_create("MissingData", "DapBreakpoint") 13 | hl_create("FileName", "qfFileName") 14 | hl_create("LineNumber", "qfLineNr") 15 | hl_create("Separator", "Comment") 16 | 17 | hl_create("Thread", "Tag") 18 | hl_create("ThreadStopped", "Conditional") 19 | hl_create("ThreadError", "DiagnosticError") 20 | 21 | hl_create("ExceptionFilterEnabled", "DiagnosticOk") 22 | hl_create("ExceptionFilterDisabled", "DiagnosticError") 23 | 24 | hl_create("Tab", "TabLine") 25 | hl_create("TabSelected", "TabLineSel") 26 | 27 | hl_create("ControlNC", "Comment") 28 | hl_create("ControlPlay", "Keyword") 29 | hl_create("ControlPause", "Boolean") 30 | hl_create("ControlStepInto", "Function") 31 | hl_create("ControlStepOut", "Function") 32 | hl_create("ControlStepOver", "Function") 33 | hl_create("ControlStepBack", "Function") 34 | hl_create("ControlRunLast", "Keyword") 35 | hl_create("ControlTerminate", "DapBreakpoint") 36 | hl_create("ControlDisconnect", "DapBreakpoint") 37 | 38 | hl_create("WatchExpr", "Identifier") 39 | hl_create("WatchError", "DiagnosticError") 40 | hl_create("WatchUpdated", "DiagnosticVirtualTextWarn") 41 | 42 | hl_create("Boolean", "Boolean") 43 | hl_create("String", "String") 44 | hl_create("Number", "Number") 45 | hl_create("Float", "Float") 46 | hl_create("Function", "Function") 47 | end 48 | 49 | define_base_links() 50 | 51 | api.nvim_create_autocmd("ColorScheme", { 52 | callback = define_base_links, 53 | }) 54 | -------------------------------------------------------------------------------- /lua/dap-view/options/autocmd.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local api = vim.api 4 | 5 | ---@param bufnr integer 6 | ---@param callback fun(): nil 7 | M.quit_buf_autocmd = function(bufnr, callback) 8 | api.nvim_create_autocmd("BufWipeout", { 9 | buffer = bufnr, 10 | callback = callback, 11 | }) 12 | end 13 | 14 | return M 15 | -------------------------------------------------------------------------------- /lua/dap-view/options/controls.lua: -------------------------------------------------------------------------------- 1 | local dap = require("dap") 2 | 3 | local statusline = require("dap-view.util.statusline") 4 | local setup = require("dap-view.setup") 5 | local module = ... 6 | 7 | local M = {} 8 | 9 | ---@type table 10 | local buttons = { 11 | play = { 12 | render = function() 13 | local session = dap.session() 14 | if not session or session.stopped_thread_id then 15 | return statusline.hl(setup.config.winbar.controls.icons.play, "ControlPlay") 16 | else 17 | return statusline.hl(setup.config.winbar.controls.icons.pause, "ControlPause") 18 | end 19 | end, 20 | action = function() 21 | local session = dap.session() 22 | if not session or session.stopped_thread_id then 23 | dap.continue() 24 | else 25 | dap.pause() 26 | end 27 | end, 28 | }, 29 | step_into = { 30 | render = function() 31 | local session = dap.session() 32 | local active = session and session.stopped_thread_id 33 | local hl = active and "ControlStepInto" or "ControlNC" 34 | return statusline.hl(setup.config.winbar.controls.icons.step_into, hl) 35 | end, 36 | action = function() 37 | dap.step_into() 38 | end, 39 | }, 40 | step_over = { 41 | render = function() 42 | local session = dap.session() 43 | local active = session and session.stopped_thread_id 44 | local hl = active and "ControlStepOver" or "ControlNC" 45 | return statusline.hl(setup.config.winbar.controls.icons.step_over, hl) 46 | end, 47 | action = function() 48 | dap.step_over() 49 | end, 50 | }, 51 | step_out = { 52 | render = function() 53 | local session = dap.session() 54 | local active = session and session.stopped_thread_id 55 | local hl = active and "ControlStepOut" or "ControlNC" 56 | return statusline.hl(setup.config.winbar.controls.icons.step_out, hl) 57 | end, 58 | action = function() 59 | dap.step_out() 60 | end, 61 | }, 62 | step_back = { 63 | render = function() 64 | local session = dap.session() 65 | local active = session and session.stopped_thread_id 66 | local hl = active and "ControlStepBack" or "ControlNC" 67 | return statusline.hl(setup.config.winbar.controls.icons.step_back, hl) 68 | end, 69 | action = function() 70 | dap.step_back() 71 | end, 72 | }, 73 | run_last = { 74 | render = function() 75 | return statusline.hl(setup.config.winbar.controls.icons.run_last, "ControlRunLast") 76 | end, 77 | action = function() 78 | dap.run_last() 79 | end, 80 | }, 81 | terminate = { 82 | render = function() 83 | local hl = dap.session() and "ControlTerminate" or "ControlNC" 84 | return statusline.hl(setup.config.winbar.controls.icons.terminate, hl) 85 | end, 86 | action = function() 87 | dap.terminate() 88 | end, 89 | }, 90 | disconnect = { 91 | render = function() 92 | local hl = dap.session() and "ControlDisconnect" or "ControlNC" 93 | return statusline.hl(setup.config.winbar.controls.icons.disconnect, hl) 94 | end, 95 | action = function() 96 | dap.disconnect() 97 | end, 98 | }, 99 | } 100 | 101 | ---@param idx integer 102 | ---@param clicks integer 103 | ---@param button '"l"' | '"r"' | '"m"' 104 | ---@param modifiers string 105 | M.on_click = function(idx, clicks, button, modifiers) 106 | local config = setup.config.winbar.controls 107 | local key = config.buttons[idx] 108 | local control = config.custom_buttons[key] or buttons[key] 109 | control.action(clicks, button, modifiers) 110 | end 111 | 112 | ---@return string 113 | M.render = function() 114 | local config = setup.config.winbar.controls 115 | local bar = "" 116 | for idx, key in ipairs(config.buttons) do 117 | local control = config.custom_buttons[key] or buttons[key] 118 | local icon = " " .. control.render() .. " " 119 | bar = bar .. statusline.clickable(icon, module, "on_click", idx) 120 | end 121 | return bar 122 | end 123 | 124 | return M 125 | -------------------------------------------------------------------------------- /lua/dap-view/options/winbar.lua: -------------------------------------------------------------------------------- 1 | local state = require("dap-view.state") 2 | local setup = require("dap-view.setup") 3 | local controls = require("dap-view.options.controls") 4 | local statusline = require("dap-view.util.statusline") 5 | local module = ... 6 | 7 | local M = {} 8 | 9 | local api = vim.api 10 | 11 | local winbar_info = { 12 | breakpoints = { 13 | keymap = "B", 14 | action = function() 15 | if vim.tbl_contains(setup.config.winbar.sections, "breakpoints") then 16 | require("dap-view.views").switch_to_view("breakpoints") 17 | end 18 | end, 19 | }, 20 | scopes = { 21 | keymap = "S", 22 | action = function() 23 | if vim.tbl_contains(setup.config.winbar.sections, "scopes") then 24 | require("dap-view.views").switch_to_view("scopes") 25 | end 26 | end, 27 | }, 28 | exceptions = { 29 | keymap = "E", 30 | action = function() 31 | if vim.tbl_contains(setup.config.winbar.sections, "exceptions") then 32 | require("dap-view.views").switch_to_view("exceptions") 33 | end 34 | end, 35 | }, 36 | watches = { 37 | keymap = "W", 38 | action = function() 39 | if vim.tbl_contains(setup.config.winbar.sections, "watches") then 40 | require("dap-view.views").switch_to_view("watches") 41 | end 42 | end, 43 | }, 44 | threads = { 45 | keymap = "T", 46 | action = function() 47 | if vim.tbl_contains(setup.config.winbar.sections, "threads") then 48 | require("dap-view.views").switch_to_view("threads") 49 | end 50 | end, 51 | }, 52 | repl = { 53 | keymap = "R", 54 | action = function() 55 | if vim.tbl_contains(setup.config.winbar.sections, "repl") then 56 | if not state.winnr or not api.nvim_win_is_valid(state.winnr) then 57 | return 58 | end 59 | -- Jump to dap-view's window to make the experience seamless 60 | local cmd = "lua vim.api.nvim_set_current_win(" .. state.winnr .. ")" 61 | local repl_buf, _ = require("dap").repl.open(nil, cmd) 62 | -- The REPL is a new buffer, so we need to set the winbar keymaps again 63 | M.set_winbar_action_keymaps(repl_buf) 64 | M.update_section("repl") 65 | end 66 | end, 67 | }, 68 | console = { 69 | keymap = "C", 70 | action = function() 71 | if vim.tbl_contains(setup.config.winbar.sections, "console") then 72 | if not state.winnr or not api.nvim_win_is_valid(state.winnr) then 73 | return 74 | end 75 | 76 | if not state.term_bufnr then 77 | require("dap-view.term.init").setup_term_win_cmd() 78 | end 79 | 80 | -- Set options before changing the buffer 81 | -- The change in buffer would unassign the state.winnr 82 | -- Since the buffer wouldn't have a filetype 83 | -- See https://github.com/igorlfs/nvim-dap-view/issues/69 84 | require("dap-view.term.options").set_options(state.winnr, state.term_bufnr) 85 | 86 | api.nvim_win_call(state.winnr, function() 87 | api.nvim_set_current_buf(state.term_bufnr) 88 | end) 89 | 90 | M.set_winbar_action_keymaps(state.term_bufnr) 91 | M.update_section("console") 92 | end 93 | end, 94 | }, 95 | } 96 | 97 | ---@param bufnr? integer 98 | M.set_winbar_action_keymaps = function(bufnr) 99 | if bufnr or state.bufnr then 100 | for _, value in pairs(winbar_info) do 101 | vim.keymap.set("n", value.keymap, function() 102 | value.action() 103 | end, { buffer = bufnr or state.bufnr }) 104 | end 105 | end 106 | end 107 | 108 | ---@param idx integer 109 | M.on_click = function(idx) 110 | local key = setup.config.winbar.sections[idx] 111 | local section = winbar_info[key] 112 | section.action() 113 | end 114 | 115 | local set_winbar_opt = function() 116 | if state.winnr and api.nvim_win_is_valid(state.winnr) then 117 | local winbar = setup.config.winbar.sections 118 | local winbar_title = {} 119 | local controls_config = setup.config.winbar.controls 120 | 121 | if controls_config.enabled and controls_config.position == "left" then 122 | table.insert(winbar_title, controls.render() .. "%=") 123 | end 124 | 125 | for idx, key in ipairs(winbar) do 126 | local info = winbar_info[key] 127 | 128 | if info ~= nil then 129 | local desc = " " .. setup.config.winbar.headers[key] .. " " 130 | desc = statusline.clickable(desc, module, "on_click", idx) 131 | 132 | if state.current_section == key then 133 | desc = statusline.hl(desc, "TabSelected") 134 | else 135 | desc = statusline.hl(desc, "Tab") 136 | end 137 | 138 | table.insert(winbar_title, desc) 139 | end 140 | end 141 | 142 | if controls_config.enabled and controls_config.position == "right" then 143 | table.insert(winbar_title, "%=" .. controls.render()) 144 | end 145 | 146 | local value = table.concat(winbar_title, "") 147 | 148 | vim.wo[state.winnr][0].winbar = value 149 | end 150 | end 151 | 152 | ---@param selected_section dapview.SectionType 153 | M.show_content = function(selected_section) 154 | winbar_info[selected_section].action() 155 | end 156 | 157 | ---@param section_name dapview.SectionType 158 | M.update_section = function(section_name) 159 | if setup.config.winbar.show then 160 | state.current_section = section_name 161 | set_winbar_opt() 162 | end 163 | end 164 | 165 | M.redraw_controls = function() 166 | if setup.config.winbar.show and setup.config.winbar.controls.enabled then 167 | set_winbar_opt() 168 | end 169 | end 170 | 171 | return M 172 | -------------------------------------------------------------------------------- /lua/dap-view/scopes/view.lua: -------------------------------------------------------------------------------- 1 | local dap = require("dap") 2 | 3 | local widgets = require("dap.ui.widgets") 4 | 5 | local views = require("dap-view.views") 6 | local winbar = require("dap-view.options.winbar") 7 | local state = require("dap-view.state") 8 | 9 | local M = {} 10 | 11 | local scopes_widget 12 | 13 | local new_widget = function() 14 | return widgets 15 | .builder(widgets.scopes) 16 | .new_buf(function() 17 | return state.bufnr 18 | end) 19 | .new_win(function() 20 | return state.winnr 21 | end) 22 | .build() 23 | end 24 | 25 | M.show = function() 26 | winbar.update_section("scopes") 27 | 28 | if views.cleanup_view(not dap.session(), "No active session") then 29 | return 30 | end 31 | 32 | if scopes_widget == nil then 33 | scopes_widget = new_widget() 34 | end 35 | 36 | scopes_widget.open() 37 | end 38 | 39 | M.refresh = function() 40 | if scopes_widget == nil then 41 | scopes_widget = new_widget() 42 | 43 | scopes_widget.open() 44 | else 45 | scopes_widget.refresh() 46 | end 47 | end 48 | 49 | return M 50 | -------------------------------------------------------------------------------- /lua/dap-view/setup/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | M.config = require("dap-view.config").config 4 | 5 | ---@param config dapview.Config? 6 | M.setup = function(config) 7 | M.config = vim.tbl_deep_extend("force", M.config, config or {}) 8 | 9 | require("dap-view.setup.validate").validate(M.config) 10 | end 11 | 12 | return M 13 | -------------------------------------------------------------------------------- /lua/dap-view/setup/validate/help.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---@param config dapview.HelpConfig 4 | function M.validate(config) 5 | local validate = require("dap-view.setup.validate.util").validate 6 | 7 | validate("help", { 8 | border = { config.border, { "string", "table", "nil" } }, 9 | }, config) 10 | end 11 | 12 | return M 13 | -------------------------------------------------------------------------------- /lua/dap-view/setup/validate/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---@param config dapview.ConfigStrict 4 | function M.validate(config) 5 | require("dap-view.setup.validate.util").validate("config", { 6 | windows = { config.windows, "table" }, 7 | winbar = { config.winbar, "table" }, 8 | help = { config.help, "table" }, 9 | switchbuf = { config.switchbuf, "string" }, 10 | }, config) 11 | 12 | require("dap-view.setup.validate.winbar").validate(config.winbar) 13 | require("dap-view.setup.validate.windows").validate(config.windows) 14 | require("dap-view.setup.validate.help").validate(config.help) 15 | end 16 | 17 | return M 18 | -------------------------------------------------------------------------------- /lua/dap-view/setup/validate/util.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | -- Code taken from @MariaSolOs in a indent-blankline.nvim PR: 4 | -- https://github.com/lukas-reineke/indent-blankline.nvim/pull/934/files#diff-09ebcaa8c75cd1e92d25640e377ab261cfecaf8351c9689173fd36c2d0c23d94R16 5 | 6 | --- @param spec table 7 | local _validate = function(spec) 8 | for key, key_spec in pairs(spec) do 9 | local message = type(key_spec[3]) == "string" and key_spec[3] or nil --[[@as string?]] 10 | local optional = type(key_spec[3]) == "boolean" and key_spec[3] or nil --[[@as boolean?]] 11 | vim.validate(key, key_spec[1], key_spec[2], optional, message) 12 | end 13 | end 14 | 15 | --- @param path string The path to the field being validated 16 | --- @param tbl table The table to validate 17 | --- @param source table The original table that we're validating against 18 | --- @see vim.validate 19 | function M.validate(path, tbl, source) 20 | -- Validate 21 | local _, err = pcall(_validate, tbl) 22 | if err then 23 | error(path .. "." .. err) 24 | end 25 | 26 | -- Check for erroneous fields 27 | for k, _ in pairs(source) do 28 | if tbl[k] == nil then 29 | error(path .. "." .. k .. ": unexpected field found in configuration") 30 | end 31 | end 32 | end 33 | 34 | return M 35 | -------------------------------------------------------------------------------- /lua/dap-view/setup/validate/winbar.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---@param config dapview.WinbarConfig 4 | function M.validate(config) 5 | local validate = require("dap-view.setup.validate.util").validate 6 | 7 | validate("winbar", { 8 | show = { config.show, "boolean" }, 9 | sections = { config.sections, "table" }, 10 | default_section = { config.default_section, "string" }, 11 | headers = { config.headers, "table" }, 12 | controls = { config.controls, "table" }, 13 | }, config) 14 | 15 | validate("winbar.headers", { 16 | breakpoints = { config.headers.breakpoints, "string" }, 17 | scopes = { config.headers.scopes, "string" }, 18 | exceptions = { config.headers.exceptions, "string" }, 19 | watches = { config.headers.watches, "string" }, 20 | threads = { config.headers.threads, "string" }, 21 | repl = { config.headers.repl, "string" }, 22 | console = { config.headers.console, "string" }, 23 | }, config.headers) 24 | 25 | validate("winbar.controls", { 26 | enabled = { config.controls.enabled, "boolean" }, 27 | position = { config.controls.position, "string" }, 28 | buttons = { config.controls.buttons, "table" }, 29 | icons = { config.controls.icons, "table" }, 30 | custom_buttons = { config.controls.custom_buttons, "table" }, 31 | }, config.controls) 32 | 33 | local icons = config.controls.icons 34 | validate("winbar.controls.icons", { 35 | pause = { icons.pause, "string" }, 36 | play = { icons.play, "string" }, 37 | step_into = { icons.step_into, "string" }, 38 | step_over = { icons.step_over, "string" }, 39 | step_out = { icons.step_out, "string" }, 40 | step_back = { icons.step_back, "string" }, 41 | disconnect = { icons.disconnect, "string" }, 42 | terminate = { icons.terminate, "string" }, 43 | run_last = { icons.run_last, "string" }, 44 | }, icons) 45 | 46 | local sections = config.sections 47 | local default = config.default_section 48 | 49 | -- Also check the "semantics" 50 | if #sections == 0 then 51 | error("There must be at least one section") 52 | end 53 | if not vim.tbl_contains(sections, default) then 54 | local pretty_sections = vim.inspect(sections) 55 | error("Default section (" .. default .. ") not listed as one of the sections " .. pretty_sections) 56 | end 57 | end 58 | 59 | return M 60 | -------------------------------------------------------------------------------- /lua/dap-view/setup/validate/windows.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local validate = require("dap-view.setup.validate.util").validate 4 | 5 | ---@param config dapview.WindowsConfig 6 | function M.validate(config) 7 | validate("windows", { 8 | height = { config.height, "number" }, 9 | position = { config.position, "string" }, 10 | terminal = { config.terminal, "table" }, 11 | anchor = { config.anchor, { "function", "nil" } }, 12 | }, config) 13 | 14 | validate("windows.terminal", { 15 | position = { config.terminal.position, "string" }, 16 | hide = { config.terminal.hide, "table" }, 17 | width = { config.terminal.width, "number" }, 18 | start_hidden = { config.terminal.start_hidden, "boolean" }, 19 | }, config.terminal) 20 | end 21 | 22 | return M 23 | -------------------------------------------------------------------------------- /lua/dap-view/state.lua: -------------------------------------------------------------------------------- 1 | ---@class dapview.ExceptionsOption 2 | ---@field exception_filter dap.ExceptionBreakpointsFilter 3 | ---@field enabled boolean 4 | 5 | ---@class dapview.ThreadWithErr: dap.Thread 6 | ---@field err? string 7 | 8 | ---@class dapview.VariablePack 9 | ---@field variable dap.Variable 10 | ---@field updated boolean 11 | ---@field reference number 12 | ---@field expanded boolean 13 | ---@field children string|dapview.VariablePack[] 14 | 15 | -- Necessary for some type assertions 16 | ---@class dapview.VariablePackStrict : dapview.VariablePack 17 | ---@field children dapview.VariablePack[] 18 | 19 | ---@class dapview.ExpressionPack 20 | ---@field response? dap.EvaluateResponse | string 21 | ---@field children? dapview.VariablePack[] | string 22 | ---@field expanded boolean 23 | ---@field updated boolean 24 | 25 | ---@class dapview.State 26 | ---@field bufnr? integer 27 | ---@field winnr? integer 28 | ---@field term_bufnr? integer 29 | ---@field term_winnr? integer 30 | ---@field last_active_adapter? string 31 | ---@field subtle_frames boolean 32 | ---@field current_section? dapview.SectionType 33 | ---@field exceptions_options dapview.ExceptionsOption[] 34 | ---@field threads dapview.ThreadWithErr[] 35 | ---@field threads_err? string 36 | ---@field frames_by_line table 37 | ---@field expressions_by_line table 38 | ---@field variables_by_line table 39 | ---@field watched_expressions table 40 | ---@field cur_pos table 41 | local M = { 42 | exceptions_options = {}, 43 | threads = {}, 44 | frames_by_line = {}, 45 | expressions_by_line = {}, 46 | variables_by_line = {}, 47 | subtle_frames = false, 48 | watched_expressions = {}, 49 | cur_pos = {}, 50 | } 51 | 52 | return M 53 | -------------------------------------------------------------------------------- /lua/dap-view/term/init.lua: -------------------------------------------------------------------------------- 1 | local dap = require("dap") 2 | 3 | local state = require("dap-view.state") 4 | local autocmd = require("dap-view.options.autocmd") 5 | local setup = require("dap-view.setup") 6 | 7 | local M = {} 8 | 9 | local api = vim.api 10 | 11 | ---Hide the term win, does not affect the term buffer 12 | M.hide_term_buf_win = function() 13 | if state.term_winnr and api.nvim_win_is_valid(state.term_winnr) then 14 | api.nvim_win_hide(state.term_winnr) 15 | end 16 | end 17 | 18 | M.force_delete_term_buf = function() 19 | if state.term_bufnr then 20 | api.nvim_buf_delete(state.term_bufnr, { force = true }) 21 | end 22 | end 23 | 24 | ---Open the term buf in a new window if 25 | ---I. A session is active 26 | ---II. The term buf exists 27 | ---III. The adapter isn't configured to be hidden 28 | ---IV. There's no term win or it is invalid 29 | ---@return integer? 30 | M.open_term_buf_win = function() 31 | local windows_config = setup.config.windows 32 | local term_config = windows_config.terminal 33 | local should_term_be_hidden = vim.tbl_contains(term_config.hide, state.last_active_adapter) 34 | 35 | if dap.session() and state.term_bufnr and not should_term_be_hidden then 36 | if not state.term_winnr or state.term_winnr and not api.nvim_win_is_valid(state.term_winnr) then 37 | local is_win_valid = state.winnr ~= nil and api.nvim_win_is_valid(state.winnr) 38 | 39 | state.term_winnr = api.nvim_open_win(state.term_bufnr, false, { 40 | split = is_win_valid and term_config.position or windows_config.position, 41 | win = is_win_valid and state.winnr or -1, 42 | height = windows_config.height < 1 and math.floor(vim.go.lines * windows_config.height) 43 | or windows_config.height, 44 | width = term_config.width < 1 and math.floor(vim.go.columns * term_config.width) 45 | or term_config.width, 46 | }) 47 | 48 | require("dap-view.term.options").set_options(state.term_winnr, state.term_bufnr) 49 | end 50 | end 51 | 52 | return state.term_winnr 53 | end 54 | 55 | local quit_term_buf = function() 56 | if state.term_bufnr then 57 | state.term_bufnr = nil 58 | end 59 | end 60 | 61 | ---Create the term buf and setup nvim-dap's `terminal_win_cmd` to use it 62 | M.setup_term_win_cmd = function() 63 | if not state.term_bufnr then 64 | -- Can't use an unlisted buffer here 65 | -- See https://github.com/igorlfs/nvim-dap-view/pull/37#issuecomment-2785076872 66 | state.term_bufnr = api.nvim_create_buf(true, false) 67 | 68 | assert(state.term_bufnr ~= 0, "Failed to create nvim-dap-view buffer") 69 | 70 | autocmd.quit_buf_autocmd(state.term_bufnr, quit_term_buf) 71 | 72 | dap.defaults.fallback.terminal_win_cmd = function() 73 | assert(state.term_bufnr, "Failed to get term bufnr") 74 | 75 | return state.term_bufnr 76 | end 77 | end 78 | end 79 | 80 | return M 81 | -------------------------------------------------------------------------------- /lua/dap-view/term/options.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---@param winnr integer 4 | ---@param bufnr integer 5 | M.set_options = function(winnr, bufnr) 6 | local win = vim.wo[winnr][0] 7 | win.scrolloff = 0 8 | win.wrap = false 9 | win.number = false 10 | win.relativenumber = false 11 | win.winfixheight = true 12 | win.statuscolumn = "" 13 | win.foldcolumn = "0" 14 | 15 | local buf = vim.bo[bufnr] 16 | buf.filetype = "dap-view-term" 17 | -- Can't set the buftype here, see https://github.com/neovim/neovim/issues/31457 18 | end 19 | 20 | return M 21 | -------------------------------------------------------------------------------- /lua/dap-view/threads/actions.lua: -------------------------------------------------------------------------------- 1 | local dap = require("dap") 2 | 3 | local state = require("dap-view.state") 4 | local util = require("dap-view.views.util") 5 | 6 | local M = {} 7 | 8 | local log = vim.log.levels 9 | 10 | ---@param lnum number 11 | M.jump_or_noop = function(lnum) 12 | local line = vim.fn.getline(".") 13 | 14 | if string.find(line, "\t") then 15 | util.jump_to_location("^\t(.-)|(%d+)|") 16 | 17 | local frame = state.frames_by_line[lnum] 18 | if frame then 19 | local session = assert(dap.session(), "has active session") 20 | session:_frame_set(frame) 21 | end 22 | else 23 | vim.notify("Can't jump to a thread", log.INFO) 24 | end 25 | end 26 | 27 | return M 28 | -------------------------------------------------------------------------------- /lua/dap-view/threads/init.lua: -------------------------------------------------------------------------------- 1 | local dap = require("dap") 2 | 3 | local state = require("dap-view.state") 4 | 5 | local M = {} 6 | 7 | M.get_threads = function() 8 | local session = assert(dap.session(), "has active session") 9 | 10 | coroutine.wrap(function() 11 | local err, result = session:request("threads") 12 | 13 | state.threads_err = nil 14 | 15 | if err then 16 | state.threads_err = tostring(err) 17 | state.threads = {} 18 | elseif result then 19 | state.threads = result.threads 20 | end 21 | 22 | M.get_stack_frames(session) 23 | end)() 24 | end 25 | 26 | ---@param session dap.Session 27 | M.get_stack_frames = function(session) 28 | for _, thread in pairs(state.threads) do 29 | coroutine.wrap(function() 30 | local err, result = session:request("stackTrace", { threadId = thread.id }) 31 | 32 | thread.err = nil 33 | 34 | if err then 35 | thread.err = tostring(err) 36 | thread.frames = {} 37 | elseif result then 38 | thread.frames = result.stackFrames 39 | end 40 | end)() 41 | end 42 | end 43 | 44 | return M 45 | -------------------------------------------------------------------------------- /lua/dap-view/threads/view.lua: -------------------------------------------------------------------------------- 1 | local dap = require("dap") 2 | 3 | local views = require("dap-view.views") 4 | local state = require("dap-view.state") 5 | local hl = require("dap-view.util.hl") 6 | 7 | local M = {} 8 | 9 | local api = vim.api 10 | 11 | M.show = function() 12 | if state.bufnr and state.winnr then 13 | local session = dap.session() 14 | -- Redundant check to appease the type checker 15 | if views.cleanup_view(session == nil, "No active session") or session == nil then 16 | return 17 | end 18 | 19 | if views.cleanup_view(state.threads_err ~= nil, state.threads_err) then 20 | return 21 | end 22 | 23 | if views.cleanup_view(vim.tbl_isempty(state.threads), "Debug adapter returned no threads") then 24 | return 25 | end 26 | 27 | for k, _ in pairs(state.frames_by_line) do 28 | state.frames_by_line[k] = nil 29 | end 30 | 31 | local line = 0 32 | 33 | for _, thread in pairs(state.threads) do 34 | local is_stopped_thread = session.stopped_thread_id == thread.id 35 | local thread_name = is_stopped_thread and thread.name .. " " or thread.name 36 | api.nvim_buf_set_lines(state.bufnr, line, line, true, { thread_name }) 37 | 38 | hl.hl_range(is_stopped_thread and "ThreadStopped" or "Thread", { line, 0 }, { line, -1 }) 39 | 40 | line = line + 1 41 | 42 | local valid_frames = vim.iter(thread.frames or {}) 43 | :filter( 44 | ---@param f dap.StackFrame 45 | function(f) 46 | return f.source ~= nil 47 | and f.source.path ~= nil 48 | and vim.uv.fs_stat(f.source.path) ~= nil 49 | end 50 | ) 51 | :totable() 52 | 53 | if vim.tbl_isempty(valid_frames) then 54 | if thread.err then 55 | api.nvim_buf_set_lines(state.bufnr, line, line, true, { thread.err }) 56 | hl.hl_range("ThreadError", { line, 0 }, { line, -1 }) 57 | line = line + 1 58 | end 59 | else 60 | local content = vim.iter(valid_frames):fold( 61 | {}, 62 | ---@param acc string[] 63 | ---@param t dap.StackFrame 64 | function(acc, t) 65 | local show_frame = not t.presentationHint 66 | or state.subtle_frames 67 | or t.presentationHint ~= "subtle" 68 | if show_frame then 69 | local path = t.source.path 70 | local relative_path = path and vim.fn.fnamemodify(path, ":.") or "" 71 | local label = "\t" .. relative_path .. "|" .. t.line .. "|" .. t.name 72 | table.insert(acc, label) 73 | end 74 | return acc 75 | end 76 | ) 77 | 78 | api.nvim_buf_set_lines(state.bufnr, line, line + #content, false, content) 79 | 80 | for i, c in pairs(content) do 81 | local pipe1 = string.find(c, "|") 82 | local pipe2 = #c - string.find(string.reverse(c), "|") 83 | 84 | hl.highlight_file_name_and_line_number(line - 1 + i, pipe1 - 1, pipe2 - pipe1) 85 | 86 | state.frames_by_line[line + i] = valid_frames[i] 87 | end 88 | 89 | line = line + #content 90 | end 91 | end 92 | 93 | -- Clear previous content 94 | api.nvim_buf_set_lines(state.bufnr, line, -1, true, {}) 95 | end 96 | end 97 | 98 | return M 99 | -------------------------------------------------------------------------------- /lua/dap-view/tree/traversal.lua: -------------------------------------------------------------------------------- 1 | local state = require("dap-view.state") 2 | 3 | local M = {} 4 | 5 | ---@param children dapview.VariablePack[] 6 | ---@param reference number 7 | ---@return dapview.VariablePack? 8 | local function dfs(children, reference) 9 | for _, v in pairs(children) do 10 | if v.variable.variablesReference == reference then 11 | return v 12 | end 13 | if v.children and type(v.children) ~= "string" then 14 | ---@cast v dapview.VariablePackStrict 15 | local ref = dfs(v.children, reference) 16 | if ref then 17 | return ref 18 | end 19 | end 20 | end 21 | end 22 | 23 | ---@param reference number 24 | ---@return dapview.VariablePack? 25 | M.find_node = function(reference) 26 | for _, v in pairs(state.watched_expressions) do 27 | local children = v.children 28 | if children and type(children) ~= "string" then 29 | local ref = dfs(children, reference) 30 | if ref then 31 | return ref 32 | end 33 | end 34 | end 35 | end 36 | 37 | return M 38 | -------------------------------------------------------------------------------- /lua/dap-view/types.lua: -------------------------------------------------------------------------------- 1 | ---@class dapview.TerminalConfigPartial : dapview.TerminalConfig, {} 2 | 3 | ---@class dapview.WindowsConfigPartial: dapview.WindowsConfig, {} 4 | ---@field terminal? dapview.TerminalConfigPartial 5 | 6 | ---@class dapview.WinbarHeadersPartial : dapview.WinbarHeaders, {} 7 | 8 | ---@class dapview.ControlsIconsPartial : dapview.ControlsIcons, {} 9 | 10 | ---@class dapview.ControlsConfigPartial : dapview.ControlsConfig, {} 11 | ---@field icons? dapview.ControlsIconsPartial Icons for each button 12 | 13 | ---@class dapview.WinbarConfigPartial : dapview.WinbarConfig, {} 14 | ---@field headers? dapview.WinbarHeadersPartial Header label for each section. 15 | ---@field controls? dapview.ControlsConfigPartial 16 | 17 | ---@class dapview.HelpConfigPartial : dapview.HelpConfig, {} 18 | 19 | ---@class dapview.Config : dapview.ConfigStrict, {} 20 | ---@field winbar? dapview.WinbarConfigPartial 21 | ---@field windows? dapview.WindowsConfigPartial 22 | ---@field help? dapview.HelpConfigPartial 23 | -------------------------------------------------------------------------------- /lua/dap-view/util/exprs.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local api = vim.api 4 | 5 | ---@return string 6 | M.get_trimmed_selection = function() 7 | local start = vim.fn.getpos("'<") 8 | local finish = vim.fn.getpos("'>") 9 | 10 | local start_line, start_col = start[2], start[3] 11 | local finish_line, finish_col = finish[2], finish[3] 12 | 13 | if start_line > finish_line or (start_line == finish_line and start_col > finish_col) then 14 | start_line, start_col, finish_line, finish_col = finish_line, finish_col, start_line, start_col 15 | end 16 | 17 | local lines = vim.fn.getline(start_line, finish_line) 18 | if #lines == 0 then 19 | return "" 20 | end 21 | 22 | -- It's easier to manipulate a single line as if it were a string 23 | if #lines == 1 then 24 | lines = lines[1] 25 | end 26 | 27 | if type(lines) == "string" then 28 | return string.sub(lines, start_col, finish_col) 29 | end 30 | 31 | lines[1] = string.sub(lines[1], start_col) 32 | lines[#lines] = string.sub(lines[#lines], 1, finish_col) 33 | 34 | -- Trim to simplify final expression 35 | local trimmed_lines = vim.iter(lines) 36 | :map(function(line) 37 | return vim.trim(line) 38 | end) 39 | :totable() 40 | 41 | return table.concat(trimmed_lines, "") 42 | end 43 | 44 | ---@return string 45 | M.get_current_expr = function() 46 | local mode = vim.fn.mode() 47 | 48 | if mode == "v" or mode == "V" then 49 | -- Return to normal mode 50 | local keys = api.nvim_replace_termcodes("", true, true, true) 51 | api.nvim_feedkeys(keys, "x", false) 52 | 53 | return M.get_trimmed_selection() 54 | end 55 | 56 | return vim.fn.expand("") 57 | end 58 | 59 | return M 60 | -------------------------------------------------------------------------------- /lua/dap-view/util/hl.lua: -------------------------------------------------------------------------------- 1 | local state = require("dap-view.state") 2 | local globals = require("dap-view.globals") 3 | 4 | local M = {} 5 | 6 | ---@param hl_group string 7 | ---@param start [integer,integer] 8 | ---@param finish [integer,integer] 9 | M.hl_range = function(hl_group, start, finish) 10 | vim.hl.range(state.bufnr, globals.NAMESPACE, "NvimDapView" .. hl_group, start, finish) 11 | end 12 | 13 | ---@param row integer 14 | ---@param len_path integer 15 | ---@param len_lnum integer 16 | M.highlight_file_name_and_line_number = function(row, len_path, len_lnum) 17 | local lnum_start = len_path + 1 18 | local lnum_end = lnum_start + len_lnum 19 | 20 | M.hl_range("FileName", { row, 0 }, { row, len_path }) 21 | M.hl_range("LineNumber", { row, lnum_start }, { row, lnum_end }) 22 | M.hl_range("Separator", { row, lnum_start - 1 }, { row, lnum_start }) 23 | M.hl_range("Separator", { row, lnum_end }, { row, lnum_end + 1 }) 24 | end 25 | 26 | return M 27 | -------------------------------------------------------------------------------- /lua/dap-view/util/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | M.inverted_directions = { 4 | ["above"] = "below", 5 | ["below"] = "above", 6 | ["right"] = "left", 7 | ["left"] = "right", 8 | } 9 | 10 | return M 11 | -------------------------------------------------------------------------------- /lua/dap-view/util/statusline.lua: -------------------------------------------------------------------------------- 1 | local global = require("dap-view.globals") 2 | 3 | local M = {} 4 | 5 | ---@param text string 6 | ---@param group string 7 | M.hl = function(text, group) 8 | return "%#" .. global.HL_PREFIX .. group .. "#" .. text .. "%*" 9 | end 10 | 11 | ---@param text string 12 | ---@param module string 13 | ---@param handler string 14 | ---@param idx integer 15 | M.clickable = function(text, module, handler, idx) 16 | return "%" .. idx .. "@v:lua.require'" .. module .. "'." .. handler .. "@" .. text .. "%T" 17 | end 18 | 19 | return M 20 | -------------------------------------------------------------------------------- /lua/dap-view/views/init.lua: -------------------------------------------------------------------------------- 1 | local state = require("dap-view.state") 2 | local winbar = require("dap-view.options.winbar") 3 | local hl = require("dap-view.util.hl") 4 | 5 | local M = {} 6 | 7 | local api = vim.api 8 | 9 | ---@param condition boolean 10 | ---@param message string 11 | M.cleanup_view = function(condition, message) 12 | assert(state.winnr ~= nil, "has nvim-dap-view window") 13 | 14 | if condition then 15 | vim.wo[state.winnr][0].cursorline = false 16 | 17 | api.nvim_buf_set_lines(state.bufnr, 0, -1, false, { message }) 18 | 19 | hl.hl_range("MissingData", { 0, 0 }, { 0, #message }) 20 | else 21 | vim.wo[state.winnr][0].cursorline = true 22 | end 23 | 24 | return condition 25 | end 26 | 27 | ---@param view dapview.SectionType 28 | M.switch_to_view = function(view) 29 | if not state.bufnr or not state.winnr or not api.nvim_win_is_valid(state.winnr) then 30 | return 31 | end 32 | 33 | local cursor_line = state.cur_pos[view] or 1 34 | 35 | if vim.tbl_contains({ "repl", "console" }, state.current_section) then 36 | api.nvim_win_call(state.winnr, function() 37 | api.nvim_set_current_buf(state.bufnr) 38 | end) 39 | end 40 | 41 | winbar.update_section(view) 42 | 43 | require("dap-view." .. view .. ".view").show() 44 | 45 | local buf_len = api.nvim_buf_line_count(state.bufnr) 46 | state.cur_pos[view] = math.min(cursor_line, buf_len) 47 | 48 | api.nvim_win_set_cursor(state.winnr, { state.cur_pos[view], 1 }) 49 | end 50 | 51 | return M 52 | -------------------------------------------------------------------------------- /lua/dap-view/views/keymaps/docs.lua: -------------------------------------------------------------------------------- 1 | local setup = require("dap-view.setup") 2 | 3 | local M = {} 4 | 5 | local api = vim.api 6 | 7 | ---@param height number 8 | ---@return number,number 9 | local create_win = function(height) 10 | local help_buf = api.nvim_create_buf(true, false) 11 | 12 | local width = math.floor(vim.go.columns * 0.4) 13 | 14 | local help_win = api.nvim_open_win(help_buf, true, { 15 | relative = "editor", 16 | row = math.floor(vim.go.lines / 2 - height / 2 - 1), 17 | col = math.floor(vim.go.columns / 2 - width / 2 - 1), 18 | width = width, 19 | height = height, 20 | border = setup.config.help.border, 21 | style = "minimal", 22 | title = "Keymaps", 23 | title_pos = "center", 24 | }) 25 | 26 | return help_buf, help_win 27 | end 28 | 29 | M.show_help = function() 30 | local content = { 31 | "## Scopes", 32 | "`` Expand or collapse a variable", 33 | " `o` Trigger actions", 34 | "## Threads", 35 | "`` Jump to a frame", 36 | " `t` Toggle subtle frames", 37 | "## Breakpoints", 38 | "`` Jump to a breakpoint", 39 | " `d` Delete a breakpoint", 40 | "## Watches", 41 | "`` Expand or collapse a variable", 42 | " `i` Insert an expression", 43 | " `d` Delete an expression", 44 | " `e` Edit an expression", 45 | " `c` Copy the value of an expression or variable", 46 | " `s` Set the value of an expression or variable", 47 | "## Exceptions", 48 | "`` Toggle filter", 49 | } 50 | 51 | local help_buf, help_win = create_win(#content) 52 | 53 | vim.keymap.set("n", "q", function() 54 | api.nvim_win_close(help_win, true) 55 | end, { buffer = help_buf }) 56 | 57 | api.nvim_buf_set_lines(help_buf, 0, -1, true, content) 58 | 59 | vim.treesitter.language.register("markdown", "dap-view-help") 60 | 61 | vim.bo[help_buf].filetype = "dap-view-help" 62 | vim.bo[help_buf].modifiable = false 63 | 64 | vim.wo[help_win][0].conceallevel = 2 65 | vim.wo[help_win][0].concealcursor = "nvc" 66 | 67 | vim.wo[help_win][0].cursorline = true 68 | vim.wo[help_win][0].cursorlineopt = "line" 69 | 70 | api.nvim_create_autocmd("WinClosed", { 71 | buffer = help_buf, 72 | callback = function() 73 | api.nvim_buf_delete(help_buf, { force = true }) 74 | end, 75 | }) 76 | end 77 | 78 | return M 79 | -------------------------------------------------------------------------------- /lua/dap-view/views/keymaps/init.lua: -------------------------------------------------------------------------------- 1 | local state = require("dap-view.state") 2 | local threads_view = require("dap-view.threads.view") 3 | local watches_actions = require("dap-view.watches.actions") 4 | local docs = require("dap-view.views.keymaps.docs") 5 | local keymap = require("dap-view.views.keymaps.util").keymap 6 | 7 | local M = {} 8 | 9 | local api = vim.api 10 | 11 | M.set_keymaps = function() 12 | keymap("", function() 13 | local cursor_line = api.nvim_win_get_cursor(state.winnr)[1] 14 | 15 | if state.current_section == "breakpoints" then 16 | require("dap-view.views.util").jump_to_location("^(.-)|(%d+)|") 17 | elseif state.current_section == "threads" then 18 | require("dap-view.threads.actions").jump_or_noop(cursor_line) 19 | elseif state.current_section == "exceptions" then 20 | require("dap-view.exceptions.actions").toggle_exception_filter() 21 | elseif state.current_section == "scopes" then 22 | require("dap.ui").trigger_actions({ mode = "first" }) 23 | elseif state.current_section == "watches" then 24 | watches_actions.expand_or_collapse(cursor_line) 25 | end 26 | end) 27 | 28 | keymap("o", function() 29 | if state.current_section == "scopes" then 30 | require("dap.ui").trigger_actions() 31 | end 32 | end) 33 | 34 | keymap("i", function() 35 | if state.current_section == "watches" then 36 | vim.ui.input({ prompt = "Expression: " }, function(input) 37 | if input then 38 | watches_actions.add_watch_expr(input) 39 | end 40 | end) 41 | end 42 | end) 43 | 44 | keymap("d", function() 45 | if state.current_section == "watches" then 46 | local cursor_line = api.nvim_win_get_cursor(state.winnr)[1] 47 | 48 | watches_actions.remove_watch_expr(cursor_line) 49 | 50 | require("dap-view.views").switch_to_view("watches") 51 | elseif state.current_section == "breakpoints" then 52 | require("dap-view.breakpoints.actions").remove_breakpoint() 53 | 54 | require("dap-view.breakpoints.view").show() 55 | end 56 | end) 57 | 58 | keymap("e", function() 59 | if state.current_section == "watches" then 60 | local cursor_line = api.nvim_win_get_cursor(state.winnr)[1] 61 | 62 | local expression = state.expressions_by_line[cursor_line] 63 | if expression then 64 | vim.ui.input({ prompt = "Expression: ", default = expression.name }, function(input) 65 | if input then 66 | watches_actions.edit_watch_expr(input, cursor_line) 67 | end 68 | end) 69 | end 70 | end 71 | end) 72 | 73 | keymap("c", function() 74 | if state.current_section == "watches" then 75 | local cursor_line = api.nvim_win_get_cursor(state.winnr)[1] 76 | 77 | watches_actions.copy_watch_expr(cursor_line) 78 | end 79 | end) 80 | 81 | keymap("s", function() 82 | if state.current_section == "watches" then 83 | local cursor_line = api.nvim_win_get_cursor(state.winnr)[1] 84 | 85 | local get_default = function() 86 | local expr = state.expressions_by_line[cursor_line] 87 | if expr and expr.expression and type(expr.expression.response) ~= "string" then 88 | return expr.expression.response.result 89 | end 90 | 91 | local var = state.variables_by_line[cursor_line] 92 | if var then 93 | return var.response.value 94 | end 95 | 96 | return "" 97 | end 98 | 99 | vim.ui.input({ prompt = "New value: ", default = get_default() }, function(input) 100 | if input then 101 | watches_actions.set_watch_expr(input, cursor_line) 102 | end 103 | end) 104 | end 105 | end) 106 | 107 | keymap("t", function() 108 | if state.current_section == "threads" then 109 | state.subtle_frames = not state.subtle_frames 110 | 111 | threads_view.show() 112 | end 113 | end) 114 | 115 | keymap("g?", function() 116 | docs.show_help() 117 | end) 118 | end 119 | 120 | return M 121 | -------------------------------------------------------------------------------- /lua/dap-view/views/keymaps/util.lua: -------------------------------------------------------------------------------- 1 | local state = require("dap-view.state") 2 | 3 | local M = {} 4 | 5 | ---@param lhs string 6 | ---@param rhs string|function 7 | ---@param opts? table 8 | ---@param mode? string|string[] 9 | function M.keymap(lhs, rhs, opts, mode) 10 | opts = opts == nil and { buffer = state.bufnr, nowait = true } or opts 11 | mode = mode or "n" 12 | vim.keymap.set(mode, lhs, rhs, opts) 13 | end 14 | 15 | return M 16 | -------------------------------------------------------------------------------- /lua/dap-view/views/options.lua: -------------------------------------------------------------------------------- 1 | local state = require("dap-view.state") 2 | 3 | local M = {} 4 | 5 | M.set_options = function() 6 | local win = vim.wo[state.winnr][0] 7 | win.scrolloff = 99 8 | win.wrap = false 9 | win.number = false 10 | win.relativenumber = false 11 | win.winfixheight = true 12 | win.cursorlineopt = "line" 13 | win.cursorline = true 14 | win.statuscolumn = "" 15 | win.foldcolumn = "0" 16 | 17 | local buf = vim.bo[state.bufnr] 18 | buf.buftype = "nofile" 19 | buf.swapfile = false 20 | buf.filetype = "dap-view" 21 | end 22 | 23 | return M 24 | -------------------------------------------------------------------------------- /lua/dap-view/views/util.lua: -------------------------------------------------------------------------------- 1 | local setup = require("dap-view.setup") 2 | local window = require("dap-view.views.windows") 3 | local util = require("dap-view.util") 4 | 5 | local M = {} 6 | 7 | local api = vim.api 8 | local log = vim.log.levels 9 | 10 | ---@param pattern string 11 | ---@return number?, number? 12 | M.get_bufnr = function(pattern) 13 | local line = vim.fn.getline(".") 14 | 15 | if not line or line == "" then 16 | vim.notify("No valid line under the cursor", log.ERROR) 17 | return 18 | end 19 | 20 | local file, line_num = line:match(pattern) 21 | if not file or not line_num then 22 | vim.notify("Invalid format: " .. line, log.ERROR) 23 | return 24 | end 25 | 26 | line_num = tonumber(line_num) 27 | if not line_num then 28 | vim.notify("Invalid line number: " .. line_num, log.ERROR) 29 | return 30 | end 31 | 32 | local abs_path = vim.fn.fnamemodify(file, ":p") 33 | if not vim.uv.fs_stat(abs_path) then 34 | vim.notify("File not found: " .. abs_path, log.ERROR) 35 | return 36 | end 37 | 38 | local bufnr = vim.uri_to_bufnr(vim.uri_from_fname(abs_path)) 39 | 40 | return bufnr, line_num 41 | end 42 | 43 | ---@param pattern string 44 | ---@param column? number 45 | M.jump_to_location = function(pattern, column) 46 | local bufnr, line_num = M.get_bufnr(pattern) 47 | 48 | if bufnr == nil then 49 | return 50 | end 51 | 52 | local config = setup.config 53 | 54 | local switchbufopt = config.switchbuf 55 | local win = window.get_win_respecting_switchbuf(switchbufopt, bufnr) 56 | 57 | if not win then 58 | win = api.nvim_open_win(0, true, { 59 | split = util.inverted_directions[config.windows.position], 60 | win = -1, 61 | height = config.windows.height < 1 and math.floor(vim.go.lines * (1 - config.windows.height)) 62 | or vim.go.lines - config.windows.height, 63 | }) 64 | end 65 | 66 | api.nvim_win_call(win, function() 67 | api.nvim_set_current_buf(bufnr) 68 | end) 69 | 70 | api.nvim_win_set_cursor(win, { line_num, column or 0 }) 71 | 72 | api.nvim_set_current_win(win) 73 | end 74 | 75 | return M 76 | -------------------------------------------------------------------------------- /lua/dap-view/views/windows/init.lua: -------------------------------------------------------------------------------- 1 | local switchbuf = require("dap-view.views.windows.switchbuf") 2 | 3 | local M = {} 4 | 5 | local api = vim.api 6 | 7 | ---@param switchbufopt string 8 | ---@param bufnr integer 9 | M.get_win_respecting_switchbuf = function(switchbufopt, bufnr) 10 | local winnr = api.nvim_get_current_win() 11 | 12 | local switchbuf_winfn = switchbuf.switchbuf_winfn 13 | 14 | if switchbufopt:find("usetab") then 15 | switchbuf_winfn.useopen = switchbuf_winfn.usetab 16 | end 17 | 18 | local opts = vim.split(switchbufopt, ",", { plain = true }) 19 | for _, opt in pairs(opts) do 20 | local winfn = switchbuf.switchbuf_winfn[opt] 21 | if winfn then 22 | local win = winfn(bufnr, winnr) 23 | if win then 24 | return win 25 | end 26 | end 27 | end 28 | end 29 | 30 | return M 31 | -------------------------------------------------------------------------------- /lua/dap-view/views/windows/switchbuf.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local api = vim.api 4 | 5 | ---@type table 6 | M.switchbuf_winfn = {} 7 | 8 | M.switchbuf_winfn.newtab = function(bufnr) 9 | -- Can't create a new tab with lua API 10 | -- https://github.com/neovim/neovim/pull/27223 11 | -- Even when assigning the buffer, the tab is created with the original buffer 12 | vim.cmd.tabnew(api.nvim_buf_get_name(bufnr)) 13 | return api.nvim_get_current_win() 14 | end 15 | 16 | M.switchbuf_winfn.useopen = function(bufnr, winnr) 17 | if api.nvim_win_get_buf(winnr) == bufnr then 18 | return winnr 19 | end 20 | 21 | local windows = api.nvim_tabpage_list_wins(0) 22 | 23 | for _, win in ipairs(windows) do 24 | if api.nvim_win_get_buf(win) == bufnr then 25 | return win 26 | end 27 | end 28 | 29 | return nil 30 | end 31 | 32 | M.switchbuf_winfn.usetab = function(bufnr, winnr) 33 | if api.nvim_win_get_buf(winnr) == bufnr then 34 | return winnr 35 | end 36 | 37 | local tabs = { 0 } 38 | 39 | vim.list_extend(tabs, api.nvim_list_tabpages()) 40 | 41 | for _, tabpage in ipairs(tabs) do 42 | for _, win in ipairs(api.nvim_tabpage_list_wins(tabpage)) do 43 | if api.nvim_win_get_buf(win) == bufnr then 44 | api.nvim_set_current_tabpage(tabpage) 45 | return win 46 | end 47 | end 48 | end 49 | 50 | return nil 51 | end 52 | 53 | M.switchbuf_winfn.uselast = function(_, winnr) 54 | local cur_buf = api.nvim_get_current_buf() 55 | 56 | if vim.bo[cur_buf].buftype == "" then 57 | return winnr 58 | else 59 | local win = vim.fn.win_getid(vim.fn.winnr("#")) 60 | if win then 61 | return win 62 | end 63 | 64 | return nil 65 | end 66 | end 67 | 68 | return M 69 | -------------------------------------------------------------------------------- /lua/dap-view/watches/actions.lua: -------------------------------------------------------------------------------- 1 | local state = require("dap-view.state") 2 | local guard = require("dap-view.guard") 3 | local eval = require("dap-view.watches.eval") 4 | local set = require("dap-view.watches.set") 5 | local traversal = require("dap-view.tree.traversal") 6 | 7 | local M = {} 8 | 9 | ---@param expr string 10 | ---@return boolean 11 | M.add_watch_expr = function(expr) 12 | if #expr == 0 or not guard.expect_session() then 13 | return false 14 | end 15 | 16 | eval.eval_expr(expr, function() 17 | require("dap-view.views").switch_to_view("watches") 18 | end) 19 | 20 | return true 21 | end 22 | 23 | ---@param line number 24 | M.remove_watch_expr = function(line) 25 | local expr = state.expressions_by_line[line] 26 | if expr then 27 | state.watched_expressions[expr.name] = nil 28 | else 29 | vim.notify("No expression under the under cursor") 30 | end 31 | end 32 | 33 | ---@param line number 34 | M.copy_watch_expr = function(line) 35 | local expr = state.expressions_by_line[line] 36 | if expr then 37 | eval.copy_expr(expr.name) 38 | else 39 | local var = state.variables_by_line[line] 40 | if var then 41 | if var.response.evaluateName then 42 | eval.copy_expr(var.response.evaluateName) 43 | else 44 | vim.notify("Missing `evaluateName`, can't copy variable") 45 | end 46 | else 47 | vim.notify("No expression or variable under the under cursor") 48 | end 49 | end 50 | end 51 | 52 | ---@param value string 53 | ---@param line number 54 | M.set_watch_expr = function(value, line) 55 | if not guard.expect_session() then 56 | return 57 | end 58 | 59 | local expr = state.expressions_by_line[line] 60 | if expr then 61 | -- Top level expressions are responses for the `evaluate` request, they have no `evaluateName` 62 | -- Therefore, we can always use `setExpression` if the adapter supports it 63 | set.set_expr(expr.name, value) 64 | 65 | -- Reset expanded state to avoid leftover lines from the previous expansion 66 | local watched_expr = state.watched_expressions[expr.name] 67 | watched_expr.expanded = false 68 | else 69 | local var = state.variables_by_line[line] 70 | 71 | if var then 72 | -- From the protocol: 73 | -- 74 | -- "If a debug adapter implements both `setExpression` and `setVariable`, 75 | -- a client uses `setExpression` if the variable has an evaluateName property." 76 | local session = assert(require("dap").session(), "has active session") 77 | local hasExpression = session.capabilities.supportsSetExpression 78 | local hasVariable = session.capabilities.supportsSetVariable 79 | 80 | if hasExpression and hasVariable then 81 | if var.response.evaluateName then 82 | set.set_expr(var.response.evaluateName, value) 83 | else 84 | set.set_var(var.response.name, value, var.reference) 85 | end 86 | elseif hasExpression then 87 | if var.response.evaluateName then 88 | set.set_expr(var.response.evaluateName, value) 89 | else 90 | return vim.notify( 91 | "Can't set value for " .. var.response.name .. " because it lacks an `evaluateName`" 92 | ) 93 | end 94 | elseif hasVariable then 95 | set.set_var(var.response.name, value, var.reference) 96 | else 97 | return vim.notify("Adapter lacks support for both `setExpression` and `setVariable` requests") 98 | end 99 | 100 | -- Reset expanded state to avoid leftover lines 101 | local var_state = traversal.find_node(var.response.variablesReference) 102 | var_state.expanded = false 103 | else 104 | vim.notify("No expression or variable under the under cursor") 105 | end 106 | end 107 | end 108 | 109 | ---@param expr string 110 | ---@param line number 111 | M.edit_watch_expr = function(expr, line) 112 | if #expr == 0 or not guard.expect_session() then 113 | return 114 | end 115 | 116 | -- The easiest way to edit is to delete and insert again 117 | M.remove_watch_expr(line) 118 | 119 | eval.eval_expr(expr, function() 120 | require("dap-view.views").switch_to_view("watches") 121 | end) 122 | end 123 | 124 | ---@param line number 125 | M.expand_or_collapse = function(line) 126 | if not guard.expect_session() then 127 | return 128 | end 129 | 130 | local expr = state.expressions_by_line[line] 131 | 132 | if expr then 133 | local e = state.watched_expressions[expr.name] 134 | if e then 135 | e.expanded = not e.expanded 136 | 137 | eval.eval_expr(expr.name, function() 138 | require("dap-view.views").switch_to_view("watches") 139 | end) 140 | end 141 | else 142 | local var = state.variables_by_line[line] 143 | 144 | if var then 145 | local reference = var.response.variablesReference 146 | if reference > 0 then 147 | local var_state = traversal.find_node(reference) 148 | if var_state then 149 | var_state.expanded = not var_state.expanded 150 | eval.expand_var(reference, var_state.children, function(result) 151 | var_state.children = result 152 | 153 | require("dap-view.views").switch_to_view("watches") 154 | end) 155 | end 156 | else 157 | vim.notify("Nothing to expand") 158 | end 159 | else 160 | vim.notify("No expression or variable under the under cursor") 161 | end 162 | end 163 | end 164 | 165 | return M 166 | -------------------------------------------------------------------------------- /lua/dap-view/watches/eval.lua: -------------------------------------------------------------------------------- 1 | local state = require("dap-view.state") 2 | 3 | local M = {} 4 | 5 | ---@param expr_name string 6 | ---@param callback? fun(): nil 7 | M.eval_expr = function(expr_name, callback) 8 | local session = assert(require("dap").session(), "has active session") 9 | 10 | coroutine.wrap(function() 11 | local frame_id = session.current_frame and session.current_frame.id 12 | 13 | local err, result = 14 | session:request("evaluate", { expression = expr_name, context = "watch", frameId = frame_id }) 15 | 16 | local previous_expr = state.watched_expressions[expr_name] 17 | 18 | local previous_result = previous_expr and previous_expr.response and previous_expr.response.result 19 | if previous_expr and result then 20 | previous_expr.updated = previous_result ~= result.result 21 | end 22 | 23 | if err and previous_expr then 24 | previous_expr.children = nil 25 | end 26 | 27 | local response = err and tostring(err) or result 28 | 29 | ---@type dapview.ExpressionPack 30 | local new_expr 31 | if previous_expr then 32 | previous_expr.response = response 33 | new_expr = previous_expr 34 | else 35 | new_expr = { response = response, updated = false, expanded = true, children = nil } 36 | end 37 | 38 | local variables_reference 39 | if new_expr and type(new_expr.response) == "table" then 40 | variables_reference = new_expr.response.variablesReference 41 | end 42 | 43 | local is_expanded = previous_expr and previous_expr.expanded or not previous_expr 44 | 45 | if is_expanded and variables_reference and variables_reference > 0 then 46 | M.expand_var(variables_reference, new_expr.children, function(children) 47 | new_expr.children = children 48 | 49 | state.watched_expressions[expr_name] = new_expr 50 | 51 | if callback then 52 | callback() 53 | end 54 | end) 55 | else 56 | state.watched_expressions[expr_name] = new_expr 57 | 58 | if callback then 59 | callback() 60 | end 61 | end 62 | end)() 63 | end 64 | 65 | ---@param expr string 66 | M.copy_expr = function(expr) 67 | local session = assert(require("dap").session(), "has active session") 68 | 69 | if session.capabilities.supportsClipboardContext then 70 | coroutine.wrap(function() 71 | local frame_id = session.current_frame and session.current_frame.id 72 | 73 | local err, result = 74 | session:request("evaluate", { expression = expr, context = "clipboard", frameId = frame_id }) 75 | 76 | if err == nil and result then 77 | -- TODO uses system clipboard, could be a parameter instead 78 | vim.fn.setreg("+", result.result) 79 | 80 | vim.notify("Variable " .. expr .. " copied to clipboard") 81 | end 82 | end)() 83 | else 84 | vim.notify("Adapter doesn't support clipboard evaluation") 85 | end 86 | end 87 | 88 | ---@param variables_reference number 89 | ---@param original (dapview.VariablePack[] | string)? 90 | ---@param callback fun(result: dapview.VariablePack[] | string): nil 91 | function M.expand_var(variables_reference, original, callback) 92 | local session = assert(require("dap").session(), "has active session") 93 | 94 | local frame_id = session.current_frame and session.current_frame.id 95 | 96 | session:request( 97 | "variables", 98 | { variablesReference = variables_reference, context = "watch", frameId = frame_id }, 99 | -- HACK Using a callback was the only way I could make this work 100 | function(err, result) 101 | local response = nil 102 | if err then 103 | response = tostring(err) 104 | end 105 | if result then 106 | response = result.variables 107 | end 108 | 109 | ---@type dapview.VariablePack[] | string 110 | local variables = type(response) == "string" and response or {} 111 | 112 | if type(variables) ~= "string" and type(response) ~= "string" then 113 | for k, var in pairs(response or {}) do 114 | ---@type dapview.VariablePack? 115 | local previous 116 | 117 | if type(original) ~= "string" then 118 | ---@type dapview.VariablePack? 119 | previous = vim.iter(original or {}):find( 120 | ---@param v dapview.VariablePack 121 | function(v) 122 | if v.variable.evaluateName then 123 | return v.variable.evaluateName == var.evaluateName 124 | end 125 | if v.variable.variablesReference > 0 then 126 | return v.variable.variablesReference == var.variablesReference 127 | end 128 | end 129 | ) 130 | if err and previous then 131 | previous.children = nil 132 | previous.expanded = false 133 | previous.updated = false 134 | end 135 | end 136 | 137 | if previous and not err then 138 | previous.updated = previous.variable.value ~= var.value 139 | 140 | previous.variable = var 141 | end 142 | 143 | local default_var = { variable = var, updated = false, expanded = false, children = nil } 144 | 145 | local new_var = previous or default_var 146 | 147 | if previous and previous.expanded and previous.variable.variablesReference > 0 then 148 | M.expand_var( 149 | previous.variable.variablesReference, 150 | new_var.children, 151 | function(children) 152 | new_var.children = children 153 | 154 | variables[k] = new_var 155 | end 156 | ) 157 | else 158 | variables[k] = new_var 159 | end 160 | end 161 | end 162 | 163 | callback(variables) 164 | end 165 | ) 166 | end 167 | 168 | M.reeval = function() 169 | for expr, _ in pairs(state.watched_expressions) do 170 | M.eval_expr(expr) 171 | end 172 | 173 | if state.current_section == "watches" then 174 | require("dap-view.views").switch_to_view("watches") 175 | end 176 | end 177 | 178 | return M 179 | -------------------------------------------------------------------------------- /lua/dap-view/watches/set.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---@param expr string 4 | ---@param value string 5 | M.set_expr = function(expr, value) 6 | local session = assert(require("dap").session(), "has active session") 7 | 8 | if session.capabilities.supportsSetExpression then 9 | coroutine.wrap(function() 10 | local frame_id = session.current_frame and session.current_frame.id 11 | 12 | local err, _ = 13 | session:request("setExpression", { expression = expr, value = value, frameId = frame_id }) 14 | 15 | if err then 16 | vim.notify("Failed to set expression " .. expr .. " to value " .. value) 17 | end 18 | end)() 19 | else 20 | vim.notify("Adapter doesn't support `setExpression` request") 21 | end 22 | end 23 | 24 | ---@param name string 25 | ---@param value string 26 | ---@param variables_reference number 27 | M.set_var = function(name, value, variables_reference) 28 | local session = assert(require("dap").session(), "has active session") 29 | 30 | if session.capabilities.supportsSetVariable then 31 | coroutine.wrap(function() 32 | local err, _ = session:request( 33 | "setVariable", 34 | { name = name, value = value, variablesReference = variables_reference } 35 | ) 36 | 37 | if err then 38 | vim.notify("Failed to set variable " .. name .. " to value " .. value) 39 | end 40 | end)() 41 | else 42 | vim.notify("Adapter doesn't support `setVariable` request") 43 | end 44 | end 45 | 46 | return M 47 | -------------------------------------------------------------------------------- /lua/dap-view/watches/view.lua: -------------------------------------------------------------------------------- 1 | local state = require("dap-view.state") 2 | local views = require("dap-view.views") 3 | local hl = require("dap-view.util.hl") 4 | 5 | local M = {} 6 | 7 | local api = vim.api 8 | 9 | -- TODO There's also 'list' and 'dict' 10 | local types_to_hl_group = { 11 | boolean = "Boolean", 12 | str = "String", 13 | string = "String", 14 | int = "Number", 15 | long = "Number", 16 | number = "Number", 17 | double = "Float", 18 | float = "Float", 19 | ["function"] = "Function", 20 | } 21 | 22 | ---@param children dapview.VariablePack[] 23 | ---@param reference number 24 | ---@param line number 25 | ---@param depth number 26 | ---@return integer 27 | local function show_variables(children, reference, line, depth) 28 | for _, var in pairs(children or {}) do 29 | local variable = var.variable 30 | local value = #variable.value > 0 and variable.value 31 | or variable.variablesReference > 0 and "..." 32 | or "" 33 | local content = variable.name .. " = " .. value 34 | 35 | -- Can't have linebreaks with nvim_buf_set_lines 36 | local trimmed_content = content:gsub("%s+", " ") 37 | 38 | local indent = string.rep("\t", depth) 39 | api.nvim_buf_set_lines(state.bufnr, line, line, true, { indent .. trimmed_content }) 40 | 41 | hl.hl_range("WatchExpr", { line, depth }, { line, depth + #variable.name }) 42 | 43 | local hl_group = var.updated and "WatchUpdated" 44 | or variable.type and types_to_hl_group[variable.type:lower()] 45 | if hl_group then 46 | local _hl_start = #variable.name + 3 + depth 47 | hl.hl_range(hl_group, { line, _hl_start }, { line, -1 }) 48 | end 49 | 50 | line = line + 1 51 | 52 | state.variables_by_line[line] = { response = variable, reference = reference } 53 | 54 | if var.expanded and var.children then 55 | if type(var.children) == "string" then 56 | local err_content = string.rep("\t", depth + 1) .. var.children 57 | api.nvim_buf_set_lines(state.bufnr, line, line, true, { err_content }) 58 | 59 | hl.hl_range("WatchError", { line, 0 }, { line, #err_content }) 60 | 61 | line = line + 1 62 | else 63 | ---@cast var dapview.VariablePackStrict 64 | line = show_variables(var.children, var.reference, line, depth + 1) 65 | end 66 | end 67 | end 68 | return line 69 | end 70 | 71 | ---@param line integer 72 | ---@param variables string|dapview.ExpressionPack 73 | ---@return integer 74 | local show_variables_or_err = function(line, variables) 75 | local children = variables.children 76 | if type(children) == "string" then 77 | local var_content = "\t" .. variables 78 | api.nvim_buf_set_lines(state.bufnr, line, line, true, { var_content }) 79 | 80 | hl.hl_range("WatchError", { line, 0 }, { line, #var_content }) 81 | 82 | line = line + 1 83 | elseif children ~= nil and variables.expanded then 84 | line = show_variables(children, variables.response.variablesReference, line, 1) 85 | end 86 | return line 87 | end 88 | 89 | M.show = function() 90 | if not state.winnr or not api.nvim_win_is_valid(state.winnr) then 91 | return 92 | end 93 | 94 | -- Since variables aren't ordered, lines may change unexpectedly 95 | -- To handle that, always clear the storage table 96 | for k, _ in pairs(state.expressions_by_line) do 97 | state.expressions_by_line[k] = nil 98 | end 99 | -- Also clear variables for the same reason 100 | for k, _ in pairs(state.variables_by_line) do 101 | state.variables_by_line[k] = nil 102 | end 103 | 104 | if views.cleanup_view(vim.tbl_isempty(state.watched_expressions), "No expressions") then 105 | return 106 | end 107 | 108 | if state.bufnr then 109 | local line = 0 110 | 111 | for expr_name, expr in pairs(state.watched_expressions) do 112 | local response = expr.response 113 | 114 | assert(response ~= nil, "Response exists") 115 | 116 | local result = type(response) == "string" and response or response.result 117 | 118 | local content = expr_name .. " = " .. result 119 | 120 | -- Can't have linebreaks with nvim_buf_set_lines 121 | local trimmed_content = content:gsub("%s+", " ") 122 | 123 | api.nvim_buf_set_lines(state.bufnr, line, line, true, { trimmed_content }) 124 | 125 | hl.hl_range("WatchExpr", { line, 0 }, { line, #expr_name }) 126 | 127 | local hl_group = type(response) == "string" and "WatchError" 128 | or expr.updated and "WatchUpdated" 129 | or response and response.type and types_to_hl_group[response.type:lower()] 130 | if hl_group then 131 | local hl_start = #expr_name + 3 132 | hl.hl_range(hl_group, { line, hl_start }, { line, -1 }) 133 | end 134 | 135 | line = line + 1 136 | 137 | state.expressions_by_line[line] = { name = expr_name, response = response } 138 | 139 | line = show_variables_or_err(line, expr) 140 | end 141 | 142 | api.nvim_buf_set_lines(state.bufnr, line, -1, true, {}) 143 | end 144 | end 145 | 146 | return M 147 | -------------------------------------------------------------------------------- /plugin/dap-view.lua: -------------------------------------------------------------------------------- 1 | local command = vim.api.nvim_create_user_command 2 | 3 | command("DapViewOpen", function() 4 | require("dap-view").open() 5 | end, {}) 6 | command("DapViewClose", function(opts) 7 | require("dap-view").close(opts.bang) 8 | end, { bang = true }) 9 | command("DapViewToggle", function(opts) 10 | require("dap-view").toggle(opts.bang) 11 | end, { bang = true }) 12 | command("DapViewWatch", function(opts) 13 | local expr = nil 14 | if opts.range > 0 then 15 | expr = require("dap-view.util.exprs").get_trimmed_selection() 16 | elseif #opts.fargs > 0 then 17 | expr = table.concat(opts.fargs, " ") 18 | end 19 | require("dap-view").add_expr(expr) 20 | end, { 21 | nargs = "*", 22 | range = true, 23 | }) 24 | command("DapViewJump", function(opts) 25 | require("dap-view").jump_to_view(opts.fargs[1]) 26 | end, { 27 | nargs = 1, 28 | ---@param arg_lead string 29 | complete = function(arg_lead) 30 | return require("dap-view.complete").complete_sections(arg_lead) 31 | end, 32 | }) 33 | command("DapViewShow", function(opts) 34 | require("dap-view").show_view(opts.fargs[1]) 35 | end, { 36 | nargs = 1, 37 | ---@param arg_lead string 38 | complete = function(arg_lead) 39 | return require("dap-view.complete").complete_sections(arg_lead) 40 | end, 41 | }) 42 | --------------------------------------------------------------------------------