diff --git a/home-manager/home.nix b/home-manager/home.nix index 8bf9039..dd8b000 100644 --- a/home-manager/home.nix +++ b/home-manager/home.nix @@ -261,7 +261,7 @@ vimdiffAlias = true; withPython3 = true; - extraLuaConfig = builtins.readFile ./nvim/build-utils.lua + "\n" + builtins.replaceStrings ["%PYTHONENV%"] ["${pythonEnv}"] (builtins.readFile ./nvim/init.lua); + extraLuaConfig = builtins.readFile ./nvim/build-utils.lua + "\n" + builtins.readFile ./nvim/dotnet.lua + "\n" + builtins.replaceStrings ["%PYTHONENV%"] ["${pythonEnv}"] (builtins.readFile ./nvim/init.lua); package = nixpkgs.neovim-nightly; }; diff --git a/home-manager/nvim/dotnet.lua b/home-manager/nvim/dotnet.lua new file mode 100644 index 0000000..8a595ab --- /dev/null +++ b/home-manager/nvim/dotnet.lua @@ -0,0 +1,158 @@ +local dotnet_has_set_status_line + +function DetachSolution() + vim.g.current_sln_path = nil + -- TODO: unregister key bindings again +end + +local function on_line(data, _, context) + -- Keep the window alive if there were warnings + if string.match(data, "%s[1-9]%d* Warning%(s%)") then + context.warn = context.warn + 1 + end +end +local function on_complete(context, code, _) + if code ~= 0 then + print("Exit code " .. code) + context.errs = context.errs + 1 + end + + if context.errs == 0 and context.warn == 0 then + -- Close the temporary floating window (but keep it alive if the + -- cursor is in it) + local cur_win = vim.api.nvim_get_current_win() + local cur_buf = vim.api.nvim_win_get_buf(cur_win) + if cur_buf ~= context.buffer then + vim.api.nvim_win_close(context.window, true) + end + print("All builds successful") + end +end + +function GetCurrentSln() + if vim.g.current_sln_path then + return vim.g.current_sln_path + else + return nil + end +end + +function BuildDotNetSolution() + if vim.g.current_sln_path then + local context = BuildUtils.create_window() + context.errs = 0 + context.warn = 0 + BuildUtils.run("dotnet", { "build", vim.g.current_sln_path }, "dotnet build", context, on_line, on_complete) + end +end + +function TestDotNetSolution() + if vim.g.current_sln_path then + local context = BuildUtils.create_window() + context.warn = 0 + context.errs = 0 + BuildUtils.run("dotnet", { "test", vim.g.current_sln_path }, "dotnet test", context, on_line, on_complete) + end +end + +function CurrentSlnOrEmpty() + local sln = GetCurrentSln() + if sln then + return sln + else + return "" + end +end + +function RegisterSolution(sln_path) + vim.g.current_sln_path = sln_path + + if not dotnet_has_set_status_line then + dotnet_has_set_status_line = true + vim.o.statusline = vim.o.statusline .. " %{v:lua.CurrentSlnOrEmpty()}" + end + + local status, whichkey = pcall(require, "which-key") + if status then + whichkey.register({ + s = { + name = ".NET solution", + b = { BuildDotNetSolution, "Build .NET solution" }, + t = { TestDotNetSolution, "Test .NET solution" }, + }, + }, { prefix = vim.api.nvim_get_var("maplocalleader"), buffer = vim.api.nvim_get_current_buf() }) + else + vim.api.nvim_set_keymap("n", "sb", ":call BuildDotNetSolution", { noremap = true }) + end +end + +local function find_nearest_slns() + local path = vim.fn.expand("%:p:h") -- Get the full path of the current buffer's directory + + while path and path ~= "/" do + local sln_paths = vim.fn.glob(path .. "/*.sln", nil, true) + if #sln_paths > 0 then + return sln_paths + end + path = vim.fn.fnamemodify(path, ":h") -- Move up one directory + end + + return {} +end + +local function FindAndRegisterSolution() + local solutions = find_nearest_slns() + if not solutions or #solutions == 0 then + print("No .sln file found in any parent directory.") + return + elseif #solutions == 1 then + -- Exactly one solution found; register it directly + RegisterSolution(solutions[1]) + elseif #solutions > 1 then + -- Multiple solutions found; use Telescope to pick one + local pickers = require("telescope.pickers") + local finders = require("telescope.finders") + local actions = require("telescope.actions") + local action_state = require("telescope.actions.state") + local conf = require("telescope.config").values + + pickers + .new({}, { + prompt_title = "Select a Solution File", + finder = finders.new_table({ + results = solutions, + entry_maker = function(entry) + return { + value = entry, + display = entry, + ordinal = entry, + } + end, + }), + sorter = conf.generic_sorter({}), + attach_mappings = function(prompt_bufnr, _) + actions.select_default:replace(function() + local selection = action_state.get_selected_entry() + actions.close(prompt_bufnr) + RegisterSolution(selection.value) + end) + return true + end, + }) + :find() + end +end + +vim.api.nvim_create_autocmd({ "BufReadPost", "BufNewFile" }, { + pattern = "*.sln", + callback = function() + RegisterSolution(vim.fn.expand("%:p")) + end, +}) + +vim.api.nvim_create_autocmd("FileType", { + pattern = { "fsharp", "cs" }, + callback = function() + FindAndRegisterSolution() + end, +}) diff --git a/home-manager/nvim/init.lua b/home-manager/nvim/init.lua index b26a7be..d63f1ed 100644 --- a/home-manager/nvim/init.lua +++ b/home-manager/nvim/init.lua @@ -99,7 +99,7 @@ function HasPaste() return "" end -vim.o.statusline = vim.o.statusline .. "%{v:lua.HasPaste()}%F%m%r%h %w CWD: %r%{getcwd()}%h Line: %l Column: %c" +vim.o.statusline = vim.o.statusline .. "%{v:lua.HasPaste()}%F%m%r%h %w Line: %l Column: %c" -------------------------------------------------------------- @@ -186,7 +186,6 @@ if status then print("-----") end end - -- TODO: If a command is a prefix of an existing command, prepend its description to those commands' descriptions, and append a '...' to the parent's description. end end) end diff --git a/home-manager/nvim/ionide-vim.lua b/home-manager/nvim/ionide-vim.lua index a6e436e..1e33b51 100644 --- a/home-manager/nvim/ionide-vim.lua +++ b/home-manager/nvim/ionide-vim.lua @@ -65,7 +65,7 @@ local function BuildFSharpProjects(projects) -- cursor is in it) local cur_win = vim.api.nvim_get_current_win() local cur_buf = vim.api.nvim_win_get_buf(cur_win) - if cur_buf ~= context.buf then + if cur_buf ~= context.buffer then vim.api.nvim_win_close(context.window, true) end print("All builds successful") @@ -94,45 +94,6 @@ local function BuildFSharpProjects(projects) end end --- local function fsprojAndDirCompletion(ArgLead, _, _) --- local results = {} --- local loc = ArgLead --- if not loc then --- loc = "." --- end --- local command = string.format( --- "find " --- .. vim.fn.shellescape(loc) --- .. " -maxdepth 2 \\( -type f -name '*.fsproj' -o -type d \\) -print0 2> /dev/null" --- ) --- local handle = io.popen(command) --- if handle then --- local stdout = handle:read("*all") --- handle:close() --- --- local allResults = {} --- for match in string.gmatch(stdout, "([^%z]+)") do --- table.insert(allResults, match) --- end --- table.sort(allResults, function(a, b) --- local aEndsWithProj = a:match("proj$") --- local bEndsWithProj = b:match("proj$") --- if aEndsWithProj and not bEndsWithProj then --- return true --- elseif not aEndsWithProj and bEndsWithProj then --- return false --- else --- return a < b -- If both or neither end with 'proj', sort alphabetically --- end --- end) --- --- for _, line in ipairs(allResults) do --- table.insert(results, line) --- end --- end --- return results --- end - vim.api.nvim_create_user_command("BuildFSharpProject", function(opts) if opts.fargs and opts.fargs[1] then BuildFSharpProjects(opts.fargs) @@ -144,7 +105,7 @@ vim.api.nvim_create_user_command("BuildFSharpProject", function(opts) local actions = require("telescope.actions") pickers .new({}, { - prompt_title = "Actions", + prompt_title = "Projects", finder = finders.new_table({ results = captureLoadedProjects(), }), @@ -162,6 +123,101 @@ vim.api.nvim_create_user_command("BuildFSharpProject", function(opts) end end, { nargs = "?", complete = "file" }) +local function TableConcat(tables) + local result = {} + for _, tab in ipairs(tables) do + for _, v in ipairs(tab) do + table.insert(result, v) + end + end + return result +end + +-- args is a table that will be splatted into the command line and will be immediately +-- followed by the project. +local function RunDotnet(command, args, project, configuration) + local function on_line(data, _, context) end + + local function on_complete(context, code, signal) end + + local context = BuildUtils.create_window() + + BuildUtils.run( + "dotnet", + TableConcat({ { command }, args, { project, "--configuration", configuration } }), + "dotnet", + context, + on_line, + on_complete + ) +end + +-- Call this as: +-- RunFSharpProject path/to/fsproj +-- RunFSharpProject Debug path/to/fsproj +vim.api.nvim_create_user_command("RunFSharpProject", function(opts) + local configuration = "Release" + if opts.fargs and opts.fargs[1] and opts.fargs[1]:match("sproj$") then + RunDotnet("run", { "--project" }, opts.fargs[1], configuration) + elseif opts.fargs and opts.fargs[1] and opts.fargs[2] then + configuration = opts.fargs[1] + RunDotnet("run", { "--project" }, opts.fargs[2], configuration) + else + configuration = opts.fargs[1] + local pickers = require("telescope.pickers") + local finders = require("telescope.finders") + local conf = require("telescope.config").values + local action_state = require("telescope.actions.state") + local actions = require("telescope.actions") + pickers + .new({}, { + prompt_title = "Projects", + finder = finders.new_table({ + results = captureLoadedProjects(), + }), + sorter = conf.generic_sorter({}), + attach_mappings = function(prompt_buf, _) + actions.select_default:replace(function() + actions.close(prompt_buf) + local selection = action_state.get_selected_entry() + RunDotnet("run", { "--project" }, selection.value, configuration) + end) + return true + end, + }) + :find() + end +end, { nargs = "*", complete = "file" }) + +vim.api.nvim_create_user_command("PublishFSharpProject", function(opts) + if opts.fargs and opts.fargs[1] then + RunDotnet("publish", {}, opts.fargs[1], "Release") + else + local pickers = require("telescope.pickers") + local finders = require("telescope.finders") + local conf = require("telescope.config").values + local action_state = require("telescope.actions.state") + local actions = require("telescope.actions") + pickers + .new({}, { + prompt_title = "Projects", + finder = finders.new_table({ + results = captureLoadedProjects(), + }), + sorter = conf.generic_sorter({}), + attach_mappings = function(prompt_buf, _) + actions.select_default:replace(function() + actions.close(prompt_buf) + local selection = action_state.get_selected_entry() + RunDotnet("publish", {}, selection.value, "Release") + end) + return true + end, + }) + :find() + end +end, { nargs = "*", complete = "file" }) + vim.api.nvim_create_autocmd("FileType", { pattern = "fsharp", callback = function() @@ -169,12 +225,21 @@ vim.api.nvim_create_autocmd("FileType", { if status then whichkey.register({ f = { + name = "F#", t = { ":call fsharp#showTooltip()", "Show F# Tooltip" }, ["si"] = { ":call fsharp#toggleFsi()", "Toggle FSI (F# Interactive)" }, ["sl"] = { ":call fsharp#sendLineToFsi()", "Send line to FSI (F# Interactive)" }, - }, - b = { + r = { + name = "Run F# project", + d = { ":RunFSharpProject Debug", "Run F# project in debug configuration" }, + r = { ":RunFSharpProject Release", "Run F# project in release configuration" }, + }, p = { + ":PublishFSharpProject", + "Publish F# project", + }, + b = { + "Build F# project", a = { BuildFSharpProjects, "Build all projects" }, s = { ":BuildFSharpProject", "Build specified project" }, }, @@ -184,7 +249,7 @@ vim.api.nvim_create_autocmd("FileType", { vim.api.nvim_set_keymap("n", "ft", ":call fsharp#showTooltip()", { noremap = true }) vim.api.nvim_set_keymap("n", "fsi", ":call fsharp#toggleFsi()", { noremap = true }) vim.api.nvim_set_keymap("n", "fsl", ":call fsharp#sendLineToFsi()", { noremap = true }) - vim.api.nvim_set_keymap("n", "bpa", BuildFSharpProjects, { noremap = true }) + vim.api.nvim_set_keymap("n", "bpa", ":lua BuildFSharpProjects()", { noremap = true }) vim.api.nvim_set_keymap("n", "bps", ":BuildFSharpProject", { noremap = true }) end end,