From 07b3034bc0fa396508bf564db09cfcfcaf458913 Mon Sep 17 00:00:00 2001 From: Patrick Stevens <3138005+Smaug123@users.noreply.github.com> Date: Tue, 26 Mar 2024 09:31:39 +0000 Subject: [PATCH] Commonise floating-window logic (#41) --- home-manager/home.nix | 2 +- home-manager/nvim/build-utils.lua | 110 +++++++++++++++++++ home-manager/nvim/ionide-vim.lua | 160 ++++++++-------------------- home-manager/nvim/venv-selector.lua | 107 ++----------------- 4 files changed, 163 insertions(+), 216 deletions(-) create mode 100644 home-manager/nvim/build-utils.lua diff --git a/home-manager/home.nix b/home-manager/home.nix index b089597..e5d2982 100644 --- a/home-manager/home.nix +++ b/home-manager/home.nix @@ -262,7 +262,7 @@ vimdiffAlias = true; withPython3 = true; - extraLuaConfig = builtins.replaceStrings ["%PYTHONENV%"] ["${pythonEnv}"] (builtins.readFile ./nvim/init.lua); + extraLuaConfig = builtins.readFile ./nvim/build-utils.lua + "\n" + builtins.replaceStrings ["%PYTHONENV%"] ["${pythonEnv}"] (builtins.readFile ./nvim/init.lua); package = nixpkgs.neovim-nightly; }; diff --git a/home-manager/nvim/build-utils.lua b/home-manager/nvim/build-utils.lua new file mode 100644 index 0000000..767f641 --- /dev/null +++ b/home-manager/nvim/build-utils.lua @@ -0,0 +1,110 @@ +BuildUtils = {} + +-- Create a new buffer and a new floating window to hold that buffer. +local function create_floating_window() + -- Create a new buffer for build output + local buf = vim.api.nvim_create_buf(false, true) -- No listed, scratch buffer + + -- Calculate window size and position here (example: full width, 10 lines high at the bottom) + local width = vim.api.nvim_get_option_value("columns", {}) + local height = vim.api.nvim_get_option_value("lines", {}) + local win_height = math.min(10, math.floor(height * 0.2)) -- 20% of total height or 10 lines + local win_opts = { + relative = "editor", + width = width, + height = win_height, + col = 0, + row = height - win_height, + style = "minimal", + border = "single", + } + + local win = vim.api.nvim_open_win(buf, false, win_opts) + + return { window = win, buffer = buf } +end + +local function _on_output(context, is_stdout, err, data, on_line) + local prefix + if is_stdout then + prefix = "OUT" + else + prefix = "ERR" + end + if err or data then + vim.schedule(function() + if err then + -- Append the error message to the buffer + local count = vim.api.nvim_buf_line_count(context.buffer) + vim.api.nvim_buf_set_lines(context.buffer, count, count, false, { "error " .. prefix .. ": " .. err }) + end + if data then + -- Append the data to the buffer + local count = vim.api.nvim_buf_line_count(context.buffer) + vim.api.nvim_buf_set_lines( + context.buffer, + count, + count, + false, + vim.tbl_map(function(line) + return prefix .. ": " .. line + end, vim.split(data, "\n")) + ) + end + if vim.api.nvim_win_is_valid(context.window) then + 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 + local new_line_count = vim.api.nvim_buf_line_count(context.buffer) + vim.api.nvim_win_set_cursor(context.window, { new_line_count, 0 }) + end + end + + on_line(data, is_stdout, context) + end) + end +end + +-- Arguments: +-- * exe, a string (no need to escape this) +-- * args, a table like { "-m", "venv", vim.fn.shellescape(some_path) } +-- * description of this process, visible to the user, e.g. "venv creation" +-- * context, the result of `create_floating_window` +-- * on_line, a function which takes "the string written", (true if stdout else false), and the context table; should return nothing. We'll call that on every line of stdout and stderr. +-- * on_complete, takes `context`, `code` (exit code) and `signal` ("documented" with neovim's uv.spawn, hah) +local function run_external(exe, args, description, context, on_line, on_complete) + local handle + local stdout = vim.uv.new_pipe(false) + local stderr = vim.uv.new_pipe(false) + handle, _ = vim.uv.spawn( + exe, + { + args = args, + stdio = { nil, stdout, stderr }, + }, + vim.schedule_wrap(function(code, signal) + stdout:read_stop() + stderr:read_stop() + stdout:close() + stderr:close() + handle:close() + print("External process " .. description .. " completed, exit code " .. code .. " and signal " .. signal) + on_complete(context, code, signal) + end) + ) + + if not handle then + print("Failed to start " .. description .. " process.") + return + end + + vim.uv.read_start(stdout, function(err, data) + _on_output(context, true, err, data, on_line) + end) + vim.uv.read_start(stderr, function(err, data) + _on_output(context, false, err, data, on_line) + end) +end + +BuildUtils.create_window = create_floating_window +BuildUtils.run = run_external diff --git a/home-manager/nvim/ionide-vim.lua b/home-manager/nvim/ionide-vim.lua index ecd9067..a6e436e 100644 --- a/home-manager/nvim/ionide-vim.lua +++ b/home-manager/nvim/ionide-vim.lua @@ -21,46 +21,48 @@ end -- Supply nil to get all loaded F# projects and build them. local function BuildFSharpProjects(projects) - local function on_output(context, prefix, err, data) - if err or data then - vim.schedule(function() - if err then - -- Append the error message to the buffer - local count = vim.api.nvim_buf_line_count(context.buf) - vim.api.nvim_buf_set_lines(context.buf, count, count, false, { "error " .. prefix .. ": " .. err }) - end - if data then - -- Append the data to the buffer - local count = vim.api.nvim_buf_line_count(context.buf) - vim.api.nvim_buf_set_lines( - context.buf, - count, - count, - false, - vim.tbl_map(function(line) - return prefix .. ": " .. line - end, vim.split(data, "\n")) - ) - end - if vim.api.nvim_win_is_valid(context.window) then - 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 - local new_line_count = vim.api.nvim_buf_line_count(context.buf) - vim.api.nvim_win_set_cursor(context.window, { new_line_count, 0 }) - end - end - -- 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_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 on_complete local function spawn_next(context) + BuildUtils.run( + "dotnet", + { "build", context.projects[context.completed + 1] }, + "dotnet build", + context, + on_line, + on_complete + ) + end + + on_complete = function(context, code, signal) + print("Build process exited with code " .. code .. " and signal " .. signal) + if code ~= 0 then + context.errs = context.errs + 1 + end + context.completed = context.completed + 1 + + print( + "Completed: " + .. context.completed + .. " out of " + .. context.expected + .. " (errors: " + .. context.errs + .. ", warnings: " + .. context.warn + .. ")" + ) + if context.completed == context.expected then 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.buf then @@ -69,55 +71,7 @@ local function BuildFSharpProjects(projects) print("All builds successful") end else - local handle - local stdout = vim.uv.new_pipe(false) - local stderr = vim.uv.new_pipe(false) - - handle, _ = vim.uv.spawn( - "dotnet", - { - args = { "build", context.projects[context.completed + 1] }, - stdio = { nil, stdout, stderr }, - }, - vim.schedule_wrap(function(code, signal) - stdout:read_stop() - stderr:read_stop() - stdout:close() - stderr:close() - handle:close() - print("Build process exited with code " .. code .. " and signal " .. signal) - if code ~= 0 then - context.errs = context.errs + 1 - end - context.completed = context.completed + 1 - - print( - "Completed: " - .. context.completed - .. " out of " - .. context.expected - .. " (errors: " - .. context.errs - .. ", warnings: " - .. context.warn - .. ")" - ) - - spawn_next(context) - end) - ) - - if not handle then - print("Failed to start build process.") - return - end - - vim.uv.read_start(stdout, function(err, data) - on_output(context, "OUT", err, data) - end) - vim.uv.read_start(stderr, function(err, data) - on_output(context, "ERR", err, data) - end) + spawn_next(context) end end @@ -129,40 +83,14 @@ local function BuildFSharpProjects(projects) for _, _ in ipairs(projects) do total_projects = total_projects + 1 end + local context = BuildUtils.create_window() + context.warn = 0 + context.errs = 0 + context.completed = 0 + context.expected = total_projects + context.projects = projects - -- Create a new buffer for build output - local buf = vim.api.nvim_create_buf(false, true) -- No listed, scratch buffer - - -- Calculate window size and position here (example: full width, 10 lines high at the bottom) - local width = vim.api.nvim_get_option_value("columns", {}) - local height = vim.api.nvim_get_option_value("lines", {}) - local win_height = math.min(10, math.floor(height * 0.2)) -- 20% of total height or 10 lines - local original_win = vim.api.nvim_get_current_win() - local win_opts = { - relative = "editor", - width = width, - height = win_height, - col = 0, - row = height - win_height, - style = "minimal", - border = "single", - } - - local win = vim.api.nvim_open_win(buf, true, win_opts) - -- Switch back to the original window - vim.api.nvim_set_current_win(original_win) - - local build_context = { - warn = 0, - errs = 0, - completed = 0, - expected = total_projects, - window = win, - projects = projects, - buf = buf, - } - - spawn_next(build_context) + spawn_next(context) end end diff --git a/home-manager/nvim/venv-selector.lua b/home-manager/nvim/venv-selector.lua index b8c8eb5..ac3171e 100644 --- a/home-manager/nvim/venv-selector.lua +++ b/home-manager/nvim/venv-selector.lua @@ -65,108 +65,17 @@ function CreateVenv() -- Install requirements if requirements_path then print("Installing requirements from " .. requirements_path) - local handle - local stdout = vim.uv.new_pipe(false) - local stderr = vim.uv.new_pipe(false) - - local function on_output(context, prefix, err, data) - if err or data then - vim.schedule(function() - if err then - -- Append the error message to the buffer - local count = vim.api.nvim_buf_line_count(context.buf) - vim.api.nvim_buf_set_lines( - context.buf, - count, - count, - false, - { "error " .. prefix .. ": " .. err } - ) - end - if data then - -- Append the data to the buffer - local count = vim.api.nvim_buf_line_count(context.buf) - vim.api.nvim_buf_set_lines( - context.buf, - count, - count, - false, - vim.tbl_map(function(line) - return prefix .. ": " .. line - end, vim.split(data, "\n")) - ) - end - if vim.api.nvim_win_is_valid(context.window) then - 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 - local new_line_count = vim.api.nvim_buf_line_count(context.buf) - vim.api.nvim_win_set_cursor(context.window, { new_line_count, 0 }) - end - end - end) - end - end - - -- TODO: commonise wth what's in ionide-vim - - -- Create a new buffer for build output - local buf = vim.api.nvim_create_buf(false, true) -- No listed, scratch buffer - - -- Calculate window size and position here (example: full width, 10 lines high at the bottom) - local width = vim.api.nvim_get_option_value("columns", {}) - local height = vim.api.nvim_get_option_value("lines", {}) - local win_height = math.min(10, math.floor(height * 0.2)) -- 20% of total height or 10 lines - local original_win = vim.api.nvim_get_current_win() - local win_opts = { - relative = "editor", - width = width, - height = win_height, - col = 0, - row = height - win_height, - style = "minimal", - border = "single", - } - - local win = vim.api.nvim_open_win(buf, true, win_opts) - -- Switch back to the original window - vim.api.nvim_set_current_win(original_win) - - local context = { - window = win, - buf = buf, - } - - handle, _ = vim.uv.spawn( - -- TODO: do we need to escape this? Don't know whether spawn goes via a shell + local context = BuildUtils.create_window() + BuildUtils.run( venv_dir .. "/bin/python", - { - -- TODO: and do we need to escape this? - args = { "-m", "pip", "install", "-r", requirements_path }, - stdio = { nil, stdout, stderr }, - }, - vim.schedule_wrap(function(code, signal) - stdout:read_stop() - stderr:read_stop() - stdout:close() - stderr:close() - handle:close() - print("Venv creation completed, exit code " .. code .. " and signal " .. signal) + { "-m", "pip", "install", "-r", requirements_path }, + "venv creation", + context, + function(_, _, _) end, + function(_, _, _) load_venv(venv_dir) - end) + end ) - - if not handle then - print("Failed to start venv install process.") - return - end - - vim.uv.read_start(stdout, function(err, data) - on_output(context, "OUT", err, data) - end) - vim.uv.read_start(stderr, function(err, data) - on_output(context, "ERR", err, data) - end) else load_venv(venv_dir) end