From 85dc4b1842799d23db4abc5c2a671e4975c9d49d Mon Sep 17 00:00:00 2001 From: Vasyl Zuziak Date: Sat, 25 Apr 2026 15:46:25 +0200 Subject: [PATCH] surface: respect semantic prompt boundaries for links Link detection currently expands the clicked location to a full line before running the configured regexes. When semantic prompt markers are present, this can cause prompt text and neighboring content to be matched together even though they are distinct semantic regions. Use semantic prompt boundaries when selecting the text to inspect for link matching. This keeps prompt text separate from the content beside it and avoids folding prompt text into double-click link/path selection. Add a regression test that models a prompt and command on the same line and verifies the prompt region and input region remain separate. --- src/Surface.zig | 61 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index dfc3a50ea..3a25757ea 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4295,7 +4295,9 @@ fn linkAtPin( const line = screen.selectLine(.{ .pin = mouse_pin, .whitespace = null, - .semantic_prompt_boundary = false, + // Respect semantic prompt boundaries so link/path matching doesn't + // merge shell prompt content with the text beside it. + .semantic_prompt_boundary = true, }) orelse return null; var strmap: terminal.StringMap = undefined; @@ -6658,3 +6660,60 @@ test "Surface: rectangle selection logic" { true, //rectangle selection ); } + +test "Surface: link selection line respects prompt boundary" { + const testing = std.testing; + const alloc = testing.allocator; + + var screen = try terminal.Screen.init(alloc, .{ + .cols = 20, + .rows = 5, + .max_scrollback = 0, + }); + defer screen.deinit(); + + screen.cursorSetSemanticContent(.{ .prompt = .initial }); + try screen.testWriteString("/tmp $ "); + screen.cursorSetSemanticContent(.{ .input = .clear_explicit }); + try screen.testWriteString("locale"); + + { + var sel = screen.selectLine(.{ + .pin = screen.pages.pin(.{ .active = .{ + .x = 1, + .y = 0, + } }).?, + .whitespace = null, + .semantic_prompt_boundary = true, + }).?; + defer sel.deinit(&screen); + + const contents = try screen.selectionString(alloc, .{ + .sel = sel, + .trim = false, + }); + defer alloc.free(contents); + + try testing.expectEqualStrings("/tmp $ ", contents); + } + + { + var sel = screen.selectLine(.{ + .pin = screen.pages.pin(.{ .active = .{ + .x = 8, + .y = 0, + } }).?, + .whitespace = null, + .semantic_prompt_boundary = true, + }).?; + defer sel.deinit(&screen); + + const contents = try screen.selectionString(alloc, .{ + .sel = sel, + .trim = false, + }); + defer alloc.free(contents); + + try testing.expectEqualStrings("locale", contents); + } +}