Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 3 additions & 15 deletions src/runtime/cli/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,6 @@ use crate::api::bun::process::sync;

// ──────────────────────────────────────────────────────────────────────────

#[cfg(target_os = "macos")]
const OPENER: &[u8] = b"/usr/bin/open";
#[cfg(windows)]
const OPENER: &[u8] = b"start";
#[cfg(not(any(target_os = "macos", windows)))]
const OPENER: &[u8] = b"xdg-open";

// ──────────────────────────────────────────────────────────────────────────

#[repr(u8)]
#[derive(Copy, Clone, PartialEq, Eq, Hash, strum::IntoStaticStr, enum_map::Enum)]
#[strum(serialize_all = "snake_case")] // Vscode → "vscode"
Expand Down Expand Up @@ -177,14 +168,11 @@ impl Editor {
}};
}

#[cfg(target_os = "macos")]
if matches!(self, Editor::Vim | Editor::Emacs | Editor::Neovim) {
push_arg!(OPENER);
push_arg!(super::open::OPENER);
push_arg!(binary);

#[cfg(target_os = "macos")]
{
push_arg!(b"--args");
}
push_arg!(b"--args");
}

push_arg!(binary);
Expand Down
86 changes: 83 additions & 3 deletions test/js/bun/util/open-in-editor-gc.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, isLinux, tempDir } from "harness";
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isLinux, isMacOS, isWindows, mergeWindowEnvs, tempDir } from "harness";
import { chmodSync, existsSync, symlinkSync } from "node:fs";
import { join } from "node:path";
import { delimiter, join } from "node:path";

// Both ways of naming an editor (EDITOR in the environment, the `editor`
// option) end in a PATH lookup of the editor's binary name. The fake editors
Expand Down Expand Up @@ -152,3 +152,83 @@ test.skipIf(!isLinux)("Bun.openInEditor does not break GC signal handling", asyn

await Promise.all(runs);
});

// vim, nvim and emacs used to be launched through the platform opener, in the
// shape of macOS's `open <editor> --args ...`: `xdg-open <editor> <editor> <file>`
// on Linux, which xdg-open rejects, and `start ...` on Windows, which is a cmd.exe
// builtin that cannot be spawned. Either way the editor never opened. They are
// now run directly, like every other editor. macOS keeps the opener (it would
// open Terminal.app here), so it is skipped.
describe.skipIf(isMacOS)("Bun.openInEditor runs terminal editors directly", () => {
// Stands in for the editor, and on Linux also for xdg-open: writes its own
// name followed by its arguments into its last argument, which is the file
// being opened in both argv shapes (so on Linux the old shape shows up in the
// diff; on Windows the old shape spawned nothing at all).
const recordArgv = isWindows
? [
"@echo off",
"setlocal enabledelayedexpansion",
'set "last="',
'for %%a in (%*) do set "last=%%~a"',
"(echo %~n0",
'for %%a in (%*) do echo %%~a) > "!last!.tmp"',
'move /y "!last!.tmp" "!last!" >nul',
"",
].join("\r\n")
: `#!/bin/sh
for file; do :; done
printf '%s\\n' "\${0##*/}" "$@" > "$file.tmp" && mv "$file.tmp" "$file"
`;

const cases: [editor: string, how: "$EDITOR" | "name" | "absolute path"][] = [
["vim", "$EDITOR"],
["nvim", "$EDITOR"],
["emacs", "$EDITOR"],
["vim", "name"],
];
// An absolute editor path is classified by its exact basename, which the
// .cmd stub does not have, so this form only reaches these editors on Linux.
if (!isWindows) cases.push(["nvim", "absolute path"]);

// Not concurrent: five debug builds starting at once on a loaded machine ran
// past the default per-test timeout; one at a time each row takes ~0.5s.
test.each(cases)("%s given as %s", async (editor, how) => {
const stub = isWindows ? `${editor}.cmd` : editor;
using dir = tempDir("open-in-editor-terminal", {
[stub]: recordArgv,
// In cwd as well as on PATH so it is found however argv[0] gets resolved.
...(isWindows ? {} : { "xdg-open": recordArgv }),
"run.js": `
const [file, editor] = process.argv.slice(2);
if (editor) Bun.openInEditor(file, { editor });
Comment thread
robobun marked this conversation as resolved.
else Bun.openInEditor(file);
const deadline = Date.now() + 3000;
while (!(await Bun.file(file).exists())) {
if (Date.now() > deadline) throw new Error("nothing was spawned: " + file + " was never written");
await Bun.sleep(5);
}
await Bun.write(Bun.stdout, await Bun.file(file).text());
`,
});
if (!isWindows) {
chmodSync(join(String(dir), stub), 0o755);
chmodSync(join(String(dir), "xdg-open"), 0o755);
}
const file = join(String(dir), "opened.txt");

const overrides: Record<string, string> = { PATH: `${String(dir)}${delimiter}${process.env.PATH}` };
const cmd = [bunExe(), "run.js", file];
if (how === "$EDITOR") overrides.EDITOR = editor;
else cmd.push(how === "name" ? editor : join(String(dir), stub));
// bunEnv spells the variable "Path" on Windows; a second, differently
// cased PATH key would leave it up to the spawn which one the child sees.
const env = mergeWindowEnvs([bunEnv, overrides]);

await using proc = Bun.spawn({ cmd, env, cwd: String(dir), stdout: "pipe", stderr: "pipe" });
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).toBe("");
expect(stdout.split(/\r?\n/)).toEqual([editor, file, ""]);
expect(exitCode).toBe(0);
});
});
Loading