From 3aa5d3e0673002526cc8e3a98b3ed82e949ddd32 Mon Sep 17 00:00:00 2001 From: lguarda Date: Tue, 20 May 2025 21:35:42 +0200 Subject: [PATCH 01/10] Add function to parse vim.v.argv + ut --- lua/common/arg_parse.lua | 59 ++++++++++++++++++++++++++++++++++++++++ test/test.lua | 45 ++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 lua/common/arg_parse.lua create mode 100644 test/test.lua diff --git a/lua/common/arg_parse.lua b/lua/common/arg_parse.lua new file mode 100644 index 0000000..e15f2c1 --- /dev/null +++ b/lua/common/arg_parse.lua @@ -0,0 +1,59 @@ +local function detect_line_option(str) + return str:match("^%+(%d+)$") +end + +local function detect_dash_parametes(str) + return str:match("^-") ~= nil +end + +local dummy_parameters_list = { + ["-O"] = "vplit", + ["-p"] = "tab", + ["--"] = "ignore" +} + +local function detect_parameters(str) + local handled_opt = dummy_parameters_list[str] + if handled_opt then + return handled_opt + end + return detect_dash_parametes(str) +end + +function extract_args(args, options) + local files = {} + local index = 1 + for i, arg in ipairs(args) do + repeat + if i == 1 then + break + end + local opt = detect_parameters(arg) + if not options.ignore and opt then + if opt ~= true then + options[opt] = true + end + break + end + + local line = detect_line_option(arg) + if not options.ignore and line then + -- When + option used first let's apply this on the first file + local index = (index > 1) and (index - 1) or (index) + if not files[index] then + files[index] = {} + end + files[index].line = line + break + end + + if not files[index] then + files[index] = {} + end + files[index].file = arg + index = index + 1 + break + until false + end + return files +end diff --git a/test/test.lua b/test/test.lua new file mode 100644 index 0000000..c7ef850 --- /dev/null +++ b/test/test.lua @@ -0,0 +1,45 @@ +-- really need to find a better solution to include lua file into unit tests +local function get_source_path() + return debug.getinfo(2, "S").source:sub(2):gsub("[a-z-_1-9A-Z]+.lua$", "") +end +package.path = package.path .. ";" .. get_source_path() .. "../lua/?.lua" +require "common.arg_parse" + +describe("unception nvim tests", function() + describe("argument parser", function() + local tests_list = { + -- { -- NYI + -- intput = { "file", "\\+32" }, + -- output = { { file = "open" }, { file = "+32" } } + -- }, + { + intput = { "/usr/bin/nvim", "file", "+5" }, + output = { { file = "file", line = "5" } } + }, + { + intput = { "/usr/bin/dontcare", "file", "file2", "+32" }, + output = { { file = "file" }, { file = "file2", line = "32" } } + }, + { + intput = { "/usr/bin/dontcare", "--ignored-option-long", "-i", "file", "file2", "+32" }, + output = { { file = "file" }, { file = "file2", line = "32" } } + }, + -- { -- NYI + -- intput = { "/usr/bin/dontcare", "\\- file starting with dash" }, + -- output = { { file = "- file starting with dash" } } + -- }, + { + intput = { "/usr/bin/dontcare", "--", "- file starting with dash" }, + output = { { file = "- file starting with dash" } } + } + } + for i, test in ipairs(tests_list) do + it("argument parser " .. tostring(i), function() + local options = {} + local ret = extract_args(test.intput, options) + assert.are.same(test.output, ret) + end) + end + + end) +end) From 60728992c3e579155f5db4e4acfd86ab4edecea5 Mon Sep 17 00:00:00 2001 From: lguarda Date: Tue, 20 May 2025 23:25:52 +0200 Subject: [PATCH 02/10] Replace args detection with new function Improvement: - directory can be open - multiple files are supported - cmd line argument -o -O -p are supported to open in split vsplit or tab - cmd line file option + is also supported so `nvim file +32` will i open file at the line 32 --- lua/client/client.lua | 38 +++++++++--------- lua/common/arg_parse.lua | 7 +++- lua/server/server_functions.lua | 68 ++++++++++++++++++++------------- plugin/main.lua | 5 +++ 4 files changed, 68 insertions(+), 50 deletions(-) diff --git a/lua/client/client.lua b/lua/client/client.lua index fc8f0db..4e74385 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -1,31 +1,30 @@ require("common.common_functions") +require("common.arg_parse") -- We don't want to overwrite :h shada vim.o.sdf = "NONE" -- We don't want to start. Send the args to the server instance instead. -local args = vim.call("argv") - -local arg_str = "" -for index, iter in pairs(args) do - local absolute_filepath = unception_get_absolute_filepath(iter) - - if (string.len(arg_str) == 0) then - arg_str = unception_escape_special_chars(absolute_filepath) - else - arg_str = arg_str.." "..unception_escape_special_chars(absolute_filepath) +-- get only files argument already parsed by neovim +local true_file_args = vim.call("argv") + +local options = {} +-- Use raw argv to retreive some options as well as files lines when it exist +local guess_argv = extract_args(vim.v.argv, options) +local file_args = {} +local true_file_index = 1 +for _, file in ipairs(guess_argv) do + -- validate file against true file args + if file.path == true_file_args[true_file_index] then + local absolute_filepath = unception_get_absolute_filepath(file.path) + table.insert(file_args, {path=unception_escape_special_chars(absolute_filepath), line=file.line}) + true_file_index = true_file_index +1 end end -- Send messages to host on existing pipe. local sock = vim.fn.sockconnect("pipe", os.getenv(unception_pipe_path_host_env_var), {rpc = true}) -local edit_files_call = "unception_edit_files(" - .."\""..arg_str.."\", " - ..#args..", " - ..vim.inspect(vim.g.unception_open_buffer_in_new_tab)..", " - ..vim.inspect(vim.g.unception_delete_replaced_buffer)..", " - ..vim.inspect(vim.g.unception_enable_flavor_text)..")" -vim.fn.rpcrequest(sock, "nvim_exec_lua", edit_files_call, {}) +vim.rpcrequest(sock, "nvim_exec_lua", "unception_edit_files(...)", {file_args, options, vim.g.unception_open_buffer_in_new_tab, vim.g.unception_delete_replaced_buffer, vim.g.unception_enable_flavor_text}) if (not vim.g.unception_block_while_host_edits) then -- Our work here is done. Kill the nvim session that would have started otherwise. @@ -50,10 +49,7 @@ end local nested_pipe_path = vim.call("serverstart") -- Send the pipe path and edited filepath to the host so that it knows what file to look for and who to respond to. -local notify_when_done_call = "unception_notify_when_done_editing(" - ..vim.inspect(nested_pipe_path).."," - ..vim.inspect(arg_str)..")" -vim.fn.rpcnotify(sock, "nvim_exec_lua", notify_when_done_call, {}) +vim.rpcrequest(sock, "nvim_exec_lua", "unception_notify_when_done_editing(...)", {nested_pipe_path, file_args[1].path}) -- Sleep forever. The host session will kill this when it's done editing. while (true) diff --git a/lua/common/arg_parse.lua b/lua/common/arg_parse.lua index e15f2c1..dff5772 100644 --- a/lua/common/arg_parse.lua +++ b/lua/common/arg_parse.lua @@ -7,7 +7,8 @@ local function detect_dash_parametes(str) end local dummy_parameters_list = { - ["-O"] = "vplit", + ["-o"] = "split", + ["-O"] = "vsplit", ["-p"] = "tab", ["--"] = "ignore" } @@ -24,6 +25,8 @@ function extract_args(args, options) local files = {} local index = 1 for i, arg in ipairs(args) do + -- this repeat block allow break to act as continue in the for loop + -- it may need some refactoring repeat if i == 1 then break @@ -50,7 +53,7 @@ function extract_args(args, options) if not files[index] then files[index] = {} end - files[index].file = arg + files[index].path = arg index = index + 1 break until false diff --git a/lua/server/server_functions.lua b/lua/server/server_functions.lua index ebfdde8..3a083ed 100644 --- a/lua/server/server_functions.lua +++ b/lua/server/server_functions.lua @@ -41,9 +41,9 @@ function _G.unception_handle_quitpre(quitpre_buffer_filepath) if (quitpre_buffer_filepath == filepath_to_check) then -- If this buffer replaced the blocked terminal buffer, we should restore it to the same window. if (blocked_terminal_buffer_id ~= nil and vim.fn.bufexists(blocked_terminal_buffer_id) == 1) then - vim.cmd("split") -- Open a new window and switch focus to it. + vim.cmd("split") -- Open a new window and switch focus to it. vim.cmd("buffer " .. blocked_terminal_buffer_id) -- Set the buffer for that window to the buffer that was replaced. - vim.cmd("wincmd x") -- Navigate to previous (initial) window, and proceed with quitting. + vim.cmd("wincmd x") -- Navigate to previous (initial) window, and proceed with quitting. end unblock_client_and_reset_state() @@ -53,15 +53,39 @@ end function _G.unception_notify_when_done_editing(pipe_to_respond_on, filepath) filepath_to_check = filepath blocked_terminal_buffer_id = last_replaced_buffer_id - response_sock = vim.fn.sockconnect("pipe", pipe_to_respond_on, {rpc = true}) - unception_quitpre_autocmd_id = vim.api.nvim_create_autocmd("QuitPre",{ command = "lua unception_handle_quitpre(vim.fn.expand(':p'))"}) + response_sock = vim.fn.sockconnect("pipe", pipe_to_respond_on, { rpc = true }) + unception_quitpre_autocmd_id = vim.api.nvim_create_autocmd("QuitPre", + { command = "lua unception_handle_quitpre(vim.fn.expand(':p'))" }) -- Create an autocmd for BufUnload as a failsafe should QuitPre not get triggered on the target buffer (e.g. if a user runs :bdelete). - unception_bufunload_autocmd_id = vim.api.nvim_create_autocmd("BufUnload",{ command = "lua unception_handle_bufunload(vim.fn.expand(':p'))"}) + unception_bufunload_autocmd_id = vim.api.nvim_create_autocmd("BufUnload", + { command = "lua unception_handle_bufunload(vim.fn.expand(':p'))" }) end -function _G.unception_edit_files(file_args, num_files_in_list, open_in_new_tab, delete_replaced_buffer, enable_flavor_text) - vim.api.nvim_exec_autocmds("User", {pattern = "UnceptionEditRequestReceived"}) +local function unception_detect_open_method(options) + local open_method = vim.g.unception_open_buffer_method_for_other + if options["split"] then + return "split" + end + if options["vsplit"] then + return "vsplit" + end + if options["tab"] then + return "tabnew" + end + return open_method +end + +local function unception_open_file(open_method, file) + if file.line then + vim.cmd(("%s +%d %s"):format(open_method, file.line, file.path)) + else + vim.cmd(("%s %s"):format(open_method, file.path)) + end +end + +function _G.unception_edit_files(file_args, options, open_in_new_tab, delete_replaced_buffer, enable_flavor_text) + vim.api.nvim_exec_autocmds("User", { pattern = "UnceptionEditRequestReceived" }) -- log buffer number so that we can delete it later. We don't want a ton of -- running terminal buffers in the background when we switch to a new nvim buffer. @@ -69,26 +93,17 @@ function _G.unception_edit_files(file_args, num_files_in_list, open_in_new_tab, -- If there aren't arguments, we just want a new, empty buffer, but if -- there are, append them to the host Neovim session's arguments list. - if (num_files_in_list > 0) then - -- Had some issues when using argedit. Explicitly calling these - -- separately appears to work though. - vim.cmd("0argadd "..file_args) + local open_method = unception_detect_open_method(options) - if (open_in_new_tab) then - last_replaced_buffer_id = nil - vim.cmd("tab argument 1") - else - last_replaced_buffer_id = vim.fn.bufnr() - vim.cmd("argument 1") + if (#file_args > 0) then + if open_in_new_tab and open_method ~= "tabnew" then + vim.cmd("tabnew") + unception_open_file("edit", file_args[1]) + table.remove(file_args, 1) + end + for _, file in ipairs(file_args) do + unception_open_file(open_method, file) end - - -- This is kind of stupid, but basically, it appears that Neovim may - -- not always properly handle opening buffers using the method - -- above(?), notably if it's opening directly to a directory using - -- netrw. Calling "edit" here appears to give it another chance to - -- properly handle opening the buffer; otherwise it can occasionally - -- segfault. - vim.cmd("edit") else if (open_in_new_tab) then last_replaced_buffer_id = nil @@ -102,7 +117,7 @@ function _G.unception_edit_files(file_args, num_files_in_list, open_in_new_tab, -- We don't want to delete the replaced buffer if there wasn't a replaced buffer. if (delete_replaced_buffer and last_replaced_buffer_id ~= nil) then if (vim.fn.len(vim.fn.win_findbuf(tmp_buf_number)) == 0 and string.sub(vim.api.nvim_buf_get_name(tmp_buf_number), 1, 7) == "term://") then - vim.cmd("bdelete! "..tmp_buf_number) + vim.cmd("bdelete! " .. tmp_buf_number) end end @@ -110,4 +125,3 @@ function _G.unception_edit_files(file_args, num_files_in_list, open_in_new_tab, print("Unception prevented inception!") end end - diff --git a/plugin/main.lua b/plugin/main.lua index 91907ea..d46ff26 100644 --- a/plugin/main.lua +++ b/plugin/main.lua @@ -9,6 +9,11 @@ if(vim.g.unception_open_buffer_in_new_tab == nil) then vim.g.unception_open_buffer_in_new_tab = false end +-- This is the default opening method, that can be override by cmd line arguement split -o vsplit -O tab -p +if(vim.g.unception_open_buffer_method_for_other == nil) then + vim.g.unception_open_buffer_method_for_other = "tabnew" +end + if (vim.g.unception_enable_flavor_text == nil) then vim.g.unception_enable_flavor_text = true end From ab6442b21491dc3601b62603550c6004b83ccc89 Mon Sep 17 00:00:00 2001 From: lguarda Date: Wed, 21 May 2025 18:44:39 +0200 Subject: [PATCH 03/10] Open the parser to modification & use better naming & include all option in a dictionary Also change unit test since the scope of unception_arg_parse as change --- lua/client/client.lua | 31 +++---- lua/common/arg_parse.lua | 155 ++++++++++++++++++++++++-------- lua/server/server_functions.lua | 29 +++--- plugin/main.lua | 9 +- test/test.lua | 39 +++++--- 5 files changed, 182 insertions(+), 81 deletions(-) diff --git a/lua/client/client.lua b/lua/client/client.lua index 4e74385..eb470a0 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -7,24 +7,26 @@ vim.o.sdf = "NONE" -- We don't want to start. Send the args to the server instance instead. -- get only files argument already parsed by neovim local true_file_args = vim.call("argv") - -local options = {} +local options = { + open_in_new_tab = vim.g.unception_open_buffer_in_new_tab, + delete_replaced_buffer = vim.g.unception_delete_replaced_buffer, + enable_flavor_text = vim.g.unception_enable_flavor_text, + multi_file_open_method = vim.g.unception_multi_file_open_method, +} -- Use raw argv to retreive some options as well as files lines when it exist -local guess_argv = extract_args(vim.v.argv, options) +local file_lines = unception_arg_parse(vim.v.argv, options) local file_args = {} -local true_file_index = 1 -for _, file in ipairs(guess_argv) do - -- validate file against true file args - if file.path == true_file_args[true_file_index] then - local absolute_filepath = unception_get_absolute_filepath(file.path) - table.insert(file_args, {path=unception_escape_special_chars(absolute_filepath), line=file.line}) - true_file_index = true_file_index +1 - end +for _, file in ipairs(true_file_args) do + local absolute_filepath = unception_get_absolute_filepath(file) + table.insert(file_args , { + path = unception_escape_special_chars(absolute_filepath), + line = file_lines[file] + }) end -- Send messages to host on existing pipe. -local sock = vim.fn.sockconnect("pipe", os.getenv(unception_pipe_path_host_env_var), {rpc = true}) -vim.rpcrequest(sock, "nvim_exec_lua", "unception_edit_files(...)", {file_args, options, vim.g.unception_open_buffer_in_new_tab, vim.g.unception_delete_replaced_buffer, vim.g.unception_enable_flavor_text}) +local sock = vim.fn.sockconnect("pipe", os.getenv(unception_pipe_path_host_env_var), { rpc = true }) +vim.rpcrequest(sock, "nvim_exec_lua", "unception_edit_files(...)", { file_args, options }) if (not vim.g.unception_block_while_host_edits) then -- Our work here is done. Kill the nvim session that would have started otherwise. @@ -49,11 +51,10 @@ end local nested_pipe_path = vim.call("serverstart") -- Send the pipe path and edited filepath to the host so that it knows what file to look for and who to respond to. -vim.rpcrequest(sock, "nvim_exec_lua", "unception_notify_when_done_editing(...)", {nested_pipe_path, file_args[1].path}) +vim.rpcnotify(sock, "nvim_exec_lua", "unception_notify_when_done_editing(...)", { nested_pipe_path, file_args[1].path }) -- Sleep forever. The host session will kill this when it's done editing. while (true) do vim.cmd("sleep 10") end - diff --git a/lua/common/arg_parse.lua b/lua/common/arg_parse.lua index dff5772..6158157 100644 --- a/lua/common/arg_parse.lua +++ b/lua/common/arg_parse.lua @@ -6,57 +6,138 @@ local function detect_dash_parametes(str) return str:match("^-") ~= nil end -local dummy_parameters_list = { +local parameters_map = { ["-o"] = "split", ["-O"] = "vsplit", ["-p"] = "tab", - ["--"] = "ignore" } local function detect_parameters(str) - local handled_opt = dummy_parameters_list[str] + local handled_opt = parameters_map[str] if handled_opt then return handled_opt end return detect_dash_parametes(str) end -function extract_args(args, options) - local files = {} - local index = 1 - for i, arg in ipairs(args) do - -- this repeat block allow break to act as continue in the for loop - -- it may need some refactoring - repeat - if i == 1 then - break - end - local opt = detect_parameters(arg) - if not options.ignore and opt then - if opt ~= true then - options[opt] = true - end - break - end - local line = detect_line_option(arg) - if not options.ignore and line then - -- When + option used first let's apply this on the first file - local index = (index > 1) and (index - 1) or (index) - if not files[index] then - files[index] = {} - end - files[index].line = line - break - end +local function parse_double_dash(arg, state) + if state.double_dash then + return false + end + if arg == "--" then + state.double_dash = true + return true + end + return false +end + +local function parse_option(arg, state) + if state.double_dash then + return false + end + + local opt = detect_parameters(arg) + + if not opt then + return false + end + + if opt ~= true then + state.options.multi_file_open_method = opt + end + return true +end + +local function parse_line_number(arg, state) + if state.double_dash then + return false + end + + local line = detect_line_option(arg) + if not line then + return false + end + + -- When + option used first let's apply this on the first file + local index = (state.index > 1) and (state.index - 1) or (state.index) + + if not state.files[index] then + state.files[index] = {} + end + + state.files[index].line = line + + return true +end + +local function parse_file(arg, state) + if not state.files[state.index] then + state.files[state.index] = {} + end + state.files[state.index].path = arg + state.index = state.index + 1 + return true +end - if not files[index] then - files[index] = {} +---This function filter all files +---and return only the mapping of files -> line number +---@param files {[string]:any}[] +---@return { [string]: integer } dict with file -> line +local function extract_file_with_line_number(files) + local ret = {} + for _, file in ipairs(files) do + if file.line then + ret[file.path] = file.line + end + end + + return ret +end + +---This is the list of parsing function +---Note that the order matter +local parser = { + parse_double_dash, + parse_option, + parse_line_number, + parse_file, +} + +---This function parse bare neovim argument list +---It has two purpose: +--- +---1. Detect cmd line option `-o -O -p`, and fill the given options +---2. Detect +\d line number specifier, and returning a dictionary of file:line number +---Here we don't care of file without any file number since those +---will be handled by neovim argument parser which is smartest than this parser +--- +---Limitation: this is a really dumb parser if you run neovim for example with +---`nvim --cmd "this_is_cmd_argument" +32` this_is_cmd_argument will end up within +---the list of file returned by this function, we don't really care since files +---to open will be retrieved by the neovim parser via vim.call("argv") here we want +---a good enough parser to detect line number after a file and some options that's it +---@param args string[] this argument need the bare argv from neovim +---@param options table +---@return { [string]: integer } +function unception_arg_parse(args, options) + local state = { + files = {}, + index = 1, + double_dash = false, + options = options + } + + -- remove nvim in argv[1] + table.remove(args, 1) + + for _, arg in ipairs(args) do + for _, fn in ipairs(parser) do + if fn(arg, state) then + break end - files[index].path = arg - index = index + 1 - break - until false + end end - return files + + return extract_file_with_line_number(state.files) end diff --git a/lua/server/server_functions.lua b/lua/server/server_functions.lua index 3a083ed..7b8c501 100644 --- a/lua/server/server_functions.lua +++ b/lua/server/server_functions.lua @@ -62,17 +62,16 @@ function _G.unception_notify_when_done_editing(pipe_to_respond_on, filepath) { command = "lua unception_handle_bufunload(vim.fn.expand(':p'))" }) end +local open_methods_table = { + split = "split", + vsplit = "vsplit", + tab = "tabnew", + bg = "argadd", +} + local function unception_detect_open_method(options) - local open_method = vim.g.unception_open_buffer_method_for_other - if options["split"] then - return "split" - end - if options["vsplit"] then - return "vsplit" - end - if options["tab"] then - return "tabnew" - end + local open_method = open_methods_table[options.multi_file_open_method] + -- todo check error return open_method end @@ -84,7 +83,7 @@ local function unception_open_file(open_method, file) end end -function _G.unception_edit_files(file_args, options, open_in_new_tab, delete_replaced_buffer, enable_flavor_text) +function _G.unception_edit_files(file_args, options) vim.api.nvim_exec_autocmds("User", { pattern = "UnceptionEditRequestReceived" }) -- log buffer number so that we can delete it later. We don't want a ton of @@ -96,7 +95,7 @@ function _G.unception_edit_files(file_args, options, open_in_new_tab, delete_rep local open_method = unception_detect_open_method(options) if (#file_args > 0) then - if open_in_new_tab and open_method ~= "tabnew" then + if options.open_in_new_tab and open_method ~= "tabnew" then vim.cmd("tabnew") unception_open_file("edit", file_args[1]) table.remove(file_args, 1) @@ -105,7 +104,7 @@ function _G.unception_edit_files(file_args, options, open_in_new_tab, delete_rep unception_open_file(open_method, file) end else - if (open_in_new_tab) then + if (options.open_in_new_tab) then last_replaced_buffer_id = nil vim.cmd("tabnew") else @@ -115,13 +114,13 @@ function _G.unception_edit_files(file_args, options, open_in_new_tab, delete_rep end -- We don't want to delete the replaced buffer if there wasn't a replaced buffer. - if (delete_replaced_buffer and last_replaced_buffer_id ~= nil) then + if (options.delete_replaced_buffer and last_replaced_buffer_id ~= nil) then if (vim.fn.len(vim.fn.win_findbuf(tmp_buf_number)) == 0 and string.sub(vim.api.nvim_buf_get_name(tmp_buf_number), 1, 7) == "term://") then vim.cmd("bdelete! " .. tmp_buf_number) end end - if (enable_flavor_text) then + if (options.enable_flavor_text) then print("Unception prevented inception!") end end diff --git a/plugin/main.lua b/plugin/main.lua index d46ff26..5241e10 100644 --- a/plugin/main.lua +++ b/plugin/main.lua @@ -10,8 +10,13 @@ if(vim.g.unception_open_buffer_in_new_tab == nil) then end -- This is the default opening method, that can be override by cmd line arguement split -o vsplit -O tab -p -if(vim.g.unception_open_buffer_method_for_other == nil) then - vim.g.unception_open_buffer_method_for_other = "tabnew" +if(vim.g.unception_multi_file_open_method == nil) then + -- tab + -- split + -- vplit + -- argadd + --vim.g.unception_multi_file_open_method = "argadd" + vim.g.unception_multi_file_open_method = "tab" end if (vim.g.unception_enable_flavor_text == nil) then diff --git a/test/test.lua b/test/test.lua index c7ef850..e3c6b86 100644 --- a/test/test.lua +++ b/test/test.lua @@ -9,35 +9,50 @@ describe("unception nvim tests", function() describe("argument parser", function() local tests_list = { -- { -- NYI - -- intput = { "file", "\\+32" }, + -- argv = { "file", "\\+32" }, -- output = { { file = "open" }, { file = "+32" } } -- }, { - intput = { "/usr/bin/nvim", "file", "+5" }, - output = { { file = "file", line = "5" } } + argv = { "/usr/bin/nvim", "file", "+5", "-p" }, + output = { { path = "file", line = "5" } }, + option = { multi_file_open_method="tab" } }, { - intput = { "/usr/bin/dontcare", "file", "file2", "+32" }, - output = { { file = "file" }, { file = "file2", line = "32" } } + argv = { "/usr/bin/dontcare", "file", "file2", "+32" }, + output = { { path = "file" }, { path = "file2", line = "32" } }, + option = { } }, { - intput = { "/usr/bin/dontcare", "--ignored-option-long", "-i", "file", "file2", "+32" }, - output = { { file = "file" }, { file = "file2", line = "32" } } + argv = { "/usr/bin/dontcare", "--ignored-option-long", "-i", "file", "file2", "+32" }, + output = { { path = "file" }, { path = "file2", line = "32" } }, + option = { } }, -- { -- NYI - -- intput = { "/usr/bin/dontcare", "\\- file starting with dash" }, + -- argv = { "/usr/bin/dontcare", "\\- file starting with dash" }, -- output = { { file = "- file starting with dash" } } -- }, { - intput = { "/usr/bin/dontcare", "--", "- file starting with dash" }, - output = { { file = "- file starting with dash" } } - } + argv = { "/usr/bin/dontcare", "--", "- file starting with dash" }, + output = { { path = "- file starting with dash" } }, + option = { } + }, + { + argv = { "/usr/bin/nvim", "file", "+5", "-o" }, + output = { { path = "file", line = "5" } }, + option = { multi_file_open_method="split" } + }, + { + argv = { "/usr/bin/nvim", "file", "+5", "-O" }, + output = { { path = "file", line = "5" } }, + option = { multi_file_open_method="vsplit" } + }, } for i, test in ipairs(tests_list) do it("argument parser " .. tostring(i), function() local options = {} - local ret = extract_args(test.intput, options) + local ret = unception_arg_parse(test.argv, options) assert.are.same(test.output, ret) + assert.are.same(options, test.option) end) end From f9063539792c9a49e23461f171bb8908c202e8e4 Mon Sep 17 00:00:00 2001 From: lguarda Date: Thu, 22 May 2025 18:24:35 +0200 Subject: [PATCH 04/10] Put back the argadd method, note it doesn't support line number yet --- lua/server/server_functions.lua | 56 +++++++++++++++++++++++++++------ plugin/main.lua | 3 +- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/lua/server/server_functions.lua b/lua/server/server_functions.lua index 7b8c501..2223f57 100644 --- a/lua/server/server_functions.lua +++ b/lua/server/server_functions.lua @@ -66,12 +66,15 @@ local open_methods_table = { split = "split", vsplit = "vsplit", tab = "tabnew", - bg = "argadd", + argadd = "argadd", } local function unception_detect_open_method(options) local open_method = open_methods_table[options.multi_file_open_method] - -- todo check error + if open_method == nil then + print("unception can't find multi_file_open_method fall back to tab") + open_method = "tabnew" + end return open_method end @@ -83,6 +86,44 @@ local function unception_open_file(open_method, file) end end +local function unception_open_file_other(file_args, options, open_method) + if options.open_in_new_tab and open_method ~= "tabnew" then + vim.cmd("tabnew") + unception_open_file("edit", file_args[1]) + table.remove(file_args, 1) + end + for _, file in ipairs(file_args) do + unception_open_file(open_method, file) + end +end + +local function unception_open_file_argadd(file_args, options) + local path = {} + for _, file in ipairs(file_args) do + table.insert(path, file.path) + end + path = table.concat(path, " ") + -- Had some issues when using argedit. Explicitly calling these + -- separately appears to work though. + vim.cmd("0argadd "..path) + + if (options.open_in_new_tab) then + last_replaced_buffer_id = nil + vim.cmd("tab argument 1") + else + last_replaced_buffer_id = vim.fn.bufnr() + vim.cmd("argument 1") + end + + -- This is kind of stupid, but basically, it appears that Neovim may + -- not always properly handle opening buffers using the method + -- above(?), notably if it's opening directly to a directory using + -- netrw. Calling "edit" here appears to give it another chance to + -- properly handle opening the buffer; otherwise it can occasionally + -- segfault. + vim.cmd("edit") +end + function _G.unception_edit_files(file_args, options) vim.api.nvim_exec_autocmds("User", { pattern = "UnceptionEditRequestReceived" }) @@ -95,13 +136,10 @@ function _G.unception_edit_files(file_args, options) local open_method = unception_detect_open_method(options) if (#file_args > 0) then - if options.open_in_new_tab and open_method ~= "tabnew" then - vim.cmd("tabnew") - unception_open_file("edit", file_args[1]) - table.remove(file_args, 1) - end - for _, file in ipairs(file_args) do - unception_open_file(open_method, file) + if (open_method == "argadd") then + unception_open_file_argadd(file_args, options) + else + unception_open_file_other(file_args, options, open_method) end else if (options.open_in_new_tab) then diff --git a/plugin/main.lua b/plugin/main.lua index 5241e10..dd0ae47 100644 --- a/plugin/main.lua +++ b/plugin/main.lua @@ -15,8 +15,7 @@ if(vim.g.unception_multi_file_open_method == nil) then -- split -- vplit -- argadd - --vim.g.unception_multi_file_open_method = "argadd" - vim.g.unception_multi_file_open_method = "tab" + vim.g.unception_multi_file_open_method = "argadd" end if (vim.g.unception_enable_flavor_text == nil) then From 912c95ad94da6213ba6f1ea7c3880a0ff11713e1 Mon Sep 17 00:00:00 2001 From: lguarda Date: Thu, 22 May 2025 23:46:20 +0200 Subject: [PATCH 05/10] When argadd is selected with only one file fallback to other method to support line number --- lua/server/server_functions.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lua/server/server_functions.lua b/lua/server/server_functions.lua index 2223f57..fde3c4d 100644 --- a/lua/server/server_functions.lua +++ b/lua/server/server_functions.lua @@ -136,10 +136,12 @@ function _G.unception_edit_files(file_args, options) local open_method = unception_detect_open_method(options) if (#file_args > 0) then - if (open_method == "argadd") then - unception_open_file_argadd(file_args, options) - else + -- if argadd is selected but we have only one file + -- let's not use argadd so we can use line number specifier + if (open_method ~= "argadd" or #file_args == 1) then unception_open_file_other(file_args, options, open_method) + else + unception_open_file_argadd(file_args, options) end else if (options.open_in_new_tab) then From feb5ca5359eff12eb400942b5b11d462a4617b87 Mon Sep 17 00:00:00 2001 From: lguarda Date: Sat, 24 May 2025 00:07:15 +0200 Subject: [PATCH 06/10] On blocking edit switch back to the previous tab (when the edit happend in another tab) --- lua/server/server_functions.lua | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lua/server/server_functions.lua b/lua/server/server_functions.lua index fde3c4d..4a23e18 100644 --- a/lua/server/server_functions.lua +++ b/lua/server/server_functions.lua @@ -3,9 +3,11 @@ require("common.common_functions") local response_sock = nil local unception_quitpre_autocmd_id = nil local unception_bufunload_autocmd_id = nil +local unception_tabclosed_autocmd_id = nil local filepath_to_check = nil local blocked_terminal_buffer_id = nil local last_replaced_buffer_id = nil +local last_tabpage = nil local function unblock_client_and_reset_state() -- Remove the autocmds we made. @@ -25,6 +27,11 @@ local function unblock_client_and_reset_state() last_replaced_buffer_id = nil end +local function unception_reset_last_tab() + vim.api.nvim_del_autocmd(unception_tabclosed_autocmd_id) + last_tabpage = nil +end + function _G.unception_handle_bufunload(unloaded_buffer_filepath) unloaded_buffer_filepath = unception_get_absolute_filepath(unloaded_buffer_filepath) unloaded_buffer_filepath = unception_escape_special_chars(unloaded_buffer_filepath) @@ -50,6 +57,14 @@ function _G.unception_handle_quitpre(quitpre_buffer_filepath) end end +function _G.unception_handle_tabclosed() + -- only switch tab when needed this will depend on the config + if last_tabpage ~= nil then + vim.api.nvim_set_current_tabpage(last_tabpage) + end + unception_reset_last_tab() +end + function _G.unception_notify_when_done_editing(pipe_to_respond_on, filepath) filepath_to_check = filepath blocked_terminal_buffer_id = last_replaced_buffer_id @@ -60,6 +75,12 @@ function _G.unception_notify_when_done_editing(pipe_to_respond_on, filepath) -- Create an autocmd for BufUnload as a failsafe should QuitPre not get triggered on the target buffer (e.g. if a user runs :bdelete). unception_bufunload_autocmd_id = vim.api.nvim_create_autocmd("BufUnload", { command = "lua unception_handle_bufunload(vim.fn.expand(':p'))" }) + + -- When done editing in another tab we can't use QuitPre becuase if we switch + -- back to the previous tabpage within QuitPre it will prevent the tab to be closed + -- So here we use the TabClosed event + unception_tabclosed_autocmd_id = vim.api.nvim_create_autocmd("TabClosed", + { callback = unception_handle_tabclosed }) end local open_methods_table = { @@ -136,6 +157,9 @@ function _G.unception_edit_files(file_args, options) local open_method = unception_detect_open_method(options) if (#file_args > 0) then + if options.open_in_new_tab or open_method == "tabnew" then + last_tabpage = vim.api.nvim_get_current_tabpage() + end -- if argadd is selected but we have only one file -- let's not use argadd so we can use line number specifier if (open_method ~= "argadd" or #file_args == 1) then From b9c17e75615c041de3c764eac23b0b4765e9d81e Mon Sep 17 00:00:00 2001 From: lguarda Date: Sat, 24 May 2025 00:07:24 +0200 Subject: [PATCH 07/10] Suppport nvim -d and fix test --- lua/common/arg_parse.lua | 3 +- lua/server/server_functions.lua | 14 +++++-- ...{test.lua => unception_arg_parse_test.lua} | 38 +++++++++---------- 3 files changed, 32 insertions(+), 23 deletions(-) rename test/{test.lua => unception_arg_parse_test.lua} (60%) diff --git a/lua/common/arg_parse.lua b/lua/common/arg_parse.lua index 6158157..243c00b 100644 --- a/lua/common/arg_parse.lua +++ b/lua/common/arg_parse.lua @@ -10,6 +10,7 @@ local parameters_map = { ["-o"] = "split", ["-O"] = "vsplit", ["-p"] = "tab", + ["-d"] = "diff", } local function detect_parameters(str) @@ -66,7 +67,7 @@ local function parse_line_number(arg, state) state.files[index] = {} end - state.files[index].line = line + state.files[index].line = tonumber(line) return true end diff --git a/lua/server/server_functions.lua b/lua/server/server_functions.lua index 4a23e18..26b87a6 100644 --- a/lua/server/server_functions.lua +++ b/lua/server/server_functions.lua @@ -88,6 +88,7 @@ local open_methods_table = { vsplit = "vsplit", tab = "tabnew", argadd = "argadd", + diff = "diff", } local function unception_detect_open_method(options) @@ -95,26 +96,33 @@ local function unception_detect_open_method(options) if open_method == nil then print("unception can't find multi_file_open_method fall back to tab") open_method = "tabnew" + elseif open_method == "diff" then + -- For now net's assume that the only way to view diff is via vsplit + open_method = "vsplit" + options.diff = true end return open_method end -local function unception_open_file(open_method, file) +local function unception_open_file(open_method, file, diff) if file.line then vim.cmd(("%s +%d %s"):format(open_method, file.line, file.path)) else vim.cmd(("%s %s"):format(open_method, file.path)) end + if diff then + vim.cmd.diffthis() + end end local function unception_open_file_other(file_args, options, open_method) if options.open_in_new_tab and open_method ~= "tabnew" then vim.cmd("tabnew") - unception_open_file("edit", file_args[1]) + unception_open_file("edit", file_args[1], options.diff) table.remove(file_args, 1) end for _, file in ipairs(file_args) do - unception_open_file(open_method, file) + unception_open_file(open_method, file, options.diff) end end diff --git a/test/test.lua b/test/unception_arg_parse_test.lua similarity index 60% rename from test/test.lua rename to test/unception_arg_parse_test.lua index e3c6b86..152b812 100644 --- a/test/test.lua +++ b/test/unception_arg_parse_test.lua @@ -8,43 +8,44 @@ require "common.arg_parse" describe("unception nvim tests", function() describe("argument parser", function() local tests_list = { - -- { -- NYI - -- argv = { "file", "\\+32" }, - -- output = { { file = "open" }, { file = "+32" } } - -- }, { argv = { "/usr/bin/nvim", "file", "+5", "-p" }, - output = { { path = "file", line = "5" } }, - option = { multi_file_open_method="tab" } + output = { file = 5 }, + option = { multi_file_open_method = "tab" } }, { argv = { "/usr/bin/dontcare", "file", "file2", "+32" }, - output = { { path = "file" }, { path = "file2", line = "32" } }, - option = { } + output = { file2 = 32 }, + option = {} }, { - argv = { "/usr/bin/dontcare", "--ignored-option-long", "-i", "file", "file2", "+32" }, - output = { { path = "file" }, { path = "file2", line = "32" } }, - option = { } + argv = { "/usr/bin/dontcare", "--ignored-option-long", "-i", "file2", "+32" }, + output = { file2 = 32 }, + option = {} }, -- { -- NYI -- argv = { "/usr/bin/dontcare", "\\- file starting with dash" }, -- output = { { file = "- file starting with dash" } } -- }, { - argv = { "/usr/bin/dontcare", "--", "- file starting with dash" }, - output = { { path = "- file starting with dash" } }, - option = { } + argv = { "/usr/bin/dontcare", "+15", "--", "- file starting with dash" }, + output = { ["- file starting with dash"] = 15 }, + option = {} }, { argv = { "/usr/bin/nvim", "file", "+5", "-o" }, - output = { { path = "file", line = "5" } }, - option = { multi_file_open_method="split" } + output = { file = 5 }, + option = { multi_file_open_method = "split" } }, { argv = { "/usr/bin/nvim", "file", "+5", "-O" }, - output = { { path = "file", line = "5" } }, - option = { multi_file_open_method="vsplit" } + output = { file = 5 }, + option = { multi_file_open_method = "vsplit" } + }, + { + argv = { "/usr/bin/nvim", "file", "file2", "+3", "-d" }, + output = { file2 = 3 }, + option = { multi_file_open_method = "diff" } }, } for i, test in ipairs(tests_list) do @@ -55,6 +56,5 @@ describe("unception nvim tests", function() assert.are.same(options, test.option) end) end - end) end) From 25647dda6357d0d5d571a732b7190f94596f0262 Mon Sep 17 00:00:00 2001 From: lguarda Date: Mon, 26 May 2025 18:09:39 +0200 Subject: [PATCH 08/10] Fix cmd overwriting of unception config in lua number if number will always be asserted as true ```lua if 0 then print("number is == true") end ``` And in viml boolean doesn't exit So for example this: nvim --cmd "let g:unception_disable=0" will still disable unception since in lua `if 0` == `if true` we could also do this instead ``` nvim --cmd "lua vim.g.unception_disable=false" ``` but i think it's better to also support viml --- plugin/main.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugin/main.lua b/plugin/main.lua index dd0ae47..8c084d3 100644 --- a/plugin/main.lua +++ b/plugin/main.lua @@ -1,11 +1,11 @@ ------------------------------------------------------------------------------- -- Initialize all expected variables ------------------------------------------------------------------------------- -if(vim.g.unception_delete_replaced_buffer == nil) then +if(vim.g.unception_delete_replaced_buffer == nil or vim.g.unception_delete_replaced_buffer == 0) then vim.g.unception_delete_replaced_buffer = false end -if(vim.g.unception_open_buffer_in_new_tab == nil) then +if(vim.g.unception_open_buffer_in_new_tab == nil or vim.g.unception_open_buffer_in_new_tab == 0) then vim.g.unception_open_buffer_in_new_tab = false end @@ -18,11 +18,11 @@ if(vim.g.unception_multi_file_open_method == nil) then vim.g.unception_multi_file_open_method = "argadd" end -if (vim.g.unception_enable_flavor_text == nil) then +if (vim.g.unception_enable_flavor_text == nil or vim.g.unception_enable_flavor_text == 0) then vim.g.unception_enable_flavor_text = true end -if (vim.g.unception_block_while_host_edits == nil) then +if (vim.g.unception_block_while_host_edits == nil or vim.g.unception_block_while_host_edits == 0) then vim.g.unception_block_while_host_edits = false end @@ -31,7 +31,7 @@ if (vim.g.unception_block_while_host_edits) then vim.g.unception_delete_replaced_buffer = false end -if (vim.g.unception_disable == nil) then +if (vim.g.unception_disable == nil or vim.g.unception_disable == 0) then vim.g.unception_disable = false end From 1eb9b1eeff25bc49f825dc5a6b150003401f54de Mon Sep 17 00:00:00 2001 From: lguarda Date: Wed, 15 Oct 2025 10:23:41 +0200 Subject: [PATCH 09/10] Fix tentative for file with space and backslash --- lua/client/client.lua | 2 +- lua/common/common_functions.lua | 6 +++--- lua/server/server_functions.lua | 13 +++++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lua/client/client.lua b/lua/client/client.lua index eb470a0..a2c08df 100644 --- a/lua/client/client.lua +++ b/lua/client/client.lua @@ -19,7 +19,7 @@ local file_args = {} for _, file in ipairs(true_file_args) do local absolute_filepath = unception_get_absolute_filepath(file) table.insert(file_args , { - path = unception_escape_special_chars(absolute_filepath), + path = absolute_filepath, line = file_lines[file] }) end diff --git a/lua/common/common_functions.lua b/lua/common/common_functions.lua index 2c6e68d..9faba8e 100644 --- a/lua/common/common_functions.lua +++ b/lua/common/common_functions.lua @@ -39,9 +39,9 @@ function _G.unception_escape_special_chars(str) -- filepaths. Lua needs \\ to define a \, so to escape special chars, -- there are twice as many backslashes as you would think that there -- should be. - str = string.gsub(str, "\\", "\\\\\\\\") - str = string.gsub(str, "\"", "\\\\\\\"") - str = string.gsub(str, " ", "\\\\ ") + str = string.gsub(str, "\\", "\\\\") + str = string.gsub(str, "\"", "\\\"") + str = string.gsub(str, " ", "\\ ") return str else return "" diff --git a/lua/server/server_functions.lua b/lua/server/server_functions.lua index 26b87a6..c6ae7b1 100644 --- a/lua/server/server_functions.lua +++ b/lua/server/server_functions.lua @@ -34,7 +34,7 @@ end function _G.unception_handle_bufunload(unloaded_buffer_filepath) unloaded_buffer_filepath = unception_get_absolute_filepath(unloaded_buffer_filepath) - unloaded_buffer_filepath = unception_escape_special_chars(unloaded_buffer_filepath) + --unloaded_buffer_filepath = unception_escape_special_chars(unloaded_buffer_filepath) if (unloaded_buffer_filepath == filepath_to_check) then unblock_client_and_reset_state() @@ -43,7 +43,7 @@ end function _G.unception_handle_quitpre(quitpre_buffer_filepath) quitpre_buffer_filepath = unception_get_absolute_filepath(quitpre_buffer_filepath) - quitpre_buffer_filepath = unception_escape_special_chars(quitpre_buffer_filepath) + --quitpre_buffer_filepath = unception_escape_special_chars(quitpre_buffer_filepath) if (quitpre_buffer_filepath == filepath_to_check) then -- If this buffer replaced the blocked terminal buffer, we should restore it to the same window. @@ -105,10 +105,11 @@ local function unception_detect_open_method(options) end local function unception_open_file(open_method, file, diff) + local path = unception_escape_special_chars(file.path) if file.line then - vim.cmd(("%s +%d %s"):format(open_method, file.line, file.path)) + vim.cmd(("%s +%d %s"):format(open_method, file.line, path)) else - vim.cmd(("%s %s"):format(open_method, file.path)) + vim.cmd(("%s %s"):format(open_method, path)) end if diff then vim.cmd.diffthis() @@ -129,12 +130,12 @@ end local function unception_open_file_argadd(file_args, options) local path = {} for _, file in ipairs(file_args) do - table.insert(path, file.path) + table.insert(path, unception_escape_special_chars(file.path)) end path = table.concat(path, " ") -- Had some issues when using argedit. Explicitly calling these -- separately appears to work though. - vim.cmd("0argadd "..path) + vim.cmd("0argadd " .. path) if (options.open_in_new_tab) then last_replaced_buffer_id = nil From 08175e1029eec24e5ad29d9f16ffaac1b00d3bf2 Mon Sep 17 00:00:00 2001 From: lguarda Date: Wed, 15 Oct 2025 10:27:39 +0200 Subject: [PATCH 10/10] Add bdelete in quitpre as tentative to fix persistent buffer --- lua/server/server_functions.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lua/server/server_functions.lua b/lua/server/server_functions.lua index c6ae7b1..839fb8f 100644 --- a/lua/server/server_functions.lua +++ b/lua/server/server_functions.lua @@ -54,6 +54,9 @@ function _G.unception_handle_quitpre(quitpre_buffer_filepath) end unblock_client_and_reset_state() + -- this will delete holding buffer, so neovim will be forced + -- to reload the file, this fix sequential git rebase -i for example + vim.cmd(("bdelete! %d"):format(vim.api.nvim_get_current_buf())) end end @@ -79,6 +82,9 @@ function _G.unception_notify_when_done_editing(pipe_to_respond_on, filepath) -- When done editing in another tab we can't use QuitPre becuase if we switch -- back to the previous tabpage within QuitPre it will prevent the tab to be closed -- So here we use the TabClosed event + if unception_tabclosed_autocmd_id then + vim.api.nvim_del_autocmd(unception_tabclosed_autocmd_id) + end unception_tabclosed_autocmd_id = vim.api.nvim_create_autocmd("TabClosed", { callback = unception_handle_tabclosed }) end