diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/http.zig | 191 | ||||
| -rw-r--r-- | src/http/mime_type.zig | 20 | ||||
| -rw-r--r-- | src/open.zig | 292 |
3 files changed, 500 insertions, 3 deletions
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(); + } +}; |
