fix: address newline issues in output window
This commit is contained in:
+58
-13
@@ -30,11 +30,49 @@ local state = {
|
||||
---@return string
|
||||
local function strip_ansi(text)
|
||||
local result =
|
||||
text:gsub("\x1b%[%??[0-9;]*[a-zA-Z]", ""):gsub("\x1b%][0-9;]*.-(\x1b\\|\x07)", ""):gsub("\x1b[()][0-9A-Za-z]", "")
|
||||
text:gsub("\x1b%[%??[0-9;]*[a-zA-Z]", ""):gsub("\x1b%][0-9;]*.-(\x1b\\|\x07)", ""):gsub("\x1b[()][0-9A-Za-z]", "")
|
||||
|
||||
return result
|
||||
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.
|
||||
---Matches error, warning, success patterns using case-insensitive patterns.
|
||||
---@param buf integer
|
||||
@@ -42,17 +80,17 @@ end
|
||||
---@param line string
|
||||
local function apply_highlights(buf, line_idx, line)
|
||||
for _, p in ipairs({
|
||||
{ p = "[Ee][Rr][Rr][Oo][Rr]", h = "DiagnosticError" },
|
||||
{ p = "[Ff][Aa][Ii][Ll][Ee][Dd]?", h = "DiagnosticError" },
|
||||
{ p = "[Ee][Rr][Rr]!", h = "DiagnosticError" },
|
||||
{ p = "[Ff][Aa][Tt][Aa][Ll]", h = "DiagnosticError" },
|
||||
{ p = "[Ee][Rr][Rr][Oo][Rr]", h = "DiagnosticError" },
|
||||
{ p = "[Ff][Aa][Ii][Ll][Ee][Dd]?", h = "DiagnosticError" },
|
||||
{ p = "[Ee][Rr][Rr]!", h = "DiagnosticError" },
|
||||
{ p = "[Ff][Aa][Tt][Aa][Ll]", h = "DiagnosticError" },
|
||||
{ p = "[Ww][Aa][Rr][Nn][Ii][Nn][Gg]", h = "DiagnosticWarn" },
|
||||
{ p = "[Ww][Aa][Rr][Nn]!", h = "DiagnosticWarn" },
|
||||
{ p = "[Ww][Aa][Rr][Nn]!", h = "DiagnosticWarn" },
|
||||
{ p = "[Ss][Uu][Cc][Cc][Ee][Ss][Ss]", h = "DiagnosticOk" },
|
||||
{ p = "[Pp][Aa][Ss][Ss][Ee][Dd]", h = "DiagnosticOk" },
|
||||
{ p = "%^%d+ .-[Ss]ucceed", h = "DiagnosticOk" },
|
||||
{ p = "%[ok%]", h = "DiagnosticOk" },
|
||||
{ p = "[Oo][Kk]!", h = "DiagnosticOk" },
|
||||
{ p = "[Pp][Aa][Ss][Ss][Ee][Dd]", h = "DiagnosticOk" },
|
||||
{ p = "%^%d+ .-[Ss]ucceed", h = "DiagnosticOk" },
|
||||
{ p = "%[ok%]", h = "DiagnosticOk" },
|
||||
{ p = "[Oo][Kk]!", h = "DiagnosticOk" },
|
||||
}) do
|
||||
local s, e = line:find(p.p)
|
||||
|
||||
@@ -69,9 +107,13 @@ local function process_data(text)
|
||||
return
|
||||
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").refresh()
|
||||
|
||||
@@ -159,7 +201,7 @@ function M.start(command)
|
||||
on_stderr = function(_, data, _)
|
||||
vim.schedule(function()
|
||||
local text = table.concat(data, "\n")
|
||||
local clean = strip_ansi(text)
|
||||
local clean, _ = normalize_output(text)
|
||||
|
||||
if clean ~= "" then
|
||||
process_data(clean)
|
||||
@@ -228,4 +270,7 @@ function M.get_cmd()
|
||||
return state.cmd
|
||||
end
|
||||
|
||||
M._normalize_output = normalize_output
|
||||
M._strip_ansi = strip_ansi
|
||||
|
||||
return M
|
||||
|
||||
@@ -25,6 +25,7 @@ local state = {
|
||||
---@return integer buf
|
||||
function M.create_buf()
|
||||
local existing = state.buf
|
||||
|
||||
if existing and vim.api.nvim_buf_is_valid(existing) then
|
||||
return existing
|
||||
end
|
||||
@@ -155,10 +156,13 @@ end
|
||||
|
||||
---Append text to the output buffer.
|
||||
---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.
|
||||
---Auto-scrolls to the bottom when enabled.
|
||||
---@param text string
|
||||
function M.append(text)
|
||||
---@param overwrite boolean|nil
|
||||
function M.append(text, overwrite)
|
||||
local buf = state.buf
|
||||
|
||||
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
|
||||
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
|
||||
vim.api.nvim_buf_set_lines(buf, current, -1, false, lines)
|
||||
end
|
||||
|
||||
@@ -186,6 +186,58 @@ describe("watchexec runner", function()
|
||||
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()
|
||||
it("ignores stale on_exit from replaced job", function()
|
||||
local call_count = 0
|
||||
|
||||
@@ -254,6 +254,48 @@ describe("watchexec window", function()
|
||||
|
||||
assert.equals(line_count, cursor[1])
|
||||
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)
|
||||
|
||||
describe("keymaps", function()
|
||||
@@ -262,12 +304,14 @@ describe("watchexec window", function()
|
||||
local buf = window.get_buf()
|
||||
local maps = vim.api.nvim_buf_get_keymap(buf, "n")
|
||||
local found = false
|
||||
|
||||
for _, m in ipairs(maps) do
|
||||
if m.lhs == "<Esc>" then
|
||||
found = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
assert.is_true(found)
|
||||
end)
|
||||
|
||||
@@ -276,12 +320,14 @@ describe("watchexec window", function()
|
||||
local buf = window.get_buf()
|
||||
local maps = vim.api.nvim_buf_get_keymap(buf, "n")
|
||||
local found = false
|
||||
|
||||
for _, m in ipairs(maps) do
|
||||
if m.lhs == "q" then
|
||||
found = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
assert.is_true(found)
|
||||
end)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user