├── .gitignore ├── .luacheckrc ├── LICENSE ├── README.md ├── images └── configuration_selection.png └── lua └── dap-cs.lua /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | globals = { 2 | "vim", 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-dap-cs 2 | 3 | An extension for [nvim-dap][1] providing adapter and configurations for launching .NET Core debugger ([netcoredbg][2]). 4 | 5 | 6 | ![Configuration Selection Screenshot](./images/configuration_selection.png "Configuration Selection Screenshot") 7 | 8 | ## Features 9 | 10 | - Launch a .NET Core project 11 | - Attach to a .NET Core process 12 | - Smart attach to a .NET Core process 13 | 14 | ### Requirements 15 | 16 | - [Neovim][3] 17 | - [nvim-dap][1] - DAP client for [Neovim][3] 18 | - [netcoredbg][2] - .NET Core debugger 19 | 20 | ### Installation 21 | - Install like any other neovim plugin: 22 | - If using [vim-plug][4]: `Plug 'nicholasmata/nvim-dap-cs'` 23 | - If using [packer.nvim][5]: `use 'nicholasmata/nvim-dap-cs'` 24 | - If using [lazy.nvim][6]: `{ 'nicholasmata/nvim-dap-cs', dependencies = { 'mfussenegger/nvim-dap' } }` 25 | 26 | ### Usage 27 | 28 | ### Register the plugin 29 | 30 | Call the setup function in your `init.vim` to register the adapter and the configurations: 31 | 32 | ```vimL 33 | require('dap-cs').setup() 34 | ``` 35 | 36 | ### Configuring 37 | 38 | It is possible to customize nvim-dap-cs by passing a config table in the setup function. 39 | 40 | The example below shows all the possible configurations: 41 | 42 | ```lua 43 | require('dap-cs').setup( 44 | -- Additional dap configurations can be added. 45 | -- dap_configurations accepts a list of tables where each entry 46 | -- represents a dap configuration. For more details do: 47 | -- :help dap-configuration 48 | dap_configurations = { 49 | { 50 | -- Must be "coreclr" or it will be ignored by the plugin 51 | type = "coreclr", 52 | name = "Attach remote", 53 | mode = "remote", 54 | request = "attach", 55 | }, 56 | }, 57 | netcoredbg = { 58 | -- the path to the executable netcoredbg which will be used for debugging. 59 | -- by default, this is the "netcoredbg" executable on your PATH. 60 | path = "netcoredbg" 61 | } 62 | ) 63 | ``` 64 | 65 | [1]: https://github.com/mfussenegger/nvim-dap 66 | [2]: https://github.com/Samsung/netcoredbg 67 | [3]: https://github.com/neovim/neovim 68 | [4]: https://github.com/junegunn/vim-plug 69 | [5]: https://github.com/wbthomason/packer.nvim 70 | [6]: https://github.com/folke/lazy.nvim 71 | -------------------------------------------------------------------------------- /images/configuration_selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NicholasMata/nvim-dap-cs/16e5debe8cb7fb73c8799d20969ee00883586602/images/configuration_selection.png -------------------------------------------------------------------------------- /lua/dap-cs.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local default_config = { 4 | netcoredbg = { 5 | path = "netcoredbg", 6 | }, 7 | } 8 | 9 | local load_module = function(module_name) 10 | local ok, module = pcall(require, module_name) 11 | assert(ok, string.format("dap-cs dependency error: %s not installed", module_name)) 12 | return module 13 | end 14 | 15 | local number_indices = function(array) 16 | local result = {} 17 | for i, value in ipairs(array) do 18 | result[i] = i .. ": " .. value 19 | end 20 | return result 21 | end 22 | 23 | local display_options = function(prompt_title, options) 24 | options = number_indices(options) 25 | table.insert(options, 1, prompt_title) 26 | 27 | local choice = vim.fn.inputlist(options) 28 | 29 | if choice > 0 then 30 | return options[choice + 1] 31 | else 32 | return nil 33 | end 34 | end 35 | 36 | 37 | local file_selection = function(cmd, opts) 38 | local results = vim.fn.systemlist(cmd) 39 | 40 | if #results == 0 then 41 | print(opts.empty_message) 42 | return 43 | end 44 | 45 | if opts.allow_multiple then 46 | return results 47 | end 48 | 49 | local result = results[1] 50 | if #results > 1 then 51 | result = display_options(opts.multiple_title_message, results) 52 | end 53 | 54 | return result 55 | end 56 | 57 | local project_selection = function(project_path, allow_multiple) 58 | local check_csproj_cmd = string.format('find %s -type f -name "*.csproj"', project_path) 59 | local project_file = file_selection(check_csproj_cmd, { 60 | empty_message = 'No csproj files found in ' .. project_path, 61 | multiple_title_message = 'Select project:', 62 | allow_multiple = allow_multiple 63 | }) 64 | return project_file 65 | end 66 | 67 | local select_dll = function(project_path) 68 | local bin_path = project_path .. '/bin' 69 | 70 | local check_net_folders_cmd = string.format('find %s -type d -name "net*"', bin_path) 71 | local net_bin = file_selection(check_net_folders_cmd, { 72 | empty_message = 'No dotnet directories found in the "bin" directory. Ensure project has been built.', 73 | multiple_title_message = "Select NET Version:" 74 | }) 75 | if net_bin == nil then 76 | return 77 | end 78 | 79 | local project_file = project_selection(project_path) 80 | if project_file == nil then 81 | return 82 | end 83 | local project_name = vim.fn.fnamemodify(project_file, ":t:r") 84 | 85 | local dll_path = net_bin .. '/' .. project_name .. '.dll' 86 | return dll_path 87 | end 88 | 89 | 90 | --- Attempts to pick a process smartly. 91 | --- 92 | --- Does the following: 93 | --- 1. Gets all project files 94 | --- 2. Build filter 95 | --- 2a. If a single project is found then will filter for processes ending with project name. 96 | --- 2b. If multiple projects found then will filter for processes ending with any of the project file names. 97 | --- 2c. If no project files found then will filter for processes starting with "dotnet" 98 | --- 3. If a single process matches then auto selects it. If multiple found then displays it user for selection. 99 | local smart_pick_process = function(dap_utils, project_path) 100 | local project_file = project_selection(project_path, true) 101 | if project_file == nil then 102 | return 103 | end 104 | 105 | local filter = function(proc) 106 | if type(project_file) == "table" then 107 | for _, file in pairs(project_file) do 108 | local project_name = vim.fn.fnamemodify(file, ":t:r") 109 | if vim.endswith(proc.name, project_name) then 110 | return true 111 | end 112 | end 113 | return false 114 | elseif type(project_file) == "string" then 115 | local project_name = vim.fn.fnamemodify(project_file, ":t:r") 116 | return vim.startswith(proc.name, project_name or "dotnet") 117 | end 118 | end 119 | 120 | local processes = dap_utils.get_processes() 121 | processes = vim.tbl_filter(filter, processes) 122 | 123 | if #processes == 0 then 124 | print("No dotnet processes could be found automatically. Try 'Attach' instead") 125 | return 126 | end 127 | 128 | if #processes > 1 then 129 | return dap_utils.pick_process({ 130 | filter = filter 131 | }) 132 | end 133 | 134 | return processes[1].pid 135 | end 136 | 137 | local setup_configuration = function(dap, dap_utils, config) 138 | dap.configurations.cs = { 139 | { 140 | type = "coreclr", 141 | name = "Launch", 142 | request = "launch", 143 | program = function() 144 | local current_working_dir = vim.fn.getcwd() 145 | return select_dll(current_working_dir) or dap.ABORT 146 | end, 147 | }, 148 | { 149 | type = "coreclr", 150 | name = "Attach", 151 | request = "attach", 152 | processId = dap_utils.pick_process, 153 | }, 154 | 155 | { 156 | type = "coreclr", 157 | name = "Attach (Smart)", 158 | request = "attach", 159 | processId = function() 160 | local current_working_dir = vim.fn.getcwd() 161 | return smart_pick_process(dap_utils, current_working_dir) or dap.ABORT 162 | end, 163 | }, 164 | } 165 | 166 | 167 | if config == nil or config.dap_configurations == nil then 168 | return 169 | end 170 | 171 | for _, dap_config in ipairs(config.dap_configurations) do 172 | if dap_config.type == "coreclr" then 173 | table.insert(dap.configurations.cs, dap_config) 174 | end 175 | end 176 | end 177 | 178 | local setup_adapter = function(dap, config) 179 | dap.adapters.coreclr = { 180 | type = 'executable', 181 | command = config.netcoredbg.path, 182 | args = { '--interpreter=vscode' } 183 | } 184 | end 185 | 186 | function M.setup(opts) 187 | local config = vim.tbl_deep_extend("force", default_config, opts or {}) 188 | local dap = load_module("dap") 189 | local dap_utils = load_module("dap.utils") 190 | setup_adapter(dap, config) 191 | setup_configuration(dap, dap_utils, config) 192 | end 193 | 194 | return M 195 | --------------------------------------------------------------------------------