From dca2d0b4fe9082a8c217530b56e41b19b808ccd4 Mon Sep 17 00:00:00 2001 From: Erich L Foster Date: Sun, 28 Apr 2024 21:16:01 +0200 Subject: [PATCH] Switch to using toggleterm --- Makefile | 2 +- README.md | 25 +- doc/devcontainer-cli.nvim.txt | 5 + lua/devcontainer-cli/devcontainer_cli.lua | 44 ++- lua/devcontainer-cli/devcontainer_utils.lua | 252 +++++++++++++----- lua/devcontainer-cli/folder_utils.lua | 14 +- lua/devcontainer-cli/init.lua | 9 + lua/devcontainer-cli/windows_utils.lua | 76 ------ .../unit_tests.lua | 10 - 9 files changed, 264 insertions(+), 173 deletions(-) delete mode 100644 lua/devcontainer-cli/windows_utils.lua rename tests/{devcontainer_cli => devcontainer-cli}/unit_tests.lua (90%) diff --git a/Makefile b/Makefile index 05bfb7c..f640a42 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ test: --headless \ --noplugin \ -u ${TESTS_INIT} \ - -c "PlenaryBustedFile tests/devcontainer_cli/unit_tests.lua" + -c "PlenaryBustedFile tests/devcontainer-cli/unit_tests.lua" test_all: @nvim \ diff --git a/README.md b/README.md index 1e1d571..17c0292 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ make assumptions about how you work. - [docker](https://docs.docker.com/get-docker/) - [devcontainer-cli](https://github.com/devcontainers/cli#npm-install) +- [toggleterm](https://github.com/akinsho/toggleterm.nvim) ## 🔧 Installation @@ -84,6 +85,7 @@ make assumptions about how you work. ```lua { "erichlf/devcontainer-cli.nvim", + dependencies = { 'akinsho/toggleterm.nvim' }, opts = { -- whather to verify that the final devcontainer should be run interactive = false, @@ -107,29 +109,34 @@ make assumptions about how you work. -- stylua: ignore { "Du", - ":DevcontainerUp", + ":DevcontainerUp", desc = "Bring up the DevContainer", }, { "Dc", - ":DevcontainerConnect", + ":DevcontainerConnect", desc = "Connect to DevContainer", }, { "De", - ":DevcontainerExec", + ":DevcontainerExec direction='vertical'", desc = "Execute a command in DevContainer", }, { "Db", - ":DevcontainerExec cd build && make", + ":DevcontainerExec cd build && make", desc = "Execute build command in DevContainer", }, { "Dt", - ":DevcontainerExec cd build && make test", + ":DevcontainerExec cmd='cd build && make test' direction='horizontal'", desc = "Execute test command in DevContainer", }, + { + "DT", + "DevContainerToggle", + desc = "Toggle the current DevContainer Terminal" + }, } }, ``` @@ -160,6 +167,14 @@ There are 3 main commands: `:DevcontainerUp`, `:DevcontainerExec`, and `:Devcont continue working in your current session and run commands in the devcontainer via `DevcontainerExec`. +During execution using `DevcontainerUp` or `DevcontainerExec` it is possible +to toggle the terminal via `t` while in normal mode and then to bring it back +you can run `:DevContainerToggle`. Additionally you could bring it back through +`:TermSelect`. + +During the execution of a Devcontainer process you can also type `q` or `` +to kill the process and exit the terminal window. + ## Tests Tests are executed automatically on each PR using Github Actions. diff --git a/doc/devcontainer-cli.nvim.txt b/doc/devcontainer-cli.nvim.txt index 2074477..810b14b 100644 --- a/doc/devcontainer-cli.nvim.txt +++ b/doc/devcontainer-cli.nvim.txt @@ -19,6 +19,11 @@ DevcontainerUp *DevcontainerUp* DevcontainerExec *DevcontainerExec* Runs a given command in the projects devcontainer. +DevcontainerToggle *DevcontainerToggle* + Toggles the current devcontainer window. It is expected that only one + devcontainer window is open at a time and so this will only toggle the last + devcontainer window. + DevcontainerConnect *DevcontainerConnect* Closes the nvim sessions (all sessions fromt the terminal) and opens a new terminal which is connected in the docker container, ready to execute the diff --git a/lua/devcontainer-cli/devcontainer_cli.lua b/lua/devcontainer-cli/devcontainer_cli.lua index 2aedc47..de5dbca 100644 --- a/lua/devcontainer-cli/devcontainer_cli.lua +++ b/lua/devcontainer-cli/devcontainer_cli.lua @@ -1,5 +1,5 @@ local config = require("devcontainer-cli.config") -local devcontainer_utils = require("devcontainer-cli.devcontainer_utils") +local utils = require("devcontainer-cli.devcontainer_utils") local M = {} @@ -11,10 +11,12 @@ local function define_autocommands() -- It connects with the Devcontainer just after quiting neovim. -- TODO: checks that the devcontainer is not already connected -- TODO: checks that there is a devcontainer running - vim.schedule(function() - local command = config.nvim_plugin_folder .. "/bin/connect_to_devcontainer.sh" - vim.fn.jobstart(command, { detach = true }) - end) + vim.schedule( + function() + local command = config.nvim_plugin_folder .. "/bin/connect_to_devcontainer.sh" + vim.fn.jobstart(command, { detach = true }) + end + ) end, }) end @@ -22,17 +24,37 @@ end -- executes a given command in the devcontainer of the current project directory ---@param opts (table) options for executing the command function M.exec(opts) - vim.validate({ args = { opts.args, "string" } }) - if opts.args == nil or opts.args == "" then - devcontainer_utils.exec() - else - devcontainer_utils.exec_cmd(opts.args) + local args = opts.args + vim.validate({ args = { args, "string", true } }) + + local parsed = { + cmd = nil, + direction = nil, + } + + if args ~= nil then + parsed = utils.parse(args) + + vim.validate({ + cmd = { parsed.cmd, "string", true }, + direction = { parsed.direction, "string", true }, + }) + if parsed.cmd == nil and parsed.direction == nil then + parsed.cmd = args + end end + + utils.exec(parsed.cmd, parsed.direction) +end + +-- toggle the current devcontainer window +function M.toggle() + utils.toggle() end -- bring up the devcontainer in the current project directory function M.up() - devcontainer_utils.bringup(vim.loop.cwd()) + utils.bringup() end -- Thanks to the autocommand executed after leaving the UI, after closing the diff --git a/lua/devcontainer-cli/devcontainer_utils.lua b/lua/devcontainer-cli/devcontainer_utils.lua index d7ce5dd..d102b93 100644 --- a/lua/devcontainer-cli/devcontainer_utils.lua +++ b/lua/devcontainer-cli/devcontainer_utils.lua @@ -1,24 +1,52 @@ -local config = require("devcontainer-cli.config") -local windows_utils = require("devcontainer-cli.windows_utils") -local folder_utils = require("devcontainer-cli.folder_utils") +local config = require("devcontainer-cli.config") +local folder_utils = require("devcontainer-cli.folder_utils") +local Terminal = require('toggleterm.terminal').Terminal +local mode = require('toggleterm.terminal').mode -local M = {} +local M = {} + +-- valid window directions +local directions = { + "float", + "horizontal", + "tab", + "vertical", +} -- window management variables -local prev_win = -1 -local win = -1 -local buffer = -1 +local _terminal = nil +-- +-- number of columns for displaying text +local terminal_columns = config.terminal_columns + +-- wrap the given text at max_width +---@param text (string) the text to wrap +---@return (string) the text wrapped +local function _wrap_text(text) + local wrapped_lines = {} + for line in text:gmatch("[^\n]+") do + local current_line = "" + for word in line:gmatch("%S+") do + if #current_line + #word <= terminal_columns then + current_line = current_line .. word .. " " + else + table.insert(wrapped_lines, current_line) + current_line = word .. " " + end + end + table.insert(wrapped_lines, current_line) + end + return table.concat(wrapped_lines, "\n") +end -- window the created window detaches set things back to -1 -local on_detach = function() - prev_win = -1 - win = -1 - buffer = -1 +local _on_detach = function() + _terminal = nil end -- on_fail callback ---@param exit_code (integer) the exit code from the failed job -local on_fail = function(exit_code) +local _on_fail = function(exit_code) vim.notify( "Devcontainer process has failed! exit_code: " .. exit_code, vim.log.levels.ERROR @@ -27,56 +55,81 @@ local on_fail = function(exit_code) vim.cmd("silent! :checktime") end -local on_success = function() +local _on_success = function() vim.notify("Devcontainer process succeeded!", vim.log.levels.INFO) end -- on_exit callback function to delete the open buffer when devcontainer exits -- in a neovim terminal ---@param code (integer) the exit code -local on_exit = function(_, code, _) +local _on_exit = function(code) if code == 0 then - on_success() + _on_success() return end - on_fail(code) + _on_fail(code) end ---- execute command ----@param cmd (string) the command to execute in the devcontainer terminal -function M.exec_command(cmd) - vim.fn.termopen( - cmd, - { - on_exit = on_exit, - on_stdout = function(_, _, _) - if win ~= -1 then - vim.api.nvim_win_call( - win, - function() - vim.cmd("normal! G") - end - ) - else - vim.notify("Executed " .. cmd) - end - end, - } - ) - if buffer ~= -1 then - vim.api.nvim_set_current_buf(buffer) - else - vim.notify("No buffer created, therefore no output from command will be visible", vim.log.levels.WARN) +-- check if the value is in the given table +local function tableContains(tbl, value) + for _, item in ipairs(tbl) do + if item == value then + return true + end end + + return false end -- create a new window and execute the given command ---@param cmd (string) the command to execute in the devcontainer terminal -local function spawn_and_execute(cmd) - prev_win = vim.api.nvim_get_current_win() - win, buffer = windows_utils.open_floating_window(on_detach) - M.exec_command(cmd) +---@param direction (string|nil) the placement of the window to be created (float, horizontal, vertical) +---@param close_on_exit (boolean|nil) whether to close terminal when process exits +local function _spawn_and_execute(cmd, direction, close_on_exit) + direction = vim.F.if_nil(direction, "float") + if tableContains(directions, direction) == false then + vim.notify("Invalid direction: " .. direction, vim.log.levels.ERROR) + return + end + + -- create the terminal + _terminal = Terminal:new { + cmd = cmd, + hidden = false, + display_name = "devcontainer-cli", + direction = vim.F.if_nil(direction, "float"), + dir = folder_utils.get_root(config.toplevel), + close_on_exit = vim.F.if_nil(close_on_exit, false), + on_open = function(term) + -- ensure that we are not in insert mode + vim.cmd("stopinsert") + vim.api.nvim_buf_set_keymap( + term.bufnr, + 'n', + '', + 'lua vim.api.nvim_buf_delete(' .. term.bufnr .. ', { force = true } )close', + { noremap = true, silent = true } + ) + vim.api.nvim_buf_set_keymap( + term.bufnr, + 'n', + 'q', + 'lua vim.api.nvim_buf_delete(' .. term.bufnr .. ', { force = true } )close', + { noremap = true, silent = true } + ) + vim.api.nvim_buf_set_keymap(term.bufnr, 'n', 't', 'close', { noremap = true, silent = true }) + end, + auto_scroll = true, + on_exit = function(_, _, code, _) + _on_exit(code) + _on_detach() + end, -- callback for when process closes + } + -- start in insert mode + _terminal:set_mode(mode.NORMAL) + -- now execute the command + _terminal:open() end -- build the initial part of a devcontainer command @@ -84,7 +137,7 @@ end -- (see man devcontainer) ---@return (string|nil) nil if no devcontainer_parent could be found otherwise -- the basic devcontainer command for the given type -local function devcontainer_command(action) +local function _devcontainer_command(action) local devcontainer_root = folder_utils.get_root(config.toplevel) if devcontainer_root == nil then vim.notify("Unable to find devcontainer directory...", vim.log.levels.ERROR) @@ -100,8 +153,8 @@ end -- helper function to generate devcontainer bringup command ---@return (string|nil) nil if no devcontainer_parent could be found otherwise the -- devcontainer bringup command -local function get_devcontainer_up_cmd() - local command = devcontainer_command("up") +local function _get_devcontainer_up_cmd() + local command = _devcontainer_command("up") if command == nil then return command end @@ -133,9 +186,49 @@ local function get_devcontainer_up_cmd() return command end +---@class ParsedArgs +---@field direction string? +---@field cmd string? + +---Take a users command arguments in the format "cmd='git commit' direction='float'" +---and parse this into a table of arguments +---{cmd = "git commit", direction = "float"} +---@param args string +---@return ParsedArgs +function M.parse(args) + local p = { + single = "'(.-)'", + double = '"(.-)"', + } + local result = {} + if args then + local quotes = args:match(p.single) and p.single or args:match(p.double) and p.double or nil + if quotes then + -- 1. extract the quoted command + local pattern = "(%S+)=" .. quotes + for key, value in args:gmatch(pattern) do + quotes = p.single + value = vim.fn.shellescape(value) + result[vim.trim(key)] = vim.fn.expandcmd(value:match(quotes)) + end + -- 2. then remove it from the rest of the argument string + args = args:gsub(pattern, "") + end + + for _, part in ipairs(vim.split(args, " ")) do + if #part > 1 then + local arg = vim.split(part, "=") + local key, value = arg[1], arg[2] + result[key] = value + end + end + end + return result +end + -- issues command to bringup devcontainer function M.bringup() - local command = get_devcontainer_up_cmd() + local command = _get_devcontainer_up_cmd() if command == nil then return @@ -144,7 +237,7 @@ function M.bringup() if config.interactive then vim.ui.input( { - prompt = windows_utils.wrap_text( + prompt = _wrap_text( "Spawning devcontainer with command: " .. command ) .. "\n\n" .. "Press q to cancel or any other key to continue\n" }, @@ -154,38 +247,71 @@ function M.bringup() "\nUser cancelled bringing up devcontainer" ) else - spawn_and_execute(command) + _spawn_and_execute(command) end end ) return end - spawn_and_execute(command) + _spawn_and_execute(command) +end + +--- execute command +---@param cmd (string) the command to execute in the devcontainer terminal +---@param direction (string|nil) the placement of the window to be created (left, right, bottom, float) +---@param close_on_exit (boolean|nil) whether to close terminal when process exits +function M._exec_command(cmd, direction, close_on_exit) + _spawn_and_execute(cmd, direction, close_on_exit) end -- execute the given cmd within the given devcontainer_parent ---@param cmd (string) the command to issue in the devcontainer terminal -function M.exec_cmd(cmd) - local command = devcontainer_command("exec") +---@param direction (string|nil) the placement of the window to be created +-- (left, right, bottom, float) +function M._exec_cmd(cmd, direction) + local command = _devcontainer_command("exec") if command == nil then return end command = command .. " " .. config.shell .. " -c '" .. cmd .. "'" - spawn_and_execute(command) + M._exec_command(command, direction, false) end -- execute a given cmd within the given devcontainer_parent -function M.exec() - vim.ui.input( - { prompt = "Enter command:" }, - function(input) - if input ~= nil then - M.exec_cmd(input) +---@param cmd (string|nil) the command to issue in the devcontainer terminal +---@param direction (string|nil) the placement of the window to be created +-- (left, right, bottom, float) +function M.exec(cmd, direction) + if _terminal ~= nil then + vim.notify("There is already a devcontainer process running.", vim.log.levels.WARN) + return + end + + if cmd == nil or cmd == "" then + vim.ui.input( + { prompt = "Enter command:" }, + function(input) + if input ~= nil then + M._exec_cmd(input, direction) + else + vim.notify("No command received, ignoring.", vim.log.levels.WARN) + end end - end - ) + ) + else + M._exec_cmd(cmd, direction) + end +end + +-- toggle the current terminal +function M.toggle() + if _terminal == nil then + vim.notify("No devcontainer window to toggle.", vim.log.levels.WARN) + return + end + _terminal:toggle() end return M diff --git a/lua/devcontainer-cli/folder_utils.lua b/lua/devcontainer-cli/folder_utils.lua index b86c6a6..643a76e 100644 --- a/lua/devcontainer-cli/folder_utils.lua +++ b/lua/devcontainer-cli/folder_utils.lua @@ -1,17 +1,17 @@ local M = {} -- return true if directory exists -local function directory_exists(target_folder) +local function _directory_exists(target_folder) return (vim.fn.isdirectory(target_folder) == 1) end -- get the devcontainer path for the given directory ---@param directory (string) the directory containing .devcontainer ---@return (string|nil) directory if a devcontainer exists within it or nil otherwise -local function get_devcontainer_parent(directory) +local function _get_devcontainer_parent(directory) local devcontainer_directory = directory .. '/.devcontainer' - if directory_exists(devcontainer_directory) then + if _directory_exists(devcontainer_directory) then return directory end @@ -24,9 +24,9 @@ end -- returned ---@return (string|nil) the devcontainer directory closest to the root directory -- or the first if toplevel is true, and nil if no directory was found -local function get_root_directory(directory, toplevel) +local function _get_root_directory(directory, toplevel) local parent_directory = vim.fn.fnamemodify(directory, ':h') - local devcontainer_parent = get_devcontainer_parent(directory) + local devcontainer_parent = _get_devcontainer_parent(directory) -- Base case: If we've reached the root directory if parent_directory == directory then @@ -37,7 +37,7 @@ local function get_root_directory(directory, toplevel) return devcontainer_parent end - local upper_devcontainer_directory = get_root_directory(parent_directory, toplevel) + local upper_devcontainer_directory = _get_root_directory(parent_directory, toplevel) -- no devcontainer higher up so return what was found here if upper_devcontainer_directory == nil then return devcontainer_parent @@ -55,7 +55,7 @@ end -- or the first if toplevel is true, and nil if no directory was found function M.get_root(toplevel) local current_directory = vim.fn.getcwd() - return get_root_directory(current_directory, toplevel) + return _get_root_directory(current_directory, toplevel) end return M diff --git a/lua/devcontainer-cli/init.lua b/lua/devcontainer-cli/init.lua index a690231..2e8e29c 100644 --- a/lua/devcontainer-cli/init.lua +++ b/lua/devcontainer-cli/init.lua @@ -35,6 +35,15 @@ function M.setup(opts) } ) + vim.api.nvim_create_user_command( + "DevcontainerToggle", + devcontainer_cli.toggle, + { + nargs = 0, + desc = "Toggle the current devcontainer window.", + } + ) + vim.api.nvim_create_user_command( "DevcontainerConnect", devcontainer_cli.connect, diff --git a/lua/devcontainer-cli/windows_utils.lua b/lua/devcontainer-cli/windows_utils.lua deleted file mode 100644 index 485daea..0000000 --- a/lua/devcontainer-cli/windows_utils.lua +++ /dev/null @@ -1,76 +0,0 @@ -local config = require("devcontainer-cli.config") - -local M = {} - --- number of columns for displaying text -local terminal_columns = config.terminal_columns - --- wrap the given text at max_width ----@param text (string) the text to wrap ----@return (string) the text wrapped -function M.wrap_text(text) - local wrapped_lines = {} - for line in text:gmatch("[^\n]+") do - local current_line = "" - for word in line:gmatch("%S+") do - if #current_line + #word <= terminal_columns then - current_line = current_line .. word .. " " - else - table.insert(wrapped_lines, current_line) - current_line = word .. " " - end - end - table.insert(wrapped_lines, current_line) - end - return table.concat(wrapped_lines, "\n") -end - --- create a floating window ----@param on_detach (function) call back for when the window is detached ----@return integer, integer the window and buffer numbers -function M.open_floating_window(on_detach) - local buf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_buf_set_option(buf, 'bufhidden', 'wipe') - vim.api.nvim_buf_set_option(buf, 'filetype', 'devcontainer-cli') - vim.api.nvim_buf_set_keymap(buf, 'n', 'q', 'close', {}) - vim.api.nvim_buf_set_keymap(buf, 'n', '', 'close', {}) - - local width = math.ceil( - math.min(vim.o.columns, math.max(terminal_columns, vim.o.columns - 20)) - ) - local height = math.ceil(math.min(vim.o.lines, math.max(20, vim.o.lines - 10))) - - local row = math.ceil(vim.o.lines - height) * 0.5 - 1 - local col = math.ceil(vim.o.columns - width) * 0.5 - 1 - - local win = vim.api.nvim_open_win(buf, true, { - relative = "editor", - width = width, - height = height, - row = row, - col = col, - style = "minimal", - border = "rounded", - title = "devcontainer-cli", - title_pos = "center", - -- noautocommand = false, - }) - -- Attach autocommand for when the buffer is detached (closed) - vim.api.nvim_buf_attach(buf, false, { - on_detach = on_detach - }) - - return win, buf -end - --- send text to the given buffer ----@param text (string) the text to send ----@param buffer (integer) the buffer to send text to -function M.send_text(text, buffer) - local text_tbl = vim.split(M.wrap_text(text), "\n") - - -- Set the content of the buffer - vim.api.nvim_buf_set_lines(buffer, 0, -1, false, text_tbl) -end - -return M diff --git a/tests/devcontainer_cli/unit_tests.lua b/tests/devcontainer-cli/unit_tests.lua similarity index 90% rename from tests/devcontainer_cli/unit_tests.lua rename to tests/devcontainer-cli/unit_tests.lua index e5f74ea..a785e9a 100644 --- a/tests/devcontainer_cli/unit_tests.lua +++ b/tests/devcontainer-cli/unit_tests.lua @@ -1,5 +1,4 @@ local folder_utils = require("devcontainer-cli.folder_utils") -local utils = require("devcontainer-cli.devcontainer_utils") describe("folder_utils.get_root:", function() it( @@ -51,12 +50,3 @@ describe("folder_utils.get_root:", function() end ) end) - -describe("devcontainer_utils.exec_command:", function() - it( - "check if a command can be executed", - function() - utils.exec_command("echo 'Hello World!'") - end - ) -end)