diff --git a/CHANGELOG.md b/CHANGELOG.md
index 280f1f8..7d030d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Enhanced experience to select and launch debugging target
- Commands to build and run dotnet projects
+## [0.2.0] - 2024-02-19
+
+### Added
+
+- Automatic debugger installation and configuration
+- Effortless debugging experience
+- Run projects
+- Support [launch settings](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-8.0#lsj)
+
+### Fixed
+
+- The logger is using an incorrect log level
+
## [0.1.0] - 2024-02-14
### Added
diff --git a/README.md b/README.md
index 07f5764..08f898d 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
## Prerequisites
-- Install [fd](https://github.com/sharkdp/fd#installation) locally.
+- Locally Install [fd](https://github.com/sharkdp/fd#installation).
## 🚀 Installation
@@ -25,8 +25,6 @@ Using lazy.nvim:
}
```
-:warning: This plugin removes the usage of lspconfig to configure and run Omnisharp, and it shouldn't be used alongside lspconfig. Please remove the configuration of omnisharp in lspconfig. If you want to use lspconfig to configure Omnisharp, you can still use the other functionality provided by the plugin (e.g., remove unused using statements, etc.). However, you should set `config.lsp.enable` to `false`.
-
## âš™ Configuration
```lua
@@ -59,14 +57,48 @@ Using lazy.nvim:
-- The minimum log level.
level = "INFO",
},
+ dap = {
+ -- When set, csharp.nvim won't launch install and debugger automatically. Instead, it'll use the debug adapter specified.
+ --- @type string?
+ adapter_name = nil,
+ }
}
```
## 🌟 Features
-### Remove Unnecessary Using Statements
+### Automatically Installs and Configures LSP
-![csharp_fix_usings](https://github.com/iabdelkareem/csharp.nvim/assets/13891133/3902ef06-b2a0-4be8-b138-222c820cf4d6)
+The plugin will automatically install the LSP `omnisharp` and configure it for use.
+
+_:warning: Remove omnisharp configuration from lspconfig as the plugin handles configuring and running omnisharp. If you prefer configuring omnisharp manually using lspconfig, disable this feature by setting lsp.enable = false in the configuration._
+
+
+
+### Effortless Debugging
+
+The plugin will automatically install the debugger `netcoredbg` and configure it for use. The goal of this functionality is to provide an effortless debugging experience to .NET developers, all you need to do is install the plugin and execute `require("csharp").debug_project()` and the plugin will take care of the rest. To make this possible the debugger supports the following features:
+
+- Automatically detects the executable project in the solution, or let you select if there are multiple executable projects.
+- Supports [launch settings](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-8.0#lsj) to configure `environmentVariables`, `applicationUrl`, and `commandLineArgs`.
+ - _Support is limited to launch profiles with `CommandName == Project`._
+- Uses .NET CLI to build the debugee project.
+
+![debugging](https://gist.github.com/assets/13891133/ff442270-4c0e-46d6-bf8e-25deab8dec37)
+
+_In the illustration above, there's a solution with 3 projects, 2 of which are executable, and only one has launch settings file._
+
+
+
+### Run Project
+
+Similar to the debugger, the plugin exposes the function `require("csharp").run_project()` that supports selection of an executable project, launch profile, builds and runs the project.
+
+![run](https://github.com/iabdelkareem/csharp.nvim/assets/13891133/aa1df4e3-d3ce-43b8-a0d5-476e1b567125)
+
+
+
+### Remove Unnecessary Using Statements
Removes all unnecessary using statements from a document. Trigger this feature via the Command `:CsharpFixUsings` or use the Lua function below.
@@ -74,6 +106,8 @@ Removes all unnecessary using statements from a document. Trigger this feature v
require("csharp").fix_usings()
```
+![csharp_fix_usings](https://github.com/iabdelkareem/csharp.nvim/assets/13891133/3902ef06-b2a0-4be8-b138-222c820cf4d6)
+
_TIP: You can run this feature automatically before a buffer is saved._
```lua
@@ -99,9 +133,9 @@ vim.api.nvim_create_autocmd("LspAttach", {
})
```
-### Fix All
+
-![csharp_fix_all](https://github.com/iabdelkareem/csharp.nvim/assets/13891133/5d815ce4-b9b1-40b9-a049-df1570bea100)
+### Fix All
This feature allows developers to efficiently resolve a specific problem across multiple instances in the codebase (e.g., a document, project, or solution) with a single command. You can run this feature using the Command `:CsharpFixAll` or the Lua function below. When the command runs, it'll launch a dropdown menu asking you to choose the scope in which you want the plugin to search for fixes before it presents the different options to you.
@@ -109,9 +143,11 @@ This feature allows developers to efficiently resolve a specific problem across
require("csharp").fix_all()
```
-### Enhanced Go-To-Definition (Decompilation Support)
+![csharp_fix_all](https://github.com/iabdelkareem/csharp.nvim/assets/13891133/5d815ce4-b9b1-40b9-a049-df1570bea100)
-![csharp_go_to_definition](https://github.com/iabdelkareem/csharp.nvim/assets/13891133/1b8ea6fa-6d6b-4cab-a060-2123247b0d74)
+
+
+### Enhanced Go-To-Definition (Decompilation Support)
Similar to [omnisharp-extended-lsp.nvim](https://github.com/Hoffs/omnisharp-extended-lsp.nvim), this feature allows developers to navigate to the definition of a symbol in the codebase with decompilation support for external code.
@@ -119,9 +155,26 @@ Similar to [omnisharp-extended-lsp.nvim](https://github.com/Hoffs/omnisharp-exte
require("csharp").go_to_definition()
```
+![csharp_go_to_definition](https://github.com/iabdelkareem/csharp.nvim/assets/13891133/1b8ea6fa-6d6b-4cab-a060-2123247b0d74)
+
+
+
+## :beetle: Reporting Bugs
+
+1. Set debug level to TRACE via the configurations.
+2. Reproduce the issue.
+3. Open an issue in [GitHub](https://github.com/iabdelkareem/csharp.nvim/issues) with the following details:
+ - Description of the bug.
+ - How to reproduce.
+ - Relevant logs, if possible.
+
+## :heart_eyes: Contributing & Feature Suggestions
+
+I'd love to hear your ideas and suggestions for new features! Feel free to create an issue and share your thoughts. We can't wait to discuss them and bring them to life!
+
## TODO
-- [ ] Setup Debugger
+- [x] Setup Debugger
- [ ] Solution Explorer
- [ ] Switching Solution
- [ ] Support Source Generator
diff --git a/lua/csharp.lua b/lua/csharp.lua
index f701710..fa36b83 100644
--- a/lua/csharp.lua
+++ b/lua/csharp.lua
@@ -5,7 +5,7 @@ local function setup(user_config)
config.save(user_config)
require("csharp.commands").setup()
- require("csharp.lsp").setup()
+ require("csharp.modules.lsp").setup()
require("csharp.log").setup()
end
@@ -14,4 +14,6 @@ return {
fix_usings = require("csharp.features.fix-usings").execute,
fix_all = require("csharp.features.fix-all").select_scope_and_execute,
go_to_definition = require("csharp.features.go-to-definition").execute,
+ debug_project = require("csharp.features.debugger").execute,
+ run_project = require("csharp.features.code-runner").execute,
}
diff --git a/lua/csharp/config.lua b/lua/csharp/config.lua
index 37bb087..e326b30 100644
--- a/lua/csharp/config.lua
+++ b/lua/csharp/config.lua
@@ -22,6 +22,11 @@ local default_config = {
---@type fun(client: lsp.Client, bufnr: number)|nil
on_attach = nil,
},
+ --- @class CsharpConfig.Dap
+ dap = {
+ --- @type string?
+ adapter_name = nil,
+ },
---@class CsharpConfig.Logging
logging = {
level = "INFO",
diff --git a/lua/csharp/features/code-runner.lua b/lua/csharp/features/code-runner.lua
new file mode 100644
index 0000000..95db782
--- /dev/null
+++ b/lua/csharp/features/code-runner.lua
@@ -0,0 +1,36 @@
+local M = {}
+local utils = require("csharp.utils")
+local notify = require("csharp.notify")
+
+--- @async
+local function _execute()
+ local project_information = require("csharp.features.workspace-information").select_project()
+
+ if project_information == nil then
+ logger.error("No project selected", { feature = "code-runner" })
+ return
+ end
+
+ local project_folder_path = vim.fn.fnamemodify(project_information.Path, ":h")
+
+ local launch_profile = require("csharp.modules.launch-settings").select_launch_profile(project_folder_path)
+
+ local opt = {
+ "--project",
+ project_information.Path,
+ "-c",
+ "Debug",
+ }
+
+ if launch_profile then
+ opt = vim.list_extend(opt, { "--launch-profile", launch_profile.name })
+ end
+
+ require("csharp.modules.dotnet-cli").run(opt)
+end
+
+function M.execute()
+ utils.run_async(_execute)
+end
+
+return M
diff --git a/lua/csharp/features/debugger/config_factories/attach-debugger.lua b/lua/csharp/features/debugger/config_factories/attach-debugger.lua
new file mode 100644
index 0000000..dbc9f4c
--- /dev/null
+++ b/lua/csharp/features/debugger/config_factories/attach-debugger.lua
@@ -0,0 +1,26 @@
+local ui = require("csharp.ui")
+
+--- @async
+--- @param args DebugConfigFactoryArgs
+--- @return table
+local function create_config(args)
+ local processes = require("dap.utils").get_processes()
+ local process = ui.select_sync(processes, {
+ format_item = function(item)
+ return item.name
+ end,
+ })
+
+ return {
+ name = "Attach - .NET",
+ request = "attach",
+ type = "coreclr",
+ processId = process.pid,
+ }
+end
+
+return {
+ name = "Attach - .NET",
+ request = "attach",
+ create_config = create_config,
+}
diff --git a/lua/csharp/features/debugger/config_factories/init.lua b/lua/csharp/features/debugger/config_factories/init.lua
new file mode 100644
index 0000000..a64afae
--- /dev/null
+++ b/lua/csharp/features/debugger/config_factories/init.lua
@@ -0,0 +1,29 @@
+local M = {}
+local ui = require("csharp.ui")
+
+--- @class DebugConfigFactoryArgs
+--- @field project_information OmnisharpProjectInformation?
+
+--- @class DebugConfigFactory
+--- @field name string
+--- @field request "launch"|"attach"
+--- @field create_config fun(args: DebugConfigFactoryArgs): table
+
+--- @type DebugConfigFactory[]
+local debug_config_factories = {
+ require("csharp.features.debugger.config_factories.launch-debugger"),
+ require("csharp.features.debugger.config_factories.attach-debugger"),
+}
+
+--- @async
+--- @return DebugConfigFactory
+function M.select_debug_config()
+ return ui.select_sync(debug_config_factories, {
+ prompt = "Start Debugging:",
+ format_item = function(item)
+ return item.name
+ end,
+ })
+end
+
+return M
diff --git a/lua/csharp/features/debugger/config_factories/launch-debugger.lua b/lua/csharp/features/debugger/config_factories/launch-debugger.lua
new file mode 100644
index 0000000..acccee5
--- /dev/null
+++ b/lua/csharp/features/debugger/config_factories/launch-debugger.lua
@@ -0,0 +1,31 @@
+local dap = require("dap")
+
+--- @async
+--- @param args DebugConfigFactoryArgs
+--- @return table
+local function create_config(args)
+ local project_folder_path = vim.fn.fnamemodify(args.project_information.Path, ":h")
+
+ local build_succeded = require("csharp.modules.dotnet-cli").build(args.project_information.Path, { "-c Debug" })
+
+ if not build_succeded then
+ logger.debug("Skip debugging, build failed!", { feature = "debugger" })
+ error("Skip debugging, build failed!")
+ end
+
+ return {
+ name = "Launch - .NET",
+ request = "launch",
+ type = "coreclr",
+ cwd = project_folder_path,
+ program = args.project_information.TargetPath,
+ args = {},
+ env = {},
+ }
+end
+
+return {
+ name = "Launch - .NET",
+ request = "launch",
+ create_config = create_config,
+}
diff --git a/lua/csharp/features/debugger/init.lua b/lua/csharp/features/debugger/init.lua
new file mode 100644
index 0000000..06784b0
--- /dev/null
+++ b/lua/csharp/features/debugger/init.lua
@@ -0,0 +1,78 @@
+local M = {}
+local ui = require("csharp.ui")
+local dap = require("dap")
+local logger = require("csharp.log")
+local utils = require("csharp.utils")
+local notify = require("csharp.notify")
+local next = next
+
+--- @param debug_config table
+--- @param launch_profile DotNetLaunchProfile
+--- @return table
+local function apply_launch_profile(debug_config, launch_profile)
+ if launch_profile.environmentVariables then
+ for key, value in pairs(launch_profile.environmentVariables) do
+ debug_config.env[key] = value
+ end
+ end
+
+ if launch_profile.commandLineArgs then
+ vim.tbl_deep_extend("force", debug_config.args, vim.split(launch_profile.commandLineArgs, " ", { trimempty = true }))
+ end
+
+ if launch_profile.applicationUrl then
+ table.insert(debug_config.args, "--urls=" .. launch_profile.applicationUrl)
+ end
+
+ return debug_config
+end
+
+--- @async
+local function _execute()
+ local debug_adapter = require("csharp.modules.dap").get_debug_adapter()
+ if debug_adapter == nil then
+ logger.error("Debug Adapter is not installed or configured.", { feature = "debugger" })
+ return
+ end
+
+ if next(dap.sessions()) ~= nil then
+ logger.debug("Debugging is already running, using dap.continue().", { feature = "debugger" })
+ dap.continue()
+ return
+ end
+
+ notify.info("Preparing debugger!")
+ local debug_config_factory = require("csharp.features.debugger.config_factories").select_debug_config()
+ local debug_config
+
+ logger.debug("Selected debug config factory", { feature = "debugger", debug_config_factory = debug_config_factory })
+ if debug_config_factory.request == "attach" then
+ debug_config = debug_config_factory.create_config({})
+ else
+ local project_information = require("csharp.features.workspace-information").select_project()
+
+ if project_information == nil then
+ logger.error("No project selected", { feature = "debugger" })
+ return
+ end
+
+ debug_config = debug_config_factory.create_config({ project_information = project_information })
+ local project_folder_path = vim.fn.fnamemodify(project_information.Path, ":h")
+ local launch_profile = require("csharp.modules.launch-settings").select_launch_profile(project_folder_path)
+
+ if launch_profile then
+ logger.debug("Applying launch profile to debug config.", { feature = "debugger", launch_profile = launch_profile, debug_config })
+ debug_config = apply_launch_profile(debug_config, launch_profile)
+ end
+ end
+
+ logger.debug("Starting debugger", { feature = "debugger", debug_config = debug_config })
+ notify.info("Starting debugger!")
+ dap.launch(debug_adapter, debug_config)
+end
+
+function M.execute()
+ utils.run_async(_execute)
+end
+
+return M
diff --git a/lua/csharp/features/fix-all.lua b/lua/csharp/features/fix-all.lua
index 0515d71..cf5e7b7 100644
--- a/lua/csharp/features/fix-all.lua
+++ b/lua/csharp/features/fix-all.lua
@@ -42,15 +42,12 @@ M.scope = {
--- @param response RunFixAllResponse
--- @param ctx LspHandlerContext
local function handle_run_fix_all(error, response, ctx)
- -- vim.notify("response: " .. vim.inspect(response))
- -- Handle workspace edit
-
--- @type LspWorkspaceEdit
local workspace_edits = { changes = {} }
for _, change in pairs(response.Changes) do
if change.ModificationType ~= 0 then
- logger.error("Unsupported modification type.", { feature = "fix-all", buffer = ctx.bufnr, change = change })
+ logger.error("Unsupported modification type.", { feature = "fix-all", change = change })
goto continue
end
@@ -79,7 +76,7 @@ local function run_fix_all(client_id, buffer, params)
ApplyChanges = false,
}
- logger.info("Sending runfixall request to LSP Server", { feature = "fix-all", request = request, buffer = buffer })
+ logger.info("Sending runfixall request to LSP Server", { feature = "fix-all", request = request })
omnisharp_client.request("o#/runfixall", request, handle_run_fix_all, buffer)
end
@@ -102,7 +99,7 @@ end
function M.execute(params)
if not M.scope[params.scope] then
- logger.error("Invalid scope. Scope must be Document, Project or Solution", { feature = "fix-all", buffer = buffer })
+ logger.error("Invalid scope. Scope must be Document, Project or Solution", { feature = "fix-all", })
return
end
@@ -110,7 +107,7 @@ function M.execute(params)
local omnisharp_client = utils.get_omnisharp_client(buffer)
if omnisharp_client == nil then
- logger.error("Omnisharp isn't attached to buffer.", { feature = "fix-all", buffer = buffer })
+ logger.error("Omnisharp isn't attached to buffer.", { feature = "fix-all", })
return
end
@@ -124,7 +121,7 @@ function M.execute(params)
Scope = params.scope,
}
- logger.info("Sending getfixall request to LSP Server", { feature = "fix-all", request = request, buffer = buffer })
+ logger.info("Sending getfixall request to LSP Server", { feature = "fix-all", request = request, })
omnisharp_client.request("o#/getfixall", request, handle_get_fix_all, buffer)
end
diff --git a/lua/csharp/features/fix-usings.lua b/lua/csharp/features/fix-usings.lua
index a8766c5..4f5aec1 100644
--- a/lua/csharp/features/fix-usings.lua
+++ b/lua/csharp/features/fix-usings.lua
@@ -5,18 +5,18 @@ local logger = require("csharp.log")
local function handle(response, buffer)
if response.err ~= nil then
- logger.error("LSP client responded with error", { feature = "fix-usings", buffer = buffer, error = response.err })
+ logger.error("LSP client responded with error", { feature = "fix-usings", error = response.err })
return
end
if vim.tbl_isempty(response.result.Changes) then
- logger.info("No changes found", { feature = "fix-usings", buffer = buffer })
+ logger.info("No changes found", { feature = "fix-usings" })
return
end
local text_edits = utils.omnisharp_text_changes_to_text_edits(response.result.Changes)
vim.lsp.util.apply_text_edits(text_edits, buffer, "utf-8")
- logger.info("Applied changes.", { feature = "fix-usings", buffer = buffer })
+ logger.info("Applied changes.", { feature = "fix-usings" })
end
function M.execute()
@@ -25,7 +25,7 @@ function M.execute()
local config = config_store.get_config()
if omnisharp_client == nil then
- logger.error("Omnisharp isn't attached to buffer.", { feature = "fix-usings", buffer = buffer })
+ logger.error("Omnisharp isn't attached to buffer.", { feature = "fix-usings" })
return
end
@@ -39,7 +39,7 @@ function M.execute()
ApplyTextChanges = false,
}
- logger.info("Sending request to LSP Server", { feature = "fix-usings", buffer = buffer })
+ logger.info("Sending request to LSP Server", { feature = "fix-usings" })
local response = omnisharp_client.request_sync("o#/fixusings", request, config.lsp.default_timeout, buffer)
handle(response, buffer)
end
diff --git a/lua/csharp/features/get-metadata.lua b/lua/csharp/features/get-metadata.lua
index c337624..226fdff 100644
--- a/lua/csharp/features/get-metadata.lua
+++ b/lua/csharp/features/get-metadata.lua
@@ -1,6 +1,17 @@
local M = {}
local utils = require("csharp.utils")
+--- @class OmnisharpMetadataSource
+--- @field AssemblyName string
+--- @field TypeName string
+--- @field ProjectName string
+--- @field VersionNumber string
+--- @field Language string
+
+--- @class OmnisharpMetadataResponse
+--- @field Source string
+--- @field SourceName string
+
--- @class GetMetadataParams
--- @field metadata_source OmnisharpMetadataSource
diff --git a/lua/csharp/features/go-to-definition.lua b/lua/csharp/features/go-to-definition.lua
index 888c7e5..506f93f 100644
--- a/lua/csharp/features/go-to-definition.lua
+++ b/lua/csharp/features/go-to-definition.lua
@@ -2,6 +2,10 @@ local M = {}
local utils = require("csharp.utils")
local get_metadata = require("csharp.features.get-metadata").execute
+--- @class OmnisharpDefinition
+--- @field Location OmnisharpLocation
+--- @field MetadataSource OmnisharpMetadataSource?
+
--- @class GoToDefinitionResponse
--- @field Definitions OmnisharpDefinition[]?
diff --git a/lua/csharp/features/workspace-information.lua b/lua/csharp/features/workspace-information.lua
new file mode 100644
index 0000000..612a917
--- /dev/null
+++ b/lua/csharp/features/workspace-information.lua
@@ -0,0 +1,105 @@
+local M = {}
+local ui = require("csharp.ui")
+local utils = require("csharp.utils")
+local logger = require("csharp.log")
+local notify = require("csharp.notify")
+local config_store = require("csharp.config")
+
+--- @class OmnisharpWorkspaceInformation
+--- @field MsBuild OmnisharpMsBuildProjects
+
+--- @class OmnisharpMsBuildProjects
+--- @field Projects OmnisharpProjectInformation[]
+
+--- @class OmnisharpProjectInformation
+--- @field AssemblyName string
+--- @field IsExe boolean
+--- @field OutputPath string The output relative path
+--- @field Path string the project absolute path
+--- @field TargetPath string The target dll absolute path
+
+--- @class GetProjectsRequest
+--- @field ExcludeSourceFiles boolean
+
+--- @param error LspError?
+--- @param result OmnisharpWorkspaceInformation
+--- @param ctx LspHandlerContext
+--- @return nil
+local function handle(error, result, ctx)
+ vim.g.csharp.get_projects_callback(result)
+end
+
+--- @async
+--- @return OmnisharpWorkspaceInformation|nil
+local function get_workspace_information()
+ local buffer = vim.api.nvim_get_current_buf()
+ local omnisharp_client = utils.get_omnisharp_client(buffer)
+
+ if omnisharp_client == nil then
+ logger.error("Omnisharp isn't attached to buffer.", { feature = "get-projects" })
+ return
+ end
+
+ local config = config_store.get_config()
+
+ local request_method = "o#/projects"
+ --- @type GetProjectsRequest
+ local request = {
+ ExcludeSourceFiles = true,
+ }
+
+ logger.debug("Sending request to LSP Server", { feature = "get-workspace-information", request = request, method = request_method })
+
+ --- @type LspRequestSyncResponse
+ local response = omnisharp_client.request_sync(request_method, request, config.lsp.default_timeout, buffer)
+
+ if response.err ~= nil then
+ logger.error("LSP client responded with error!", { feature = "get-workspace-information", error = response.err, request = request, method = request_method })
+ return
+ end
+ return response.result
+end
+
+--- @async
+--- @return OmnisharpProjectInformation|nil
+function M.select_project()
+ local workspace_information = get_workspace_information()
+
+ if workspace_information == nil then
+ logger.error("Workspace information couldn't be fetched.")
+ return
+ end
+
+ --- @type OmnisharpProjectInformation[]
+ local executable_projects = {}
+
+ for _, project in ipairs(workspace_information.MsBuild.Projects) do
+ if project.IsExe then
+ table.insert(executable_projects, project)
+ end
+ end
+
+ if #executable_projects == 0 then
+ logger.error("No executable projects")
+ return
+ elseif #executable_projects == 1 then
+ local selected_project = executable_projects[1]
+ logger.debug("Found only one executable project", { feature = "select-project", project = selected_project, workspace_information = workspace_information })
+ notify.info(string.format("Found only one executable project %s, using it.", selected_project.AssemblyName))
+ return selected_project
+ else
+ logger.debug("Found multiple projects! Selecting one", { feature = "select-project", executable_projects = executable_projects, workspace_information = workspace_information })
+
+ local selected_project = ui.select_sync(executable_projects, {
+ prompt = "Select Project:",
+ format_item = function(item)
+ return item.AssemblyName
+ end,
+ })
+
+ logger.debug("Selected project", { feature = "select-project", project = selected_project })
+ return selected_project
+ end
+end
+
+return M
diff --git a/lua/csharp/log.lua b/lua/csharp/log.lua
index 1360c93..d136fb6 100644
--- a/lua/csharp/log.lua
+++ b/lua/csharp/log.lua
@@ -1,14 +1,10 @@
local M = {}
-local logger = nil
function M.setup()
local ok, structlog = pcall(require, "structlog")
if not ok then
- vim.notify(
- "csharp.nvim: structlog.nvim dependency is not installed. This won't prevent the plugin from working, but it's recommended to install it.",
- vim.log.levels.WARN
- )
+ vim.notify("csharp.nvim: structlog.nvim dependency is not installed. This won't prevent the plugin from working, but it's recommended to install it.", vim.log.levels.WARN)
return
end
@@ -20,18 +16,20 @@ function M.setup()
processors = {
structlog.processors.StackWriter({ "line", "file" }, { max_parents = 3 }),
structlog.processors.Timestamper("%H:%M:%S"),
+ function(log)
+ log["buffer"] = vim.api.nvim_get_current_buf()
+ return log
+ end,
},
formatter = structlog.formatters.Format( --
- "%s [%s] %s: %-30s",
- { "timestamp", "level", "logger_name", "msg" }
+ "%s [%s] %s: %-30s buffer=%s",
+ { "timestamp", "level", "logger_name", "msg", "buffer" }
),
sink = structlog.sinks.File(vim.fn.stdpath("log") .. "/csharp.log"),
},
},
},
})
-
- logger = structlog.get_logger("csharp_logger")
end
---@param level string
@@ -39,11 +37,12 @@ end
---@param data table?
function M.log(level, message, data)
local config = require("csharp.config").get_config().logging
+ local logger = require("structlog").get_logger("csharp_logger")
if logger == nil or vim.log.levels[level] < vim.log.levels[config.level] then
return
end
- require("structlog").get_logger("csharp_logger"):log(vim.log.levels[level], message, data)
+ logger:log(vim.log.levels[level] + 1, message, data)
end
---@param message string
diff --git a/lua/csharp/modules/dap.lua b/lua/csharp/modules/dap.lua
new file mode 100644
index 0000000..fc96f23
--- /dev/null
+++ b/lua/csharp/modules/dap.lua
@@ -0,0 +1,38 @@
+local M = {}
+local config_store = require("csharp.config")
+local dap = require("dap")
+
+function M.get_debug_adapter()
+ local config = config_store.get_config().dap
+
+ if config.adapter_name ~= nil then
+ return dap.adapters[config.adapter_name]
+ end
+
+ local debug_adapter = dap.adapters.coreclr
+
+ if debug_adapter ~= nil then
+ return debug_adapter
+ end
+
+ local mason = require("mason-registry")
+ local package = mason.get_package("netcoredbg")
+
+ if not package:is_installed() then
+ package:install()
+ end
+
+ local path = package:get_install_path() .. "/netcoredbg"
+
+ dap.adapters.coreclr = {
+ type = "executable",
+ command = path,
+ args = {
+ "--interpreter=vscode",
+ },
+ }
+
+ return dap.adapters.coreclr
+end
+
+return M
diff --git a/lua/csharp/modules/dotnet-cli.lua b/lua/csharp/modules/dotnet-cli.lua
new file mode 100644
index 0000000..af6ebf0
--- /dev/null
+++ b/lua/csharp/modules/dotnet-cli.lua
@@ -0,0 +1,50 @@
+local M = {}
+local logger = require("csharp.log")
+
+local function execute_command(cmd)
+ local file = io.popen(cmd .. " 2>&1")
+ local output = file:read("*all")
+ local _, _, exit_code = file:close()
+ return output, exit_code
+end
+
+--- @param target string File path to solution or project
+--- @param options string[]?
+--- @return boolean
+function M.build(target, options)
+ local command = "dotnet build " .. target
+
+ if options then
+ command = command .. " " .. table.concat(options, " ")
+ end
+
+ logger.debug("Executing: " .. command, { feature = "dotnet-cli" })
+
+ local output, exit_code = execute_command(command)
+
+ --- @type boolean
+ local build_succeded = exit_code == 0
+
+ if build_succeded then
+ else
+ logger.debug("Build failed", { feature = "dotnet-cli" })
+ end
+
+ return build_succeded
+end
+
+--- @param options string[]?
+function M.run(options)
+ local command = "dotnet run"
+
+ if options then
+ command = command .. " " .. table.concat(options, " ")
+ end
+
+ logger.debug("Executing: " .. command, { feature = "dotnet-cli" })
+ local current_window = vim.api.nvim_get_current_win()
+ vim.cmd("split | term " .. command)
+ vim.api.nvim_set_current_win(current_window)
+end
+
+return M
diff --git a/lua/csharp/modules/launch-settings.lua b/lua/csharp/modules/launch-settings.lua
new file mode 100644
index 0000000..414083d
--- /dev/null
+++ b/lua/csharp/modules/launch-settings.lua
@@ -0,0 +1,80 @@
+local M = {}
+local logger = require("csharp.log")
+local ui = require("csharp.ui")
+
+--- @class DotNetLaunchProfile
+--- @field name string
+--- @field commandName string
+--- @field environmentVariables table
+--- @field applicationUrl string
+--- @field commandLineArgs string
+
+--- @class DotNetLaunchSettings
+--- @field profiles table?
+
+--- @param file_name string
+--- @return DotNetLaunchSettings|nil
+local function readFileWithoutBom(file_name)
+ local file = io.open(file_name, "rb")
+ if file then
+ local content = file:read("*all")
+ file:close()
+ -- Check if the content starts with the UTF-8 BOM and exclude it if present
+ if content:sub(1, 3) == "\xEF\xBB\xBF" then
+ content = content:sub(4)
+ end
+ return vim.json.decode(content)
+ else
+ return nil
+ end
+end
+
+--- @param project_folder string
+--- @return DotNetLaunchProfile[]
+local function get_launch_profiles(project_folder)
+ local file_name = project_folder .. "/Properties/launchSettings.json"
+ local launch_settings = readFileWithoutBom(file_name)
+
+ if launch_settings == nil then
+ logger.warn("Launch profile file could not be opened, or it doesn't exist. Skipping it.", { feature = "get-launch-profiles", file_name = file_name })
+ return {}
+ end
+
+ --- @type DotNetLaunchProfile[]
+ local profiles = {}
+ for profile_name, profile in pairs(launch_settings.profiles) do
+ if profile.commandName ~= "Project" then
+ logger.debug("Skipping profile.", { feature = "get-launch-profiles", profile_name = profile_name, profile = profile, file_name = file_name })
+ goto continue
+ end
+
+ profile.name = profile_name
+ table.insert(profiles, profile)
+ ::continue::
+ end
+
+ logger.debug("Found profiles.", { feature = "get-launch-profiles", profiles = profiles, file_name = file_name })
+ return profiles
+end
+
+--- @async
+--- @param project_folder string
+--- @return DotNetLaunchProfile|nil
+function M.select_launch_profile(project_folder)
+ local launch_profiles = get_launch_profiles(project_folder)
+
+ if #launch_profiles == 0 then
+ return
+ elseif #launch_profiles == 1 then
+ return launch_profiles[1]
+ end
+
+ return ui.select_sync(launch_profiles, {
+ prompt = "Select Launch Profile:",
+ format_item = function(item)
+ return item.name
+ end,
+ })
+end
+
+return M
diff --git a/lua/csharp/lsp.lua b/lua/csharp/modules/lsp.lua
similarity index 100%
rename from lua/csharp/lsp.lua
rename to lua/csharp/modules/lsp.lua
diff --git a/lua/csharp/notify.lua b/lua/csharp/notify.lua
new file mode 100644
index 0000000..9c346ef
--- /dev/null
+++ b/lua/csharp/notify.lua
@@ -0,0 +1,26 @@
+local M = {}
+
+---@param level string
+---@param message string
+function M.notify(level, message)
+ vim.schedule(function()
+ vim.notify(message, vim.log.levels[level], nil)
+ end)
+end
+
+---@param message string
+function M.info(message)
+ M.notify("INFO", message)
+end
+
+---@param message string
+function M.warn(message)
+ M.notify("WARN", message)
+end
+
+---@param message string
+function M.error(message)
+ M.notify("ERROR", message)
+end
+
+return M
diff --git a/lua/csharp/types.lua b/lua/csharp/types.lua
index fbf69a9..c3f46bc 100644
--- a/lua/csharp/types.lua
+++ b/lua/csharp/types.lua
@@ -11,21 +11,6 @@
--- @field FileName string
--- @field Range OmnisharpRange
---- @class OmnisharpMetadataSource
---- @field AssemblyName string
---- @field TypeName string
---- @field ProjectName string
---- @field VersionNumber string
---- @field Language string
-
---- @class OmnisharpMetadataResponse
---- @field Source string
---- @field SourceName string
-
---- @class OmnisharpDefinition
---- @field Location OmnisharpLocation
---- @field MetadataSource OmnisharpMetadataSource?
-
--- @class OmnisharpTextChange
--- @field NewText string
--- @field StartLine number
@@ -54,7 +39,7 @@
--- @class LspHandlerContext: {["params"]: T, ["client_id"]: number, ["bufnr"]: number, ["method"]: string}
---- @class LspRequestSyncResponse: {["result"]: T, ["err"] = LspError|nil}
+--- @class LspRequestSyncResponse: {["result"]: T, ["err"]: LspError|nil}
--- @class LspWorkspaceEdit
--- @field changes table
diff --git a/lua/csharp/ui.lua b/lua/csharp/ui.lua
new file mode 100644
index 0000000..a89e2f4
--- /dev/null
+++ b/lua/csharp/ui.lua
@@ -0,0 +1,29 @@
+local M = {}
+
+---@async
+---@generic T: any
+---@param items T[] Arbitrary items
+---@param opts table Additional options
+--- - prompt (string|nil)
+--- Text of the prompt. Defaults to `Select one of:`
+--- - format_item (function item -> text)
+--- Function to format an
+--- individual item from `items`. Defaults to `tostring`.
+--- - kind (string|nil)
+--- Arbitrary hint string indicating the item shape.
+--- Plugins reimplementing `vim.ui.select` may wish to
+--- use this to infer the structure or semantics of
+--- `items`, or the context in which select() was called.
+--- @return T
+function M.select_sync(items, opts)
+ local co = assert(coroutine.running())
+ vim.schedule(function()
+ vim.ui.select(items, opts, function(selected)
+ coroutine.resume(co, selected)
+ end)
+ end)
+
+ return coroutine.yield()
+end
+
+return M
diff --git a/lua/csharp/utils.lua b/lua/csharp/utils.lua
index cfd20de..58e5216 100644
--- a/lua/csharp/utils.lua
+++ b/lua/csharp/utils.lua
@@ -38,4 +38,13 @@ function M.get_omnisharp_client(buffer)
end
end
+function M.run_async(fn)
+ local co = coroutine.create(fn)
+ local success, result = coroutine.resume(co)
+
+ if not success then
+ require("csharp.log").error("Error has occurred!", { feature = "run-async", error_message = result, stack_trace = debug.traceback(co) })
+ end
+end
+
return M
diff --git a/stylua.toml b/stylua.toml
index 7cdff23..36858ae 100644
--- a/stylua.toml
+++ b/stylua.toml
@@ -1,4 +1,4 @@
quote_style = "ForceDouble"
indent_type = "Spaces"
-column_width = 150
+column_width = 180
indent_width = 2
diff --git a/tests/lsp_spec.lua b/tests/lsp_spec.lua
index e8fb5a2..4f59eeb 100644
--- a/tests/lsp_spec.lua
+++ b/tests/lsp_spec.lua
@@ -10,7 +10,7 @@ describe("get_root_dir", function()
before_each(function()
_G._TEST = true
snapshot = assert:snapshot()
- lsp = require("csharp.lsp")
+ lsp = require("csharp.modules.lsp")
end)
after_each(function()
@@ -87,7 +87,7 @@ describe("get_omnisharp_cmd", function()
before_each(function()
_G._TEST = true
snapshot = assert:snapshot()
- lsp = require("csharp.lsp")
+ lsp = require("csharp.modules.lsp")
end)
after_each(function()