Merge pull request 'fix: address carriage return line endings in output window' (#3) from stevanfreeborn/fix/remove-carriage-return-characters into main
This commit is contained in:
@@ -35,6 +35,44 @@ local function strip_ansi(text)
|
|||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Normalize output text: strip ANSI, handle \\r characters (CRLF -> LF,
|
||||||
|
---inline progress overwrites), and detect leading \\r for line overwrite.
|
||||||
|
---@param text string
|
||||||
|
---@return string, boolean
|
||||||
|
local function normalize_output(text)
|
||||||
|
if not text or text == "" then
|
||||||
|
return "", false
|
||||||
|
end
|
||||||
|
|
||||||
|
text = text:gsub("\r\n", "\n")
|
||||||
|
text =
|
||||||
|
text:gsub("\x1b%[%??[0-9;]*[a-zA-Z]", ""):gsub("\x1b%][0-9;]*.-(\x1b\\|\x07)", ""):gsub("\x1b[()][0-9A-Za-z]", "")
|
||||||
|
|
||||||
|
local start_cr = text:len() > 0 and text:byte(1) == 0x0D
|
||||||
|
|
||||||
|
local lines = vim.split(text, "\n", { plain = true })
|
||||||
|
local out_lines = {}
|
||||||
|
|
||||||
|
for i = 1, #lines do
|
||||||
|
local current = lines[i]
|
||||||
|
local found = false
|
||||||
|
|
||||||
|
for p = #current, 1, -1 do
|
||||||
|
if current:byte(p) == 0x0D then
|
||||||
|
current = current:sub(p + 1)
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not found or current ~= "" then
|
||||||
|
table.insert(out_lines, current)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(out_lines, "\n"), start_cr
|
||||||
|
end
|
||||||
|
|
||||||
---Apply diagnostic highlights to keywords found in a line.
|
---Apply diagnostic highlights to keywords found in a line.
|
||||||
---Matches error, warning, success patterns using case-insensitive patterns.
|
---Matches error, warning, success patterns using case-insensitive patterns.
|
||||||
---@param buf integer
|
---@param buf integer
|
||||||
@@ -69,9 +107,13 @@ local function process_data(text)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local clean = strip_ansi(text)
|
local clean, overwrite = normalize_output(text)
|
||||||
|
|
||||||
require("watchexec.window").append(clean)
|
if clean == "" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
require("watchexec.window").append(clean, overwrite)
|
||||||
require("watchexec.indicator").process_output(clean)
|
require("watchexec.indicator").process_output(clean)
|
||||||
require("watchexec.indicator").refresh()
|
require("watchexec.indicator").refresh()
|
||||||
|
|
||||||
@@ -159,7 +201,7 @@ function M.start(command)
|
|||||||
on_stderr = function(_, data, _)
|
on_stderr = function(_, data, _)
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
local text = table.concat(data, "\n")
|
local text = table.concat(data, "\n")
|
||||||
local clean = strip_ansi(text)
|
local clean, _ = normalize_output(text)
|
||||||
|
|
||||||
if clean ~= "" then
|
if clean ~= "" then
|
||||||
process_data(clean)
|
process_data(clean)
|
||||||
@@ -228,4 +270,7 @@ function M.get_cmd()
|
|||||||
return state.cmd
|
return state.cmd
|
||||||
end
|
end
|
||||||
|
|
||||||
|
M._normalize_output = normalize_output
|
||||||
|
M._strip_ansi = strip_ansi
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ local state = {
|
|||||||
---@return integer buf
|
---@return integer buf
|
||||||
function M.create_buf()
|
function M.create_buf()
|
||||||
local existing = state.buf
|
local existing = state.buf
|
||||||
|
|
||||||
if existing and vim.api.nvim_buf_is_valid(existing) then
|
if existing and vim.api.nvim_buf_is_valid(existing) then
|
||||||
return existing
|
return existing
|
||||||
end
|
end
|
||||||
@@ -155,10 +156,13 @@ end
|
|||||||
|
|
||||||
---Append text to the output buffer.
|
---Append text to the output buffer.
|
||||||
---On first append, replaces the "waiting for output" placeholder.
|
---On first append, replaces the "waiting for output" placeholder.
|
||||||
|
---When overwrite is true and the buffer has content, replaces the last line
|
||||||
|
---instead of appending (handles \\r-based progress overwrites).
|
||||||
---Truncates the buffer when max_lines is exceeded.
|
---Truncates the buffer when max_lines is exceeded.
|
||||||
---Auto-scrolls to the bottom when enabled.
|
---Auto-scrolls to the bottom when enabled.
|
||||||
---@param text string
|
---@param text string
|
||||||
function M.append(text)
|
---@param overwrite boolean|nil
|
||||||
|
function M.append(text, overwrite)
|
||||||
local buf = state.buf
|
local buf = state.buf
|
||||||
|
|
||||||
if not buf or not vim.api.nvim_buf_is_valid(buf) then
|
if not buf or not vim.api.nvim_buf_is_valid(buf) then
|
||||||
@@ -174,6 +178,8 @@ function M.append(text)
|
|||||||
|
|
||||||
if first_line:match("^ No job running") then
|
if first_line:match("^ No job running") then
|
||||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||||
|
elseif overwrite and current > 0 then
|
||||||
|
vim.api.nvim_buf_set_lines(buf, current - 1, -1, false, lines)
|
||||||
else
|
else
|
||||||
vim.api.nvim_buf_set_lines(buf, current, -1, false, lines)
|
vim.api.nvim_buf_set_lines(buf, current, -1, false, lines)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -186,6 +186,57 @@ describe("watchexec runner", function()
|
|||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
describe("normalize_output", function()
|
||||||
|
it("removes ANSI sequences", function()
|
||||||
|
local result = runner._strip_ansi("\x1b[31mred\x1b[0m")
|
||||||
|
|
||||||
|
assert.equals("red", result)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("converts \\r\\n to \\n", function()
|
||||||
|
local result, _ = runner._normalize_output("line1\r\nline2\r\nline3")
|
||||||
|
|
||||||
|
assert.equals("line1\nline2\nline3", result)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("resolves inline overwrites (\\r splits)", function()
|
||||||
|
local result, _ = runner._normalize_output("a\rb\rc")
|
||||||
|
|
||||||
|
assert.equals("c", result)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("resolves mixed inline and line (\\r in last line)", function()
|
||||||
|
local result, _ = runner._normalize_output("a\rb\nc\rd")
|
||||||
|
|
||||||
|
assert.equals("b\nd", result)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("detects leading overwrite", function()
|
||||||
|
local _, overwrite = runner._normalize_output("\rxyz")
|
||||||
|
|
||||||
|
assert.is_true(overwrite)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("does not detect overwrite on normal text", function()
|
||||||
|
local _, overwrite = runner._normalize_output("xyz")
|
||||||
|
|
||||||
|
assert.is_false(overwrite)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("does not detect overwrite on CRLF that starts with \\r", function()
|
||||||
|
local _, overwrite = runner._normalize_output("\r\nxyz")
|
||||||
|
|
||||||
|
assert.is_false(overwrite)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("handles empty text", function()
|
||||||
|
local result, overwrite = runner._normalize_output("")
|
||||||
|
|
||||||
|
assert.equals("", result)
|
||||||
|
assert.is_false(overwrite)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
describe("on_exit guard", function()
|
describe("on_exit guard", function()
|
||||||
it("ignores stale on_exit from replaced job", function()
|
it("ignores stale on_exit from replaced job", function()
|
||||||
local call_count = 0
|
local call_count = 0
|
||||||
|
|||||||
@@ -254,6 +254,48 @@ describe("watchexec window", function()
|
|||||||
|
|
||||||
assert.equals(line_count, cursor[1])
|
assert.equals(line_count, cursor[1])
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it("overwrite replaces the last line", function()
|
||||||
|
window.open()
|
||||||
|
local buf = window.get_buf()
|
||||||
|
|
||||||
|
window.append("first")
|
||||||
|
window.append("second")
|
||||||
|
window.append("replacement", true)
|
||||||
|
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||||
|
|
||||||
|
assert.equals(2, #lines)
|
||||||
|
assert.equals("first", lines[1])
|
||||||
|
assert.equals("replacement", lines[2])
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("overwrite with multiple lines replaces last line and appends", function()
|
||||||
|
window.open()
|
||||||
|
local buf = window.get_buf()
|
||||||
|
|
||||||
|
window.append("first")
|
||||||
|
window.append("second")
|
||||||
|
window.append("replacement1\nreplacement2", true)
|
||||||
|
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||||
|
|
||||||
|
assert.equals(3, #lines)
|
||||||
|
assert.equals("first", lines[1])
|
||||||
|
assert.equals("replacement1", lines[2])
|
||||||
|
assert.equals("replacement2", lines[3])
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("overwrite on empty buffer appends normally", function()
|
||||||
|
window.open()
|
||||||
|
local buf = window.get_buf()
|
||||||
|
|
||||||
|
window.append("first", true)
|
||||||
|
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||||
|
|
||||||
|
assert.equals("first", lines[1])
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe("keymaps", function()
|
describe("keymaps", function()
|
||||||
@@ -262,12 +304,14 @@ describe("watchexec window", function()
|
|||||||
local buf = window.get_buf()
|
local buf = window.get_buf()
|
||||||
local maps = vim.api.nvim_buf_get_keymap(buf, "n")
|
local maps = vim.api.nvim_buf_get_keymap(buf, "n")
|
||||||
local found = false
|
local found = false
|
||||||
|
|
||||||
for _, m in ipairs(maps) do
|
for _, m in ipairs(maps) do
|
||||||
if m.lhs == "<Esc>" then
|
if m.lhs == "<Esc>" then
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
assert.is_true(found)
|
assert.is_true(found)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -276,12 +320,14 @@ describe("watchexec window", function()
|
|||||||
local buf = window.get_buf()
|
local buf = window.get_buf()
|
||||||
local maps = vim.api.nvim_buf_get_keymap(buf, "n")
|
local maps = vim.api.nvim_buf_get_keymap(buf, "n")
|
||||||
local found = false
|
local found = false
|
||||||
|
|
||||||
for _, m in ipairs(maps) do
|
for _, m in ipairs(maps) do
|
||||||
if m.lhs == "q" then
|
if m.lhs == "q" then
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
assert.is_true(found)
|
assert.is_true(found)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user