Compare commits

...

4 Commits

Author SHA1 Message Date
Zack Radisic
023a67f0eb stuff 2025-07-14 14:18:38 -07:00
Zack Radisic
f021f91ca2 hyperlinks 2025-07-14 12:58:28 -07:00
Zack Radisic
1b99346fd4 Merge branch 'main' into zack/osc-8-stacktraces 2025-07-13 23:51:39 -07:00
Zack Radisic
d08fa54e3f terminal hyperlink 2025-07-04 14:17:09 -07:00
2 changed files with 137 additions and 4 deletions

View File

@@ -52,6 +52,9 @@ jsc: *VM = undefined,
/// bun:wrap is very noisy
hide_bun_stackframes: bool = true,
/// Terminal hyperlink strategy for stack traces
hyperlink_strategy: HyperlinkStrategy = .cursor,
is_printing_plugin: bool = false,
is_shutting_down: bool = false,
plugin_runner: ?PluginRunner = null,
@@ -395,6 +398,12 @@ pub const GCLevel = enum(u3) {
aggressive = 2,
};
pub const HyperlinkStrategy = enum(u8) {
none = 0,
vscode = 1,
cursor = 2,
};
pub threadlocal var is_main_thread_vm: bool = false;
pub const UnhandledRejectionScope = struct {
@@ -2831,6 +2840,53 @@ pub fn printExternallyRemappedZigException(
);
}
fn printHyperlink(
this: *VirtualMachine,
writer: anytype,
source_url: []const u8,
line: i32,
column: i32,
) !void {
if (this.hyperlink_strategy == .none) return;
// Build the hyperlink URL
var url_buf: [4096]u8 = undefined;
const url = if (this.hyperlink_strategy == .vscode or this.hyperlink_strategy == .cursor) blk: {
// For vscode, use the vscode:// protocol
const proto = switch (this.hyperlink_strategy) {
.vscode => "vscode",
.cursor => "cursor",
else => unreachable,
};
const url_str = if (source_url[0] == '/')
std.fmt.bufPrint(&url_buf, "{s}://file{s}:{d}:{d}", .{ proto, source_url, line, column }) catch return
else
std.fmt.bufPrint(&url_buf, "{s}://file/{s}:{d}:{d}", .{ proto, source_url, line, column }) catch return;
break :blk url_str;
} else blk: {
// For cursor and other editors, use file:// protocol
const url_str = if (source_url[0] == '/')
std.fmt.bufPrint(&url_buf, "file://{s}", .{source_url}) catch return
else
std.fmt.bufPrint(&url_buf, "file:///{s}", .{source_url}) catch return;
break :blk url_str;
};
// OSC 8 escape sequence: ESC]8;;URL\a
try writer.writeAll("\x1b]8;;");
try writer.writeAll(url);
try writer.writeAll("\x07");
}
fn endHyperlink(
this: *VirtualMachine,
writer: anytype,
) !void {
if (this.hyperlink_strategy == .none) return;
// End hyperlink: ESC]8;;\a
try writer.writeAll("\x1b]8;;\x07");
}
fn printErrorInstance(
this: *VirtualMachine,
comptime mode: enum { js, zig_exception },
@@ -2891,6 +2947,17 @@ fn printErrorInstance(
var source_lines = exception.stack.sourceLineIterator();
var last_pad: u64 = 0;
// Get the top frame to get source URL for hyperlinks
var top_frame: ?*const JSC.ZigStackFrame = if (exception.stack.frames_len > 0) &exception.stack.frames()[0] else null;
if (this.hide_bun_stackframes) {
for (exception.stack.frames()) |*frame| {
if (frame.position.isInvalid() or frame.source_url.hasPrefixComptime("bun:") or frame.source_url.hasPrefixComptime("node:")) continue;
top_frame = frame;
break;
}
}
while (source_lines.untilLast()) |source| {
defer source.text.deinit();
const display_line = source.line + 1;
@@ -2899,6 +2966,21 @@ fn printErrorInstance(
const pad = max_line_number_pad - int_size;
last_pad = pad;
try writer.writeByteNTimes(' ', pad);
// Add hyperlink for this source line if we have a valid source URL
const should_add_hyperlink = brk: {
if (top_frame) |frame| {
const source_url = frame.source_url.toUTF8(bun.default_allocator);
defer source_url.deinit();
if (source_url.slice().len > 0 and !strings.hasPrefix(source_url.slice(), "bun:") and !strings.hasPrefix(source_url.slice(), "node:")) {
// For lines before the error, we don't have exact column info, so use column 1
try this.printHyperlink(writer, source_url.slice(), display_line, 1);
break :brk true;
}
}
break :brk false;
};
const trimmed = std.mem.trimRight(u8, std.mem.trim(u8, source.text.slice(), "\n"), "\t ");
const clamped = trimmed[0..@min(trimmed.len, max_line_length)];
@@ -2921,6 +3003,11 @@ fn printErrorInstance(
.{ display_line, bun.fmt.fmtJavaScript(clamped, .{ .enable_colors = allow_ansi_color }) },
);
}
// End hyperlink after printing the line
if (should_add_hyperlink) {
try this.endHyperlink(writer);
}
}
const name = exception.name;
@@ -2951,17 +3038,17 @@ fn printErrorInstance(
if (source_lines.next()) |source| brk: {
if (source.text.len == 0) break :brk;
var top_frame = if (exception.stack.frames_len > 0) &exception.stack.frames()[0] else null;
var current_top_frame = if (exception.stack.frames_len > 0) &exception.stack.frames()[0] else null;
if (this.hide_bun_stackframes) {
for (exception.stack.frames()) |*frame| {
if (frame.position.isInvalid() or frame.source_url.hasPrefixComptime("bun:") or frame.source_url.hasPrefixComptime("node:")) continue;
top_frame = frame;
current_top_frame = frame;
break;
}
}
if (top_frame == null or top_frame.?.position.isInvalid()) {
if (current_top_frame == null or current_top_frame.?.position.isInvalid()) {
defer did_print_name = true;
defer source.text.deinit();
const trimmed = std.mem.trimRight(u8, std.mem.trim(u8, source.text.slice(), "\n"), "\t ");
@@ -2988,7 +3075,7 @@ fn printErrorInstance(
}
try this.printErrorNameAndMessage(name, message, !exception.browser_url.isEmpty(), code, Writer, writer, allow_ansi_color, formatter.error_display_level);
} else if (top_frame) |top| {
} else if (current_top_frame) |top| {
defer did_print_name = true;
const display_line = source.line + 1;
const int_size = std.fmt.count("{d}", .{display_line});
@@ -2998,6 +3085,15 @@ fn printErrorInstance(
const text = source.text.slice();
const trimmed = std.mem.trimRight(u8, std.mem.trim(u8, text, "\n"), "\t ");
// Get source URL for hyperlink
const source_url = top.source_url.toUTF8(bun.default_allocator);
defer source_url.deinit();
// Add hyperlink to line number
if (source_url.slice().len > 0 and !strings.hasPrefix(source_url.slice(), "bun:") and !strings.hasPrefix(source_url.slice(), "node:")) {
try this.printHyperlink(writer, source_url.slice(), display_line, @intCast(top.position.column.oneBased()));
}
// TODO: preserve the divot position and possibly use stringWidth() to figure out where to put the divot
const clamped = trimmed[0..@min(trimmed.len, max_line_length)];
@@ -3018,6 +3114,11 @@ fn printErrorInstance(
),
.{ display_line, bun.fmt.fmtJavaScript(clamped, .{ .enable_colors = allow_ansi_color }) },
);
// End hyperlink before printing the caret
if (source_url.slice().len > 0 and !strings.hasPrefix(source_url.slice(), "bun:") and !strings.hasPrefix(source_url.slice(), "node:")) {
try this.endHyperlink(writer);
}
if (clamped.len < max_line_length_with_divot or top.position.column.zeroBased() > max_line_length_with_divot) {
const indent = max_line_number_pad + " | ".len + @as(u64, @intCast(top.position.column.zeroBased()));
@@ -3032,6 +3133,7 @@ fn printErrorInstance(
}
}
try this.printErrorNameAndMessage(name, message, !exception.browser_url.isEmpty(), code, Writer, writer, allow_ansi_color, formatter.error_display_level);
}
}

View File

@@ -3757,6 +3757,37 @@ pub const MemoryReportingAllocator = @import("allocators/MemoryReportingAllocato
pub const mach_port = if (Environment.isMac) std.c.mach_port_t else u32;
pub const TerminalHyperlink = struct {
link: []const u8,
text: []const u8,
enabled: bool,
const Protocol = enum {
vscode,
cursor,
};
pub fn new(link: []const u8, text: []const u8, enabled: bool) TerminalHyperlink {
return TerminalHyperlink{
.link = link,
.text = text,
.enabled = enabled,
};
}
pub fn format(this: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
if (this.enabled) {
const ESC = "\x1b";
const OSC8 = ESC ++ "];8;";
const ST = ESC ++ "\\";
const link_fmt_string = OSC8 ++ "{s}" ++ ST ++ "{s}" ++ OSC8 ++ ST;
try writer.print(link_fmt_string, .{ this.link, this.text });
} else {
try writer.print("{s}", .{this.text});
}
}
};
pub fn contains(item: anytype, list: *const std.ArrayListUnmanaged(@TypeOf(item))) bool {
const T = @TypeOf(item);
return switch (T) {