Expand coop vscode into coop editor with Zed support#416
Conversation
`coop editor` opens VS Code or Zed connected to the guest over SSH; `coop vscode` remains as an alias. `--editor` is now a clap ValueEnum (`code` | `zed`) instead of a free-form string. When omitted, coop tries VS Code first, then falls through to Zed. Zed connects with `zed ssh://coop-<name>/<path>` (macOS fallback: `open zed://ssh/...`), reusing the same `~/.ssh/config` alias block that VS Code's Remote-SSH uses — no new SSH machinery. `.cargo/mutants.toml`: the renamed `open_editor` wrapper replaces the `vscode` exclude; the pure strategy helpers (`zed_strategies`, `editor_strategies`) stay in scope and are unit-tested. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| } | ||
| } | ||
|
|
||
| let hints: &[&str] = match editor { |
There was a problem hiding this comment.
This per-editor hint selection has no test, and it lives inside launch_editor, which is excluded from the mutation sweep (\blaunch_editor\b in .cargo/mutants.toml). A mutant that swaps the Code/Zed arms or collapses None to a single hint survives both cargo test and cargo mutants. Extracting it into a small pure helper kept in mutation scope (e.g. fn install_hints(editor: Option<EditorKind>) -> &'static [&'static str]), with a per-arm assertion, would close the gap.
| }]; | ||
| if cfg!(target_os = "macos") { | ||
| strategies.push(LaunchStrategy { | ||
| name: "macOS open zed:// URL", |
There was a problem hiding this comment.
The macOS open zed://ssh/... fallback has no coverage — zed_strategies_first_is_zed_cli only asserts strategies[0] (the CLI form). This branch uses a different scheme/shape than the CLI arm (zed://ssh/... vs ssh://...), and since the mutation sweep runs on Linux the cfg!(target_os = "macos") arm is compiled out, so a broken URL here surfaces in neither tests nor mutants. A #[cfg(target_os = "macos")] test asserting strategies[1] is open zed://ssh/coop-test/workspace would cover it (the parallel vscode_strategies macOS arm is pre-existing but equally uncovered).
| fn zed_strategies(host: &str, remote_path: &GuestPath) -> Vec<LaunchStrategy> { | ||
| // Zed shells out to the system `ssh`, so the `coop-<name>` alias in | ||
| // ~/.ssh/config supplies host, port, user, and key. | ||
| let url = format!("ssh://{host}{remote_path}"); |
There was a problem hiding this comment.
The Zed strategies interpolate the guest path straight into a URL — ssh://{host}{remote_path} here, and zed://ssh/{host}{remote_path} in the macOS fallback — with no percent-encoding. --project is validated only by GuestPath::absolute, which checks nothing but the leading / (src/paths.rs:41). So a path with URL-significant characters produces a malformed target: --project '/a#b' yields ssh://coop-test/a#b, where Zed treats #b as a fragment and opens /a; a space stays unencoded. The VS Code path is immune because it goes through argv (--remote <arg> <path>), not a URL. Consider percent-encoding the path segment, or noting the limitation.
| editor: Option<EditorKind>, | ||
| ) -> Result<()> { | ||
| let host = ssh_config_host(inst); | ||
| let strategies = editor_strategies(editor, &host, remote_path); |
There was a problem hiding this comment.
The default (auto-detect) order built here is code CLI → (macOS) open VS Code → zed CLI → (macOS) open zed://, and the launch loop below only falls through on ErrorKind::NotFound. Any other spawn error on an earlier strategy — e.g. an earlier binary present but non-executable (PermissionDenied) — hits the Err(e) => return Err(...) arm and returns early, so Zed is never tried even when it is installed and working. With VS Code as the only editor this was harmless (nothing left to fall back to); now that Zed is a real fallback, this defeats the documented "try VS Code first, then Zed" contract. Treating a spawn error like a miss (push to tried, continue) would preserve fallthrough.
| host: &str, | ||
| remote_path: &GuestPath, | ||
| ) -> Vec<LaunchStrategy> { | ||
| let remote_arg = format!("ssh-remote+{host}"); |
There was a problem hiding this comment.
remote_arg is built unconditionally, but only the VS Code strategies use it (the Some(Code) and None arms). On the Some(EditorKind::Zed) arm it is a wasted allocation. Moving it into the arms that consume it (or into vscode_strategies) keeps it scoped to its users.
| fn editor_strategies_explicit_choice_pins_editor() { | ||
| let path = GuestPath::new("/workspace"); | ||
| let code = editor_strategies(Some(EditorKind::Code), "coop-test", &path); | ||
| assert!(code.iter().all(|s| s.cmd != "zed")); |
There was a problem hiding this comment.
These assertions only check the absence of the other editor (all(|s| s.cmd != "zed")), which is vacuously true for an empty Vec — the test still passes if the pinned arm returned no strategies at all. Add a positive assertion that the chosen editor is actually present, e.g. assert_eq!(code[0].cmd, "code") / assert_eq!(zed[0].cmd, "zed").
|
|
||
| 1. Writes an SSH config block for the instance into `~/.ssh/config`. | ||
| 2. Launches VS Code with `code --remote ssh-remote+coop-{name} /workspace`. | ||
| 2. Launches VS Code with `code --remote ssh-remote+coop-{name} /workspace`, falling back to Zed with `zed ssh://coop-{name}/workspace` when `code` is not installed. |
There was a problem hiding this comment.
This says coop falls back to Zed "when code is not installed", but on macOS editor_strategies(None, ...) inserts an open -a 'Visual Studio Code' strategy between the code CLI and Zed, so VS Code can still launch via the app when the code CLI is absent — Zed is only reached after that fallback also fails. The ## The coop editor command and ### Zed sections describe the full chain correctly; only this quick-start summary glosses the macOS step.
Review summary7 findings posted inline. Scope: commit Themes:
No security, convention, API-usage, or comment issues. The rename is consistent across code, docs, completions, and Coverage: ran review-correctness, review-design, review-conventions, review-security, review-api-usage, review-tests, review-docs, review-comments (all 8). None skipped. |
coop editoropens VS Code or Zed connected to the guest over SSH;coop vscoderemains as an alias.--editoris now a clap ValueEnum (code|zed) instead of a free-form string. When omitted, coop tries VS Code first, then falls through to Zed.