diff options
| -rw-r--r-- | README.md | 16 | ||||
| -rw-r--r-- | packages/bun-error/bun-error.css | 16 | ||||
| -rw-r--r-- | packages/bun-error/index.tsx | 76 | ||||
| -rw-r--r-- | src/http.zig | 191 | ||||
| -rw-r--r-- | src/http/mime_type.zig | 20 | ||||
| -rw-r--r-- | src/open.zig | 292 |
6 files changed, 594 insertions, 17 deletions
@@ -350,6 +350,22 @@ port = 5000 # When loading a .bagel file, run the JS parser ".bagel" = "js" +[debug] +# When navigating to a blob: or src: link, open the file in your editor +editor = "code" + +# List of editors: +# - "subl", "sublime" +# - "vscode", "code" +# - "textmate", "mate" +# - "idea" +# - "webstorm" +# - "nvim", "neovim" +# - "vim","vi" +# - "emacs" +# - "atom" +# If you pass it a file path, it will open with the file path instead +# It will recognize non-GUI editors, but I don't think it will work yet ``` TODO: list each property name diff --git a/packages/bun-error/bun-error.css b/packages/bun-error/bun-error.css index c5ed9881b..01866218e 100644 --- a/packages/bun-error/bun-error.css +++ b/packages/bun-error/bun-error.css @@ -21,7 +21,7 @@ a:hover { border: inset 1px solid rgba(0, 0, 0, 0.2); border-radius: 17px; background-color: rgba(255, 255, 255, 0.92); - width: 480px; + width: 512px; position: fixed; top: 120px; @@ -166,8 +166,11 @@ a:hover { .BunError-SourceLine-number { font-variant: tabular-nums; + display: block; + cursor: pointer; text-align: right; user-select: none; + text-decoration: none; -webkit-user-select: none; } @@ -175,6 +178,10 @@ a:hover { color: rgb(165, 165, 165); } +.BunError-SourceLine-number:hover { + text-decoration: underline; +} + .BunError-SourceLine-number, .BunError-SourceLine-text { height: 18px; @@ -281,9 +288,12 @@ a:hover { .BunError-StackFrames { display: table; table-layout: auto; - padding: 13px 10px; - margin: 8px auto; + padding: 16px 12px; + width: 100%; + box-sizing: border-box; + margin: 0 auto; border-radius: 4px; + line-height: 1.2; background-color: rgb(244, 244, 244); } diff --git a/packages/bun-error/index.tsx b/packages/bun-error/index.tsx index f0943b8c1..4be467d9e 100644 --- a/packages/bun-error/index.tsx +++ b/packages/bun-error/index.tsx @@ -97,8 +97,38 @@ const normalizedFilename = (filename: string, cwd: string): string => { return filename; }; -const blobFileURL = (filename: string): string => { - return new URL("/blob:" + filename, location.href).href; +const blobFileURL = ( + filename: string, + line?: number, + column?: number +): string => { + var base = `/blob:${filename}`; + if (Number.isFinite(line)) { + base += `:${line}`; + + if (Number.isFinite(column)) { + base += `:${column}`; + } + } + + return new URL(base, location.href).href; +}; + +const openWithoutFlashOfNewTab = (event: MouseEvent) => { + const href = event.currentTarget.getAttribute("href"); + if (!href || event.button !== 0) { + return true; + } + event.target.removeAttribute("target"); + event.preventDefault(); + event.nativeEvent.preventDefault(); + event.nativeEvent.stopPropagation(); + event.nativeEvent.stopImmediatePropagation(); + globalThis.fetch(href + "?editor=1").then( + () => {}, + (er) => {} + ); + return false; }; const srcFileURL = (filename: string, line: number, column: number): string => { @@ -107,10 +137,10 @@ const srcFileURL = (filename: string, line: number, column: number): string => { } var base = `/src:${filename}`; - if (line > -1) { + if (Number.isFinite(line) && line > -1) { base = base + `:${line}`; - if (column > -1) { + if (Number.isFinite(column) && column > -1) { base = base + `:${column}`; } } @@ -197,11 +227,13 @@ const SourceLines = ({ highlightColumnStart = 0, highlightColumnEnd = Infinity, children, + buildURL, }: { sourceLines: SourceLine[]; highlightColumnStart: number; highlightColumnEnd: number; highlight: number; + buildURL: (line?: number, column?: number) => string; }) => { let start = sourceLines.length; let end = 0; @@ -261,14 +293,17 @@ const SourceLines = ({ </div> ); numbers[i] = ( - <div + <a + href={buildURL(line, highlightColumnStart)} + onClickCapture={openWithoutFlashOfNewTab} + target="_blank" key={line} className={`BunError-SourceLine-number ${ classes.empty ? "BunError-SourceLine-number--empty" : "" } ${classes.highlight ? "BunError-SourceLine-number--highlight" : ""}`} > {line} - </div> + </a> ); if (classes.highlight && children) { @@ -313,13 +348,24 @@ const SourceLines = ({ ); }; -const BuildErrorSourceLines = ({ location }: { location: Location }) => { - const { line, line_text, column, file } = location; +const BuildErrorSourceLines = ({ + location, + filename, +}: { + location: Location; + filename: string; +}) => { + const { line, line_text, column } = location; const sourceLines: SourceLine[] = [{ line, text: line_text }]; + const buildURL = React.useCallback( + (line, column) => srcFileURL(filename, line, column), + [srcFileURL, filename] + ); return ( <SourceLines sourceLines={sourceLines} highlight={line} + buildURL={buildURL} highlightColumnStart={column} highlightColumnEnd={column} /> @@ -335,11 +381,12 @@ const BuildErrorStackTrace = ({ location }: { location: Location }) => { <a href={srcFileURL(filename, line, column)} target="_blank" + onClickCapture={openWithoutFlashOfNewTab} className="BunError-NativeStackTrace-filename" > {filename}:{line}:{column} </a> - <BuildErrorSourceLines location={location} /> + <BuildErrorSourceLines filename={filename} location={location} /> </div> ); }; @@ -414,7 +461,8 @@ const NativeStackFrame = ({ <a target="_blank" - href={blobFileURL(fileName)} + href={blobFileURL(fileName, line + 1, column)} + onClickCapture={openWithoutFlashOfNewTab} className="BunError-StackFrame-link" > <div className="BunError-StackFrame-link-content"> @@ -451,11 +499,16 @@ const NativeStackTrace = ({ const { file = "", position } = frames[0]; const { cwd } = useContext(ErrorGroupContext); const filename = normalizedFilename(file, cwd); + const buildURL = React.useCallback( + (line, column) => blobFileURL(file, line, column), + [file, blobFileURL] + ); return ( <div className={`BunError-NativeStackTrace`}> <a - href={blobFileURL(filename)} + href={blobFileURL(filename, position.line + 1, position.column_start)} target="_blank" + onClickCapture={openWithoutFlashOfNewTab} className="BunError-NativeStackTrace-filename" > {filename}:{position.line + 1}:{position.column_start} @@ -465,6 +518,7 @@ const NativeStackTrace = ({ highlight={position.line} sourceLines={sourceLines} highlightColumnStart={position.column_start} + buildURL={buildURL} highlightColumnEnd={position.column_stop} > {children} diff --git a/src/http.zig b/src/http.zig index 4a0fdc347..738ed9e46 100644 --- a/src/http.zig +++ b/src/http.zig @@ -2375,9 +2375,18 @@ pub const RequestContext = struct { fn handleBlobURL(ctx: *RequestContext, _: *Server) !void { var id = ctx.url.path["blob:".len..]; + var line: string = ""; + var column: string = ""; + // This makes it Just Work if you pass a line/column number if (strings.indexOfChar(id, ':')) |colon| { + line = id[@minimum(id.len, colon + 1)..]; id = id[0..colon]; + + if (strings.indexOfChar(line, ':')) |col| { + column = line[@minimum(line.len, col + 1)..]; + line = line[0..col]; + } } const Blob = @import("./blob.zig"); @@ -2388,12 +2397,29 @@ pub const RequestContext = struct { if (vm.blobs.?.get(id)) |blob| { break :brk blob; } + + if (strings.eqlComptime(id, "node_modules.server.bun")) { + if (vm.node_modules) |bun| { + if (bun.code_string) |code| { + break :brk Blob{ .ptr = code.str.ptr, .len = code.str.len }; + } + } + } } if (JavaScript.VirtualMachine.vm_loaded) { - if (JavaScript.VirtualMachine.vm.blobs.?.get(id)) |blob| { + var vm = JavaScript.VirtualMachine.vm; + if (vm.blobs.?.get(id)) |blob| { break :brk blob; } + + if (strings.eqlComptime(id, "node_modules.server.bun")) { + if (vm.node_modules) |bun| { + if (bun.code_string) |code| { + break :brk Blob{ .ptr = code.str.ptr, .len = code.str.len }; + } + } + } } return try ctx.sendNotFound(); @@ -2404,6 +2430,33 @@ pub const RequestContext = struct { return; } + const always_open = strings.contains(ctx.url.query_string, "editor"); + + if (always_open) { + const real_mime_type = MimeType.byExtension(strings.trim(std.fs.path.extension(id), ".")); + + if (real_mime_type.canOpenInEditor() or ctx.bundler.options.loader(std.fs.path.extension(id)) != .file) { + if (Server.editor == null) { + Server.detectEditor(ctx.bundler.env); + } + + if (Server.editor) |editor| { + if (editor != .none) { + Server.openInEditor(editor, blob.ptr[0..blob.len], id, Fs.FileSystem.instance.tmpdir(), line, column); + if (Server.editor.? != .none) { + defer ctx.done(); + try ctx.writeStatus(200); + ctx.appendHeader("Content-Type", MimeType.html.value); + const auto_close = "<html><body><script>window.close();</script></body></html>"; + try ctx.prepareToSendBody(auto_close.len, false); + try ctx.writeBodyBuf(auto_close); + return; + } + } + } + } + } + defer ctx.done(); try ctx.writeStatus(200); ctx.appendHeader("Content-Type", MimeType.text.value); @@ -2541,9 +2594,20 @@ pub const RequestContext = struct { fn handleSrcURL(ctx: *RequestContext, _: *Server) !void { var input_path = ctx.url.path["src:".len..]; - while (std.mem.indexOfScalar(u8, input_path, ':')) |i| { + var line: string = ""; + var column: string = ""; + if (std.mem.indexOfScalar(u8, input_path, ':')) |i| { + line = input_path[i + 1 ..]; input_path = input_path[0..i]; + + if (line.len > 0) { + if (std.mem.indexOfScalar(u8, line, ':')) |j| { + column = line[j + 1 ..]; + line = line[0..j]; + } + } } + if (input_path.len == 0) return ctx.sendNotFound(); const pathname = Fs.PathName.init(input_path); @@ -2552,6 +2616,33 @@ pub const RequestContext = struct { switch (result.file.value) { .pending => |resolve_result| { const path = resolve_result.pathConst() orelse return try ctx.sendNotFound(); + const always_open = strings.contains(ctx.url.query_string, "editor"); + if (always_open) { + if (Server.editor == null) + Server.detectEditor(ctx.bundler.env); + + if (Server.editor) |editor| { + if (editor != .none) { + editor.open(Server.editor_path, path.text, line, column, _global.default_allocator) catch |err| { + if (editor != .other) { + Output.prettyErrorln("Error {s} opening in {s}", .{ @errorName(err), @tagName(editor) }); + } + + Server.editor = Editor.none; + }; + + if (Server.editor.? != .none) { + defer ctx.done(); + try ctx.writeStatus(200); + ctx.appendHeader("Content-Type", MimeType.html.value); + const auto_close = "<html><body><script>window.close();</script></body></html>"; + try ctx.prepareToSendBody(auto_close.len, false); + try ctx.writeBodyBuf(auto_close); + return; + } + } + } + } var needs_close = false; const fd = if (resolve_result.file_fd != 0) @@ -2780,6 +2871,7 @@ var serve_as_package_path = false; // - Resolver time // - Parsing time // - IO read time +const Editor = @import("./open.zig").Editor; pub const Server = struct { log: logger.Log, @@ -2791,6 +2883,99 @@ pub const Server = struct { javascript_enabled: bool = false, fallback_only: bool = false, + pub var editor: ?Editor = null; + pub var editor_name: string = ""; + pub var editor_path: string = ""; + + pub fn openInEditor(editor_: Editor, blob: []const u8, id: string, tmpdir: std.fs.Dir, line: string, column: string) void { + _openInEditor(editor_, blob, id, tmpdir, line, column) catch |err| { + if (editor_ != .other) { + Output.prettyErrorln("Error {s} opening in {s}", .{ @errorName(err), @tagName(editor_) }); + } + + editor = Editor.none; + }; + } + + fn _openInEditor(editor_: Editor, blob: []const u8, id: string, tmpdir: std.fs.Dir, line: string, column: string) !void { + var basename_buf: [512]u8 = undefined; + var basename = std.fs.path.basename(id); + if (strings.endsWith(basename, ".bun") and basename.len < 499) { + std.mem.copy(u8, &basename_buf, basename); + basename_buf[basename.len..][0..3].* = ".js".*; + basename = basename_buf[0 .. basename.len + 3]; + } + + try tmpdir.writeFile(basename, blob); + + var opened = try tmpdir.openFile(basename, .{}); + defer opened.close(); + var path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + try editor_.open( + Server.editor_path, + try std.os.getFdPath(opened.handle, &path_buf), + line, + column, + default_allocator, + ); + } + + pub fn detectEditor(env: *DotEnv.Loader) void { + var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; + + var out: string = ""; + // first: choose from user preference + if (Server.editor_name.len > 0) { + // /usr/bin/vim + if (std.fs.path.isAbsolute(Server.editor_name)) { + Server.editor = Editor.byName(std.fs.path.basename(Server.editor_name)) orelse Editor.other; + editor_path = Server.editor_name; + return; + } + + // "vscode" + if (Editor.byName(std.fs.path.basename(Server.editor_name))) |editor_| { + if (Editor.byPATHForEditor(env, editor_, &buf, Fs.FileSystem.instance.top_level_dir, &out)) { + editor = editor_; + editor_path = Fs.FileSystem.instance.dirname_store.append(string, out) catch unreachable; + return; + } + + // not in path, try common ones + if (Editor.byFallbackPathForEditor(editor_, &out)) { + editor = editor_; + editor_path = Fs.FileSystem.instance.dirname_store.append(string, out) catch unreachable; + return; + } + } + } + + // EDITOR=code + if (Editor.detect(env)) |editor_| { + if (Editor.byPATHForEditor(env, editor_, &buf, Fs.FileSystem.instance.top_level_dir, &out)) { + editor = editor_; + editor_path = Fs.FileSystem.instance.dirname_store.append(string, out) catch unreachable; + return; + } + + // not in path, try common ones + if (Editor.byFallbackPathForEditor(editor_, &out)) { + editor = editor_; + editor_path = Fs.FileSystem.instance.dirname_store.append(string, out) catch unreachable; + return; + } + } + + // Don't know, so we will just guess based on what exists + if (Editor.byFallback(env, &buf, Fs.FileSystem.instance.top_level_dir, &out)) |editor_| { + editor = editor_; + editor_path = Fs.FileSystem.instance.dirname_store.append(string, out) catch unreachable; + return; + } + + Server.editor = Editor.none; + } + threadlocal var filechange_buf: [32]u8 = undefined; threadlocal var filechange_buf_hinted: [32]u8 = undefined; @@ -3510,6 +3695,8 @@ pub const Server = struct { return; } + Server.editor_name = debug.editor; + server.bundler.options.macro_remap = debug.macros orelse .{}; if (debug.fallback_only or server.bundler.env.map.get("BUN_DISABLE_BUN_JS") != null) { diff --git a/src/http/mime_type.zig b/src/http/mime_type.zig index 8969623b8..743560931 100644 --- a/src/http/mime_type.zig +++ b/src/http/mime_type.zig @@ -20,6 +20,17 @@ const MimeType = @This(); value: string, category: Category, +pub fn canOpenInEditor(this: MimeType) bool { + if (this.category == .text or this.category.isCode()) + return true; + + if (this.category == .image) { + return strings.eqlComptime(this.value, "image/svg+xml"); + } + + return false; +} + pub const Category = enum { image, text, @@ -33,6 +44,13 @@ pub const Category = enum { javascript, wasm, + pub fn isCode(this: Category) bool { + return switch (this) { + .wasm, .json, .css, .html, .javascript => true, + else => false, + }; + } + pub fn isTextLike(this: Category) bool { return switch (this) { .javascript, .html, .text, .css, .json => true, @@ -145,6 +163,8 @@ pub fn byExtension(ext: string) MimeType { 3 => { const four = [4]u8{ ext[0], ext[1], ext[2], 0 }; return switch (std.mem.readIntNative(u32, &four)) { + Four.case("bun") => javascript, + Four.case("css") => css, Four.case("jpg") => MimeType.initComptime("image/jpeg", .image), Four.case("gif") => MimeType.initComptime("image/gif", .image), diff --git a/src/open.zig b/src/open.zig index 7af79457d..52062609d 100644 --- a/src/open.zig +++ b/src/open.zig @@ -9,7 +9,7 @@ const stringZ = _global.stringZ; const default_allocator = _global.default_allocator; const C = _global.C; const std = @import("std"); - +const DotEnv = @import("env_loader.zig"); const opener = switch (@import("builtin").target.os.tag) { .macos => "/usr/bin/open", .windows => "start", @@ -32,3 +32,293 @@ pub fn openURL(url: string) !void { _ = try child_process.wait(); return; } + +pub const Editor = enum(u8) { + none, + sublime, + vscode, + atom, + textmate, + intellij, + webstorm, + vim, + neovim, + emacs, + other, + + const StringMap = std.EnumMap(Editor, string); + const StringArrayMap = std.EnumMap(Editor, []const [:0]const u8); + + const name_map = std.ComptimeStringMap(Editor, .{ + .{ "sublime", Editor.sublime }, + .{ "subl", Editor.sublime }, + .{ "vscode", Editor.vscode }, + .{ "code", Editor.vscode }, + .{ "textmate", Editor.textmate }, + .{ "mate", Editor.textmate }, + .{ "atom", Editor.atom }, + .{ "idea", Editor.intellij }, + .{ "webstorm", Editor.webstorm }, + .{ "nvim", Editor.neovim }, + .{ "neovim", Editor.neovim }, + .{ "vim", Editor.vim }, + .{ "vi", Editor.vim }, + .{ "emacs", Editor.emacs }, + }); + + pub fn byName(name: string) ?Editor { + if (strings.indexOfChar(name, ' ')) |i| { + return name_map.get(name[0..i]); + } + + return name_map.get(name); + } + + pub fn detect(env: *DotEnv.Loader) ?Editor { + const vars = .{ "EDITOR", "VISUAL" }; + inline for (vars) |name| { + if (env.get(name)) |value| { + const basename = std.fs.path.basename(value); + if (byName(basename)) |editor| { + return editor; + } + } + } + + return null; + } + + const which = @import("./which.zig").which; + pub fn byPATH(env: *DotEnv.Loader, buf: *[std.fs.MAX_PATH_BYTES]u8, cwd: string, out: *[]const u8) ?Editor { + const PATH = env.get("PATH") orelse return null; + + inline for (default_preference_list) |editor| { + if (bin_name.get(editor)) |path| { + if (which(buf, PATH, cwd, path)) |bin| { + out.* = std.mem.span(bin); + return editor; + } + } + } + + return null; + } + + pub fn byPATHForEditor(env: *DotEnv.Loader, editor: Editor, buf: *[std.fs.MAX_PATH_BYTES]u8, cwd: string, out: *[]const u8) bool { + const PATH = env.get("PATH") orelse return false; + + if (bin_name.get(editor)) |path| { + if (path.len > 0) { + if (which(buf, PATH, cwd, path)) |bin| { + out.* = std.mem.span(bin); + return true; + } + } + } + + return false; + } + + pub fn byFallbackPathForEditor(editor: Editor, out: ?*[]const u8) bool { + if (bin_path.get(editor)) |paths| { + for (paths) |path| { + if (std.os.open(path, 0, 0)) |opened| { + std.os.close(opened); + if (out != null) { + out.?.* = std.mem.span(path); + } + return true; + } else |_| {} + } + } + + return false; + } + + pub fn byFallback(env: *DotEnv.Loader, buf: *[std.fs.MAX_PATH_BYTES]u8, cwd: string, out: *[]const u8) ?Editor { + inline for (default_preference_list) |editor| { + if (byPATHForEditor(env, editor, buf, cwd, out)) { + return editor; + } + + if (byFallbackPathForEditor(editor, out)) { + return editor; + } + } + + return null; + } + + pub const default_preference_list = [_]Editor{ + .vscode, + .sublime, + .atom, + .neovim, + + .webstorm, + .intellij, + .textmate, + .vim, + }; + + pub const bin_name: StringMap = brk: { + var map = StringMap{}; + + map.put(.sublime, "subl"); + map.put(.vscode, "code"); + map.put(.atom, "atom"); + map.put(.textmate, "mate"); + map.put(.intellij, "idea"); + map.put(.webstorm, "webstorm"); + map.put(.vim, "vim"); + map.put(.neovim, "nvim"); + map.put(.emacs, "emacs"); + map.put(.other, ""); + break :brk map; + }; + + pub const bin_path: StringArrayMap = brk: { + var map = StringArrayMap{}; + + if (Environment.isMac) { + map.put(.vscode, &.{ + "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code", + "/Applications/VSCodium.app/Contents/Resources/app/bin/code", + }); + map.put( + .atom, + &.{ + "/Applications/Atom.app/Contents/Resources/app/atom.sh", + }, + ); + map.put( + .sublime, + &.{ + "/Applications/Sublime Text 3.app/Contents/SharedSupport/bin/subl", + "/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl", + "/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl", + }, + ); + } + + break :brk map; + }; + + pub fn isJetBrains(editor: Editor) bool { + return switch (editor) { + .intellij, .webstorm => true, + else => false, + }; + } + + pub fn open( + editor: Editor, + binary: string, + file: []const u8, + line: ?string, + column: ?string, + allocator: std.mem.Allocator, + ) !void { + var file_path_buf: [std.fs.MAX_PATH_BYTES + 1024]u8 = undefined; + var file_path_buf_stream = std.io.fixedBufferStream(&file_path_buf); + var file_path_buf_writer = file_path_buf_stream.writer(); + var args_buf: [10]string = undefined; + + var i: usize = 0; + + if (editor == .vim or editor == .emacs or editor == .neovim) { + args_buf[0] = opener; + i += 1; + + args_buf[i] = binary; + i += 1; + + if (Environment.isMac) { + args_buf[i] = "--args"; + i += 1; + } + } + + args_buf[i] = binary; + i += 1; + + if (editor == .vscode and line != null and line.?.len > 0) { + args_buf[i] = "--goto"; + + i += 1; + } + + switch (editor) { + .sublime, .atom, .vscode, .webstorm, .intellij => { + try file_path_buf_writer.writeAll(file); + if (line) |line_| { + if (line_.len > 0) { + try file_path_buf_writer.print(":{s}", .{line_}); + + if (!editor.isJetBrains()) { + if (column) |col| { + if (col.len > 0) + try file_path_buf_writer.print(":{s}", .{col}); + } + } + } + } + if (file_path_buf_stream.pos > 0) { + args_buf[i] = file_path_buf_stream.getWritten(); + i += 1; + } + }, + .textmate => { + try file_path_buf_writer.writeAll(file); + var file_path = file_path_buf_stream.getWritten(); + + if (line) |line_| { + if (line_.len > 0) { + args_buf[i] = "--line"; + i += 1; + + try file_path_buf_writer.print("{s}", .{line_}); + + if (column) |col| { + if (col.len > 0) + try file_path_buf_writer.print(":{s}", .{col}); + } + + var line_column = file_path_buf_stream.getWritten()[file_path.len..]; + if (line_column.len > 0) { + args_buf[i] = line_column; + i += 1; + } + } + } + + if (file_path_buf_stream.pos > 0) { + args_buf[i] = file_path; + i += 1; + } + }, + else => { + if (file.len > 0) { + try file_path_buf_writer.writeAll(file); + var file_path = file_path_buf_stream.getWritten(); + args_buf[i] = file_path; + i += 1; + } + }, + } + + var child_process = try std.ChildProcess.init(args_buf[0..i], allocator); + child_process.stderr_behavior = .Pipe; + child_process.stdin_behavior = .Ignore; + child_process.stdout_behavior = .Pipe; + try child_process.spawn(); + var thread = try std.Thread.spawn(.{}, autoClose, .{child_process}); + thread.detach(); + } + + fn autoClose(child_process: *std.ChildProcess) void { + Global.setThreadName("Open Editor"); + _ = child_process.wait() catch {}; + child_process.deinit(); + } +}; |
