chore: strip every comment from the Rust sources (#268)

Removed all Rust comments -- line, block, and doc -- from the 139 tracked
.rs files with `uncomment` 3.5.1. It parses each file with tree-sitter
instead of matching text, so comment-like content inside string literals
is left alone: the JavaScript plugin source embedded in agent_hooks.rs
raw strings keeps its own `//` lines.

Left alone: Cargo.toml comments and the shell scripts under scripts/.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
l0ng-ai
2026-07-30 21:36:15 +08:00
committed by GitHub
co-authored by l0ng-ai
parent ac8968643d
commit 8c1946d763
139 changed files with 483 additions and 38751 deletions
+2 -390
View File
@@ -1,32 +1,3 @@
//! The code panel: a full-body editor overlay that covers the terminal,
//! settings-overlay style. It stops short of the right panel rather than
//! covering it, so the file tree there stays beside the editor — the tree used
//! to be this overlay's own left column and is not any more.
//!
//! A lightweight "look at / touch up code without leaving the terminal"
//! editor, not a full IDE. The text engine is `gpui_component::input::
//! InputState` in CodeEditor mode, which brings rope storage, tree-sitter
//! syntax highlighting, line numbers, indent guides, code folding,
//! auto-indent, undo/redo and an in-buffer search/replace bar. This module
//! owns everything around that engine: the open-file set and tab strip, dirty
//! tracking and save, external-modification reload (via `notify`), the
//! unsaved-close confirmation, and the overlay chrome itself. The file tree is
//! `ui::file_tree`'s, drawn by the right panel's Files tab.
//!
//! Deliberately *not* an IDE: there is no language-server integration, and
//! adding one is not a wanted feature. Opening a `.rs` file silently spawning
//! rust-analyzer — a background process indexing the whole workspace for
//! hundreds of megabytes of RAM — is not something a terminal emulator should
//! do to its user. Highlighting comes from tree-sitter grammars compiled into
//! gpui-component (see [`language_for_path`]), which is static, in-process,
//! and costs nothing beyond parsing the open buffer.
//!
//! Layout: overlaying the body (like Settings and the diff overlay) rather
//! than docking a side column means toggling never resizes the terminal — no
//! PTY resize, no reflow — and the editor gets the full body width. The tab
//! sidebar stays visible; switching tabs re-roots the tree. One entry point:
//! the title-bar tile in `tab_strip` (`ToggleCodePanel`, ⌘⇧E; Esc closes).
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@@ -45,68 +16,28 @@ use gpui_component::{
use crate::ui::app::Tty7App;
use crate::ui::host_ops::{HostOps, MTime, SharedHost, WatchSub};
/// Refuse to open files larger than this: the component's code editor is rated
/// to ~50K lines, and a multi-megabyte blob is almost never what a terminal
/// user meant to open in a side panel.
const MAX_FILE_BYTES: u64 = 4 * 1024 * 1024;
/// Debounce for external-change reloads, matching the config hot-reload: a
/// save is often a truncate→write→rename burst that should collapse to one.
const RELOAD_DEBOUNCE: std::time::Duration = std::time::Duration::from_millis(200);
/// One open file: the component editor state plus the bookkeeping that turns
/// it into a *file* editor (path, dirty flag, on-disk snapshot identity).
pub(crate) struct OpenFile {
pub(crate) path: PathBuf,
pub(crate) input: Entity<InputState>,
/// The buffer has edits not yet written to `path`.
pub(crate) dirty: bool,
/// mtime of the content we last loaded from / saved to disk; used to drop
/// watcher echoes of our own saves.
///
/// [`MTime`], not `SystemTime`, and nanosecond-precise on purpose: the echo
/// test is mtime equality, so a coarser clock would swallow a genuine
/// external edit that landed in the same tick as our own write.
disk_mtime: Option<MTime>,
/// Bumped on every buffer change, so a save that lands can tell whether the
/// text it wrote is still the text in the buffer.
edit_seq: u64,
/// The `edit_seq` the in-flight write's snapshot was taken at, or `None`
/// when nothing is being written. Also the single-flight latch: a second
/// ⌘S while this is set queues rather than races.
saving: Option<u64>,
/// A save was asked for while one was in flight. Re-issued when that one
/// lands, so the last content the user asked to save is the content on disk.
save_pending: bool,
/// The user answered "Save" to a close prompt, so this buffer closes once
/// its write lands.
///
/// Lives on the buffer rather than being threaded through the save call
/// because a save can be *queued* behind one already in flight: passing it
/// as an argument meant the queued request's intent was dropped and the
/// in-flight one's was replayed, so a close silently did nothing.
save_then_close: bool,
/// Bumped every time a reload is issued; a landing that is no longer the
/// newest discards itself. Two watcher batches can put two reads in flight,
/// and without this the older one can land last and install stale text
/// *marked clean* — a buffer that no longer matches disk and never
/// re-checks.
reload_seq: u64,
/// Disk changed under unsaved edits: show the reload/keep banner instead
/// of silently clobbering either side.
pub(crate) conflict: bool,
/// Markdown files can flip the buffer into a rendered preview.
pub(crate) preview: bool,
/// Soft-wrap state (mirrored here — the input's own flag isn't readable).
pub(crate) wrap: bool,
_sub: Subscription,
/// Repaints the app when the input notifies (cursor moves, scrolls…) so
/// the status bar's Ln/Col stays live.
_observe: Subscription,
}
impl OpenFile {
/// Tab label: the file name (the path differentiates in the tooltip).
fn label(&self) -> SharedString {
self.path
.file_name()
@@ -116,19 +47,10 @@ impl OpenFile {
}
}
/// Per-tab code-panel state, hung on [`Tab::code`](crate::ui::app::Tab) with
/// the same lifecycle contract as the diff overlay: only the active tab's
/// panel renders, switching away hides it, closing the tab drops it. The
/// shared caches (directory listings, gitignore matchers, filesystem
/// watchers) live on [`Tty7App`] — this holds only what is truly this tab's:
/// its open files and its tree view state.
pub(crate) struct TabCode {
/// Whether the overlay is currently shown for this tab. The open-file set
/// survives hiding (Esc) — only closing the tab drops it.
pub(crate) visible: bool,
pub(crate) files: Vec<OpenFile>,
pub(crate) active: usize,
/// File-tree roots: this tab's pane cwds resolved to repo roots.
pub(crate) roots: Vec<PathBuf>,
pub(crate) expanded: std::collections::HashSet<PathBuf>,
pub(crate) selected: Option<PathBuf>,
@@ -137,12 +59,6 @@ pub(crate) struct TabCode {
impl TabCode {
pub(crate) fn new() -> Self {
Self {
// Born hidden. This state used to be created only by opening the
// overlay, so defaulting to visible was harmless; now the right
// panel's Files tab creates it just to hold the tree's roots and
// expansion, and a default of `true` popped an empty editor open
// ("No file open") the moment you looked at the tree. Every path
// that actually wants the overlay sets `visible` itself.
visible: false,
files: Vec::new(),
active: 0,
@@ -157,52 +73,19 @@ impl TabCode {
}
}
/// App-global editor infrastructure shared by every tab's panel.
pub(crate) struct EditorPanelState {
/// Watches the parent directories of open files (across all tabs) for
/// external changes.
///
/// One long-lived subscription whose set moves with the open files, rather
/// than a watcher rebuilt per open — remotely, a rebuild is a round trip
/// and a server-side watcher recreated every time a file is opened or
/// closed. `Arc` because `set_dirs` is itself a host call.
watch: Option<Arc<WatchSub>>,
/// The host `watch` was opened against, kept so a subscription is never
/// reused across a different one.
///
/// A `HostId` is not enough to tell them apart: reconnecting removes the
/// dead `RemoteHost` and inserts a fresh one under the *same* id, so the id
/// matches while the `ControlClient` behind the old subscription is gone.
/// Compared by pointer, which distinguishes both that and an outright
/// switch to another machine.
watch_host: Option<SharedHost>,
/// A subscription is being opened; keeps a burst of opens from asking for
/// one each.
watch_opening: bool,
/// A `set_dirs` is in flight, and whether the set moved again while it was.
///
/// `set_dirs` replaces the watched set wholesale, so two of them in flight
/// resolve by arrival order, not issue order — and the loser strands the
/// watcher on a stale set *permanently*, because the caller only re-issues
/// when the desired set changes. Single-flight instead: one out at a time,
/// re-issued from the current set when it lands.
watch_busy: bool,
watch_dirty: bool,
/// The directories the watch spans — every open file's parent.
watched_dirs: HashSet<PathBuf>,
/// The open files themselves. The watch is per-directory, so this is what
/// separates "a file we care about changed" from "something else in that
/// directory did".
watched_files: HashSet<PathBuf>,
/// Feeds changed paths from the watch into the UI-side reload loop spawned
/// in [`EditorPanelState::new`].
events_tx: smol::channel::Sender<Vec<PathBuf>>,
}
impl EditorPanelState {
pub(crate) fn new(window: &mut Window, cx: &mut Context<Tty7App>) -> Self {
// The reload loop lives for the app: it debounces watcher pings and
// routes them to `handle_external_change` on the UI thread.
let (tx, rx) = smol::channel::unbounded::<Vec<PathBuf>>();
cx.spawn_in(window, async move |app, cx| {
while let Ok(first) = rx.recv().await {
@@ -213,15 +96,13 @@ impl EditorPanelState {
}
let ok = app.update_in(cx, |app, window, cx| {
for path in changed {
// The watch is on directories, so most of what arrives
// is about files nobody has open.
if app.editor.watched_files.contains(&path) {
app.editor_handle_external_change(&path, window, cx);
}
}
});
if ok.is_err() {
break; // app dropped; stop the loop
break;
}
}
})
@@ -239,15 +120,7 @@ impl EditorPanelState {
}
}
// ---------------------------------------------------------------------------
// Pure helpers (tested).
// ---------------------------------------------------------------------------
/// The tree-sitter language name for a path, matching the grammars compiled
/// into gpui-component's `tree-sitter-languages` feature. Falls back to
/// `"text"` (plain, no highlighting) for anything unknown.
pub(crate) fn language_for_path(path: &Path) -> &'static str {
// Whole-filename matches first (no useful extension).
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
let lowered = name.to_ascii_lowercase();
match lowered.as_str() {
@@ -255,7 +128,6 @@ pub(crate) fn language_for_path(path: &Path) -> &'static str {
"cmakelists.txt" => return "cmake",
_ => {}
}
// Dotfile shell rc's: .zshrc, .bashrc, .profile…
if lowered.starts_with('.') && (lowered.contains("shrc") || lowered.ends_with("profile")) {
return "bash";
}
@@ -302,36 +174,17 @@ pub(crate) fn language_for_path(path: &Path) -> &'static str {
}
}
/// Quick binary sniff: a NUL byte in the head of the file. Text files never
/// contain NULs; this catches executables/images before `from_utf8` chokes on
/// them with a less helpful error.
fn looks_binary(bytes: &[u8]) -> bool {
bytes.iter().take(8192).any(|b| *b == 0)
}
/// What a watcher event means for one buffer holding the changed file.
#[derive(Debug, PartialEq, Eq)]
enum ExternalChange {
/// Not a change we should act on — our own write, or one we cannot yet
/// distinguish from our own write.
Ignore,
/// Disk moved under unsaved edits: raise the banner and let the user pick.
Conflict,
/// Clean buffer, changed file: take the new content silently.
Reload,
}
/// Decide what a changed file means for one buffer.
///
/// Pulled out of the event handler because it is the whole of the
/// external-change contract and the only part of it worth testing directly:
/// everything around it is GPUI plumbing.
///
/// `saving` is the subtle one. While our own write is in flight `disk_mtime`
/// still names the *previous* content, so the echo test below would call our
/// own save an external change and — on a clean buffer — reload the file out
/// from under the write. The write's landing sets the new mtime; anything
/// genuinely external gets reported again after it.
fn classify_external_change(
saving: bool,
dirty: bool,
@@ -341,9 +194,6 @@ fn classify_external_change(
if saving {
return ExternalChange::Ignore;
}
// Our own save's echo: the mtime matches what we last wrote or loaded.
// `Some` on both sides deliberately — a filesystem with no mtime cannot
// prove an echo, and guessing "echo" there would drop real changes.
if observed.is_some() && observed == disk_mtime {
return ExternalChange::Ignore;
}
@@ -354,26 +204,12 @@ fn classify_external_change(
}
}
/// What a landed write does to the buffer it wrote.
///
/// Separated for the same reason: this is the three-way answer that the ⌘S
/// exemption turns on, and it is pure.
#[derive(Debug, PartialEq, Eq)]
struct SaveLanding {
/// The buffer still holds what reached disk, so it may be marked clean.
clean: bool,
/// Another save was asked for while this one flew; re-issue it.
requeue: bool,
}
/// Settle an in-flight write.
///
/// `wrote_seq` is the buffer's edit counter when the snapshot was taken and
/// `current_seq` is where it is now: unequal means the user kept typing, so the
/// bytes on disk are already stale and the buffer stays dirty.
///
/// A failed write never requeues — a path that cannot be written would
/// otherwise re-issue forever, one notification per round.
fn settle_save(ok: bool, wrote_seq: u64, current_seq: u64, pending: bool) -> SaveLanding {
SaveLanding {
clean: ok && wrote_seq == current_seq,
@@ -381,12 +217,7 @@ fn settle_save(ok: bool, wrote_seq: u64, current_seq: u64, pending: bool) -> Sav
}
}
// ---------------------------------------------------------------------------
// Tty7App: open / save / close / external reload.
// ---------------------------------------------------------------------------
impl Tty7App {
/// The active tab's code-panel state, if the panel was ever opened there.
pub(crate) fn tab_code(&self) -> Option<&TabCode> {
self.tabs.get(self.active)?.code.as_deref()
}
@@ -395,24 +226,15 @@ impl Tty7App {
self.tabs.get_mut(self.active)?.code.as_deref_mut()
}
/// Like [`tab_code_mut`], but creates the state instead of returning `None`.
/// The panel state used to be born with the code overlay, so anything that
/// needed it could assume the overlay had been opened at least once — no
/// longer true now that the right panel's Files tab renders the same tree
/// without ever opening the overlay.
pub(crate) fn tab_code_mut_or_init(&mut self) -> Option<&mut TabCode> {
let tab = self.tabs.get_mut(self.active)?;
Some(tab.code.get_or_insert_with(|| Box::new(TabCode::new())))
}
/// Whether the active tab's code panel is currently shown.
pub(crate) fn code_panel_visible(&self) -> bool {
self.tab_code().is_some_and(|c| c.visible)
}
/// Rebuild the external-change watcher over every tab's open files.
/// Watches each file's *parent directory* (non-recursively): editors that
/// save via rename replace the inode, which a direct file watch loses.
fn editor_rebuild_watcher(&mut self, cx: &mut Context<Self>) {
let files: HashSet<PathBuf> = self
.tabs
@@ -432,27 +254,12 @@ impl Tty7App {
self.editor_watch_apply(cx);
}
/// Push `editor.watched_dirs` at the subscription, opening one first if
/// there isn't one yet.
///
/// Split from [`editor_rebuild_watcher`](Self::editor_rebuild_watcher)
/// because that one returns early when the set hasn't moved — which is
/// right for a caller reacting to an open or a close, and wrong for the
/// landing below, whose whole job is to apply a set that moved while there
/// was nothing to apply it to.
fn editor_watch_apply(&mut self, cx: &mut Context<Self>) {
let want: Vec<PathBuf> = self.editor.watched_dirs.iter().cloned().collect();
let Some(host) = self.active_host(cx) else {
return;
};
// Same rule as the file tree's: a subscription belongs to the host that
// opened it. A reconnect inserts a fresh `RemoteHost` under the same
// `HostId`, so the id matches while the `ControlClient` behind this
// subscription is gone — `set_dirs` then fails, is warned and dropped,
// and nothing opens a new one. The cost here is quieter and worse than
// a stale tree: external-change detection is what stops a save
// clobbering an edit made on the other side.
if !self
.editor
.watch_host
@@ -488,8 +295,6 @@ impl Tty7App {
return;
}
if self.editor.watch_opening {
// The landing re-reads `watched_dirs`, so a set that moved while
// the subscription was opening is applied when it arrives.
return;
}
self.editor.watch_opening = true;
@@ -532,9 +337,6 @@ impl Tty7App {
);
}
/// Open `path` in the active tab's editor (activating an existing file tab
/// when it is already open) and reveal the panel. Errors surface as window
/// notifications rather than a half-open tab.
pub(crate) fn open_file_in_editor(
&mut self,
path: &Path,
@@ -544,15 +346,7 @@ impl Tty7App {
if self.tabs.get(self.active).is_none() {
return;
}
// Opening a file is an act on the editor, so it comes forward — the
// file tree lives in the right panel and stays clickable even while the
// diff overlay covers the column.
self.raise_code_overlay();
// The already-open check runs twice: once here against the path as
// given, so the overwhelmingly common case (a click on a tree row,
// whose path is already canonical) costs nothing, and once more when
// the canonical path comes back, which is the one that is actually
// authoritative.
if self.editor_activate_open(path, window, cx) {
return;
}
@@ -564,14 +358,7 @@ impl Tty7App {
host,
window,
cx,
// The failure arm carries the finished message rather than an
// error value: every one of these is phrased around the path, and
// the path is only settled once `canonicalize` has run out here.
move |h| -> Result<(PathBuf, String, Option<MTime>), String> {
// Canonicalize first — it decides identity, and two paths to
// one file must not become two buffers. A failure keeps the
// path as given, which is the habit this call site has always
// had.
let path = h.canonicalize(&p).unwrap_or(p);
let meta = match h.stat(&path) {
Ok(m) => m,
@@ -602,8 +389,6 @@ impl Tty7App {
);
}
/// Bring an already-open `path` to the front, reporting whether it was
/// open at all.
fn editor_activate_open(
&mut self,
path: &Path,
@@ -617,9 +402,6 @@ impl Tty7App {
return false;
};
code.visible = true;
// Activating always surfaces to the front of the strip: the strip
// is MRU-ordered and only its head fits on screen (see
// `render_editor_tabs`), so the active file must live there.
let f = code.files.remove(ix);
code.files.insert(0, f);
code.active = 0;
@@ -628,7 +410,6 @@ impl Tty7App {
true
}
/// Put a file that finished loading into the active tab.
fn editor_install_file(
&mut self,
path: PathBuf,
@@ -637,9 +418,6 @@ impl Tty7App {
window: &mut Window,
cx: &mut Context<Self>,
) {
// The canonical path is the authoritative identity, and the load took
// long enough that the file may have been opened by another route in
// the meantime.
if self.editor_activate_open(&path, window, cx) {
return;
}
@@ -662,9 +440,6 @@ impl Tty7App {
.soft_wrap(false)
.default_value(text)
});
// Dirty tracking: `set_value` suppresses events, so every Change here
// is a real user edit. Files may be open in any tab, not just the
// active one, so the lookup scans all tabs.
let sub = cx.subscribe_in(&input, window, {
let path = path.clone();
move |this: &mut Tty7App, _input, ev, _window, cx| {
@@ -679,9 +454,6 @@ impl Tty7App {
return;
};
f.dirty = true;
// Every edit moves the buffer away from whatever an
// in-flight save is writing, which is how that save knows
// not to declare the buffer clean when it lands.
f.edit_seq = f.edit_seq.wrapping_add(1);
cx.notify();
}
@@ -693,7 +465,6 @@ impl Tty7App {
.expect("checked at function entry");
let code = tab.code.get_or_insert_with(|| Box::new(TabCode::new()));
let observe = cx.observe(&input, |_, _, cx| cx.notify());
// New files join at the front of the MRU strip (always visible).
code.files.insert(
0,
OpenFile {
@@ -720,17 +491,10 @@ impl Tty7App {
cx.notify();
}
/// `ToggleCodePanel` (⌘⇧E / the title-bar tree icon / Esc): flip the
/// active tab's code overlay. First open creates the tab's panel state;
/// hiding keeps it (open files survive Esc), and only closing the tab
/// drops it. Opening re-roots the file tree from the tab's panes and
/// focuses the panel; closing hands focus back to the terminal.
pub(crate) fn toggle_code_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(tab) = self.tabs.get_mut(self.active) else {
return;
};
// Buried under the diff overlay, this shortcut means "come forward" —
// hiding a panel the user can't see would look like it did nothing.
let buried = tab.overlay_top == crate::ui::app::OverlayTop::Diff
&& tab.diff_overlay.is_some()
&& tab.code.as_ref().is_some_and(|c| c.visible);
@@ -761,23 +525,18 @@ impl Tty7App {
cx.notify();
}
/// Bring the code overlay in front of the diff overlay. See
/// [`Tab::overlay_top`](crate::ui::app::Tab).
fn raise_code_overlay(&mut self) {
if let Some(tab) = self.tabs.get_mut(self.active) {
tab.overlay_top = crate::ui::app::OverlayTop::Code;
}
}
/// Focus the active file's text input (e.g. right after opening a file).
fn focus_editor(&self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(f) = self.tab_code().and_then(|c| c.active_file()) {
f.input.update(cx, |input, cx| input.focus(window, cx));
}
}
/// Whether keyboard focus currently sits inside the editor panel. Lets
/// shared shortcuts (⌘S, ⌘W) route here before their terminal meaning.
pub(crate) fn editor_has_focus(&self, window: &Window, cx: &Context<Self>) -> bool {
self.code_panel_visible()
&& self
@@ -791,7 +550,6 @@ impl Tty7App {
})
}
/// `EditorSave` (⌘S): write the active buffer back to its path.
pub(crate) fn editor_save_active(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(id) = self
.tab_code()
@@ -803,26 +561,6 @@ impl Tty7App {
self.editor_save_file(id, false, window, cx);
}
/// Write one buffer back to its path, optionally closing it once the write
/// lands.
///
/// The write is asynchronous (an explicit exemption): ⌘S no longer
/// blocks the UI thread, so the dirty marker clears a frame later rather
/// than instantly. Three things that costs us, and how each is paid:
///
/// | Case | Handling |
/// |---|---|
/// | The user keeps typing while the write is in flight | The snapshot's `edit_seq` is compared on landing; a buffer that moved stays dirty, because it no longer matches what reached disk |
/// | Two ⌘S in a row | Single-flight. The second sets `save_pending` and is re-issued when the first lands, so the newest content wins and two writes never race for the same file |
/// | The write fails | The buffer stays dirty, `save_pending` is dropped so a failing path can't notify in a loop, and the error is shown |
///
/// The buffer is named by the `EntityId` of its input, which is the only
/// identity that survives the wait. A tab index does not: closing or
/// reordering a *terminal* tab shifts `self.tabs` under an in-flight write,
/// and the landing would then either miss the buffer — stranding `saving`
/// set, which silently disables every later save *and* every external-change
/// check for that file — or find a different buffer of the same path in
/// another tab and settle that one instead.
fn editor_save_file(
&mut self,
id: gpui::EntityId,
@@ -830,22 +568,14 @@ impl Tty7App {
window: &mut Window,
cx: &mut Context<Self>,
) {
// Resolved before the buffer is borrowed: the write cannot go anywhere
// without a machine to write to, and taking it after would hold a
// mutable borrow of `self` across an immutable read of it.
let Some(host) = self.active_host(cx) else {
return;
};
let Some(f) = self.editor_file_mut(id) else {
return;
};
// Sticky, and OR-accumulated: a close asked for while a plain ⌘S is in
// flight must still close when that write lands.
f.save_then_close |= then_close;
if f.saving.is_some() {
// A write is already out for this buffer. Queue rather than race:
// two writes of the same file can land on disk in either order, and
// the loser would leave stale content behind.
f.save_pending = true;
return;
}
@@ -857,9 +587,6 @@ impl Tty7App {
host,
window,
cx,
// One call, one round trip: the write answers with its own
// post-write metadata, so no external edit can land between the
// write and a follow-up `stat` and be mistaken for ours.
move |h| h.write_file(&target, text.as_bytes()).map(|m| m.mtime),
move |app, result: std::io::Result<Option<MTime>>, window, cx| {
let Some(f) = app.editor_file_mut(id) else {
@@ -874,8 +601,6 @@ impl Tty7App {
);
match result {
Ok(mtime) => {
// The mtime of the bytes we just wrote, so the watcher
// echo of our own save is recognised and ignored.
f.disk_mtime = mtime;
}
Err(e) => HostOps::notify_err(window, cx, "Save failed", &e),
@@ -885,8 +610,6 @@ impl Tty7App {
f.conflict = false;
}
if landing.requeue {
// `save_then_close` stays on the buffer, so the queued
// round inherits it rather than the first caller's copy.
app.editor_save_file(id, false, window, cx);
cx.notify();
return;
@@ -903,10 +626,6 @@ impl Tty7App {
cx.notify();
}
/// One open buffer, by the identity of its input entity.
///
/// Scans every tab: a file may be open in more than one, and the entity id
/// is what tells those buffers apart.
fn editor_file_mut(&mut self, id: gpui::EntityId) -> Option<&mut OpenFile> {
self.tabs
.iter_mut()
@@ -915,8 +634,6 @@ impl Tty7App {
.find(|f| f.input.entity_id() == id)
}
/// Where a buffer sits right now, as `(tab index, file index)`. Both move,
/// so this is only ever valid for the duration of one UI-thread turn.
fn editor_file_position(&self, id: gpui::EntityId) -> Option<(usize, usize)> {
self.tabs.iter().enumerate().find_map(|(tab_ix, t)| {
let code = t.code.as_deref()?;
@@ -925,8 +642,6 @@ impl Tty7App {
})
}
/// Close the file tab at `ix`. Dirty buffers get a native three-way prompt
/// (save / discard / cancel) before anything is lost.
pub(crate) fn editor_close_file(
&mut self,
ix: usize,
@@ -948,15 +663,10 @@ impl Tty7App {
&["Save", "Discard", "Cancel"],
cx,
);
// The prompt is awaited, so the buffer is named by its input entity
// rather than by an index that closing another tab would shift.
let id = f.input.entity_id();
cx.spawn_in(window, async move |app, cx| {
let Ok(choice) = answer.await else { return };
let _ = app.update_in(cx, |app, window, cx| match choice {
// Save, then close — the close rides on the write landing (see
// `editor_save_file`), so a failed save keeps the tab open
// without the caller having to re-check anything.
0 => app.editor_save_file(id, true, window, cx),
1 => {
if let Some((tab_ix, ix)) = app.editor_file_position(id) {
@@ -969,8 +679,6 @@ impl Tty7App {
.detach();
}
/// If focus is in the editor, close the active file tab and report `true`
/// (so ⌘W routes here instead of closing the terminal tab).
pub(crate) fn editor_close_active_if_focused(
&mut self,
window: &mut Window,
@@ -996,9 +704,6 @@ impl Tty7App {
self.editor_remove_file_in(self.active, ix, cx);
}
/// [`editor_remove_file`](Self::editor_remove_file) for a named tab — the
/// save-then-close path lands after an await, by which time the active tab
/// may not be the one the file is in.
fn editor_remove_file_in(&mut self, tab_ix: usize, ix: usize, cx: &mut Context<Self>) {
let Some(code) = self
.tabs
@@ -1018,9 +723,6 @@ impl Tty7App {
cx.notify();
}
/// A watched file changed on disk. Clean buffers reload silently; dirty
/// ones raise the conflict banner and let the user pick a side. The file
/// may be open in several tabs — each buffer is handled on its own.
pub(crate) fn editor_handle_external_change(
&mut self,
path: &Path,
@@ -1043,8 +745,6 @@ impl Tty7App {
);
}
/// Decide what a changed file means for each buffer holding it, once the
/// host has answered with its mtime.
fn editor_apply_external_change(
&mut self,
path: &Path,
@@ -1080,9 +780,6 @@ impl Tty7App {
}
}
/// Replace one buffer with the on-disk content (used by the silent reload
/// and the conflict banner's "Reload" choice). A vanished file just keeps
/// the buffer and marks it dirty — saving will recreate it.
pub(crate) fn editor_reload_from_disk(
&mut self,
tab_ix: usize,
@@ -1100,10 +797,6 @@ impl Tty7App {
};
let target = f.path.clone();
let id = f.input.entity_id();
// Only the newest reload may install. Two watcher batches can put two
// reads in flight, and background completion order is unconstrained —
// an older answer landing last would install stale text and mark it
// clean, leaving a buffer that does not match disk and never rechecks.
f.reload_seq = f.reload_seq.wrapping_add(1);
let seq = f.reload_seq;
let Some(host) = self.active_host(cx) else {
@@ -1114,8 +807,6 @@ impl Tty7App {
window,
cx,
move |h| {
// One hop for both, so the mtime belongs to the bytes we read
// rather than to whatever the file became in between.
let bytes = h.read_file(&target, MAX_FILE_BYTES)?;
let text = String::from_utf8(bytes).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "not valid UTF-8")
@@ -1128,11 +819,9 @@ impl Tty7App {
return;
};
if f.reload_seq != seq {
return; // a newer reload supersedes this answer
return;
}
let Ok((text, mtime)) = result else {
// A vanished (or unreadable) file keeps the buffer and
// marks it dirty — saving will recreate it.
f.dirty = true;
f.conflict = false;
cx.notify();
@@ -1141,10 +830,6 @@ impl Tty7App {
f.disk_mtime = mtime;
f.dirty = false;
f.conflict = false;
// The reload replaces the text wholesale, and `set_value`
// suppresses the Change event, so `edit_seq` must move by hand
// — otherwise a save in flight would look like it still
// matched the buffer.
f.edit_seq = f.edit_seq.wrapping_add(1);
let input = f.input.clone();
input.update(cx, |input, cx| input.set_value(text, window, cx));
@@ -1154,14 +839,7 @@ impl Tty7App {
}
}
// ---------------------------------------------------------------------------
// Rendering.
// ---------------------------------------------------------------------------
impl Tty7App {
/// The code panel: a full-body overlay of `[file tree | editor]` covering
/// the terminal (settings/diff-overlay style), or `None` while closed.
/// The terminal underneath keeps its size — toggling never reflows it.
pub(crate) fn render_code_overlay(
&mut self,
window: &mut Window,
@@ -1172,7 +850,6 @@ impl Tty7App {
}
let body = match self.tab_code().and_then(|c| c.active_file()) {
None => self.render_editor_empty(cx).into_any_element(),
// Markdown preview replaces the buffer with a rendered view.
Some(f) if f.preview => {
let markdown = f.input.read(cx).text().to_string();
div()
@@ -1189,8 +866,6 @@ impl Tty7App {
}
Some(f) => {
let input = f.input.clone();
// `appearance(false)`: no border/background of its own — the
// buffer sits flush in the panel instead of in a rounded box.
Input::new(&input)
.appearance(false)
.font_family(cx.theme().mono_font_family.clone())
@@ -1217,41 +892,20 @@ impl Tty7App {
v_flex()
.id("code-panel")
.absolute()
// Fills its column, which is now everything *except* the detail
// panel — the panel is a sibling of that column, not a child of it,
// so the tree that opens files stays visible beside the editor
// without the overlay needing to know the panel's width.
.inset_0()
// The overlay must swallow input to the terminal behind it.
.occlude()
.bg(cx.theme().background)
// Escape (not consumed by the editor's own search/completion
// handling, which stops propagation) drops back to the terminal.
.on_key_down(cx.listener(|this, ev: &gpui::KeyDownEvent, window, cx| {
if ev.keystroke.key == "escape" {
this.toggle_code_panel(window, cx);
}
}))
// No top inset: the header row below *is* the title bar's row, and
// it clears the window controls itself (see `render_editor_header`).
// Padding the whole overlay down would cost a blank 40px band and
// still misalign the editor's top edge with the panel's tab row.
// No tree column here: the right panel owns the file tree now, and
// the overlay stops short of it (see the `right` inset above), so
// the tree stays visible beside the editor instead of being
// duplicated inside it.
.child(h_flex().flex_1().min_h_0().w_full().child(editor_col))
.child(self.render_code_status_bar(window, cx))
.into_any_element(),
)
}
/// The editor's one header row: which file is open, and a way back to the
/// terminal. Not a tab strip — the file tree is the switcher now, so this only
/// has to answer "what am I looking at" without earning a row of chrome for
/// every buffer that was ever opened. Sits on the title bar's line and matches
/// its height, so the editor's top edge lines up with the panel's tab row and
/// the rail's controls across the window.
fn render_editor_header(
&self,
window: &mut Window,
@@ -1260,20 +914,11 @@ impl Tty7App {
let active = self.tab_code().and_then(|c| c.active_file());
let name = active.map(|f| f.label());
let dirty = active.is_some_and(|f| f.dirty);
// The overlay fills the column left of the detail panel. With the rail out
// that column starts after it, and the traffic lights sit on the rail's
// surface — but with the rail collapsed (or in horizontal-tabs mode) the
// column starts at the window's left edge and the lights are right where
// the filename would go, so the header takes the window controls' reserve
// as its inset instead.
let lead = if self.left_panel_open(cx) {
crate::ui::app::CONTENT_INSET
} else {
crate::ui::app::TITLE_BAR_LEAD
};
// The overlay covers the real title bar, so this row inherits its drag and
// zoom gestures — otherwise opening a file turns the top of the window into
// a strip that looks like the caption and can't move it.
crate::ui::app::title_bar_drag(h_flex().id("editor-header"), "editor-header", window, cx)
.flex_none()
.h(px(crate::ui::app::TITLE_BAR_HEIGHT))
@@ -1294,7 +939,6 @@ impl Tty7App {
})
.child(name.unwrap_or_else(|| SharedString::from("No file open"))),
)
// Same amber dot the tree marks unsaved files with.
.when(dirty, |d| {
d.child(
div()
@@ -1305,15 +949,8 @@ impl Tty7App {
)
})
.child(
// `occlude()` for the same reason the title bar's own tiles carry
// it: this row is a `WindowControlArea::Drag`, which on Windows is
// HTCAPTION, and the OS takes the press before gpui hit-tests.
div().occlude().flex_shrink_0().child(
crate::ui::tab_strip::chrome_tile_sized(
// This header is the title bar's own height and sits flush
// with it, so its one control is a full chrome tile — not the
// half-size one it used to be, which read as a different
// class of button on the same line.
Button::new("editor-panel-close").icon(Icon::new(IconName::Close)),
crate::ui::app::TILE_SIZE,
crate::ui::app::TILE_GLYPH_LINE,
@@ -1329,12 +966,9 @@ impl Tty7App {
)
}
/// The Zed-style status bar along the panel bottom: repo-relative path on
/// the left; preview/wrap toggles and the cursor position on the right.
fn render_code_status_bar(&self, _window: &Window, cx: &mut Context<Self>) -> gpui::Div {
let code = self.tab_code();
let muted = cx.theme().muted_foreground;
// `repo relative/path` for the active file; just the repo otherwise.
let path_text: Option<SharedString> = code.map(|c| {
let repo = c
.roots
@@ -1420,7 +1054,6 @@ impl Tty7App {
.when_some(cursor, |this, t| this.child(div().child(t)))
}
/// Empty state: the panel is open with nothing loaded.
fn render_editor_empty(&self, cx: &Context<Self>) -> gpui::Div {
v_flex()
.size_full()
@@ -1440,7 +1073,6 @@ impl Tty7App {
)
}
/// Banner shown when the file changed on disk while the buffer is dirty.
fn render_editor_conflict_banner(&self, cx: &mut Context<Self>) -> AnyElement {
let tab_ix = self.active;
let ix = self.tab_code().map(|c| c.active).unwrap_or(0);
@@ -1514,58 +1146,43 @@ mod tests {
Some(MTime { secs, nanos })
}
/// M2 regression guard: the save → external-change →
/// reload states still decide correctly now that the write is asynchronous.
#[test]
fn external_changes_are_told_apart_from_our_own_saves() {
let ours = t(100, 0);
// The echo of our own save: same mtime, nothing to do.
assert_eq!(
classify_external_change(false, false, ours, ours),
ExternalChange::Ignore
);
// A real external edit to a clean buffer reloads silently.
assert_eq!(
classify_external_change(false, false, ours, t(101, 0)),
ExternalChange::Reload
);
// The same edit under unsaved work raises the banner instead of
// clobbering either side.
assert_eq!(
classify_external_change(false, true, ours, t(101, 0)),
ExternalChange::Conflict
);
// Nanosecond precision is the point of `MTime`: an external write in
// the same second as ours must not read as an echo.
assert_eq!(
classify_external_change(false, false, t(100, 0), t(100, 1)),
ExternalChange::Reload
);
// While our own write is in flight, `disk_mtime` still names the old
// content — acting on it would reload the file out from under the save.
assert_eq!(
classify_external_change(true, false, ours, t(101, 0)),
ExternalChange::Ignore
);
// A filesystem with no mtime cannot prove an echo, so a change there is
// treated as real rather than silently dropped.
assert_eq!(
classify_external_change(false, false, None, None),
ExternalChange::Reload
);
}
/// M2 regression guard (the ⌘S exemption): the three things
/// asynchronous saving has to get right.
#[test]
fn a_landed_save_only_cleans_a_buffer_that_did_not_move() {
// Nothing happened during the write: the buffer is clean.
assert_eq!(
settle_save(true, 7, 7, false),
SaveLanding {
@@ -1574,8 +1191,6 @@ mod tests {
}
);
// The user kept typing: what reached disk is already stale, so the
// buffer stays dirty and the amber dot stays up.
assert_eq!(
settle_save(true, 7, 9, false),
SaveLanding {
@@ -1584,7 +1199,6 @@ mod tests {
}
);
// A second ⌘S arrived mid-write: re-issue it so the newest content wins.
assert_eq!(
settle_save(true, 7, 9, true),
SaveLanding {
@@ -1593,8 +1207,6 @@ mod tests {
}
);
// A failed write never cleans and never re-issues — requeueing a path
// that cannot be written is an infinite notification loop.
assert_eq!(
settle_save(false, 7, 7, true),
SaveLanding {