Skip to content

Bun.openInEditor: run vim, nvim and emacs directly on Linux and Windows - #38032

Open
robobun wants to merge 6 commits into
mainfrom
farm/fb6930a7/open-in-editor-terminal-editors
Open

Bun.openInEditor: run vim, nvim and emacs directly on Linux and Windows#38032
robobun wants to merge 6 commits into
mainfrom
farm/fb6930a7/open-in-editor-terminal-editors

Conversation

@robobun

@robobun robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • Bun.openInEditor(file) never opens anything on Linux or Windows when the editor resolves to vim, nvim or emacs ($EDITOR/$VISUAL, { editor: "vim" }, { editor: "/usr/bin/vim" }).
  • Editor::open (src/runtime/cli/open.rs:180 on main) builds the argv for those three variants as <opener> <editor> <editor> <file> on every platform. That is the macOS form, /usr/bin/open <editor> --args ..., and it has been built this way for every platform since the editor support was first written (the Zig open.zig had the same layout), so this is not a port regression.
  • On Linux the opener is xdg-open, which takes one argument: when it runs, it prints xdg-open: unexpected argument '/usr/bin/vim' and exits 1. On main it does not even run, because auto_close execs the bare name without a PATH search (the spawn layer openInEditor: spawn the editor without spawnSync's signal forwarding #31297 fixes), so the spawn fails with ENOENT, or runs whatever xdg-open is in cwd. Whichever layer fails first, the editor never starts, and fixing the spawn alone only gets as far as the xdg-open error.
  • On Windows the opener is start, a cmd.exe builtin rather than an executable, so the spawn fails and nothing opens (the same root cause as Open the browser via cmd.exe on Windows #32514, which changed the URL opener and left this call site for a follow-up).

Fix

  • The opener block is #[cfg(target_os = "macos")] and uses the existing cli::open::OPENER (/usr/bin/open) that the URL opener already uses; the per-platform copy in open.rs is gone. On Linux and Windows vim, nvim and emacs are spawned as <editor> <file>.
  • That argv is the one Editor::open already produces for every editor outside this special case, including terminal editors it has no name for ({ editor: "/usr/bin/nano" }), and on Windows it is already what { editor: "C:\\...\\vim.exe" } got, because an absolute path is classified by its exact basename and vim.exe falls into Other. It also runs the editor the user configured; xdg-open <file> would not (it picks the desktop's handler for the file type).
  • Opening a separate window is deliberately not attempted. Linux has no standard way to start a terminal emulator ($TERMINAL, x-terminal-emulator, per-emulator -e conventions). Windows does have one (cmd.exe /c start "" <editor> <file>), but it would send the user-supplied editor and file paths through cmd.exe's re-tokenizer and need the escaping or rejection logic that made Open the browser via cmd.exe on Windows #32514 leave this site out; a direct spawn needs none of that. This settles the call site Open the browser via cmd.exe on Windows #32514 deferred.
  • macOS is unchanged: the argv it builds is byte for byte what main builds. I have no macOS machine, so its behaviour is not verified here and the test skips there. It is likely wrong in its own way: open hands a Unix executable to Terminal.app, and --args goes to the launched application, not to the editor, which matches the note in src/runtime/bake/DevServer.rs:1826 ("opens apple terminal + vim"). If someone with a Mac confirms the file does not open, the follow-up is to delete the block on macOS as well and let this test run there; I did not want to change macOS behaviour blind in this PR.
  • Line and column are still not passed to these editors, as before.
  • Test: test/js/bun/util/open-in-editor-gc.test.ts, "runs terminal editors directly". Each row installs a stub editor (shell script, or .cmd on Windows) that records its own name and arguments into the file it is asked to open, and asserts the record is exactly <editor>, <file>. Rows: vim, nvim and emacs via $EDITOR, vim via { editor: "vim" }, and on Linux nvim via an absolute path (a .cmd stub's basename cannot select these editors). On Linux the stub is also installed as xdg-open, in cwd and on PATH so it is hit however argv[0] is resolved, so without the fix the rows fail showing xdg-open <editor> <editor> <file> being spawned. On Windows nothing is spawned without the fix, so run.js gives up after 3s with a "nothing was spawned" error that fails the stderr assertion.
  • Runs: bun bd test test/js/bun/util/open-in-editor-gc.test.ts passes on Linux (7) and on Windows x64 (4; the two Linux-only tests skip). Without the src change the 5 Linux rows fail (USE_SYSTEM_BUN=1 gives the same), and on Windows the 4 rows fail against the release build with the "nothing was spawned" message.
  • openInEditor: spawn the editor without spawnSync's signal forwarding #31297 changes how this argv is spawned and is independent of this change, but once every argv[0] built here outside macOS is an absolute editor path, its "finds the xdg-open opener on PATH" test has nothing left to observe and will need to go on whichever side lands second.

Background

  • Bun.openInEditor first resolves an editor to an enum variant plus an absolute binary path (EditorContext::detect_editor: explicit absolute path, known name looked up on PATH, $EDITOR/$VISUAL, then a built-in preference list). Editor::open then builds the argv and hands it to a detached thread that spawns it with inherited stdio and waits for it to exit.
  • /usr/bin/open (macOS) opens a file or application; for a Unix executable the handler is Terminal.app. xdg-open (freedesktop) opens one file or URL with its default handler and has no way to pass arguments to a program. start is a cmd.exe builtin of the form start ["title"] program [args]; it can open a console program in a new window, but only when invoked through cmd.exe /c.
  • The test builds the child env with mergeWindowEnvs because bunEnv spells the variable Path on Windows, and which of two differently cased PATH keys a child sees is currently not deterministic (Bun.spawn: de-duplicate case-insensitive env names on Windows #37382). The rows are not concurrent because five debug builds starting at once ran past the default per-test timeout on a loaded machine; sequentially each row takes about half a second.

no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/util/open-in-editor-gc.test.ts

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Limit details: You’ve used all 5 included reviews currently available under your plan.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4609508c-9e9d-4a3d-9de8-6e4462857466

📥 Commits

Reviewing files that changed from the base of the PR and between 8326d1b and b95af81.

📒 Files selected for processing (2)
  • src/runtime/cli/open.rs
  • test/js/bun/util/open-in-editor-gc.test.ts

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b0c643cd-41ee-4683-a7a3-5b747983b108

📥 Commits

Reviewing files that changed from the base of the PR and between bdb7382 and b0c373b.

📒 Files selected for processing (2)
  • src/runtime/cli/open.rs
  • test/js/bun/util/open-in-editor-gc.test.ts

Walkthrough

The editor launch path now uses /usr/bin/open with --args only on macOS. Linux and Windows launch Vim, Neovim, and Emacs directly. Tests cover editor selection through $EDITOR, editor names, and absolute paths.

Changes

Editor launch behavior

Layer / File(s) Summary
Platform-specific editor launch path
src/runtime/cli/open.rs
The opener-based launch branch is restricted to macOS. Non-macOS platforms launch terminal editors directly.
Cross-platform launch validation
test/js/bun/util/open-in-editor-gc.test.ts
Parameterized tests stub terminal editors and verify direct launches, file arguments, and editor selection on Linux and Windows.

Suggested reviewers: dylan-conway, jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: direct launching of vim, nvim, and emacs on Linux and Windows.
Description check ✅ Passed The description explains the problem, fix, platform behavior, and verification results, although it does not use the template headings exactly.

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 4:30 AM PT - Aug 16th, 2026

@robobun, your commit b95af81dae6ceddfb1ead934e4b2924ecba35a2a passed in Build #99422! 🎉


🧪   To try this PR locally:

bunx bun-pr 38032

That installs a local version of the PR into your bun-38032 executable, so you can run:

bun-38032 --bun

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced on Linux with bun 1.4.0 and on main by putting an xdg-open that logs its argv in cwd/PATH and calling Bun.openInEditor(file, { editor: "/usr/bin/vim" }): it receives /usr/bin/vim /usr/bin/vim <file>, and the real /usr/bin/xdg-open exits 1 with xdg-open: unexpected argument '/usr/bin/vim'. On Windows (checked on a Windows Server 2019 box) the same editors spawn nothing, since start is not an executable. Fix in this PR: the opener is only used on macOS, which is left byte for byte as it was; test/js/bun/util/open-in-editor-gc.test.ts covers vim, nvim and emacs on Linux and Windows.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — gating the opener prefix to macOS is the right shape; Linux/Windows now fall through to the same <binary> <file> argv every other editor already uses.

What was reviewed:

  • Confirmed the removed OPENER constants were module-private to open.rs; publish_command.rs and open_url use the separate pub(crate) const OPENER in cli/mod.rs, which is untouched.
  • macOS argv is byte-identical to before (the --args push was already macOS-gated inside the block).
  • New tests cover all three detection paths (absolute, name-on-PATH, $EDITOR), assert the exact argv via a recording stub, and would fail on the old xdg-open <editor> <editor> <file> shape since the stub also stands in for xdg-open.
Extended reasoning...

Overview

This PR fixes Bun.openInEditor for vim/nvim/emacs on Linux and Windows. The bug: Editor::open unconditionally prefixed the argv with a platform "opener" (xdg-open on Linux, start on Windows) for these three editors, producing xdg-open <binary> <binary> <file> — a shape only meaningful on macOS where /usr/bin/open <binary> --args ... launches a terminal editor in Terminal.app. xdg-open takes exactly one argument and start is a cmd builtin, so nothing ever opened. The fix gates the entire opener block behind #[cfg(target_os = "macos")] and deletes the now-unused Windows/Linux OPENER constants (net -4 lines in src). On non-macOS these editors now fall through to push_arg!(binary) and the default match arm, giving <binary> <file> — the same argv already used for Editor::Other and every non-special-cased editor.

Security risks

None. This is developer-tooling code that spawns a user-configured editor binary with a user-provided file path via inherited stdio. No new inputs are introduced; the change only removes an argv prefix. The detached-thread spawn path is unchanged.

Level of scrutiny

Low-to-medium. The src change is a 4-line removal plus a #[cfg] attribute — mechanically simple with a clear rationale. I verified the deleted OPENER constants were private to open.rs (no pub); the open::OPENER referenced by publish_command.rs:1117 and open_url in cli/mod.rs:246 is a separate pub(crate) constant defined in mod.rs (the open.rs file is mounted as mod open_full and only re-exports Editor/EditorContext into pub mod open). macOS behavior is byte-for-byte unchanged: the --args push was already inside a #[cfg(target_os = "macos")] inner block, so lifting the cfg to the whole if produces identical argv on macOS.

Other factors

The tests are well-constructed per REVIEW.md: they use test.concurrent.each over the variant matrix (three editors × three detection paths sampled), install a recording shell stub for both the editor and xdg-open (on PATH and in cwd, so regression is caught however argv[0] resolves), poll for the atomic-mv'd output file rather than sleeping, drain stdout/stderr/exited concurrently, and assert the exact argv ([name, file, ""]). The stub writes ${0##*/} first, so the old broken argv would surface as xdg-open in position 0 and fail the assertion — the PR description confirms all five fail under USE_SYSTEM_BUN=1. The $EDITOR case correctly relies on detect_editor checking the explicit editor option before env vars, so any inherited EDITOR/VISUAL in bunEnv cannot interfere with the absolute-path and name cases. The PR description also flags the interaction with #31297 (an in-flight change to argv spawning) — that's a heads-up for whoever lands second, not a blocker here.

Comment thread src/runtime/cli/open.rs Outdated
Comment thread src/runtime/cli/open.rs Outdated
Comment thread test/js/bun/util/open-in-editor-gc.test.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the poll-deadline nit from my earlier pass is addressed in b0c373b, and the comment-cop flags were resolved by shortening the opener comment to one line.

What was reviewed:

  • Confirmed the removed OPENER constants in open.rs were file-private and unused outside the now-macOS-gated block; publish_command.rs uses the separate mod.rs open::OPENER, so Linux/Windows still compile.
  • Traced the argv on macOS before and after — byte-identical; on Linux/Windows vim/nvim/emacs now fall through to the _ => arm like every other editor.
  • Checked the test's env handling: mergeWindowEnvs makes the PATH/EDITOR overrides win over bunEnv, and the explicit { editor } option takes precedence over any inherited $EDITOR/$VISUAL in the non-$EDITOR rows.
Extended reasoning...

Overview

The PR gates the vim/nvim/emacs opener-wrapping block in Editor::open (src/runtime/cli/open.rs) behind #[cfg(target_os = "macos")] and deletes the now-unused Windows (start) and non-macOS (xdg-open) OPENER constants from that file. On Linux/Windows these three editors now build the same [<binary>, <file>] argv every other editor already uses via the _ => match arm. macOS keeps [/usr/bin/open, <binary>, --args, <binary>, <file>] unchanged. A new describe.skipIf(isMacOS) block in test/js/bun/util/open-in-editor-gc.test.ts installs argv-recording stubs for vim/nvim/emacs (and xdg-open on Linux) and asserts the exact argv shape across $EDITOR, { editor: "name" }, and { editor: "/abs/path" } selection.

Security risks

None. The change narrows what gets spawned (drops the opener wrapper on two platforms) rather than introducing any new external input into an argv. No user-controlled data reaches a shell — sync::spawn execs the argv directly.

Level of scrutiny

Low-to-medium. The source change is a 6-line net deletion that cfg-gates existing code; I verified the removed constants were file-private (the open::OPENER used by publish_command.rs lives in mod.rs's inline pub mod open, not in open.rs). The behavioral surface is Bun.openInEditor, a developer convenience API with no correctness dependents. The bulk of the diff is the test.

Other factors

  • All three prior review threads are resolved: my unbounded-poll nit was fixed in b0c373b (run.js now throws "nothing was spawned" after 3s, so a regression fails the stderr assertion instead of timing out), and the two comment-cop hits were addressed by trimming the block comment to one line in 18119d3.
  • The test is hermetic (own tempDir per row, stub prepended to PATH, no network), uses mergeWindowEnvs to avoid the Windows PATH-casing race, and writes the argv record via tmp-file + rename on both platforms so the poll never observes a partial write.
  • The PR description documents that the fix was verified to fail with USE_SYSTEM_BUN=1 on Linux and against the release build on Windows, satisfying the fails-for-the-right-reason requirement.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the earlier poll-deadline nit is addressed in b0c373b, and cf50348 dedupes against the shared cli::open::OPENER in mod.rs.

What was reviewed

  • Editor::open: the #[cfg(target_os = "macos")] gate leaves the macOS argv byte-identical; Linux/Windows now fall through to the same push_arg!(binary); … file path already used for Editor::Other.
  • Confirmed super::open::OPENER resolves to the inline mod open in cli/mod.rs (this file is loaded as open_full), so removing the file-local duplicate constant leaves open_url and publish_command unaffected.
  • Test: stub writes atomically (tmp+mv) so the existence poll can't read a partial file; mergeWindowEnvs avoids the Path/PATH casing race; the 3s deadline surfaces spawn failures via the stderr assertion.
Extended reasoning...

Overview

Two files. In src/runtime/cli/open.rs, the vim/nvim/emacs branch of Editor::open that prefixed the argv with a platform "opener" (/usr/bin/open / xdg-open / start) is now gated behind #[cfg(target_os = "macos")]. The file-local OPENER constant is deleted and the macOS block references the pre-existing shared cli::open::OPENER in mod.rs instead (this file is mounted as mod open_full via #[path], so super::open is the inline module in mod.rs, not a self-reference). The rest of Editor::open is untouched, so on Linux/Windows the argv for these three editors is now <binary> <file> — the same shape the _ => arm already produces for unrecognised editors like nano. The macOS argv is byte-for-byte unchanged.

The test file gains a describe.skipIf(isMacOS) block with a test.each matrix over vim/nvim/emacs × ($EDITOR / name / absolute path), each installing a stub that records its own name plus argv into the target file, then asserting the record is exactly [editor, file]. On Linux the same stub also stands in for xdg-open, so the pre-fix argv (xdg-open <editor> <editor> <file>) would be captured and produce a visible diff rather than a silent pass.

Security risks

None. Bun.openInEditor is a developer convenience that already spawns whatever binary the user names in $EDITOR or the editor option; this PR only changes which arguments precede that binary in argv on non-macOS. No new user input is interpreted, no new privilege boundary is crossed, and no shell interpretation is introduced (spawn is via sync::spawn with an argv array, not a shell string).

Level of scrutiny

Low-to-medium. The Rust change is a net -12/+3 that gates an already-conditional block more tightly and removes a duplicate constant. The blast radius is one rarely-called API on two platforms where it was demonstrably broken (xdg-open rejects extra args; start is not spawnable), so the downside of the fix being wrong is bounded by "still doesn't open the editor". The design question — spawn directly vs. try to find a terminal emulator — is argued convincingly in the PR description (matches existing Editor::Other behaviour, avoids inventing Linux terminal-detection heuristics), and the current behaviour is strictly worse than either alternative.

Other factors

All three prior review threads are resolved: my unbounded-poll nit was fixed in b0c373b (3s deadline throwing an actionable message that surfaces via the expect(stderr).toBe("") assertion), and both comment-cop notes on the long code comment were addressed in 18119d3/cf50348 by moving the rationale to the PR description and the test's describe-block comment. The test follows harness conventions (tempDir with using, bunEnv/bunExe, mergeWindowEnvs for the Windows PATH-casing hazard, pipes drained concurrently, exit code asserted last), writes the record file atomically via tmp+mv so the poll never observes a partial write, and the author reports it fails under USE_SYSTEM_BUN=1 on Linux and against the unfixed release build on Windows. The rows are intentionally sequential with a stated reason (concurrent debug-build starts blew the default timeout), which satisfies the repo's "staying serial needs a stated reason" rule.

Editor::open built the argv for these editors as
`<opener> <editor> <editor> <file>` on every platform. That shape is
macOS's `open <editor> --args ...`, which runs the editor in a new
Terminal window. xdg-open takes a single argument and exits 1 when given
three, and `start` is a cmd.exe builtin that cannot be spawned at all, so
on Linux and Windows these editors never opened.

Only use the opener on macOS. Everywhere else the editor is spawned as
`<editor> <file>`, the same way every other editor already is.
@robobun
robobun force-pushed the farm/fb6930a7/open-in-editor-terminal-editors branch from cf50348 to b95af81 Compare August 16, 2026 11:11

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the earlier poll-deadline nit is addressed and the comment-cop feedback is resolved.

What was reviewed

  • super::open::OPENER resolves to the inline pub mod open in src/runtime/cli/mod.rs (this file is mounted as open_full), so the macOS path is byte-for-byte unchanged and the deleted local OPENER had no other consumers.
  • On Linux/Windows the vim/nvim/emacs arms now fall through to the _ branch (<binary> <file>), matching what Editor::Other already produces for unrecognized terminal editors.
  • Test: bounded 3s poll, mergeWindowEnvs for the Windows PATH-casing hazard, stub also installed as xdg-open so the unfixed argv shape shows up in the diff, sequential rows with a stated reason.
Extended reasoning...

Overview

Two files. src/runtime/cli/open.rs gates the vim/nvim/emacs "launch through the platform opener" block to #[cfg(target_os = "macos")] and deletes the file-local OPENER constant in favour of the shared one in cli::mod.rs's inline open module. On macOS the argv is identical to main (/usr/bin/open <binary> --args <binary> <file>); on Linux and Windows these three editors now fall through to the default _ match arm, producing <binary> <file> — exactly what every other editor variant (including Editor::Other) already gets. test/js/bun/util/open-in-editor-gc.test.ts gains a describe.skipIf(isMacOS) block with a test.each matrix over {vim, nvim, emacs} × {$EDITOR, name, absolute path} that installs a shell/.cmd stub which records its argv into the file it is asked to open.

Deleted-constant reachability

The removed OPENER in open.rs was file-local (private). Grep confirms the two remaining references (publish_command.rs:1100 and mod.rs:246) both point at cli::open::OPENER — the inline pub mod open in mod.rs, not this file. open.rs itself is mounted as mod open_full (mod.rs:224-225), so from inside it super::open::OPENER correctly reaches the shared constant. No broken references, no cfg-gated dead code left behind.

Security risks

None material. Bun.openInEditor already spawned a user-configured binary with a user-supplied file path; this PR only changes which binary is argv[0] on Linux/Windows (the editor itself instead of xdg-open/start). No new shell interpretation is introduced — the PR explicitly avoids cmd.exe /c start on Windows to sidestep re-tokenizing user paths, which is the safer choice. Arguments are passed via argv arrays, not shell strings.

Level of scrutiny

Low-to-moderate. Bun.openInEditor is a developer-convenience API, not a hot path or security boundary, and the current Linux/Windows behaviour is simply broken (nothing opens). The src change is ~10 lines, mostly deletion, and one platform is provably unchanged. The design question — spawning a TUI editor with inherited stdio from a detached thread while the main process continues — is what Editor::Other (e.g. nano) already does today, so this is consistency rather than new behaviour, and the PR description reasons through why a new-terminal-window approach was rejected on each platform.

Other factors

My prior inline nit (unbounded poll in run.js) was addressed in b0c373b with a 3s deadline and a diagnostic throw, verified by the author against the unfixed Windows build. The two comment-cop hits on the paragraph-long src comment were addressed by removing the comment entirely (b95af81 uses the shared constant with no inline explanation). All three inline threads are resolved. The test follows the file's established conventions (tempDir, bunEnv, pipes drained via Promise.all, stderr asserted before exitCode, sequential rows with a stated reason for not being concurrent), and the author reports it fails on both platforms without the src change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant