fix(encoding): preserve UTF-16/32 content in jj file reads and diffs (#122)

Use raw command output for jj file reads, add encoding-aware decode/encode
handling, preserve buffer encoding settings across revision buffers, and add
tests for BOMs, endianness, and roundtrip behavior.
This commit is contained in:
Lars Hansen
2026-06-16 18:35:27 +02:00
committed by GitHub
parent 33179e2f15
commit 6409ab0a66
5 changed files with 361 additions and 36 deletions
+54 -8
View File
@@ -1,6 +1,13 @@
--- @class jj.core.runner
local M = {}
local function error_notify(msg, error_prefix, silent)
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
end
--- Execute a system command and return output with error handling
--- @param cmd string The command to execute
--- @param error_prefix string|nil Optional error message prefix
@@ -18,10 +25,7 @@ function M.execute_command(cmd, error_prefix, input, silent)
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
error_notify(msg, error_prefix, silent)
return nil, false
end
@@ -29,6 +33,24 @@ function M.execute_command(cmd, error_prefix, input, silent)
return output, success
end
--- Execute a system command and return its raw stdout bytes.
--- Skips replacement of NUL bytes with SOH (0x01),
--- which corrupts UTF-16 and other binary-ish content.
--- @param cmd string The command to execute
--- @param error_prefix string|nil Optional error message prefix
--- @param silent boolean|nil Optional to silent the notification
--- @return string|nil output Raw stdout bytes, or nil if failed
--- @return boolean success Whether the command succeeded
function M.execute_command_raw(cmd, error_prefix, silent)
local result = vim.system({ "sh", "-c", cmd }):wait()
if result.code ~= 0 then
local msg = result.stderr ~= "" and result.stderr or result.stdout or ""
error_notify(msg, error_prefix, silent)
return nil, false
end
return result.stdout or "", true
end
--- Execute a system command synchronously and call success callback.
--- @param cmd string The command to execute
--- @param on_success function|nil Callback on success, receives output as parameter
@@ -74,10 +96,7 @@ function M.execute_command_async(cmd, on_success, error_prefix, input, silent, o
else
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
error_notify(msg, error_prefix, silent)
if on_error then
on_error(msg)
end
@@ -92,4 +111,31 @@ function M.execute_command_async(cmd, on_success, error_prefix, input, silent, o
end
end
--- Execute a system command asynchronously and receive raw stdout bytes.
--- Skips replacement of NUL bytes with SOH (0x01).
--- @param cmd string The command to execute
--- @param on_success function|nil Callback on success, receives raw stdout bytes
--- @param error_prefix string|nil Optional error message prefix
--- @param silent boolean|nil Optional to silent the notification
--- @param on_error function|nil Callback on error, receives the error message
function M.execute_command_raw_async(cmd, on_success, error_prefix, silent, on_error)
vim.system(
{ "sh", "-c", cmd },
{},
vim.schedule_wrap(function(res)
if res.code == 0 then
if on_success then
on_success(res.stdout or "")
end
else
local msg = res.stderr ~= "" and res.stderr or res.stdout or ""
error_notify(msg, error_prefix, silent)
if on_error then
on_error(msg)
end
end
end)
)
end
return M