mirror of
https://github.com/zoriya/jj.nvim.git
synced 2026-08-15 23:53:18 +00:00
Feat: Add file module with Jread, Jedit, Jsplit , Jvsplit and Jtabedit similar to fugitive's (#105)
Co-authored-by: NicolasGB <ngou0210@gmail.com>
This commit is contained in:
co-authored by
NicolasGB
parent
2dbe2c73c5
commit
53a3196210
+54
-16
@@ -10,6 +10,48 @@ local parser = require("jj.core.parser")
|
||||
-- Track the last annotation tooltip buffer
|
||||
local last_tooltip_buf = nil
|
||||
|
||||
--- Resolve current buffer into an annotate target.
|
||||
--- Supports normal file buffers and jj:// virtual buffers.
|
||||
--- @return string|nil path Repository-relative or absolute path for `jj file annotate`
|
||||
--- @return string|nil rev Optional revision for virtual buffers
|
||||
--- @return string|nil err Error message when target cannot be resolved
|
||||
local function get_annotate_target()
|
||||
local filename = vim.api.nvim_buf_get_name(0)
|
||||
if filename == "" then
|
||||
return nil, nil, "Could not extract file from buffer"
|
||||
end
|
||||
|
||||
local change_id, path = utils.parse_jj_uri(filename)
|
||||
if change_id then
|
||||
return path, change_id, nil
|
||||
end
|
||||
if vim.startswith(filename, "jj://") then
|
||||
return nil, nil, "Invalid jj:// buffer name"
|
||||
end
|
||||
|
||||
local normalized, err = utils.normalize_repo_path(filename)
|
||||
if not normalized then
|
||||
return nil, nil, err or "Could not normalize file path"
|
||||
end
|
||||
|
||||
return normalized, nil, nil
|
||||
end
|
||||
|
||||
--- Build `jj file annotate` command with optional revision.
|
||||
--- @param path string
|
||||
--- @param template string
|
||||
--- @param rev? string
|
||||
--- @return string
|
||||
local function build_annotate_cmd(path, template, rev)
|
||||
local rev_flag = rev and rev ~= "" and string.format("-r %s ", vim.fn.shellescape(rev)) or ""
|
||||
return string.format(
|
||||
"jj file annotate %s%s -T %s",
|
||||
rev_flag,
|
||||
vim.fn.shellescape(path),
|
||||
vim.fn.shellescape(template)
|
||||
)
|
||||
end
|
||||
|
||||
--- Sets the highlights for the blame bufer
|
||||
--- @param buf integer
|
||||
--- @param annotations string[]
|
||||
@@ -136,11 +178,11 @@ local function handle_enter()
|
||||
return
|
||||
end
|
||||
|
||||
-- Create a new buffer with the filetype gitdiff and the output
|
||||
-- Create a new buffer with the filetype diff and the output
|
||||
local buf, win = buffer.create({
|
||||
name = "jj-diff://" .. vim.fn.fnamemodify(filename, ":t") .. "//" .. parts.rev.value,
|
||||
split = "tab",
|
||||
filetype = "gitdiff",
|
||||
filetype = "diff",
|
||||
bufhidden = "wipe",
|
||||
})
|
||||
|
||||
@@ -172,16 +214,14 @@ function M.file()
|
||||
local template =
|
||||
'join(" | ", commit.change_id().short(6), commit.author().name(), commit.author().timestamp().format("%Y-%m-%d %H:%M:%S %Z")) ++ "\n"'
|
||||
|
||||
local filename = vim.api.nvim_buf_get_name(0)
|
||||
if filename == "" then
|
||||
utils.notify("Could extract file from buffer", vim.log.levels.ERROR)
|
||||
local filename, revision, err = get_annotate_target()
|
||||
if not filename then
|
||||
utils.notify(err or "Could not extract file from buffer", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local raw_output, success = runner.execute_command(
|
||||
string.format("jj file annotate %s -T '%s'", filename, template),
|
||||
"Failed to annotate file"
|
||||
)
|
||||
local raw_output, success =
|
||||
runner.execute_command(build_annotate_cmd(filename, template, revision), "Failed to annotate file")
|
||||
if not success or not raw_output then
|
||||
return
|
||||
end
|
||||
@@ -307,17 +347,15 @@ function M.line()
|
||||
local template =
|
||||
'join(" | ", commit.change_id().short(6), commit.author().name(), commit.author().timestamp().format("%Y-%m-%d %H:%M:%S %Z")) ++ "\n"'
|
||||
|
||||
local filename = vim.api.nvim_buf_get_name(0)
|
||||
if filename == "" then
|
||||
utils.notify("Could not extract file from buffer", vim.log.levels.ERROR)
|
||||
local filename, revision, err = get_annotate_target()
|
||||
if not filename then
|
||||
utils.notify(err or "Could not extract file from buffer", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local line_num = vim.fn.line(".")
|
||||
local raw_output, success = runner.execute_command(
|
||||
string.format("jj file annotate %s -T '%s'", filename, template),
|
||||
"Failed to annotate line"
|
||||
)
|
||||
local raw_output, success =
|
||||
runner.execute_command(build_annotate_cmd(filename, template, revision), "Failed to annotate line")
|
||||
if not success or not raw_output then
|
||||
return
|
||||
end
|
||||
|
||||
@@ -245,4 +245,32 @@ function M.parse_diff_range(range_str)
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Parse a `<rev>:<path>` argument as used by file commands.
|
||||
--- With no colon, the whole input is treated as a revision with no file.
|
||||
--- A trailing colon (`<rev>:`) is accepted and treated as no file path.
|
||||
--- @param input string
|
||||
--- @return {rev: string|nil, path: string|nil}|nil
|
||||
function M.parse_file_module_input(input)
|
||||
if not input then
|
||||
return nil
|
||||
end
|
||||
|
||||
input = vim.trim(input)
|
||||
if input == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local rev, file = input:match("^([^:]+):(.*)$")
|
||||
if rev then
|
||||
if file == "" then
|
||||
return { rev = rev, path = nil }
|
||||
end
|
||||
return { rev = rev, path = file }
|
||||
end
|
||||
if input:find(":", 1, true) then
|
||||
return nil
|
||||
end
|
||||
return { rev = input, path = nil }
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
+19
-27
@@ -9,23 +9,23 @@ local M = {}
|
||||
--- @return string|nil output The command output, or nil if failed
|
||||
--- @return boolean success Whether the command succeeded
|
||||
function M.execute_command(cmd, error_prefix, input, silent)
|
||||
local output = vim.fn.system({ "sh", "-c", cmd }, input)
|
||||
local stderr_file = vim.fn.tempname()
|
||||
local output = vim.fn.system({ "sh", "-c", string.format("(%s) 2>%s", cmd, vim.fn.shellescape(stderr_file)) }, input)
|
||||
local success = vim.v.shell_error == 0
|
||||
|
||||
if not success then
|
||||
local error_message
|
||||
if error_prefix then
|
||||
error_message = string.format("%s: %s", error_prefix, output)
|
||||
else
|
||||
error_message = output
|
||||
end
|
||||
local stderr_lines = vim.fn.readfile(stderr_file)
|
||||
vim.fn.delete(stderr_file)
|
||||
local error_output = table.concat(stderr_lines, "\n")
|
||||
local msg = error_output ~= "" and error_output or output
|
||||
local error_message = error_prefix and string.format("%s: %s", error_prefix, msg) or msg
|
||||
if not silent then
|
||||
vim.notify(error_message, vim.log.levels.ERROR, { title = "JJ" })
|
||||
end
|
||||
|
||||
return nil, false
|
||||
end
|
||||
|
||||
vim.fn.delete(stderr_file)
|
||||
return output, success
|
||||
end
|
||||
|
||||
@@ -53,41 +53,33 @@ end
|
||||
--- @param silent boolean|nil Optional to silent the notification
|
||||
--- @param on_error function|nil Callback on error, receives ouptut as the parameter
|
||||
function M.execute_command_async(cmd, on_success, error_prefix, input, silent, on_error)
|
||||
local output_lines = {}
|
||||
local stdout_lines = {}
|
||||
local stderr_lines = {}
|
||||
|
||||
local job_id = vim.fn.jobstart({ "sh", "-c", cmd }, {
|
||||
stdout_buffered = true,
|
||||
stderr_buffered = true,
|
||||
on_stdout = function(_, data)
|
||||
for _, line in ipairs(data) do
|
||||
if line ~= "" then
|
||||
table.insert(output_lines, line)
|
||||
end
|
||||
end
|
||||
vim.list_extend(stdout_lines, data)
|
||||
end,
|
||||
on_stderr = function(_, data)
|
||||
for _, line in ipairs(data) do
|
||||
if line ~= "" then
|
||||
table.insert(output_lines, line)
|
||||
end
|
||||
end
|
||||
vim.list_extend(stderr_lines, data)
|
||||
end,
|
||||
on_exit = function(_, exit_code)
|
||||
local output = table.concat(output_lines, "\n")
|
||||
local output = table.concat(stdout_lines, "\n")
|
||||
if exit_code == 0 then
|
||||
if on_success then
|
||||
on_success(output)
|
||||
end
|
||||
else
|
||||
local error_message
|
||||
if error_prefix then
|
||||
error_message = string.format("%s: %s", error_prefix, output)
|
||||
else
|
||||
error_message = output
|
||||
end
|
||||
local error_output = table.concat(stderr_lines, "\n")
|
||||
local msg = error_output ~= "" and error_output or output
|
||||
local error_message = error_prefix and string.format("%s: %s", error_prefix, msg) or msg
|
||||
if not silent then
|
||||
vim.notify(error_message, vim.log.levels.ERROR, { title = "JJ" })
|
||||
end
|
||||
if on_error then
|
||||
on_error(output)
|
||||
on_error(msg)
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
+40
-24
@@ -1,46 +1,60 @@
|
||||
local utils = require("jj.utils")
|
||||
local buffer = require("jj.core.buffer")
|
||||
local runner = require("jj.core.runner")
|
||||
|
||||
local diff = require("jj.diff")
|
||||
local file = require("jj.file")
|
||||
|
||||
--- Get the content of a file at a specific revision
|
||||
--- Open a writable buffer for a specific revision of a file.
|
||||
--- @param rev string The revision
|
||||
--- @param path string The file path
|
||||
--- @return table lines The file content
|
||||
local function get_file_content(rev, path)
|
||||
local cmd = string.format("jj file show -r %s %s", vim.fn.shellescape(rev), vim.fn.shellescape(path))
|
||||
local content = vim.fn.system(cmd)
|
||||
local success = vim.v.shell_error == 0
|
||||
if success then
|
||||
return vim.split(content, "\n", { trimempty = true })
|
||||
else
|
||||
return {}
|
||||
end
|
||||
end
|
||||
|
||||
--- Open a read-only buffer for a specific revision of a file
|
||||
--- @param rev string The revision
|
||||
--- @param path string The file path
|
||||
--- @param path string The file path (absolute or repo-relative)
|
||||
local function open_revision(rev, path)
|
||||
local lines = get_file_content(rev, path)
|
||||
local raw_ids, ok = runner.execute_command(
|
||||
string.format([[jj log --no-graph -r %s -T 'change_id ++ "\n"' --quiet]], vim.fn.shellescape(rev)),
|
||||
"jj: failed to resolve revision"
|
||||
)
|
||||
if not ok then return end
|
||||
local ids = vim.split(vim.trim(raw_ids), "\n", { trimempty = true })
|
||||
if #ids ~= 1 then
|
||||
utils.notify(string.format("Revision '%s' is ambiguous", rev), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local change_id = ids[1]
|
||||
|
||||
local rel_path, err = utils.normalize_repo_path(path)
|
||||
if not rel_path then
|
||||
utils.notify(err or "Could not resolve path", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local lines, had_eol = file.get_file_content(change_id, rel_path)
|
||||
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
|
||||
local buf_name = string.format("jj://%s/%s", rev, path)
|
||||
local buf_name = string.format("jj://%s/%s", change_id, rel_path)
|
||||
vim.api.nvim_buf_set_name(buf, buf_name)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
vim.bo[buf].eol = had_eol
|
||||
|
||||
local ft = vim.filetype.match({ filename = path })
|
||||
local ft = vim.filetype.match({ filename = rel_path })
|
||||
if ft then
|
||||
vim.bo[buf].filetype = ft
|
||||
end
|
||||
|
||||
vim.bo[buf].buftype = "nofile"
|
||||
vim.bo[buf].buftype = "acwrite"
|
||||
vim.bo[buf].bufhidden = "wipe"
|
||||
vim.bo[buf].readonly = true
|
||||
vim.bo[buf].readonly = false
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.bo[buf].modifiable = true
|
||||
|
||||
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
||||
buffer = buf,
|
||||
callback = function()
|
||||
file.write_revision_file(buf, change_id, rel_path)
|
||||
end,
|
||||
})
|
||||
|
||||
vim.bo[buf].modified = false
|
||||
vim.api.nvim_win_set_buf(0, buf)
|
||||
end
|
||||
|
||||
@@ -61,8 +75,10 @@ diff.register_backend("native", {
|
||||
local prev_buf = vim.api.nvim_get_current_buf()
|
||||
local prev_cur_pos = buffer.get_cursor(prev_buf) or { 1, 0 }
|
||||
|
||||
local rev = opts.rev or "@-"
|
||||
local path = opts.path or vim.api.nvim_buf_get_name(0)
|
||||
local buf_name = vim.api.nvim_buf_get_name(0)
|
||||
local change_id, jj_path = utils.parse_jj_uri(buf_name)
|
||||
local rev = opts.rev or (change_id and (change_id .. "-")) or "@-"
|
||||
local path = opts.path or jj_path or buf_name
|
||||
local layout = opts.layout or "vertical"
|
||||
|
||||
local split_fun = layout == "horizontal" and vim.cmd.split or vim.cmd.vsplit
|
||||
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
--- @class jj.file
|
||||
local M = {}
|
||||
|
||||
local runner = require("jj.core.runner")
|
||||
local buffer = require("jj.core.buffer")
|
||||
local utils = require("jj.utils")
|
||||
local parser = require("jj.core.parser")
|
||||
|
||||
--- @class jj.file.read_target_opts
|
||||
--- @field rev? string Revision to read the file from
|
||||
--- @field path? string Path to the file (`%`, absolute, or repository-relative)
|
||||
|
||||
--- @class jj.file.open_target_opts
|
||||
--- @field rev? string Revision to open the file from
|
||||
--- @field path? string Path to the file (`%`, absolute, or repository-relative)
|
||||
--- @field split? "horizontal"|"vertical"|"tab"|"current" Open in split direction (default: "current")
|
||||
|
||||
--- Fetch file content from jj synchronously.
|
||||
--- Returns lines with blank lines preserved; trailing empty line removed.
|
||||
--- @param rev string The revision (change ID or other revset)
|
||||
--- @param path string Repository-relative path
|
||||
--- @return string[] lines
|
||||
--- @return boolean had_eol Whether the content had a trailing newline
|
||||
local function get_file_content(rev, path)
|
||||
local content, ok = runner.execute_command(
|
||||
string.format("jj file show -r %s %s", vim.fn.shellescape(rev), vim.fn.shellescape(path)),
|
||||
nil,
|
||||
nil,
|
||||
true
|
||||
)
|
||||
if not ok or not content then
|
||||
return {}, false
|
||||
end
|
||||
local lines = vim.split(content, "\n", { plain = true, trimempty = false })
|
||||
local had_eol = #lines > 0 and lines[#lines] == ""
|
||||
if had_eol then
|
||||
table.remove(lines, #lines)
|
||||
end
|
||||
return lines, had_eol
|
||||
end
|
||||
M.get_file_content = get_file_content
|
||||
|
||||
--- Reads a target file revision into the current buffer (undoable).
|
||||
--- @param opts? jj.file.read_target_opts
|
||||
function M.read_target(opts)
|
||||
local revision = opts and opts.rev or "@"
|
||||
local raw_path = opts and opts.path or "%"
|
||||
local path, normalize_err = utils.normalize_repo_path(raw_path)
|
||||
if not path then
|
||||
utils.notify(normalize_err or "Could not normalize path", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
local cmd = string.format("jj file show -r %s %s", vim.fn.shellescape(revision), vim.fn.shellescape(path))
|
||||
runner.execute_command_async(cmd, function(out)
|
||||
local lines = vim.split(out, "\n", { plain = true, trimempty = false })
|
||||
if #lines > 0 and lines[#lines] == "" then
|
||||
table.remove(lines, #lines)
|
||||
end
|
||||
if vim.bo[buf].modifiable == false then
|
||||
utils.notify("Current buffer is not modifiable", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
vim.bo[buf].modified = true
|
||||
end, string.format("Could not read `%s` from `%s`", path, revision))
|
||||
end
|
||||
|
||||
--- Write buffer content back into a jj revision, bypassing the working copy.
|
||||
--- @param buf integer
|
||||
--- @param change_id string
|
||||
--- @param rel_path string Repository-relative path of the file
|
||||
--- @param force boolean Whether to bypass the immutability check (`:w!`)
|
||||
local function write_revision_file(buf, change_id, rel_path, force)
|
||||
if utils.is_change_immutable(change_id) then
|
||||
if not force then
|
||||
utils.notify("Cannot write to immutable revision: " .. change_id, vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local new_content = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n")
|
||||
if vim.bo[buf].eol then
|
||||
new_content = new_content .. "\n"
|
||||
end
|
||||
|
||||
local tmp = vim.fn.tempname()
|
||||
local cf = io.open(tmp, "w")
|
||||
if not cf then
|
||||
utils.notify("Failed to create temp file", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
cf:write(new_content)
|
||||
cf:close()
|
||||
|
||||
-- Configure `cp` as the diffedit tool inline via --config.
|
||||
-- jj expands $right to the directory it populates with <rev>'s content,
|
||||
-- so the parent directory for rel_path already exists there.
|
||||
local prog_config = 'merge-tools.jj-nvim-write.program="cp"'
|
||||
-- json_encode produces valid TOML inline arrays.
|
||||
local args_config = "merge-tools.jj-nvim-write.edit-args="
|
||||
.. vim.fn.json_encode({ tmp, "$right/" .. rel_path })
|
||||
|
||||
local _, ok = runner.execute_command(
|
||||
string.format(
|
||||
"jj diffedit --from 'root()' --to %s --config %s --config %s --tool jj-nvim-write -- %s",
|
||||
vim.fn.shellescape(change_id),
|
||||
vim.fn.shellescape(prog_config),
|
||||
vim.fn.shellescape(args_config),
|
||||
vim.fn.shellescape(rel_path)
|
||||
),
|
||||
"jj: failed to edit revision"
|
||||
)
|
||||
|
||||
os.remove(tmp)
|
||||
|
||||
if not ok then return end
|
||||
vim.bo[buf].modified = false
|
||||
utils.notify(string.format("Written to revision %s", change_id))
|
||||
end
|
||||
M.write_revision_file = write_revision_file
|
||||
|
||||
--- Opens a target file revision in a new buffer.
|
||||
--- @param opts jj.file.open_target_opts
|
||||
function M.open_target(opts)
|
||||
local revision = opts.rev or "@"
|
||||
local raw_path = opts.path or "%"
|
||||
local path, normalize_err = utils.normalize_repo_path(raw_path)
|
||||
if not path then
|
||||
utils.notify(normalize_err or "Could not normalize path", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local raw_ids, ok = runner.execute_command(
|
||||
string.format([[jj log --no-graph -r %s -T 'change_id ++ "\n"' --quiet]], vim.fn.shellescape(revision)),
|
||||
"jj: failed to resolve revision",
|
||||
nil,
|
||||
true
|
||||
)
|
||||
if not ok then return end
|
||||
local ids = vim.split(vim.trim(raw_ids), "\n", { trimempty = true })
|
||||
if #ids ~= 1 then
|
||||
utils.notify(string.format("Revision '%s' is ambiguous", revision), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local change_id = ids[1]
|
||||
|
||||
local lines, had_eol = get_file_content(change_id, path)
|
||||
local ft = vim.filetype.match({ filename = path })
|
||||
|
||||
local buf, _ = buffer.create({
|
||||
name = string.format("jj://%s/%s", change_id, path),
|
||||
split = opts.split or "current",
|
||||
modifiable = true,
|
||||
buftype = "acwrite",
|
||||
bufhidden = "wipe",
|
||||
filetype = ft,
|
||||
})
|
||||
vim.bo[buf].buflisted = true
|
||||
vim.bo[buf].swapfile = false
|
||||
local ul = vim.bo[buf].undolevels
|
||||
vim.bo[buf].undolevels = -1
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
vim.bo[buf].undolevels = ul
|
||||
vim.bo[buf].eol = had_eol
|
||||
|
||||
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
||||
buffer = buf,
|
||||
callback = function()
|
||||
write_revision_file(buf, change_id, path, vim.v.cmdbang == 1)
|
||||
end,
|
||||
})
|
||||
|
||||
vim.bo[buf].modified = false
|
||||
vim.bo[buf].modifiable = not utils.is_change_immutable(change_id)
|
||||
end
|
||||
|
||||
--- Complete `<rev>:<file>` arguments for file commands.
|
||||
--- @param arglead string
|
||||
--- @return string[]
|
||||
local function complete_target(arglead)
|
||||
local rev, file_prefix = arglead:match("^([^:]+):(.*)$")
|
||||
if not rev then
|
||||
return {}
|
||||
end
|
||||
|
||||
local out, ok = runner.execute_command(
|
||||
string.format("jj file list -r %s", vim.fn.shellescape(rev)),
|
||||
nil,
|
||||
nil,
|
||||
true
|
||||
)
|
||||
if not ok or not out then
|
||||
return {}
|
||||
end
|
||||
|
||||
local items = {}
|
||||
local seen = {}
|
||||
for line in out:gmatch("[^\r\n]+") do
|
||||
local file = vim.trim(line)
|
||||
if file ~= "" and vim.startswith(file, file_prefix) then
|
||||
local candidate = rev .. ":" .. file
|
||||
if not seen[candidate] then
|
||||
table.insert(items, candidate)
|
||||
seen[candidate] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
return items
|
||||
end
|
||||
|
||||
function M.register_command()
|
||||
-- Allow :e on jj:// buffers to reload their content.
|
||||
vim.api.nvim_create_autocmd("BufReadCmd", {
|
||||
pattern = "jj://*",
|
||||
nested = true,
|
||||
callback = function()
|
||||
local name = vim.api.nvim_buf_get_name(0)
|
||||
local change_id, path = utils.parse_jj_uri(name)
|
||||
if not change_id then return end
|
||||
local lines, had_eol = get_file_content(change_id, path)
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
vim.bo[buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
vim.bo[buf].eol = had_eol
|
||||
vim.bo[buf].modified = false
|
||||
vim.bo[buf].modifiable = not utils.is_change_immutable(change_id)
|
||||
vim.api.nvim_exec_autocmds("BufReadPost", { buffer = buf })
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command("Jread", function(opts)
|
||||
local parsed = parser.parse_file_module_input(opts.args)
|
||||
M.read_target({
|
||||
rev = parsed and parsed.rev,
|
||||
path = parsed and parsed.path,
|
||||
})
|
||||
end, {
|
||||
desc = "Read a jj file revision into the current buffer",
|
||||
nargs = "?",
|
||||
complete = function(arglead, _, _)
|
||||
return complete_target(arglead)
|
||||
end,
|
||||
})
|
||||
|
||||
local function create_open_command(name, split, desc)
|
||||
vim.api.nvim_create_user_command(name, function(opts)
|
||||
local parsed = parser.parse_file_module_input(opts.args)
|
||||
M.open_target({
|
||||
rev = parsed and parsed.rev,
|
||||
path = parsed and parsed.path,
|
||||
split = split,
|
||||
})
|
||||
end, {
|
||||
desc = desc,
|
||||
nargs = "?",
|
||||
complete = function(arglead, _, _)
|
||||
return complete_target(arglead)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
create_open_command("Jedit", "current", "Open a jj file revision in the current window")
|
||||
create_open_command("Jtabedit", "tab", "Open a jj file revision in a new tab")
|
||||
create_open_command("Jsplit", "horizontal", "Open a jj file revision in a horizontal split")
|
||||
create_open_command("Jvsplit", "vertical", "Open a jj file revision in a vertical split")
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -5,6 +5,7 @@ local editor = require("jj.ui.editor")
|
||||
local terminal = require("jj.ui.terminal")
|
||||
local diff = require("jj.diff")
|
||||
local browse = require("jj.browse")
|
||||
local file = require("jj.file")
|
||||
|
||||
--- Jujutsu plugin configuration
|
||||
--- @class jj.Config
|
||||
@@ -61,6 +62,7 @@ function M.setup(opts)
|
||||
cmd.register_command()
|
||||
browse.register_command()
|
||||
diff.register_command()
|
||||
file.register_command()
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -149,6 +149,72 @@ function M.is_file(path)
|
||||
return st ~= nil and st.type == "file"
|
||||
end
|
||||
|
||||
--- Parse a jj:// virtual buffer URI.
|
||||
--- Returns nil, nil if the name is not a jj:// URI or has an invalid format.
|
||||
--- @param name string
|
||||
--- @return string|nil change_id
|
||||
--- @return string|nil path
|
||||
function M.parse_jj_uri(name)
|
||||
if not vim.startswith(name, "jj://") then
|
||||
return nil, nil
|
||||
end
|
||||
return name:match("^jj://([^/]+)/(.+)$")
|
||||
end
|
||||
|
||||
--- Normalize a user-provided file path to a repository-relative path for jj commands.
|
||||
---
|
||||
--- Rules:
|
||||
--- - `%` resolves to the current buffer absolute file path.
|
||||
--- - Absolute paths are converted to repository-relative paths.
|
||||
--- - Relative paths are kept relative.
|
||||
---
|
||||
--- @param path string|nil Path provided by the user (`%`, absolute, or relative)
|
||||
--- @param root? string Repository root (defaults to current jj repo root)
|
||||
--- @return string|nil normalized_path Repository-relative normalized path
|
||||
--- @return string|nil err Error message when normalization fails
|
||||
function M.normalize_repo_path(path, root)
|
||||
path = vim.trim(path or "")
|
||||
if path == "" then
|
||||
return nil, "Path is empty"
|
||||
end
|
||||
|
||||
if path == "%" then
|
||||
path = vim.api.nvim_buf_get_name(0)
|
||||
if path == "" then
|
||||
return nil, "Current buffer has no file path"
|
||||
end
|
||||
local _, jj_path = M.parse_jj_uri(path)
|
||||
if jj_path then
|
||||
return jj_path, nil
|
||||
end
|
||||
end
|
||||
|
||||
path = vim.fs.normalize(path)
|
||||
|
||||
root = root or M.get_jj_root()
|
||||
if not root or root == "" then
|
||||
return nil, "Not in a jj repository"
|
||||
end
|
||||
|
||||
root = vim.fs.normalize(root)
|
||||
|
||||
local is_absolute = vim.startswith(path, "/") or path:match("^%a:/") ~= nil
|
||||
if is_absolute then
|
||||
local rel = M.relpath(root, path)
|
||||
if not rel then
|
||||
return nil, "Path is outside repository root"
|
||||
end
|
||||
path = rel
|
||||
end
|
||||
|
||||
path = path:gsub("^%./", "")
|
||||
if path == "" then
|
||||
return nil, "Path is empty after normalization"
|
||||
end
|
||||
|
||||
return path, nil
|
||||
end
|
||||
|
||||
--- Compute a repository-relative path.
|
||||
--- Returns nil if `path` is outside of `root`.
|
||||
--- @param root string
|
||||
|
||||
Reference in New Issue
Block a user