Closes #39159 ## Summary • resolve remote terminal path candidates outside known worktrees through `Project::resolve_abs_path` • keep the local filesystem backed resolution path unchanged • preserve the existing preference for direct in-worktree matches when the terminal cwd is already inside a worktree • add regression coverage for both absolute and cwd-relative remote external paths ### Why The hyperlink parser wasn't the bug. `terminal_path_like_target` already knew how to parse these paths. The actual problem was that remote projects had no equivalent of the local background existence check. After worktree lookup failed, remote projects only fell back to worktree traversal, so cmd-click on `/tmp/a.txt` in an SSH remote terminal no-op'd. The project layer already has remote absolute-path resolution via `Project::resolve_abs_path`, so the right fix is to use that existing path for remote candidates instead of pretending remote projects cannot resolve paths outside their worktrees. ## Verification • `cargo test -p terminal_view terminal_path_like_target --quiet` • `cargo test -p terminal_view --quiet` ## Manual • Verified locally on my MBP w/ a stateless Zed build using SSH to self (`localhost`) • Opened `/tmp/zed-39159-project` as a remote project • Confirmed cmd-click opens `/tmp/zed-39159-external/a.txt` • Confirmed cmd-click opens `zed-39159-external/b.txt` from `cwd=/tmp` • Confirmed in-worktree paths still open correctly • Confirmed nonexistent external paths do not open bogus targets Release Notes: - Fixed remote terminal path opening so cmd-click can open files outside the project worktree.
Terminal View
Design Notes
This crate is split into two conceptual halves:
- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with terminal emulator backends and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here.
- Everything else. These other files integrate the
Terminalstruct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file.
ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. TerminalBuilder solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with TerminalBuilder::new(), check the result, then call TerminalBuilder::subscribe(cx) from within a model context.
The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors.
Backend Boundary
terminal.rs exposes backend-neutral domain types such as terminal content, cells, modes, points, ranges, scroll commands, vi motions, hyperlinks, and search matches. UI code should depend on those types instead of importing backend-specific terminal types directly.
The current implementation is still Alacritty-backed, but the abstraction boundary keeps backend details concentrated in the terminal crate:
terminal_viewrendersTerminalContentand dispatches backend-neutral actions.- Panels and tools use terminal-domain types for mode, cursor, range, hyperlink, and search behavior.
- Backend-specific conversions stay near the terminal event loop and render snapshot code.
This keeps the user-facing terminal behavior unchanged while making future backend experiments reviewable as backend implementations instead of UI-wide refactors.
Input
There are currently many distinct paths for getting keystrokes to the terminal:
-
Terminal specific characters and bindings. Things like ctrl-a mapping to ASCII control character 1, ANSI escape codes associated with the function keys, etc. These are caught with a raw key-down handler in the element and are processed immediately. This is done with the
try_keystroke()method on Terminal -
GPU Action handlers. GPUI clobbers a few vital keys by adding bindings to them in the global context. These keys are synthesized and then dispatched through the same
try_keystroke()API as the above mappings -
IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the
View::replace_text_in_range()method, and we then send that to the terminal directly, bypassingtry_keystroke(). -
Pasted text has a separate pathway.
Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the .input() API (which try_keystroke uses) so we have consistent input handling across the terminal