mirror of
https://github.com/aphrody-labs/opentui.git
synced 2026-10-13 21:08:53 +00:00
native: check highlight counts after shrinking setText (#1601)
native: drop highlights on removed lines when replacing text setText kept user highlights on every stored line, also on lines past the new line count. getHighlightCount and getLineHighlights reported them, and text that grew back into such a line (append) was colored by them. setPlainText now builds the kept highlight lists during preparation, for lines below the new line count only, and swaps them in when it publishes the text, like the styled-text path. The lists past the new end are freed with the old storage. This replaces the in-place internal-highlight filter. The fix covers setText, setTextFromMemId*, setTextFromMemRangeWithUndo (EditBuffer.replaceText), and replaceText (EditBuffer.setText). Source additions (net -5 lines): the staged highlight lists and their cleanup hold the kept highlights until publication, and one comment states the line-count bound. Fixes https://github.com/anomalyco/opentui/issues/1588
This commit is contained in:
1 parent
07225659bb
commit
acf13584be
3 files changed
+37
-44
No files matched your search
@@ -4,6 +4,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { TextBuffer } from "./text-buffer.js"
|
||||
import { EditBuffer } from "./edit-buffer.js"
|
||||
import { resolveRenderLib } from "./zig.js"
|
||||
import { StyledText, stringToStyledText } from "./lib/styled-text.js"
|
||||
import { RGBA } from "./lib/RGBA.js"
|
||||
@@ -193,36 +194,32 @@ describe("TextBuffer", () => {
|
||||
expect(buffer.getSyntaxStyle()).toBeNull()
|
||||
})
|
||||
|
||||
it("setText clears styled text chunk highlights", () => {
|
||||
// Replacement drops styled chunk highlights (line -1: the old text is styled) and keeps user highlights
|
||||
// only on lines that still exist. Expected: the hlRefs left on lines 0 and 1.
|
||||
it.each<[string, string, number, string, number[]]>([
|
||||
["TextBuffer.setText", "Styled", -1, "Plain", []],
|
||||
["TextBuffer.setText", "Hello World", 0, "New Text", [65535]],
|
||||
["TextBuffer.setText", "first\nsecond", 1, "xyz", []],
|
||||
["EditBuffer.setText", "first\nsecond", 1, "xyz", []],
|
||||
["EditBuffer.replaceText", "first\nsecond", 1, "xyz", []],
|
||||
["EditBuffer.replaceText", "a\nb\nc", 1, "x\ny", [65535]],
|
||||
])("%s from %j with a highlight on line %d to %j keeps %j", (operation, before, line, after, expected) => {
|
||||
const syntaxStyle = SyntaxStyle.create(resourceContext)
|
||||
buffer.setSyntaxStyle(syntaxStyle)
|
||||
buffer.setStyledText(new StyledText([{ __isChunk: true, text: "Styled", fg: RGBA.fromValues(1, 0, 0, 1) }]))
|
||||
const fg = RGBA.fromValues(0, 1, 0, 1)
|
||||
const target = operation === "TextBuffer.setText" ? buffer : EditBuffer.create("wcwidth", resourceContext)
|
||||
target.setSyntaxStyle(syntaxStyle)
|
||||
if (line < 0) buffer.setStyledText(new StyledText([{ __isChunk: true, text: before, fg }]))
|
||||
else target.setText(before)
|
||||
const styleId = syntaxStyle.registerStyle("user-highlight", { fg })
|
||||
if (line >= 0) target.addHighlight(line, { start: 0, end: 1, styleId, priority: 0, hlRef: 65535 })
|
||||
expect([0, 1].flatMap((i) => target.getLineHighlights(i)).length).toBe(1)
|
||||
|
||||
expect(buffer.getHighlightCount()).toBe(1)
|
||||
|
||||
buffer.setText("Plain")
|
||||
|
||||
expect(buffer.getPlainText()).toBe("Plain")
|
||||
expect(buffer.getHighlightCount()).toBe(0)
|
||||
|
||||
syntaxStyle.destroy()
|
||||
})
|
||||
|
||||
it("setText preserves user highlights including max hlRef", () => {
|
||||
const syntaxStyle = SyntaxStyle.create(resourceContext)
|
||||
const styleId = syntaxStyle.registerStyle("user-highlight", { fg: RGBA.fromValues(0, 1, 0, 1) })
|
||||
buffer.setSyntaxStyle(syntaxStyle)
|
||||
buffer.setText("Hello World")
|
||||
buffer.addHighlight(0, { start: 0, end: 5, styleId, priority: 0, hlRef: 65535 })
|
||||
|
||||
expect(buffer.getHighlightCount()).toBe(1)
|
||||
|
||||
buffer.setText("New Text")
|
||||
|
||||
expect(buffer.getPlainText()).toBe("New Text")
|
||||
expect(buffer.getHighlightCount()).toBe(1)
|
||||
expect(buffer.getLineHighlights(0)[0]?.hlRef).toBe(65535)
|
||||
if (operation === "EditBuffer.replaceText") (target as EditBuffer).replaceText(after)
|
||||
else target.setText(after)
|
||||
|
||||
expect([0, 1].flatMap((i) => target.getLineHighlights(i).map((hl) => hl.hlRef))).toEqual(expected)
|
||||
if (target === buffer) expect(buffer.getHighlightCount()).toBe(expected.length)
|
||||
if (target !== buffer) target.destroy()
|
||||
syntaxStyle.destroy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1688,6 +1688,7 @@ fn checkPlainTextAllocationFailures() !void {
|
||||
try std.testing.expectEqual(before.epoch + 1, tb.getContentEpoch());
|
||||
try std.testing.expect(tb.isViewDirty(view));
|
||||
try std.testing.expectEqual(@as(u32, 0), tb.link_tracker.?.getLinkCount());
|
||||
try std.testing.expectEqual(@as(u32, if (input.len == 0) 1 else 2), tb.getHighlightCount());
|
||||
break;
|
||||
}
|
||||
try std.testing.expect(succeeded);
|
||||
|
||||
@@ -921,16 +921,19 @@ pub const UnifiedTextBuffer = struct {
|
||||
};
|
||||
|
||||
var spans: @TypeOf(self.line_spans) = .empty;
|
||||
var highlights: @TypeOf(self.line_highlights) = .empty;
|
||||
defer {
|
||||
for (spans.items) |*list| list.deinit(self.global_allocator);
|
||||
spans.deinit(self.global_allocator);
|
||||
for (highlights.items) |*list| list.deinit(self.global_allocator);
|
||||
highlights.deinit(self.global_allocator);
|
||||
}
|
||||
var highlights: std.ArrayListUnmanaged(Highlight) = .empty;
|
||||
defer highlights.deinit(self.global_allocator);
|
||||
const span_count = if (self.getHighlightCount() == 0) 0 else self.line_highlights.items.len;
|
||||
try spans.ensureTotalCapacity(self.global_allocator, span_count);
|
||||
// User highlights stay only on lines that the new text still has.
|
||||
const line_count = if (self.getHighlightCount() == 0) 0 else @min(self.line_highlights.items.len, candidate.root.metrics().custom.linestart_count);
|
||||
try spans.ensureTotalCapacity(self.global_allocator, line_count);
|
||||
try highlights.ensureTotalCapacity(self.global_allocator, line_count);
|
||||
var segment_index: usize = 0;
|
||||
for (self.line_highlights.items[0..span_count], 0..) |list, line_idx| {
|
||||
for (self.line_highlights.items[0..line_count], 0..) |list, line_idx| {
|
||||
var line_width: u32 = 0;
|
||||
while (segment_index < result.segments.items.len) {
|
||||
const segment = result.segments.items[segment_index];
|
||||
@@ -938,12 +941,12 @@ pub const UnifiedTextBuffer = struct {
|
||||
if (segment.isBreak()) break;
|
||||
if (segment.asText()) |chunk| line_width += chunk.width_cols;
|
||||
}
|
||||
highlights.clearRetainingCapacity();
|
||||
highlights.appendAssumeCapacity(.empty);
|
||||
for (list.items) |hl| {
|
||||
if (!hl.internal) try highlights.append(self.global_allocator, hl);
|
||||
if (!hl.internal) try highlights.items[line_idx].append(self.global_allocator, hl);
|
||||
}
|
||||
spans.appendAssumeCapacity(.empty);
|
||||
try self.buildLineSpans(highlights.items, line_width, &spans.items[line_idx]);
|
||||
try self.buildLineSpans(highlights.items[line_idx].items, line_width, &spans.items[line_idx]);
|
||||
}
|
||||
|
||||
// History nodes are shared: publish undo on the live rope, after all preparation.
|
||||
@@ -960,16 +963,8 @@ pub const UnifiedTextBuffer = struct {
|
||||
self._rope.root = candidate.root;
|
||||
self._rope.version = candidate.version;
|
||||
}
|
||||
for (self.line_highlights.items) |*list| {
|
||||
var kept: usize = 0;
|
||||
for (list.items) |hl| {
|
||||
if (hl.internal) continue;
|
||||
list.items[kept] = hl;
|
||||
kept += 1;
|
||||
}
|
||||
list.items.len = kept;
|
||||
}
|
||||
self.internal_highlight_count = 0;
|
||||
std.mem.swap(@TypeOf(self.line_highlights), &self.line_highlights, &highlights);
|
||||
std.mem.swap(@TypeOf(self.line_spans), &self.line_spans, &spans);
|
||||
// Replacement flushes pending spans even inside a highlight transaction.
|
||||
self.dirty_span_lines.clearRetainingCapacity();
|
||||
|
||||
Reference in new issue
Block a user