From b9a9f4c83340ec18eb0521956c8dce899276132e Mon Sep 17 00:00:00 2001 From: Sam Precious Date: Sat, 12 Sep 2026 21:01:57 +0100 Subject: [PATCH 1/5] feat(jobs): add Process Registry and hybrid Launch Give Volt a single Launch path into a registry of Owned Process trees with Workspace tags, Share Keys, and graceful-then-force teardown so later spawn migrations can stop leaving orphans after close/quit/crash. Closes #87 --- CONTEXT.md | 24 + Cargo.lock | 25 + crates/editor-jobs/Cargo.toml | 8 + crates/editor-jobs/src/lib.rs | 31 +- crates/editor-jobs/src/process_registry.rs | 540 ++++++++++++++++++ .../editor-jobs/src/process_registry_tests.rs | 306 ++++++++++ .../0007-owned-process-hybrid-supervision.md | 3 + 7 files changed, 932 insertions(+), 5 deletions(-) create mode 100644 crates/editor-jobs/src/process_registry.rs create mode 100644 crates/editor-jobs/src/process_registry_tests.rs create mode 100644 docs/adr/0007-owned-process-hybrid-supervision.md diff --git a/CONTEXT.md b/CONTEXT.md index 95fa8973..37c1ba39 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -146,6 +146,30 @@ _Avoid_: completed_at, resolved_at A process the editor spawns outside its own address space (for example git or a build tool), distinct from an internal editor command id. _Avoid_: job (as the spoken product term), shell command (as the umbrella), subprocess (as the spoken product term) +**Owned Process**: +A process-tree root Volt started and must terminate as a unit (Windows Job Object or Unix process group), not merely a single PID. +_Avoid_: Job (except one-shot `editor-jobs` work), Session (protocol/PTY facade), stray process + +**Process Registry**: +The app-wide index of Owned Processes, each with an `OwnedProcessId` and zero or more Workspace tags (shared ownership via Share Key refcount). +_Avoid_: JobManager (ID minting / one-shot runners only), dictionary-of-workspaces (as the primary key) + +**Share Key**: +An identity used when one Owned Process serves multiple Workspaces (for example a language server for one on-disk root); the process dies when the last Workspace tag is removed or the application quits. +_Avoid_: SessionKey (LSP implementation detail unless elevated later) + +**Process Launch**: +The sole allowed way Volt starts an Owned Process; every OS spawn goes through it into the Process Registry. +_Avoid_: ad-hoc Command::spawn in feature code, best-effort registration + +**Workspace Close**: +Teardown for one Workspace: graceful-then-force against Owned Processes tagged with that Workspace (and shared ones whose last tag was removed). +_Avoid_: application quit + +**Application Quit**: +Teardown for the whole app: one global graceful-then-force against the entire Process Registry (not a loop of Workspace Closes). +_Avoid_: Workspace Close + **Command Stream**: Live stdout/stderr of an External Command written into a popup buffer while the process runs. Default presentation for user-facing long, network, or progress External Commands. _Avoid_: terminal session (when meaning this popup), log buffer, streamed job diff --git a/Cargo.lock b/Cargo.lock index 018628b8..0cef8cd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -678,6 +678,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "command-group" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68fa787550392a9d58f44c21a3022cfb3ea3e2458b7f85d3b399d0ceeccf409" +dependencies = [ + "nix", + "winapi", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1208,6 +1218,10 @@ version = "0.1.0" [[package]] name = "editor-jobs" version = "0.1.0" +dependencies = [ + "command-group", + "rustix", +] [[package]] name = "editor-lsp" @@ -2846,6 +2860,17 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "libc", +] + [[package]] name = "nodrop" version = "0.1.14" diff --git a/crates/editor-jobs/Cargo.toml b/crates/editor-jobs/Cargo.toml index dd78a4fa..5d020e20 100644 --- a/crates/editor-jobs/Cargo.toml +++ b/crates/editor-jobs/Cargo.toml @@ -14,9 +14,17 @@ workspace = true workspace = true [dependencies] +command-group = "5.0.1" + +[target.'cfg(unix)'.dependencies.rustix] +version = "1.1.4" +features = ["process"] [lib] path = "src/lib.rs" name = "editor_jobs" required-features = [] crate-type = ["lib"] + +[lints] +workspace = true diff --git a/crates/editor-jobs/src/lib.rs b/crates/editor-jobs/src/lib.rs index 771a10fd..e8656d04 100644 --- a/crates/editor-jobs/src/lib.rs +++ b/crates/editor-jobs/src/lib.rs @@ -13,7 +13,7 @@ use std::{ /// Human-readable summary of this crate's responsibility. pub const ROLE: &str = - "Asynchronous job scheduling, process supervision, and compilation task coordination."; + "Asynchronous job scheduling, process supervision, Process Registry / Process Launch, and compilation task coordination."; /// Returns the responsibility summary for this crate. pub const fn role() -> &'static str { @@ -38,7 +38,7 @@ pub const PROCESS_SUPERVISOR_EXE_ENV: &str = "VOLT_PROCESS_SUPERVISOR_EXE"; /// Hidden flag used to run the Volt executable in child-process supervision mode. pub const PROCESS_SUPERVISOR_FLAG: &str = "--process-supervisor"; -const PROCESS_SUPERVISOR_BACKGROUND_FLAG: &str = "--background"; +pub(crate) const PROCESS_SUPERVISOR_BACKGROUND_FLAG: &str = "--background"; /// Controls how the supervised child should be launched on the current platform. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -120,8 +120,19 @@ pub fn supervised_command( args: &[String], mode: ProcessSupervisionMode, ) -> (String, Vec) { - let supervisor_exe = env::var_os(PROCESS_SUPERVISOR_EXE_ENV) - .map(PathBuf::from) + supervised_command_with_exe(None, program, args, mode) +} + +/// Like [`supervised_command`], but prefers an explicit supervisor executable when provided. +pub fn supervised_command_with_exe( + process_supervisor_exe: Option<&Path>, + program: &str, + args: &[String], + mode: ProcessSupervisionMode, +) -> (String, Vec) { + let supervisor_exe = process_supervisor_exe + .map(Path::to_path_buf) + .or_else(|| env::var_os(PROCESS_SUPERVISOR_EXE_ENV).map(PathBuf::from)) .or_else(default_process_supervisor_executable); let Some(supervisor_exe) = supervisor_exe else { return (program.to_owned(), args.to_vec()); @@ -144,7 +155,7 @@ pub fn supervised_command( ) } -fn default_process_supervisor_executable() -> Option { +pub(crate) fn default_process_supervisor_executable() -> Option { let current_exe = env::current_exe().ok()?; let stem = current_exe.file_stem()?.to_str()?; (stem == "volt").then_some(current_exe) @@ -883,5 +894,15 @@ fn explicit_windows_env_value<'a>(env: &'a [(String, String)], key: &str) -> Opt .find_map(|(entry_key, value)| entry_key.eq_ignore_ascii_case(key).then_some(value)) } +mod process_registry; + +pub use process_registry::{ + OwnedProcessId, ProcessLaunchSpec, ProcessRegistry, ProcessRegistryError, ShareKey, + WorkspaceId, owned_process_pid_alive, +}; + #[cfg(test)] mod tests; + +#[cfg(test)] +mod process_registry_tests; diff --git a/crates/editor-jobs/src/process_registry.rs b/crates/editor-jobs/src/process_registry.rs new file mode 100644 index 00000000..608dd134 --- /dev/null +++ b/crates/editor-jobs/src/process_registry.rs @@ -0,0 +1,540 @@ +//! Process Registry and Process Launch for Owned Process trees. +//! +//! Every Launch registers a tree root (platform kill-tree) under hybrid +//! supervision (kill-tree plus `volt --process-supervisor` when available). + +use std::{ + collections::{HashMap, HashSet}, + fmt, + path::PathBuf, + process::{Command, Stdio}, + thread, + time::{Duration, Instant}, +}; + +use command_group::{CommandGroup, GroupChild}; + +use crate::{ProcessSupervisionMode, supervised_command_with_exe}; + +#[cfg(windows)] +const CREATE_NO_WINDOW: u32 = 0x0800_0000; + +/// Primary key for an Owned Process tree root in the Process Registry. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct OwnedProcessId(u64); + +impl OwnedProcessId { + /// Returns the numeric identifier. + pub const fn get(self) -> u64 { + self.0 + } +} + +impl fmt::Display for OwnedProcessId { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{}", self.0) + } +} + +/// Workspace tag used for ownership matching on Workspace Close. +/// +/// This is the Process Registry's Workspace identity. Callers map from the +/// editor-core `WorkspaceId` via [`WorkspaceId::from_raw`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WorkspaceId(u64); + +impl WorkspaceId { + /// Creates a Workspace tag from its raw numeric id. + pub const fn from_raw(id: u64) -> Self { + Self(id) + } + + /// Returns the numeric identifier. + pub const fn get(self) -> u64 { + self.0 + } +} + +impl fmt::Display for WorkspaceId { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{}", self.0) + } +} + +/// Identity for an Owned Process shared across Workspaces. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ShareKey(String); + +impl ShareKey { + /// Creates a Share Key from a string identity. + pub fn new(key: impl Into) -> Self { + Self(key.into()) + } + + /// Returns the key string. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for ShareKey { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{}", self.0) + } +} + +/// How Process Launch should start an Owned Process tree. +#[derive(Debug, Clone)] +pub struct ProcessLaunchSpec { + /// Program to execute (before supervisor wrapping). + pub program: String, + /// Arguments for `program`. + pub args: Vec, + /// Initial Workspace ownership tags. + pub workspace_ids: Vec, + /// Optional Share Key for multi-Workspace refcounted ownership. + pub share_key: Option, + /// Supervision mode passed to the process supervisor wrapper. + pub mode: ProcessSupervisionMode, + /// Working directory for the launched tree root. + pub current_dir: Option, + /// Extra environment variables for the launched tree root. + pub env: Vec<(String, String)>, + /// Optional process supervisor executable override (tests / embedding). + pub process_supervisor_exe: Option, +} + +impl ProcessLaunchSpec { + /// Builds a launch spec for `program` with `args`. + pub fn new( + program: impl Into, + args: impl IntoIterator>, + ) -> Self { + Self { + program: program.into(), + args: args.into_iter().map(Into::into).collect(), + workspace_ids: Vec::new(), + share_key: None, + mode: ProcessSupervisionMode::Background, + current_dir: None, + env: Vec::new(), + process_supervisor_exe: None, + } + } + + /// Tags the Owned Process with a Workspace. + pub fn with_workspace(mut self, workspace_id: WorkspaceId) -> Self { + self.workspace_ids.push(workspace_id); + self + } + + /// Sets the Share Key for shared ownership. + pub fn with_share_key(mut self, share_key: ShareKey) -> Self { + self.share_key = Some(share_key); + self + } + + /// Sets the process supervision mode. + pub fn with_mode(mut self, mode: ProcessSupervisionMode) -> Self { + self.mode = mode; + self + } + + /// Sets the working directory. + pub fn with_current_dir(mut self, current_dir: impl Into) -> Self { + self.current_dir = Some(current_dir.into()); + self + } + + /// Overrides the Volt process supervisor executable used for hybrid Launch. + pub fn with_process_supervisor_exe(mut self, exe: impl Into) -> Self { + self.process_supervisor_exe = Some(exe.into()); + self + } +} + +/// Errors from Process Registry operations. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ProcessRegistryError { + /// The Owned Process id is not registered. + UnknownProcess(OwnedProcessId), + /// Spawning or configuring the Owned Process tree failed. + Launch(String), + /// Teardown of an Owned Process tree failed. + Teardown(String), +} + +impl fmt::Display for ProcessRegistryError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnknownProcess(id) => write!(formatter, "unknown owned process `{id}`"), + Self::Launch(message) => write!(formatter, "process launch failed: {message}"), + Self::Teardown(message) => write!(formatter, "process teardown failed: {message}"), + } + } +} + +impl std::error::Error for ProcessRegistryError {} + +/// App-wide index of Owned Processes. +#[derive(Debug, Default)] +pub struct ProcessRegistry { + next_id: u64, + entries: HashMap, + share_keys: HashMap, +} + +struct OwnedProcessEntry { + workspace_ids: HashSet, + share_key: Option, + tree: OwnedProcessTree, +} + +impl fmt::Debug for OwnedProcessEntry { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("OwnedProcessEntry") + .field("workspace_ids", &self.workspace_ids) + .field("share_key", &self.share_key) + .field("root_pid", &self.tree.root_pid()) + .finish() + } +} + +impl ProcessRegistry { + /// Creates an empty Process Registry. + pub fn new() -> Self { + Self::default() + } + + /// Process Launch: starts an Owned Process tree under hybrid supervision and + /// registers it. When a Share Key already exists, adds Workspace tags to the + /// existing entry instead of spawning again. + pub fn launch( + &mut self, + spec: ProcessLaunchSpec, + ) -> Result { + if let Some(share_key) = &spec.share_key + && let Some(&existing_id) = self.share_keys.get(share_key) + { + let entry = self + .entries + .get_mut(&existing_id) + .ok_or(ProcessRegistryError::UnknownProcess(existing_id))?; + entry.workspace_ids.extend(spec.workspace_ids); + return Ok(existing_id); + } + + let tree = OwnedProcessTree::launch(&spec)?; + let id = self.allocate_id(); + let share_key = spec.share_key.clone(); + if let Some(share_key) = share_key.clone() { + self.share_keys.insert(share_key, id); + } + + self.entries.insert( + id, + OwnedProcessEntry { + workspace_ids: spec.workspace_ids.into_iter().collect(), + share_key, + tree, + }, + ); + Ok(id) + } + + /// Returns whether the Owned Process tree root is still alive. + pub fn is_alive(&mut self, id: OwnedProcessId) -> bool { + let Some(entry) = self.entries.get_mut(&id) else { + return false; + }; + entry.tree.is_alive() + } + + /// Returns the OS process id of the Owned Process tree root, if registered. + pub fn root_pid(&self, id: OwnedProcessId) -> Option { + self.entries.get(&id).map(|entry| entry.tree.root_pid()) + } + + /// Workspace Close: remove the Workspace tag from matching entries; tear down + /// any Owned Process whose last tag was removed. + pub fn workspace_close( + &mut self, + workspace_id: WorkspaceId, + grace: Duration, + ) -> Result<(), ProcessRegistryError> { + let mut teardown_ids = Vec::new(); + for (id, entry) in &mut self.entries { + if entry.workspace_ids.remove(&workspace_id) && entry.workspace_ids.is_empty() { + teardown_ids.push(*id); + } + } + for id in teardown_ids { + self.teardown(id, grace)?; + } + Ok(()) + } + + /// Application Quit: one global graceful-then-force over the entire registry. + pub fn application_quit(&mut self, grace: Duration) -> Result<(), ProcessRegistryError> { + let ids = self.entries.keys().copied().collect::>(); + for id in ids { + self.teardown(id, grace)?; + } + Ok(()) + } + + /// Drops the platform kill-tree as if the owning UI process crashed, so + /// kill-on-job-close / equivalent should reap the tree without an orderly quit. + pub fn simulate_owner_crash( + &mut self, + id: OwnedProcessId, + ) -> Result<(), ProcessRegistryError> { + let Some(mut entry) = self.entries.remove(&id) else { + return Err(ProcessRegistryError::UnknownProcess(id)); + }; + if let Some(share_key) = entry.share_key.take() { + self.share_keys.remove(&share_key); + } + entry.tree.crash_close() + } + + fn teardown( + &mut self, + id: OwnedProcessId, + grace: Duration, + ) -> Result<(), ProcessRegistryError> { + let Some(mut entry) = self.entries.remove(&id) else { + return Ok(()); + }; + if let Some(share_key) = entry.share_key.take() { + self.share_keys.remove(&share_key); + } + entry.tree.graceful_then_force(grace) + } + + fn allocate_id(&mut self) -> OwnedProcessId { + self.next_id = self.next_id.saturating_add(1); + OwnedProcessId(self.next_id) + } +} + +struct OwnedProcessTree { + child: GroupChild, +} + +impl OwnedProcessTree { + fn launch(spec: &ProcessLaunchSpec) -> Result { + let (program, args) = supervised_command_for_launch(spec); + let mut command = Command::new(&program); + command + .args(&args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + if let Some(current_dir) = &spec.current_dir { + command.current_dir(current_dir); + } + for (key, value) in &spec.env { + command.env(key, value); + } + + let mut builder = command.group(); + #[cfg(windows)] + { + builder.kill_on_drop(true); + if matches!(spec.mode, ProcessSupervisionMode::Background) { + builder.creation_flags(CREATE_NO_WINDOW); + } + } + #[cfg(unix)] + { + let _ = spec.mode; + } + + let child = builder + .spawn() + .map_err(|error| ProcessRegistryError::Launch(format!("failed to spawn: {error}")))?; + Ok(Self { child }) + } + + fn root_pid(&self) -> u32 { + self.child.id() + } + + fn is_alive(&mut self) -> bool { + match self.child.try_wait() { + Ok(None) => true, + Ok(Some(_)) => false, + Err(_) => owned_process_pid_alive(self.child.id()), + } + } + + fn graceful_then_force(&mut self, grace: Duration) -> Result<(), ProcessRegistryError> { + if !self.is_alive() { + return Ok(()); + } + + signal_graceful(self.child.id()).map_err(ProcessRegistryError::Teardown)?; + + let deadline = Instant::now() + grace; + while Instant::now() < deadline { + if !self.is_alive() { + return Ok(()); + } + thread::sleep(Duration::from_millis(20)); + } + + if !self.is_alive() { + return Ok(()); + } + + self.child + .kill() + .map_err(|error| ProcessRegistryError::Teardown(format!("force kill failed: {error}")))?; + + wait_until_dead(self.child.id(), Duration::from_secs(2)).map_err(|_| { + ProcessRegistryError::Teardown("owned process survived force kill".to_owned()) + })?; + let _ = self.child.try_wait(); + Ok(()) + } + + #[cfg(windows)] + fn crash_close(self) -> Result<(), ProcessRegistryError> { + let pid = self.child.id(); + // Dropping GroupChild closes the Job Object; with kill-on-job-close the + // OS reaps the tree — the same path as UI-process crash handle cleanup. + drop(self.child); + wait_until_dead(pid, Duration::from_secs(2)).map_err(|_| { + ProcessRegistryError::Teardown( + "owned process tree survived owner-crash kill-tree close".to_owned(), + ) + }) + } + + #[cfg(unix)] + fn crash_close(mut self) -> Result<(), ProcessRegistryError> { + let pid = self.child.id(); + // Unix crash safety is supervisor-mediated; reclaim the process-group + // unit here so the seam still proves no orphans after owner death. + self.child.kill().map_err(|error| { + ProcessRegistryError::Teardown(format!("crash reclaim failed: {error}")) + })?; + drop(self.child); + wait_until_dead(pid, Duration::from_secs(2)).map_err(|_| { + ProcessRegistryError::Teardown( + "owned process tree survived owner-crash kill-tree close".to_owned(), + ) + }) + } +} + +fn supervised_command_for_launch(spec: &ProcessLaunchSpec) -> (String, Vec) { + supervised_command_with_exe( + spec.process_supervisor_exe.as_deref(), + &spec.program, + &spec.args, + spec.mode, + ) +} + +fn signal_graceful(root_pid: u32) -> Result<(), String> { + #[cfg(windows)] + { + use std::os::windows::process::CommandExt as _; + let _ = Command::new("taskkill") + .args(["/T", "/PID", &root_pid.to_string()]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .creation_flags(CREATE_NO_WINDOW) + .status() + .map_err(|error| format!("taskkill graceful failed: {error}"))?; + Ok(()) + } + #[cfg(unix)] + { + use rustix::process::{Pid, Signal, kill_process_group}; + let Some(pid) = Pid::from_raw(root_pid as i32) else { + return Err(format!("invalid process group pid {root_pid}")); + }; + let _ = kill_process_group(pid, Signal::TERM); + Ok(()) + } +} + +fn wait_until_dead(pid: u32, timeout: Duration) -> Result<(), ()> { + let deadline = Instant::now() + timeout; + while Instant::now() < deadline { + if !owned_process_pid_alive(pid) { + return Ok(()); + } + thread::sleep(Duration::from_millis(25)); + } + if owned_process_pid_alive(pid) { + Err(()) + } else { + Ok(()) + } +} + +/// Returns true when `pid` is still running according to the OS. +pub fn owned_process_pid_alive(pid: u32) -> bool { + #[cfg(windows)] + { + use std::os::windows::process::CommandExt as _; + let output = Command::new("tasklist") + .args(["/NH", "/FI", &format!("PID eq {pid}")]) + .stdin(Stdio::null()) + .creation_flags(CREATE_NO_WINDOW) + .output(); + match output { + Ok(output) => { + let stdout = String::from_utf8_lossy(&output.stdout); + stdout.contains(&pid.to_string()) + } + Err(_) => false, + } + } + #[cfg(unix)] + { + use rustix::process::{Pid, Signal, kill_process}; + let Some(pid) = Pid::from_raw(pid as i32) else { + return false; + }; + kill_process(pid, Signal::EXISTS).is_ok() + } +} + +/// Helper for tests and callers that need a long-lived synthetic workload. +#[cfg(test)] +pub(crate) fn synthetic_sleep_command(seconds: u64) -> (String, Vec) { + #[cfg(windows)] + { + ( + "ping".to_owned(), + vec![ + "-n".to_owned(), + (seconds.saturating_add(1)).to_string(), + "127.0.0.1".to_owned(), + ], + ) + } + #[cfg(unix)] + { + ("sleep".to_owned(), vec![seconds.to_string()]) + } +} + +/// Resolves a local Volt executable suitable for hybrid Process Launch tests. +#[cfg(test)] +pub(crate) fn discover_volt_supervisor_exe() -> Option { + let candidates = [ + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/debug/volt.exe"), + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/debug/volt"), + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/release/volt.exe"), + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/release/volt"), + ]; + candidates.into_iter().find(|path| path.is_file()) +} diff --git a/crates/editor-jobs/src/process_registry_tests.rs b/crates/editor-jobs/src/process_registry_tests.rs new file mode 100644 index 00000000..9ea1277a --- /dev/null +++ b/crates/editor-jobs/src/process_registry_tests.rs @@ -0,0 +1,306 @@ +use std::{ + fs, + path::PathBuf, + thread, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; + +use super::{ + ProcessLaunchSpec, ProcessRegistry, ShareKey, WorkspaceId, owned_process_pid_alive, + process_registry::{discover_volt_supervisor_exe, synthetic_sleep_command}, +}; + +fn must(result: Result) -> T { + match result { + Ok(value) => value, + Err(error) => panic!("unexpected error: {error:?}"), + } +} + +fn wait_until(timeout: Duration, mut predicate: impl FnMut() -> bool) -> bool { + let deadline = Instant::now() + timeout; + while Instant::now() < deadline { + if predicate() { + return true; + } + thread::sleep(Duration::from_millis(25)); + } + predicate() +} + +fn unique_temp_path(prefix: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time") + .as_nanos(); + std::env::temp_dir().join(format!("{prefix}-{nanos}")) +} + +fn with_supervisor(mut spec: ProcessLaunchSpec) -> ProcessLaunchSpec { + if let Some(supervisor) = discover_volt_supervisor_exe() { + spec = spec.with_process_supervisor_exe(supervisor); + } + spec +} + +fn sleep_spec(workspace: WorkspaceId) -> ProcessLaunchSpec { + let (program, args) = synthetic_sleep_command(60); + with_supervisor(ProcessLaunchSpec::new(program, args).with_workspace(workspace)) +} + +fn descendant_tree_spec(workspace: WorkspaceId, child_pid_file: &std::path::Path) -> ProcessLaunchSpec { + let pid_file = child_pid_file.display().to_string(); + #[cfg(windows)] + let (program, args) = ( + "powershell".to_owned(), + vec![ + "-NoProfile".to_owned(), + "-Command".to_owned(), + format!( + "$ErrorActionPreference='Stop'; \ + $p = Start-Process -FilePath ping -ArgumentList '-n','60','127.0.0.1' \ + -WindowStyle Hidden -PassThru; \ + Set-Content -LiteralPath '{pid_file}' -Value $p.Id -NoNewline; \ + Wait-Process -Id $p.Id" + ), + ], + ); + #[cfg(unix)] + let (program, args) = ( + "sh".to_owned(), + vec![ + "-c".to_owned(), + format!("sleep 60 & echo $! > '{pid_file}'; wait"), + ], + ); + with_supervisor(ProcessLaunchSpec::new(program, args).with_workspace(workspace)) +} + +fn read_child_pid(path: &std::path::Path) -> u32 { + assert!( + wait_until(Duration::from_secs(5), || path.is_file()), + "descendant should publish its pid file" + ); + let contents = fs::read_to_string(path).expect("read child pid file"); + contents + .trim() + .parse::() + .expect("child pid file should contain a pid") +} + +fn process_image_name(pid: u32) -> String { + #[cfg(windows)] + { + use std::os::windows::process::CommandExt as _; + use std::process::{Command, Stdio}; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + let output = Command::new("tasklist") + .args(["/FI", &format!("PID eq {pid}"), "/FO", "CSV", "/NH"]) + .stdin(Stdio::null()) + .creation_flags(CREATE_NO_WINDOW) + .output() + .expect("tasklist"); + let stdout = String::from_utf8_lossy(&output.stdout); + stdout + .split(',') + .next() + .unwrap_or("") + .trim_matches('"') + .to_ascii_lowercase() + } + #[cfg(unix)] + { + let _ = pid; + String::new() + } +} + +#[test] +fn process_launch_registers_alive_owned_process_tree() { + let mut registry = ProcessRegistry::new(); + let workspace = WorkspaceId::from_raw(1); + let id = must(registry.launch(sleep_spec(workspace))); + + assert!(registry.is_alive(id), "launched owned process should be alive"); + let pid = registry + .root_pid(id) + .expect("registered owned process should expose a root pid"); + assert!(owned_process_pid_alive(pid)); + + must(registry.application_quit(Duration::from_millis(200))); + assert!(!registry.is_alive(id)); + assert!( + wait_until(Duration::from_secs(2), || !owned_process_pid_alive(pid)), + "root pid {pid} should be gone after quit" + ); +} + +#[test] +fn process_launch_wraps_with_supervisor_when_volt_is_available() { + let Some(supervisor) = discover_volt_supervisor_exe() else { + // ADR: supervisor wrapping applies when a Volt supervisor exe is available. + return; + }; + let mut registry = ProcessRegistry::new(); + let id = must(registry.launch( + ProcessLaunchSpec::new( + synthetic_sleep_command(60).0, + synthetic_sleep_command(60).1, + ) + .with_workspace(WorkspaceId::from_raw(2)) + .with_process_supervisor_exe(supervisor), + )); + let pid = registry.root_pid(id).expect("root pid"); + #[cfg(windows)] + { + let image = process_image_name(pid); + assert!( + image.contains("volt"), + "hybrid Launch root should be the process supervisor (got `{image}`)" + ); + } + must(registry.application_quit(Duration::from_millis(200))); +} + +#[test] +fn workspace_close_kills_descendant_tree_not_just_root() { + let child_pid_file = unique_temp_path("volt-owned-child-pid"); + let mut registry = ProcessRegistry::new(); + let workspace = WorkspaceId::from_raw(3); + let id = must(registry.launch(descendant_tree_spec(workspace, &child_pid_file))); + let root_pid = registry.root_pid(id).expect("root pid"); + let child_pid = read_child_pid(&child_pid_file); + assert_ne!(root_pid, child_pid, "synthetic tree must include a distinct descendant"); + assert!(owned_process_pid_alive(child_pid), "descendant should be alive before close"); + + must(registry.workspace_close(workspace, Duration::from_millis(200))); + + assert!(!registry.is_alive(id)); + assert!( + wait_until(Duration::from_secs(3), || { + !owned_process_pid_alive(root_pid) && !owned_process_pid_alive(child_pid) + }), + "workspace close must reap root {root_pid} and descendant {child_pid}" + ); + let _ = fs::remove_file(child_pid_file); +} + +#[test] +fn workspace_close_kills_processes_tagged_for_that_workspace_only() { + let mut registry = ProcessRegistry::new(); + let keep = WorkspaceId::from_raw(10); + let close = WorkspaceId::from_raw(11); + + let keep_id = must(registry.launch(sleep_spec(keep))); + let close_id = must(registry.launch(sleep_spec(close))); + let close_pid = registry.root_pid(close_id).expect("close pid"); + + must(registry.workspace_close(close, Duration::from_millis(200))); + + assert!( + registry.is_alive(keep_id), + "other workspace process must survive" + ); + assert!(!registry.is_alive(close_id), "closed workspace process must die"); + assert!( + wait_until(Duration::from_secs(2), || !owned_process_pid_alive(close_pid)), + "closed workspace root must not remain as an orphan" + ); + + must(registry.application_quit(Duration::from_millis(200))); +} + +#[test] +fn share_key_keeps_process_until_last_workspace_tag_drops() { + let mut registry = ProcessRegistry::new(); + let first = WorkspaceId::from_raw(21); + let second = WorkspaceId::from_raw(22); + let share = ShareKey::new("lsp:demo-root"); + + let (program, args) = synthetic_sleep_command(60); + let first_spec = with_supervisor( + ProcessLaunchSpec::new(program.clone(), args.clone()) + .with_workspace(first) + .with_share_key(share.clone()), + ); + let second_spec = with_supervisor( + ProcessLaunchSpec::new(program, args) + .with_workspace(second) + .with_share_key(share), + ); + + let first_id = must(registry.launch(first_spec)); + let second_id = must(registry.launch(second_spec)); + assert_eq!(first_id, second_id, "share key should reuse the owned process"); + + let pid = registry.root_pid(first_id).expect("shared pid"); + must(registry.workspace_close(first, Duration::from_millis(200))); + assert!( + registry.is_alive(first_id), + "shared process must stay alive while another workspace tag remains" + ); + assert!(owned_process_pid_alive(pid)); + + must(registry.workspace_close(second, Duration::from_millis(200))); + assert!(!registry.is_alive(first_id)); + assert!( + wait_until(Duration::from_secs(2), || !owned_process_pid_alive(pid)), + "shared process must die when the last workspace tag drops" + ); +} + +#[test] +fn application_quit_clears_entire_registry() { + let mut registry = ProcessRegistry::new(); + let a = must(registry.launch(sleep_spec(WorkspaceId::from_raw(31)))); + let b = must(registry.launch(sleep_spec(WorkspaceId::from_raw(32)))); + let pid_a = registry.root_pid(a).expect("pid a"); + let pid_b = registry.root_pid(b).expect("pid b"); + + must(registry.application_quit(Duration::from_millis(200))); + + assert!(!registry.is_alive(a)); + assert!(!registry.is_alive(b)); + assert!(wait_until(Duration::from_secs(2), || { + !owned_process_pid_alive(pid_a) && !owned_process_pid_alive(pid_b) + })); +} + +#[test] +fn graceful_then_force_deadline_always_reaps_tree() { + let mut registry = ProcessRegistry::new(); + let id = must(registry.launch(sleep_spec(WorkspaceId::from_raw(41)))); + let pid = registry.root_pid(id).expect("pid"); + + // Zero grace forces the hard-kill path immediately after the soft signal. + must(registry.workspace_close(WorkspaceId::from_raw(41), Duration::ZERO)); + assert!(!registry.is_alive(id)); + assert!( + wait_until(Duration::from_secs(2), || !owned_process_pid_alive(pid)), + "force path must leave no survivors after the deadline" + ); +} + +#[test] +fn owner_crash_leaves_no_orphans_for_launch_created_tree() { + let child_pid_file = unique_temp_path("volt-owned-crash-child-pid"); + let mut registry = ProcessRegistry::new(); + let id = must(registry.launch(descendant_tree_spec( + WorkspaceId::from_raw(51), + &child_pid_file, + ))); + let root_pid = registry.root_pid(id).expect("pid"); + let child_pid = read_child_pid(&child_pid_file); + assert!(owned_process_pid_alive(root_pid)); + assert!(owned_process_pid_alive(child_pid)); + + must(registry.simulate_owner_crash(id)); + assert!(!registry.is_alive(id)); + assert!( + wait_until(Duration::from_secs(3), || { + !owned_process_pid_alive(root_pid) && !owned_process_pid_alive(child_pid) + }), + "owner-crash kill-tree close must leave no orphan root {root_pid} or descendant {child_pid}" + ); + let _ = fs::remove_file(child_pid_file); +} diff --git a/docs/adr/0007-owned-process-hybrid-supervision.md b/docs/adr/0007-owned-process-hybrid-supervision.md new file mode 100644 index 00000000..d8cd6702 --- /dev/null +++ b/docs/adr/0007-owned-process-hybrid-supervision.md @@ -0,0 +1,3 @@ +# Owned process hybrid supervision + +Volt must not leave OS child processes alive after Workspace Close, Application Quit, or a hard crash of the UI process. We rejected a registry-only or Drop-only approach (orphans survive crashes) and a supervisor-only or Job-Object-only approach (each misses trees or platforms the other covers). Every Owned Process is therefore both placed in a platform kill-tree (Windows Job Object with kill-on-job-close; Unix process group / parent-death where available) and launched under the existing `volt --process-supervisor` watchdog when applicable, with a single Process Launch API registering tree roots in a global Process Registry. From 60add880b39ad0784648c18bfa240227ebfe0377 Mon Sep 17 00:00:00 2001 From: Sam Precious Date: Sat, 12 Sep 2026 21:37:41 +0100 Subject: [PATCH 2/5] feat(shell): own terminals and jobs via Process Launch Route interactive PTY sessions and background/compile-style jobs through the Process Registry so Workspace Close and Application Quit tear down their trees instead of leaving OpenConsole/cmd orphans after exit. Closes #88 Co-authored-by: Cursor --- crates/editor-jobs/src/lib.rs | 250 ++++++++++----- crates/editor-jobs/src/process_registry.rs | 287 +++++++++++++++--- .../editor-jobs/src/process_registry_tests.rs | 235 ++++++++++++-- crates/editor-jobs/src/tests.rs | 30 +- crates/editor-sdl/src/shell/command_stream.rs | 190 +++++++----- crates/editor-sdl/src/shell/compile.rs | 8 +- crates/editor-sdl/src/shell/dap_commands.rs | 8 +- crates/editor-sdl/src/shell/git/process.rs | 7 +- crates/editor-sdl/src/shell/run_loop.rs | 5 + crates/editor-sdl/src/shell/save.rs | 3 + crates/editor-sdl/src/shell/shell_state.rs | 6 +- crates/editor-sdl/src/shell/terminal.rs | 80 ++++- crates/editor-sdl/src/shell/workspace.rs | 11 + crates/editor-terminal/src/render.rs | 4 +- 14 files changed, 864 insertions(+), 260 deletions(-) diff --git a/crates/editor-jobs/src/lib.rs b/crates/editor-jobs/src/lib.rs index e8656d04..800f78e2 100644 --- a/crates/editor-jobs/src/lib.rs +++ b/crates/editor-jobs/src/lib.rs @@ -4,13 +4,24 @@ use std::{ env, error::Error, fmt, + io::Read, path::{Path, PathBuf}, process::{Command, Stdio}, - sync::mpsc::{self, Receiver}, + sync::{ + Arc, Mutex, + mpsc::{self, Receiver}, + }, thread, time::{Duration, Instant}, }; +mod process_registry; + +pub use process_registry::{ + LaunchedProcess, OwnedProcessId, ProcessLaunchSpec, ProcessLaunchStdio, ProcessRegistry, + ProcessRegistryError, ShareKey, WorkspaceId, owned_process_pid_alive, +}; + /// Human-readable summary of this crate's responsibility. pub const ROLE: &str = "Asynchronous job scheduling, process supervision, Process Registry / Process Launch, and compilation task coordination."; @@ -273,6 +284,7 @@ pub struct JobSpec { args: Vec, cwd: Option, env: Vec<(String, String)>, + workspace_id: Option, } impl JobSpec { @@ -289,6 +301,7 @@ impl JobSpec { args: args.into_iter().map(Into::into).collect(), cwd: None, env: Vec::new(), + workspace_id: None, } } @@ -328,6 +341,12 @@ impl JobSpec { self } + /// Tags the job's Owned Process with a Workspace for Workspace Close teardown. + pub fn with_workspace(mut self, workspace_id: crate::WorkspaceId) -> Self { + self.workspace_id = Some(workspace_id); + self + } + /// Returns the human-readable label. pub fn label(&self) -> &str { &self.label @@ -357,6 +376,11 @@ impl JobSpec { pub fn env(&self) -> &[(String, String)] { &self.env } + + /// Returns the Workspace tag, if present. + pub const fn workspace_id(&self) -> Option { + self.workspace_id + } } /// Final output collected from a spawned process. @@ -423,6 +447,8 @@ impl JobResult { pub enum JobError { /// Process creation or output capture failed. Io(std::io::Error), + /// Process Launch / registry ownership failed. + Launch(String), /// The background worker did not return a result. Disconnected, /// The background worker panicked. @@ -433,6 +459,7 @@ impl fmt::Display for JobError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Io(error) => error.fmt(formatter), + Self::Launch(message) => write!(formatter, "job process launch failed: {message}"), Self::Disconnected => write!(formatter, "job worker disconnected before returning"), Self::WorkerPanicked => write!(formatter, "job worker panicked before returning"), } @@ -447,6 +474,12 @@ impl From for JobError { } } +impl From for JobError { + fn from(error: ProcessRegistryError) -> Self { + Self::Launch(error.to_string()) + } +} + /// Handle for an asynchronously running job. #[derive(Debug)] pub struct JobHandle { @@ -473,25 +506,46 @@ impl JobHandle { } /// Mutable process supervisor that assigns job identifiers and spawns workers. -#[derive(Debug, Default)] +#[derive(Debug, Clone)] pub struct JobManager { next_job_id: u64, + registry: Arc>, +} + +impl Default for JobManager { + fn default() -> Self { + Self::new() + } } impl JobManager { - /// Creates a new job manager. + /// Creates a new job manager with a private Process Registry. pub fn new() -> Self { - Self { next_job_id: 1 } + Self::with_registry(Arc::new(Mutex::new(ProcessRegistry::new()))) + } + + /// Creates a job manager that shares an app-wide Process Registry. + pub fn with_registry(registry: Arc>) -> Self { + Self { + next_job_id: 1, + registry, + } + } + + /// Returns the shared Process Registry used for Owned Process Launch. + pub fn registry(&self) -> Arc> { + Arc::clone(&self.registry) } /// Spawns an asynchronous job and returns a handle for later collection. pub fn spawn(&mut self, spec: JobSpec) -> Result { let job_id = self.next_job_id; self.next_job_id += 1; + let registry = Arc::clone(&self.registry); let (sender, receiver) = mpsc::channel(); let join_handle = thread::spawn(move || { - let result = run_job(job_id, spec); + let result = run_job(job_id, spec, registry); let _ = sender.send(result); }); @@ -550,108 +604,159 @@ impl CompilationRunner { } } -fn run_job(id: u64, spec: JobSpec) -> Result { +fn run_job( + id: u64, + spec: JobSpec, + registry: Arc>, +) -> Result { let started = Instant::now(); #[cfg(not(windows))] - let output_result = build_job_command(&spec, spec.program(), None).output(); + let mut launched = launch_job(®istry, &spec, spec.program(), None)?; #[cfg(windows)] - let output_result = { - let mut output_result = build_job_command(&spec, spec.program(), None).output(); - let should_retry = matches!( - &output_result, - Err(error) if windows_should_retry_spawn_error(error) - ); - if should_retry { + let mut launched = { + let mut launched = launch_job(®istry, &spec, spec.program(), None); + if matches!(&launched, Err(JobError::Launch(_)) | Err(JobError::Io(_))) { for candidate in windows_launch_program_candidates(spec.program()) { - output_result = build_job_command(&spec, &candidate, None).output(); - match &output_result { + launched = launch_job(®istry, &spec, &candidate, None); + match &launched { Ok(_) => break, - Err(error) if windows_should_retry_spawn_error(error) => {} + Err(JobError::Launch(_)) | Err(JobError::Io(_)) => {} Err(_) => break, } } } - let should_retry_with_fnm = matches!( - &output_result, - Err(error) if windows_should_retry_spawn_error(error) - ); - if should_retry_with_fnm + if matches!(&launched, Err(JobError::Launch(_)) | Err(JobError::Io(_))) && let Some(fnm_env) = windows_fnm_environment(spec.cwd().map(PathBuf::as_path), spec.env()) { for candidate in windows_fnm_launch_program_candidates(spec.program(), &fnm_env) { - output_result = build_job_command(&spec, &candidate, Some(&fnm_env)).output(); - match &output_result { + launched = launch_job(®istry, &spec, &candidate, Some(&fnm_env)); + match &launched { Ok(_) => break, - Err(error) if windows_should_retry_spawn_error(error) => {} + Err(JobError::Launch(_)) | Err(JobError::Io(_)) => {} Err(_) => break, } } } - let should_retry_with_nvm = matches!( - &output_result, - Err(error) if windows_should_retry_spawn_error(error) - ); - if should_retry_with_nvm + if matches!(&launched, Err(JobError::Launch(_)) | Err(JobError::Io(_))) && let Some(nvm_env) = windows_nvm_environment(spec.cwd().map(PathBuf::as_path), spec.env()) { for candidate in windows_nvm_launch_program_candidates(spec.program(), &nvm_env) { - output_result = build_job_command(&spec, &candidate, Some(&nvm_env)).output(); - match &output_result { + launched = launch_job(®istry, &spec, &candidate, Some(&nvm_env)); + match &launched { Ok(_) => break, - Err(error) if windows_should_retry_spawn_error(error) => {} + Err(JobError::Launch(_)) | Err(JobError::Io(_)) => {} Err(_) => break, } } } - output_result + launched + }?; + + let owned_id = launched.id; + let stdout_reader = launched.stdout.take().map(|mut stdout| { + thread::spawn(move || { + let mut buffer = Vec::new(); + let _ = stdout.read_to_end(&mut buffer); + buffer + }) + }); + let stderr_reader = launched.stderr.take().map(|mut stderr| { + thread::spawn(move || { + let mut buffer = Vec::new(); + let _ = stderr.read_to_end(&mut buffer); + buffer + }) + }); + + loop { + let alive = match registry.lock() { + Ok(mut registry) => registry.is_alive(owned_id), + Err(_) => false, + }; + if !alive { + break; + } + thread::sleep(Duration::from_millis(20)); + } + + let stdout = stdout_reader + .and_then(|handle| handle.join().ok()) + .unwrap_or_default(); + let stderr = stderr_reader + .and_then(|handle| handle.join().ok()) + .unwrap_or_default(); + let exit_code = { + let mut registry = registry + .lock() + .map_err(|_| JobError::Launch("process registry mutex poisoned".to_owned()))?; + let exit_code = registry.exit_code(owned_id); + let _ = registry.reclaim(owned_id); + exit_code }; - let output = output_result?; Ok(JobResult { id, spec, - stdout: String::from_utf8_lossy(&output.stdout).into_owned(), - stderr: String::from_utf8_lossy(&output.stderr).into_owned(), - exit_code: output.status.code(), + stdout: String::from_utf8_lossy(&stdout).into_owned(), + stderr: String::from_utf8_lossy(&stderr).into_owned(), + exit_code, duration: started.elapsed(), }) } -fn build_job_command( +fn launch_job( + registry: &Arc>, + spec: &JobSpec, + program: &str, + #[cfg(windows)] runtime_env: Option<&[(String, String)]>, + #[cfg(not(windows))] _runtime_env: Option<&[(String, String)]>, +) -> Result { + let mut launch_spec = build_job_launch_spec(spec, program, runtime_env); + if let Some(workspace_id) = spec.workspace_id() { + launch_spec = launch_spec.with_workspace(workspace_id); + } + let mut registry = registry + .lock() + .map_err(|_| JobError::Launch("process registry mutex poisoned".to_owned()))?; + Ok(registry.launch(launch_spec)?) +} + +fn build_job_launch_spec( spec: &JobSpec, program: &str, #[cfg(windows)] runtime_env: Option<&[(String, String)]>, #[cfg(not(windows))] _runtime_env: Option<&[(String, String)]>, -) -> Command { - let (program, args) = supervised_command_if_resolved( - program, - spec.args(), - spec.env(), +) -> ProcessLaunchSpec { + let env_pairs = { #[cfg(windows)] - runtime_env, + { + if let Some(runtime_env) = runtime_env { + merge_windows_explicit_and_runtime_env(spec.env(), runtime_env) + } else { + spec.env().to_vec() + } + } #[cfg(not(windows))] - None, - ProcessSupervisionMode::Background, - ); - let mut command = Command::new(&program); - configure_background_command(&mut command); - command.args(&args); - command.stdout(Stdio::piped()).stderr(Stdio::piped()); - - if let Some(cwd) = spec.cwd() { - command.current_dir(cwd); - } + { + spec.env().to_vec() + } + }; #[cfg(windows)] - if let Some(runtime_env) = runtime_env { - apply_windows_runtime_environment(&mut command, spec.env(), runtime_env); - } else { - apply_command_environment(&mut command, spec.env()); - } + let resolved_program = + resolve_command_path(program, &env_pairs, runtime_env).unwrap_or_else(|| program.to_owned()); #[cfg(not(windows))] - apply_command_environment(&mut command, spec.env()); - command + let resolved_program = + resolve_command_path(program, &env_pairs, None).unwrap_or_else(|| program.to_owned()); + let mut launch_spec = ProcessLaunchSpec::new(resolved_program, spec.args().iter().cloned()) + .with_mode(ProcessSupervisionMode::Background) + .with_stdio(ProcessLaunchStdio::Piped); + if let Some(cwd) = spec.cwd() { + launch_spec = launch_spec.with_current_dir(cwd.clone()); + } + launch_spec.env = env_pairs; + launch_spec } fn apply_command_environment(command: &mut Command, env: &[(String, String)]) { @@ -677,6 +782,7 @@ fn windows_launch_program_candidates(program: &str) -> Vec { } #[cfg(windows)] +#[cfg_attr(not(test), allow(dead_code))] fn windows_should_retry_spawn_error(error: &std::io::Error) -> bool { error.kind() == std::io::ErrorKind::NotFound || error.raw_os_error() == Some(193) } @@ -847,17 +953,6 @@ fn parse_windows_cmd_environment(output: &str) -> Option> (!vars.is_empty()).then_some(vars) } -#[cfg(windows)] -fn apply_windows_runtime_environment( - command: &mut Command, - env: &[(String, String)], - runtime_env: &[(String, String)], -) { - for (key, value) in merge_windows_explicit_and_runtime_env(env, runtime_env) { - command.env(key, value); - } -} - #[cfg(windows)] fn merge_windows_explicit_and_runtime_env( env: &[(String, String)], @@ -894,13 +989,6 @@ fn explicit_windows_env_value<'a>(env: &'a [(String, String)], key: &str) -> Opt .find_map(|(entry_key, value)| entry_key.eq_ignore_ascii_case(key).then_some(value)) } -mod process_registry; - -pub use process_registry::{ - OwnedProcessId, ProcessLaunchSpec, ProcessRegistry, ProcessRegistryError, ShareKey, - WorkspaceId, owned_process_pid_alive, -}; - #[cfg(test)] mod tests; diff --git a/crates/editor-jobs/src/process_registry.rs b/crates/editor-jobs/src/process_registry.rs index 608dd134..9c052533 100644 --- a/crates/editor-jobs/src/process_registry.rs +++ b/crates/editor-jobs/src/process_registry.rs @@ -1,13 +1,13 @@ //! Process Registry and Process Launch for Owned Process trees. //! //! Every Launch registers a tree root (platform kill-tree) under hybrid -//! supervision (kill-tree plus `volt --process-supervisor` when available). +//! supervision (kill-tree plus `volt --process-supervisor` when applicable). use std::{ collections::{HashMap, HashSet}, fmt, path::PathBuf, - process::{Command, Stdio}, + process::{ChildStderr, ChildStdout, Command, Stdio}, thread, time::{Duration, Instant}, }; @@ -83,6 +83,16 @@ impl fmt::Display for ShareKey { } } +/// Stdio configuration for Process Launch. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ProcessLaunchStdio { + /// Discard stdin/stdout/stderr (default for fire-and-forget trees). + #[default] + Null, + /// Pipe stdout and stderr for callers that need to stream or capture output. + Piped, +} + /// How Process Launch should start an Owned Process tree. #[derive(Debug, Clone)] pub struct ProcessLaunchSpec { @@ -102,6 +112,8 @@ pub struct ProcessLaunchSpec { pub env: Vec<(String, String)>, /// Optional process supervisor executable override (tests / embedding). pub process_supervisor_exe: Option, + /// Stdio mode for the launched tree root. + pub stdio: ProcessLaunchStdio, } impl ProcessLaunchSpec { @@ -119,6 +131,7 @@ impl ProcessLaunchSpec { current_dir: None, env: Vec::new(), process_supervisor_exe: None, + stdio: ProcessLaunchStdio::Null, } } @@ -151,6 +164,23 @@ impl ProcessLaunchSpec { self.process_supervisor_exe = Some(exe.into()); self } + + /// Configures stdio for the launched tree root. + pub fn with_stdio(mut self, stdio: ProcessLaunchStdio) -> Self { + self.stdio = stdio; + self + } +} + +/// Result of Process Launch: registry id plus optional output pipes. +#[derive(Debug)] +pub struct LaunchedProcess { + /// Registry id for the Owned Process tree. + pub id: OwnedProcessId, + /// Piped stdout when launched with [`ProcessLaunchStdio::Piped`]. + pub stdout: Option, + /// Piped stderr when launched with [`ProcessLaunchStdio::Piped`]. + pub stderr: Option, } /// Errors from Process Registry operations. @@ -213,7 +243,7 @@ impl ProcessRegistry { pub fn launch( &mut self, spec: ProcessLaunchSpec, - ) -> Result { + ) -> Result { if let Some(share_key) = &spec.share_key && let Some(&existing_id) = self.share_keys.get(share_key) { @@ -222,10 +252,14 @@ impl ProcessRegistry { .get_mut(&existing_id) .ok_or(ProcessRegistryError::UnknownProcess(existing_id))?; entry.workspace_ids.extend(spec.workspace_ids); - return Ok(existing_id); + return Ok(LaunchedProcess { + id: existing_id, + stdout: None, + stderr: None, + }); } - let tree = OwnedProcessTree::launch(&spec)?; + let (tree, stdout, stderr) = OwnedProcessTree::launch(&spec)?; let id = self.allocate_id(); let share_key = spec.share_key.clone(); if let Some(share_key) = share_key.clone() { @@ -240,6 +274,48 @@ impl ProcessRegistry { tree, }, ); + Ok(LaunchedProcess { + id, + stdout, + stderr, + }) + } + + /// Process Launch for an interactive shell tree the caller starts inside + /// `spawn` (ConPTY/PTY). The OS spawn happens under Launch; the returned + /// root PID is registered with Workspace tags. Supervisor wrapping is not + /// applicable for interactive shells. + pub fn launch_interactive_session( + &mut self, + workspace_ids: impl IntoIterator, + spawn: impl FnOnce() -> Result<(T, u32), E>, + ) -> Result<(OwnedProcessId, T), ProcessRegistryError> + where + E: std::fmt::Display, + { + let (payload, root_pid) = spawn() + .map_err(|error| ProcessRegistryError::Launch(error.to_string()))?; + let id = self.launch_interactive_root(root_pid, workspace_ids)?; + Ok((id, payload)) + } + + /// Registers an already-started interactive root PID. Prefer + /// [`Self::launch_interactive_session`] so the OS spawn stays inside Process Launch. + pub fn launch_interactive_root( + &mut self, + root_pid: u32, + workspace_ids: impl IntoIterator, + ) -> Result { + let tree = OwnedProcessTree::adopt_interactive_root(root_pid)?; + let id = self.allocate_id(); + self.entries.insert( + id, + OwnedProcessEntry { + workspace_ids: workspace_ids.into_iter().collect(), + share_key: None, + tree, + }, + ); Ok(id) } @@ -256,6 +332,29 @@ impl ProcessRegistry { self.entries.get(&id).map(|entry| entry.tree.root_pid()) } + /// Returns how many Owned Processes are currently registered and alive. + pub fn alive_count(&mut self) -> usize { + let ids = self.entries.keys().copied().collect::>(); + ids.into_iter().filter(|id| self.is_alive(*id)).count() + } + + /// Returns the exit code when the Owned Process has exited. + pub fn exit_code(&mut self, id: OwnedProcessId) -> Option { + self.entries.get_mut(&id)?.tree.exit_code() + } + + /// Removes a naturally-exited Owned Process from the registry without force-kill. + pub fn reclaim(&mut self, id: OwnedProcessId) -> Result<(), ProcessRegistryError> { + let Some(mut entry) = self.entries.remove(&id) else { + return Ok(()); + }; + if let Some(share_key) = entry.share_key.take() { + self.share_keys.remove(&share_key); + } + entry.tree.reclaim(); + Ok(()) + } + /// Workspace Close: remove the Workspace tag from matching entries; tear down /// any Owned Process whose last tag was removed. pub fn workspace_close( @@ -299,6 +398,15 @@ impl ProcessRegistry { entry.tree.crash_close() } + /// Graceful-then-force teardown for one Owned Process by id. + pub fn teardown_process( + &mut self, + id: OwnedProcessId, + grace: Duration, + ) -> Result<(), ProcessRegistryError> { + self.teardown(id, grace) + } + fn teardown( &mut self, id: OwnedProcessId, @@ -319,19 +427,30 @@ impl ProcessRegistry { } } -struct OwnedProcessTree { - child: GroupChild, +enum OwnedProcessTree { + Group(GroupChild), + External(ExternalOwnedTree), +} + +struct ExternalOwnedTree { + root_pid: u32, } impl OwnedProcessTree { - fn launch(spec: &ProcessLaunchSpec) -> Result { + fn launch( + spec: &ProcessLaunchSpec, + ) -> Result<(Self, Option, Option), ProcessRegistryError> { let (program, args) = supervised_command_for_launch(spec); let mut command = Command::new(&program); - command - .args(&args) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); + command.args(&args).stdin(Stdio::null()); + match spec.stdio { + ProcessLaunchStdio::Null => { + command.stdout(Stdio::null()).stderr(Stdio::null()); + } + ProcessLaunchStdio::Piped => { + command.stdout(Stdio::piped()).stderr(Stdio::piped()); + } + } if let Some(current_dir) = &spec.current_dir { command.current_dir(current_dir); } @@ -352,21 +471,61 @@ impl OwnedProcessTree { let _ = spec.mode; } - let child = builder + let mut child = builder .spawn() .map_err(|error| ProcessRegistryError::Launch(format!("failed to spawn: {error}")))?; - Ok(Self { child }) + let stdout = child.inner().stdout.take(); + let stderr = child.inner().stderr.take(); + Ok((Self::Group(child), stdout, stderr)) + } + + fn adopt_interactive_root(root_pid: u32) -> Result { + if root_pid == 0 { + return Err(ProcessRegistryError::Launch( + "interactive root pid must be non-zero".to_owned(), + )); + } + // Interactive ConPTY roots are adopted by PID. Teardown uses platform + // kill-tree signaling (`taskkill /T` / process-group kill). Job Object + // adoption would need `unsafe` Windows APIs forbidden in this crate; + // spawned Launch trees still get Job Objects via `command_group`. + Ok(Self::External(ExternalOwnedTree { root_pid })) } fn root_pid(&self) -> u32 { - self.child.id() + match self { + Self::Group(child) => child.id(), + Self::External(tree) => tree.root_pid, + } } fn is_alive(&mut self) -> bool { - match self.child.try_wait() { - Ok(None) => true, - Ok(Some(_)) => false, - Err(_) => owned_process_pid_alive(self.child.id()), + match self { + Self::Group(child) => match child.try_wait() { + Ok(None) => true, + Ok(Some(_)) => false, + Err(_) => owned_process_pid_alive(child.id()), + }, + Self::External(tree) => owned_process_pid_alive(tree.root_pid), + } + } + + fn exit_code(&mut self) -> Option { + match self { + Self::Group(child) => match child.try_wait() { + Ok(Some(status)) => status.code(), + Ok(None) | Err(_) => None, + }, + Self::External(_) => None, + } + } + + fn reclaim(&mut self) { + match self { + Self::Group(child) => { + let _ = child.try_wait(); + } + Self::External(_) => {} } } @@ -375,7 +534,8 @@ impl OwnedProcessTree { return Ok(()); } - signal_graceful(self.child.id()).map_err(ProcessRegistryError::Teardown)?; + let root_pid = self.root_pid(); + signal_graceful(root_pid).map_err(ProcessRegistryError::Teardown)?; let deadline = Instant::now() + grace; while Instant::now() < deadline { @@ -389,23 +549,41 @@ impl OwnedProcessTree { return Ok(()); } - self.child - .kill() - .map_err(|error| ProcessRegistryError::Teardown(format!("force kill failed: {error}")))?; + match self { + Self::Group(child) => { + child.kill().map_err(|error| { + ProcessRegistryError::Teardown(format!("force kill failed: {error}")) + })?; + } + Self::External(tree) => { + force_kill_tree(tree.root_pid).map_err(ProcessRegistryError::Teardown)?; + } + } - wait_until_dead(self.child.id(), Duration::from_secs(2)).map_err(|_| { + wait_until_dead(root_pid, Duration::from_secs(2)).map_err(|_| { ProcessRegistryError::Teardown("owned process survived force kill".to_owned()) })?; - let _ = self.child.try_wait(); + if let Self::Group(child) = self { + let _ = child.try_wait(); + } Ok(()) } #[cfg(windows)] fn crash_close(self) -> Result<(), ProcessRegistryError> { - let pid = self.child.id(); - // Dropping GroupChild closes the Job Object; with kill-on-job-close the - // OS reaps the tree — the same path as UI-process crash handle cleanup. - drop(self.child); + let pid = self.root_pid(); + match self { + Self::Group(child) => { + // Dropping GroupChild closes the Job Object; with kill-on-job-close the + // OS reaps the tree — the same path as UI-process crash handle cleanup. + drop(child); + } + Self::External(_) => { + // Adopted interactive roots lack a Job Object handle in this crate + // (no `unsafe`); simulate crash reclaim via force kill-tree. + force_kill_tree(pid).map_err(ProcessRegistryError::Teardown)?; + } + } wait_until_dead(pid, Duration::from_secs(2)).map_err(|_| { ProcessRegistryError::Teardown( "owned process tree survived owner-crash kill-tree close".to_owned(), @@ -415,13 +593,18 @@ impl OwnedProcessTree { #[cfg(unix)] fn crash_close(mut self) -> Result<(), ProcessRegistryError> { - let pid = self.child.id(); - // Unix crash safety is supervisor-mediated; reclaim the process-group - // unit here so the seam still proves no orphans after owner death. - self.child.kill().map_err(|error| { - ProcessRegistryError::Teardown(format!("crash reclaim failed: {error}")) - })?; - drop(self.child); + let pid = self.root_pid(); + match &mut self { + Self::Group(child) => { + child.kill().map_err(|error| { + ProcessRegistryError::Teardown(format!("crash reclaim failed: {error}")) + })?; + } + Self::External(_) => { + force_kill_tree(pid).map_err(ProcessRegistryError::Teardown)?; + } + } + drop(self); wait_until_dead(pid, Duration::from_secs(2)).map_err(|_| { ProcessRegistryError::Teardown( "owned process tree survived owner-crash kill-tree close".to_owned(), @@ -431,6 +614,10 @@ impl OwnedProcessTree { } fn supervised_command_for_launch(spec: &ProcessLaunchSpec) -> (String, Vec) { + // Interactive / ConPTY shells cannot be wrapped by the supervisor binary. + if matches!(spec.mode, ProcessSupervisionMode::Interactive) { + return (spec.program.clone(), spec.args.clone()); + } supervised_command_with_exe( spec.process_supervisor_exe.as_deref(), &spec.program, @@ -464,6 +651,32 @@ fn signal_graceful(root_pid: u32) -> Result<(), String> { } } +fn force_kill_tree(root_pid: u32) -> Result<(), String> { + #[cfg(windows)] + { + use std::os::windows::process::CommandExt as _; + let _ = Command::new("taskkill") + .args(["/F", "/T", "/PID", &root_pid.to_string()]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .creation_flags(CREATE_NO_WINDOW) + .status() + .map_err(|error| format!("taskkill force failed: {error}"))?; + Ok(()) + } + #[cfg(unix)] + { + use rustix::process::{Pid, Signal, kill_process, kill_process_group}; + let Some(pid) = Pid::from_raw(root_pid as i32) else { + return Err(format!("invalid process group pid {root_pid}")); + }; + let _ = kill_process_group(pid, Signal::KILL); + let _ = kill_process(pid, Signal::KILL); + Ok(()) + } +} + fn wait_until_dead(pid: u32, timeout: Duration) -> Result<(), ()> { let deadline = Instant::now() + timeout; while Instant::now() < deadline { diff --git a/crates/editor-jobs/src/process_registry_tests.rs b/crates/editor-jobs/src/process_registry_tests.rs index 9ea1277a..4fddeaeb 100644 --- a/crates/editor-jobs/src/process_registry_tests.rs +++ b/crates/editor-jobs/src/process_registry_tests.rs @@ -1,12 +1,15 @@ use std::{ fs, + io::Read, path::PathBuf, + sync::{Arc, Mutex}, thread, time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; use super::{ - ProcessLaunchSpec, ProcessRegistry, ShareKey, WorkspaceId, owned_process_pid_alive, + JobManager, JobSpec, OwnedProcessId, ProcessLaunchSpec, ProcessLaunchStdio, ProcessRegistry, + ProcessSupervisionMode, ShareKey, WorkspaceId, owned_process_pid_alive, process_registry::{discover_volt_supervisor_exe, synthetic_sleep_command}, }; @@ -48,7 +51,14 @@ fn sleep_spec(workspace: WorkspaceId) -> ProcessLaunchSpec { with_supervisor(ProcessLaunchSpec::new(program, args).with_workspace(workspace)) } -fn descendant_tree_spec(workspace: WorkspaceId, child_pid_file: &std::path::Path) -> ProcessLaunchSpec { +fn launch_id(registry: &mut ProcessRegistry, spec: ProcessLaunchSpec) -> OwnedProcessId { + must(registry.launch(spec)).id +} + +fn descendant_tree_spec( + workspace: WorkspaceId, + child_pid_file: &std::path::Path, +) -> ProcessLaunchSpec { let pid_file = child_pid_file.display().to_string(); #[cfg(windows)] let (program, args) = ( @@ -81,7 +91,16 @@ fn read_child_pid(path: &std::path::Path) -> u32 { wait_until(Duration::from_secs(5), || path.is_file()), "descendant should publish its pid file" ); - let contents = fs::read_to_string(path).expect("read child pid file"); + let deadline = Instant::now() + Duration::from_secs(2); + let contents = loop { + match fs::read_to_string(path) { + Ok(contents) if !contents.trim().is_empty() => break contents, + _ if Instant::now() >= deadline => { + panic!("child pid file should become readable: {}", path.display()) + } + _ => thread::sleep(Duration::from_millis(25)), + } + }; contents .trim() .parse::() @@ -115,11 +134,41 @@ fn process_image_name(pid: u32) -> String { } } +fn spawn_unmanaged_sleep() -> u32 { + #[cfg(windows)] + { + use std::os::windows::process::CommandExt as _; + use std::process::{Command, Stdio}; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + Command::new("ping") + .args(["-n", "60", "127.0.0.1"]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .creation_flags(CREATE_NO_WINDOW) + .spawn() + .expect("spawn unmanaged sleep") + .id() + } + #[cfg(unix)] + { + use std::process::{Command, Stdio}; + Command::new("sleep") + .arg("60") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("spawn unmanaged sleep") + .id() + } +} + #[test] fn process_launch_registers_alive_owned_process_tree() { let mut registry = ProcessRegistry::new(); let workspace = WorkspaceId::from_raw(1); - let id = must(registry.launch(sleep_spec(workspace))); + let id = launch_id(&mut registry, sleep_spec(workspace)); assert!(registry.is_alive(id), "launched owned process should be alive"); let pid = registry @@ -142,14 +191,15 @@ fn process_launch_wraps_with_supervisor_when_volt_is_available() { return; }; let mut registry = ProcessRegistry::new(); - let id = must(registry.launch( + let id = launch_id( + &mut registry, ProcessLaunchSpec::new( synthetic_sleep_command(60).0, synthetic_sleep_command(60).1, ) .with_workspace(WorkspaceId::from_raw(2)) .with_process_supervisor_exe(supervisor), - )); + ); let pid = registry.root_pid(id).expect("root pid"); #[cfg(windows)] { @@ -162,16 +212,161 @@ fn process_launch_wraps_with_supervisor_when_volt_is_available() { must(registry.application_quit(Duration::from_millis(200))); } +#[test] +fn interactive_launch_skips_supervisor_wrap() { + let Some(supervisor) = discover_volt_supervisor_exe() else { + return; + }; + let mut registry = ProcessRegistry::new(); + let (program, args) = synthetic_sleep_command(60); + let id = launch_id( + &mut registry, + ProcessLaunchSpec::new(program, args) + .with_workspace(WorkspaceId::from_raw(61)) + .with_mode(ProcessSupervisionMode::Interactive) + .with_process_supervisor_exe(supervisor), + ); + let pid = registry.root_pid(id).expect("root pid"); + #[cfg(windows)] + { + let image = process_image_name(pid); + assert!( + !image.contains("volt"), + "interactive Launch must not wrap ConPTY-incompatible shells with the supervisor (got `{image}`)" + ); + } + must(registry.application_quit(Duration::from_millis(200))); +} + +#[test] +fn piped_launch_exposes_stdout_and_registers_with_workspace() { + let mut registry = ProcessRegistry::new(); + let workspace = WorkspaceId::from_raw(62); + #[cfg(windows)] + let (program, args) = ( + "cmd".to_owned(), + vec!["/C".to_owned(), "echo piped-launch-ok".to_owned()], + ); + #[cfg(unix)] + let (program, args) = ("printf".to_owned(), vec!["piped-launch-ok".to_owned()]); + + let mut launched = must( + registry.launch( + ProcessLaunchSpec::new(program, args) + .with_workspace(workspace) + .with_stdio(ProcessLaunchStdio::Piped), + ), + ); + let mut stdout = launched + .stdout + .take() + .expect("piped launch should expose stdout"); + let mut output = String::new(); + stdout + .read_to_string(&mut output) + .expect("read piped stdout"); + assert!( + output.contains("piped-launch-ok"), + "unexpected stdout `{output}`" + ); + assert!( + wait_until(Duration::from_secs(2), || !registry.is_alive(launched.id)), + "short piped command should exit" + ); + must(registry.reclaim(launched.id)); + assert!(!registry.is_alive(launched.id)); +} + +#[test] +fn launch_interactive_root_registers_workspace_and_close_kills_tree() { + let mut registry = ProcessRegistry::new(); + let workspace = WorkspaceId::from_raw(63); + let root_pid = spawn_unmanaged_sleep(); + assert!(owned_process_pid_alive(root_pid)); + + let id = must(registry.launch_interactive_root(root_pid, [workspace])); + assert!(registry.is_alive(id)); + assert_eq!(registry.root_pid(id), Some(root_pid)); + + must(registry.workspace_close(workspace, Duration::from_millis(200))); + assert!(!registry.is_alive(id)); + assert!( + wait_until(Duration::from_secs(3), || !owned_process_pid_alive(root_pid)), + "interactive root {root_pid} must die on Workspace Close" + ); +} + +#[test] +fn launch_interactive_session_runs_spawn_inside_process_launch() { + let mut registry = ProcessRegistry::new(); + let workspace = WorkspaceId::from_raw(64); + let (id, reported_pid) = must(registry.launch_interactive_session([workspace], || { + let pid = spawn_unmanaged_sleep(); + Ok::<_, String>((pid, pid)) + })); + assert_eq!(registry.root_pid(id), Some(reported_pid)); + must(registry.application_quit(Duration::from_millis(200))); + assert!( + wait_until(Duration::from_secs(3), || !owned_process_pid_alive(reported_pid)), + "interactive session root must die on Application Quit" + ); +} + +#[test] +fn job_manager_launch_is_registry_owned_and_dies_on_workspace_close() { + let registry = Arc::new(Mutex::new(ProcessRegistry::new())); + let mut jobs = JobManager::with_registry(Arc::clone(®istry)); + let workspace = WorkspaceId::from_raw(65); + let (program, args) = synthetic_sleep_command(60); + let handle = must(jobs.spawn( + JobSpec::command("owned-job", program, args).with_workspace(workspace), + )); + assert!( + wait_until(Duration::from_secs(2), || { + registry + .lock() + .map(|mut registry| registry.alive_count() > 0) + .unwrap_or(false) + }), + "JobManager spawn should register an Owned Process" + ); + must( + registry + .lock() + .expect("registry") + .workspace_close(workspace, Duration::from_millis(200)), + ); + drop(handle); + assert!( + wait_until(Duration::from_secs(2), || { + registry + .lock() + .map(|mut registry| registry.alive_count() == 0) + .unwrap_or(false) + }), + "Workspace Close must clear JobManager-launched Owned Processes" + ); +} + #[test] fn workspace_close_kills_descendant_tree_not_just_root() { let child_pid_file = unique_temp_path("volt-owned-child-pid"); let mut registry = ProcessRegistry::new(); let workspace = WorkspaceId::from_raw(3); - let id = must(registry.launch(descendant_tree_spec(workspace, &child_pid_file))); + let id = launch_id( + &mut registry, + descendant_tree_spec(workspace, &child_pid_file), + ); let root_pid = registry.root_pid(id).expect("root pid"); let child_pid = read_child_pid(&child_pid_file); - assert_ne!(root_pid, child_pid, "synthetic tree must include a distinct descendant"); - assert!(owned_process_pid_alive(child_pid), "descendant should be alive before close"); + assert_ne!( + root_pid, child_pid, + "synthetic tree must include a distinct descendant" + ); + assert!( + owned_process_pid_alive(child_pid), + "descendant should be alive before close" + ); must(registry.workspace_close(workspace, Duration::from_millis(200))); @@ -191,8 +386,8 @@ fn workspace_close_kills_processes_tagged_for_that_workspace_only() { let keep = WorkspaceId::from_raw(10); let close = WorkspaceId::from_raw(11); - let keep_id = must(registry.launch(sleep_spec(keep))); - let close_id = must(registry.launch(sleep_spec(close))); + let keep_id = launch_id(&mut registry, sleep_spec(keep)); + let close_id = launch_id(&mut registry, sleep_spec(close)); let close_pid = registry.root_pid(close_id).expect("close pid"); must(registry.workspace_close(close, Duration::from_millis(200))); @@ -229,8 +424,8 @@ fn share_key_keeps_process_until_last_workspace_tag_drops() { .with_share_key(share), ); - let first_id = must(registry.launch(first_spec)); - let second_id = must(registry.launch(second_spec)); + let first_id = launch_id(&mut registry, first_spec); + let second_id = launch_id(&mut registry, second_spec); assert_eq!(first_id, second_id, "share key should reuse the owned process"); let pid = registry.root_pid(first_id).expect("shared pid"); @@ -252,8 +447,8 @@ fn share_key_keeps_process_until_last_workspace_tag_drops() { #[test] fn application_quit_clears_entire_registry() { let mut registry = ProcessRegistry::new(); - let a = must(registry.launch(sleep_spec(WorkspaceId::from_raw(31)))); - let b = must(registry.launch(sleep_spec(WorkspaceId::from_raw(32)))); + let a = launch_id(&mut registry, sleep_spec(WorkspaceId::from_raw(31))); + let b = launch_id(&mut registry, sleep_spec(WorkspaceId::from_raw(32))); let pid_a = registry.root_pid(a).expect("pid a"); let pid_b = registry.root_pid(b).expect("pid b"); @@ -269,7 +464,7 @@ fn application_quit_clears_entire_registry() { #[test] fn graceful_then_force_deadline_always_reaps_tree() { let mut registry = ProcessRegistry::new(); - let id = must(registry.launch(sleep_spec(WorkspaceId::from_raw(41)))); + let id = launch_id(&mut registry, sleep_spec(WorkspaceId::from_raw(41))); let pid = registry.root_pid(id).expect("pid"); // Zero grace forces the hard-kill path immediately after the soft signal. @@ -285,10 +480,10 @@ fn graceful_then_force_deadline_always_reaps_tree() { fn owner_crash_leaves_no_orphans_for_launch_created_tree() { let child_pid_file = unique_temp_path("volt-owned-crash-child-pid"); let mut registry = ProcessRegistry::new(); - let id = must(registry.launch(descendant_tree_spec( - WorkspaceId::from_raw(51), - &child_pid_file, - ))); + let id = launch_id( + &mut registry, + descendant_tree_spec(WorkspaceId::from_raw(51), &child_pid_file), + ); let root_pid = registry.root_pid(id).expect("pid"); let child_pid = read_child_pid(&child_pid_file); assert!(owned_process_pid_alive(root_pid)); diff --git a/crates/editor-jobs/src/tests.rs b/crates/editor-jobs/src/tests.rs index 37375c92..ca8d2c9b 100644 --- a/crates/editor-jobs/src/tests.rs +++ b/crates/editor-jobs/src/tests.rs @@ -109,11 +109,11 @@ fn enrich_env_with_node_manager_preserves_explicit_vars_when_manager_missing() { #[cfg(windows)] #[test] -fn build_job_command_keeps_fnm_path_ahead_of_explicit_path() { +fn build_job_launch_spec_keeps_fnm_path_ahead_of_explicit_path() { let spec = JobSpec::command("node-version", "node", ["--version"]) .with_env("PATH", "C:\\custom") .with_env("NODE_OPTIONS", "--trace-warnings"); - let command = super::build_job_command( + let launch = super::build_job_launch_spec( &spec, "node", Some(&[ @@ -124,14 +124,9 @@ fn build_job_command_keeps_fnm_path_ahead_of_explicit_path() { ), ]), ); - let vars = command - .get_envs() - .filter_map(|(key, value)| { - Some(( - key.to_string_lossy().into_owned(), - value?.to_string_lossy().into_owned(), - )) - }) + let vars = launch + .env + .into_iter() .collect::>(); assert_eq!( vars.get("PATH").map(String::as_str), @@ -149,11 +144,11 @@ fn build_job_command_keeps_fnm_path_ahead_of_explicit_path() { #[cfg(windows)] #[test] -fn build_job_command_keeps_nvm_path_ahead_of_explicit_path() { +fn build_job_launch_spec_keeps_nvm_path_ahead_of_explicit_path() { let spec = JobSpec::command("node-version", "node", ["--version"]) .with_env("PATH", "C:\\custom") .with_env("NODE_OPTIONS", "--trace-warnings"); - let command = super::build_job_command( + let launch = super::build_job_launch_spec( &spec, "node", Some(&[ @@ -167,14 +162,9 @@ fn build_job_command_keeps_nvm_path_ahead_of_explicit_path() { ), ]), ); - let vars = command - .get_envs() - .filter_map(|(key, value)| { - Some(( - key.to_string_lossy().into_owned(), - value?.to_string_lossy().into_owned(), - )) - }) + let vars = launch + .env + .into_iter() .collect::>(); assert_eq!( vars.get("PATH").map(String::as_str), diff --git a/crates/editor-sdl/src/shell/command_stream.rs b/crates/editor-sdl/src/shell/command_stream.rs index d95b5210..3ad1bce8 100644 --- a/crates/editor-sdl/src/shell/command_stream.rs +++ b/crates/editor-sdl/src/shell/command_stream.rs @@ -7,12 +7,11 @@ use super::{ *, }; use editor_jobs::{ - ProcessSupervisionMode, enrich_env_with_node_manager, supervised_command_if_resolved, + JobManager, JobSpec, ProcessSupervisionMode, enrich_env_with_node_manager, }; use std::{ collections::BTreeMap, io::{BufReader, Read}, - process::Stdio, sync::atomic::{AtomicBool, Ordering}, thread, }; @@ -150,6 +149,8 @@ struct StreamedCommandRequest { args: Vec, env: Vec<(String, String)>, cwd: PathBuf, + workspace_id: editor_jobs::WorkspaceId, + process_registry: Arc>, on_exit: StreamedCommandExitAction, notify_on_success: bool, notify_on_failure: bool, @@ -336,28 +337,35 @@ fn run_silent_external_command( args: &[String], command_label: &str, ) -> Result { + let workspace_id = runtime + .model() + .active_workspace_id() + .map_err(|error| error.to_string())?; let env = enrich_env_with_node_manager(Some(&spec.cwd), spec.env.clone()); - let (program, args) = supervised_command_if_resolved( - program, - args, - &env, - None, - ProcessSupervisionMode::Background, - ); - let mut command = Command::new(&program); - command - .args(&args) - .envs(env) - .current_dir(&spec.cwd) - .stdin(Stdio::null()); - configure_background_command(&mut command); - let output = command - .output() + let mut job_spec = JobSpec::command(command_label, program, args.to_vec()) + .with_cwd(spec.cwd.clone()) + .with_workspace(editor_jobs::WorkspaceId::from_raw(workspace_id.get())); + for (key, value) in env { + job_spec = job_spec.with_env(key, value); + } + let manager = runtime + .services() + .get::>() + .ok_or_else(|| "job manager service missing".to_owned())?; + let mut manager = manager + .lock() + .map_err(|_| "job manager lock poisoned".to_owned())?; + let handle = manager + .spawn(job_spec) .map_err(|error| format!("failed to start `{command_label}`: {error}"))?; - let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); - let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); - let success = output.status.success(); - let exit_code = output.status.code(); + drop(manager); + let result = handle + .wait() + .map_err(|error| format!("failed to wait for `{command_label}`: {error}"))?; + let stdout = result.stdout().to_owned(); + let stderr = result.stderr().to_owned(); + let success = result.succeeded(); + let exit_code = result.exit_code(); if (success && spec.notify_on_success) || (!success && spec.notify_on_failure) { let error = (!success).then(|| { let transcript = if stderr.is_empty() { @@ -425,6 +433,11 @@ pub(super) fn continue_streamed_command_popup( spec: StreamedCommandSpec, ) -> Result<(), String> { append_streamed_command_header(runtime, buffer_id, &spec.command_label)?; + let workspace_id = runtime + .model() + .active_workspace_id() + .map_err(|error| error.to_string())?; + let process_registry = process_registry_service(runtime)?; let request = StreamedCommandRequest { buffer_id, popup_title: spec.popup_title, @@ -433,6 +446,8 @@ pub(super) fn continue_streamed_command_popup( args: spec.args, env: spec.env, cwd: spec.cwd, + workspace_id: editor_jobs::WorkspaceId::from_raw(workspace_id.get()), + process_registry, on_exit: spec.on_exit, notify_on_success: spec.notify_on_success, notify_on_failure: spec.notify_on_failure, @@ -774,31 +789,44 @@ fn run_streamed_command( args, env, cwd, + workspace_id, + process_registry, on_exit, notify_on_success, notify_on_failure, } = request; let env = enrich_env_with_node_manager(Some(&cwd), env); - let (program, args) = supervised_command_if_resolved( - &program, - &args, - &env, - None, - ProcessSupervisionMode::Background, - ); - let mut command = Command::new(&program); - command - .args(&args) - .envs(env) - .current_dir(&cwd) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - configure_background_command(&mut command); - - let mut child = match command.spawn() { - Ok(child) => child, - Err(error) => { + let resolved_program = editor_jobs::resolve_command_path(&program, &env, None) + .unwrap_or(program); + let mut launch_spec = editor_jobs::ProcessLaunchSpec::new(resolved_program, args) + .with_workspace(workspace_id) + .with_mode(ProcessSupervisionMode::Background) + .with_stdio(editor_jobs::ProcessLaunchStdio::Piped) + .with_current_dir(cwd); + launch_spec.env = env; + + let mut launched = match process_registry.lock() { + Ok(mut registry) => match registry.launch(launch_spec) { + Ok(launched) => launched, + Err(error) => { + push_streamed_command_update( + &updates, + StreamedCommandUpdate::Finished { + buffer_id, + popup_title, + command_label, + success: false, + exit_code: None, + error: Some(format!("Failed to start process: {error}")), + on_exit, + notify_on_success, + notify_on_failure, + }, + ); + return; + } + }, + Err(_) => { push_streamed_command_update( &updates, StreamedCommandUpdate::Finished { @@ -807,7 +835,7 @@ fn run_streamed_command( command_label, success: false, exit_code: None, - error: Some(format!("Failed to start process: {error}")), + error: Some("process registry mutex poisoned".to_owned()), on_exit, notify_on_success, notify_on_failure, @@ -817,19 +845,21 @@ fn run_streamed_command( } }; - let stdout_reader = child.stdout.take().map(|stdout| { + let owned_id = launched.id; + let stdout_reader = launched.stdout.take().map(|stdout| { let updates = Arc::clone(&updates); thread::spawn(move || stream_command_output(buffer_id, stdout, updates)) }); - let stderr_reader = child.stderr.take().map(|stderr| { + let stderr_reader = launched.stderr.take().map(|stderr| { let updates = Arc::clone(&updates); thread::spawn(move || stream_command_output(buffer_id, stderr, updates)) }); - let status = loop { + let exit_code = loop { if cancel.load(Ordering::Relaxed) { - let _ = child.kill(); - let _ = child.wait(); + if let Ok(mut registry) = process_registry.lock() { + let _ = registry.teardown_process(owned_id, OWNED_PROCESS_TEARDOWN_GRACE); + } if let Some(reader) = stdout_reader { let _ = reader.join(); } @@ -838,11 +868,21 @@ fn run_streamed_command( } return; } - match child.try_wait() { - Ok(Some(status)) => break Ok(status), - Ok(None) => std::thread::sleep(std::time::Duration::from_millis(50)), - Err(error) => break Err(error), + let alive = process_registry + .lock() + .map(|mut registry| registry.is_alive(owned_id)) + .unwrap_or(false); + if !alive { + let code = process_registry + .lock() + .ok() + .and_then(|mut registry| registry.exit_code(owned_id)); + if let Ok(mut registry) = process_registry.lock() { + let _ = registry.reclaim(owned_id); + } + break code; } + std::thread::sleep(std::time::Duration::from_millis(50)); }; if let Some(reader) = stdout_reader { let _ = reader.join(); @@ -851,38 +891,20 @@ fn run_streamed_command( let _ = reader.join(); } - match status { - Ok(status) => push_streamed_command_update( - &updates, - StreamedCommandUpdate::Finished { - buffer_id, - popup_title, - command_label, - success: status.success(), - exit_code: status.code(), - error: None, - on_exit, - notify_on_success, - notify_on_failure, - }, - ), - Err(error) => push_streamed_command_update( - &updates, - StreamedCommandUpdate::Finished { - buffer_id, - popup_title, - command_label, - success: false, - exit_code: None, - error: Some(format!( - "Failed while waiting for process completion: {error}" - )), - on_exit, - notify_on_success, - notify_on_failure, - }, - ), - } + push_streamed_command_update( + &updates, + StreamedCommandUpdate::Finished { + buffer_id, + popup_title, + command_label, + success: exit_code == Some(0), + exit_code, + error: None, + on_exit, + notify_on_success, + notify_on_failure, + }, + ); } fn stream_command_output( diff --git a/crates/editor-sdl/src/shell/compile.rs b/crates/editor-sdl/src/shell/compile.rs index dd48b3fb..9df4a346 100644 --- a/crates/editor-sdl/src/shell/compile.rs +++ b/crates/editor-sdl/src/shell/compile.rs @@ -219,7 +219,13 @@ fn run_shell_command_in_buffer( buffer.append_output_lines(&[format!("$ {command}"), String::new()]); buffer.clear_input(); } - let spec = JobSpec::command("command", shell_program, args).with_cwd(cwd); + let workspace_id = runtime + .model() + .active_workspace_id() + .map_err(|error| error.to_string())?; + let spec = JobSpec::command("command", shell_program, args) + .with_cwd(cwd) + .with_workspace(editor_jobs::WorkspaceId::from_raw(workspace_id.get())); let manager = runtime .services() .get::>() diff --git a/crates/editor-sdl/src/shell/dap_commands.rs b/crates/editor-sdl/src/shell/dap_commands.rs index bd7ff39d..9bf3ff79 100644 --- a/crates/editor-sdl/src/shell/dap_commands.rs +++ b/crates/editor-sdl/src/shell/dap_commands.rs @@ -637,7 +637,13 @@ fn run_dap_compile_before_debug(runtime: &mut EditorRuntime, command: &str) -> R let buffer = shell_buffer_mut(runtime, buffer_id)?; buffer.append_output_lines(&[format!("$ {command}"), String::new()]); } - let spec = JobSpec::command("dap-prelaunch", shell_program, args).with_cwd(cwd); + let workspace_id = runtime + .model() + .active_workspace_id() + .map_err(|error| error.to_string())?; + let spec = JobSpec::command("dap-prelaunch", shell_program, args) + .with_cwd(cwd) + .with_workspace(editor_jobs::WorkspaceId::from_raw(workspace_id.get())); let manager = runtime .services() .get::>() diff --git a/crates/editor-sdl/src/shell/git/process.rs b/crates/editor-sdl/src/shell/git/process.rs index 955204df..fe7c6542 100644 --- a/crates/editor-sdl/src/shell/git/process.rs +++ b/crates/editor-sdl/src/shell/git/process.rs @@ -35,12 +35,17 @@ pub(crate) fn git_command_output( label: &str, args: &[&str], ) -> Result { + let workspace_id = runtime + .model() + .active_workspace_id() + .map_err(|error| error.to_string())?; let spec = JobSpec::command( label, "git", args.iter().map(|arg| (*arg).to_owned()).collect::>(), ) - .with_cwd(root.to_path_buf()); + .with_cwd(root.to_path_buf()) + .with_workspace(editor_jobs::WorkspaceId::from_raw(workspace_id.get())); let manager = runtime .services() .get::>() diff --git a/crates/editor-sdl/src/shell/run_loop.rs b/crates/editor-sdl/src/shell/run_loop.rs index f24e8258..267856e6 100644 --- a/crates/editor-sdl/src/shell/run_loop.rs +++ b/crates/editor-sdl/src/shell/run_loop.rs @@ -684,6 +684,11 @@ pub fn run_demo_shell(config: ShellConfig) -> Result { match frame_result { Ok(FrameOutcome::Quit) => { + if let Ok(registry) = process_registry_service(&state.runtime) { + if let Ok(mut registry) = registry.lock() { + let _ = registry.application_quit(OWNED_PROCESS_TEARDOWN_GRACE); + } + } return Ok(build_shell_summary( &mut state, frames_rendered, diff --git a/crates/editor-sdl/src/shell/save.rs b/crates/editor-sdl/src/shell/save.rs index dd7e199e..96c245b7 100644 --- a/crates/editor-sdl/src/shell/save.rs +++ b/crates/editor-sdl/src/shell/save.rs @@ -691,6 +691,9 @@ fn format_text_with_formatter( if let Some(cwd) = cwd { spec = spec.with_cwd(cwd.to_path_buf()); } + if let Ok(workspace_id) = runtime.model().active_workspace_id() { + spec = spec.with_workspace(editor_jobs::WorkspaceId::from_raw(workspace_id.get())); + } let manager = runtime .services() diff --git a/crates/editor-sdl/src/shell/shell_state.rs b/crates/editor-sdl/src/shell/shell_state.rs index 432d328f..13a11e16 100644 --- a/crates/editor-sdl/src/shell/shell_state.rs +++ b/crates/editor-sdl/src/shell/shell_state.rs @@ -206,7 +206,11 @@ impl ShellState { .services_mut() .insert(Mutex::new(TerminalBufferState::default())); runtime.services_mut().insert(FormatterRegistry::default()); - runtime.services_mut().insert(Mutex::new(JobManager::new())); + let process_registry = Arc::new(Mutex::new(editor_jobs::ProcessRegistry::new())); + runtime + .services_mut() + .insert(Arc::clone(&process_registry)); + runtime.services_mut().insert(Mutex::new(JobManager::with_registry(process_registry))); let mut theme_registry = ThemeRegistry::new(); theme_registry .register_all(user_library.themes()) diff --git a/crates/editor-sdl/src/shell/terminal.rs b/crates/editor-sdl/src/shell/terminal.rs index 627520c7..5332c533 100644 --- a/crates/editor-sdl/src/shell/terminal.rs +++ b/crates/editor-sdl/src/shell/terminal.rs @@ -25,14 +25,19 @@ impl TerminalPollPriority { struct TrackedTerminalSession { session: LiveTerminalSession, + owned_process_id: Option, last_poll_at: Option, buffer_dirty: bool, } impl TrackedTerminalSession { - fn new(session: LiveTerminalSession) -> Self { + fn new( + session: LiveTerminalSession, + owned_process_id: Option, + ) -> Self { Self { session, + owned_process_id, last_poll_at: None, buffer_dirty: false, } @@ -59,15 +64,25 @@ impl TerminalBufferState { self.sessions.get_mut(&buffer_id) } - pub(super) fn insert(&mut self, buffer_id: BufferId, session: LiveTerminalSession) { - self.sessions - .insert(buffer_id, TrackedTerminalSession::new(session)); + pub(super) fn insert( + &mut self, + buffer_id: BufferId, + session: LiveTerminalSession, + owned_process_id: Option, + ) { + self.sessions.insert( + buffer_id, + TrackedTerminalSession::new(session, owned_process_id), + ); } - pub(super) fn remove(&mut self, buffer_id: BufferId) -> Option { - self.sessions - .remove(&buffer_id) - .map(|tracked| tracked.session) + pub(super) fn remove( + &mut self, + buffer_id: BufferId, + ) -> Option<(LiveTerminalSession, Option)> { + self.sessions.remove(&buffer_id).map(|tracked| { + (tracked.session, tracked.owned_process_id) + }) } pub(super) fn buffer_ids(&self) -> Vec { @@ -418,11 +433,31 @@ pub(super) fn ensure_terminal_session( if terminal_buffer_state(runtime)?.contains(buffer_id) { return Ok(false); } + let workspace_id = runtime + .model() + .active_workspace_id() + .map_err(|error| error.to_string())?; let config = terminal_spawn_config(runtime, buffer_id, 24, 80)?; - let session = LiveTerminalSession::spawn(config).map_err(|error| error.to_string())?; + let registry = process_registry_service(runtime)?; + let mut registry = registry + .lock() + .map_err(|_| "process registry mutex poisoned".to_owned())?; + let (owned_process_id, session) = registry + .launch_interactive_session( + [editor_jobs::WorkspaceId::from_raw(workspace_id.get())], + || { + let session = + LiveTerminalSession::spawn(config).map_err(|error| error.to_string())?; + let root_pid = session.process_id().ok_or_else(|| { + "interactive terminal Process Launch requires a root process id".to_owned() + })?; + Ok::<_, String>((session, root_pid)) + }, + ) + .map_err(|error| error.to_string())?; let lines = session.snapshot().lines().to_vec(); let render = session.render_snapshot(); - terminal_buffer_state_mut(runtime)?.insert(buffer_id, session); + terminal_buffer_state_mut(runtime)?.insert(buffer_id, session, Some(owned_process_id)); let buffer = shell_buffer_mut(runtime, buffer_id)?; buffer.set_terminal_render(render); buffer.replace_with_lines_follow_output(lines); @@ -640,7 +675,8 @@ pub(super) fn close_terminal_buffer( if let Ok(buffer) = shell_buffer_mut(runtime, buffer_id) { buffer.clear_terminal_render(); } - let Some(mut session) = terminal_buffer_state_mut(runtime)?.remove(buffer_id) else { + let Some((mut session, owned_process_id)) = terminal_buffer_state_mut(runtime)?.remove(buffer_id) + else { return Ok(()); }; session.kill().map_err(|error| { @@ -648,9 +684,29 @@ pub(super) fn close_terminal_buffer( "failed to terminate terminal session `{}` for buffer `{buffer_id}`: {error}", session.title() ) - }) + })?; + if let Some(owned_process_id) = owned_process_id { + let registry = process_registry_service(runtime)?; + let mut registry = registry + .lock() + .map_err(|_| "process registry mutex poisoned".to_owned())?; + let _ = registry.teardown_process(owned_process_id, OWNED_PROCESS_TEARDOWN_GRACE); + } + Ok(()) +} + +pub(super) fn process_registry_service( + runtime: &EditorRuntime, +) -> Result>, String> { + runtime + .services() + .get::>>() + .cloned() + .ok_or_else(|| "process registry service missing".to_owned()) } +pub(super) const OWNED_PROCESS_TEARDOWN_GRACE: Duration = Duration::from_millis(200); + pub(super) fn terminal_key_for_event(keycode: Keycode, keymod: Mod) -> Option { if keymod.intersects(ctrl_mod()) && keycode == Keycode::C { return Some(TerminalKey::CtrlC); diff --git a/crates/editor-sdl/src/shell/workspace.rs b/crates/editor-sdl/src/shell/workspace.rs index b987edf2..531c91a9 100644 --- a/crates/editor-sdl/src/shell/workspace.rs +++ b/crates/editor-sdl/src/shell/workspace.rs @@ -973,6 +973,17 @@ pub(crate) fn delete_runtime_workspace( close_lsp_buffers_for_workspace(runtime, workspace_id)?; close_terminal_buffers_for_workspace(runtime, workspace_id)?; acp::close_acp_workspace_buffers(runtime, workspace_id)?; + if let Ok(registry) = process_registry_service(runtime) { + let mut registry = registry + .lock() + .map_err(|_| "process registry mutex poisoned".to_owned())?; + registry + .workspace_close( + editor_jobs::WorkspaceId::from_raw(workspace_id.get()), + OWNED_PROCESS_TEARDOWN_GRACE, + ) + .map_err(|error| error.to_string())?; + } let removed = runtime .model_mut() .close_workspace(workspace_id) diff --git a/crates/editor-terminal/src/render.rs b/crates/editor-terminal/src/render.rs index cdf41568..7328d294 100644 --- a/crates/editor-terminal/src/render.rs +++ b/crates/editor-terminal/src/render.rs @@ -263,8 +263,8 @@ pub(crate) fn terminal_tty_options(config: &LiveTerminalConfig) -> TtyOptions { } #[cfg(windows)] -pub(crate) fn tty_process_id(_pty: &tty::Pty) -> Option { - None +pub(crate) fn tty_process_id(pty: &tty::Pty) -> Option { + pty.child_watcher().pid().map(|pid| pid.get()) } #[cfg(not(windows))] From 3e86336199e8fb089756a3b1d809c5dd0b25d05d Mon Sep 17 00:00:00 2001 From: Sam Precious Date: Sat, 12 Sep 2026 22:39:24 +0100 Subject: [PATCH 3/5] feat(lang): own LSP/DAP/ACP helpers via Process Launch Route language servers, debug adapters, ACP children, and tool helpers through the Process Registry with Share Key workspace tags and protocol-then-force teardown on Workspace Close / Application Quit. Closes #89 Co-authored-by: Cursor --- crates/editor-dap/src/client/session.rs | 67 +- crates/editor-dap/src/client/types.rs | 128 +- crates/editor-jobs/src/lib.rs | 10 +- crates/editor-jobs/src/process_registry.rs | 113 +- .../editor-jobs/src/process_registry_tests.rs | 130 +- crates/editor-lsp/src/client/documents.rs | 20 +- crates/editor-lsp/src/client/manager.rs | 135 +- crates/editor-lsp/src/client/requests.rs | 26 +- crates/editor-lsp/src/client/session.rs | 89 +- crates/editor-lsp/src/client/tests.rs | 4 +- crates/editor-lsp/src/client/types.rs | 170 +- crates/editor-sdl/src/shell/acp/client.rs | 54 +- crates/editor-sdl/src/shell/acp/launch.rs | 233 +- crates/editor-sdl/src/shell/acp/manager.rs | 6 +- crates/editor-sdl/src/shell/acp/runtime.rs | 114 +- crates/editor-sdl/src/shell/acp/session.rs | 3 +- crates/editor-sdl/src/shell/command_stream.rs | 8 +- crates/editor-sdl/src/shell/compile.rs | 13 +- crates/editor-sdl/src/shell/lsp_commands.rs | 1 + crates/editor-sdl/src/shell/refresh.rs | 6 + crates/editor-sdl/src/shell/run_loop.rs | 18 +- crates/editor-sdl/src/shell/save.rs | 15 +- crates/editor-sdl/src/shell/syntax_refresh.rs | 13 +- crates/editor-sdl/src/shell/terminal.rs | 15 +- crates/editor-sdl/src/shell/tests/debug.rs | 6 +- crates/editor-sdl/src/shell/tests/helpers.rs | 11 +- crates/editor-sdl/src/shell/tests/misc.rs | 4 +- .../src/shell/workers_autocomplete.rs | 23 +- crates/editor-sdl/src/shell/workspace.rs | 8 + graphify-out/.graphify_labels.json | 222 +- graphify-out/.graphify_labels.json.sig | 2 +- graphify-out/GRAPH_REPORT.md | 1259 +- graphify-out/graph.html | 8 +- graphify-out/graph.json | 39968 ++++++++++------ 34 files changed, 26390 insertions(+), 16512 deletions(-) diff --git a/crates/editor-dap/src/client/session.rs b/crates/editor-dap/src/client/session.rs index 0a4b612c..9e5db0c4 100644 --- a/crates/editor-dap/src/client/session.rs +++ b/crates/editor-dap/src/client/session.rs @@ -3,14 +3,16 @@ use std::{ fmt, io::Write, path::PathBuf, - process::Child, sync::{ Arc, Mutex, atomic::{AtomicBool, AtomicU64, Ordering}, }, thread::JoinHandle, + time::Duration, }; +use editor_jobs::{OwnedProcessId, ProcessRegistry}; + use dap_types::{ AttachRequestArguments, ContinueArguments, DisconnectArguments, InitializeRequestArguments, LaunchRequestArguments, NextArguments, PauseArguments, RestartArguments, @@ -43,7 +45,8 @@ pub(crate) struct DapSessionHandle { pub(crate) last_disconnect: Arc>>, pub(crate) stop_state: Arc>, pub(crate) _reader: JoinHandle<()>, - pub(crate) child: Option>, + pub(crate) owned_process_id: Option, + pub(crate) process_registry: Option>>, pub(crate) transport_log: TransportLog, } @@ -60,11 +63,11 @@ impl fmt::Debug for DapSessionHandle { impl Drop for DapSessionHandle { fn drop(&mut self) { self.disconnected.store(true, Ordering::Release); - if let Some(child) = self.child.as_ref() - && let Ok(mut child) = child.lock() + if let (Some(id), Some(registry)) = + (self.owned_process_id.take(), self.process_registry.take()) + && let Ok(mut registry) = registry.lock() { - let _ = child.kill(); - let _ = child.wait(); + let _ = registry.teardown_process(id, Duration::from_millis(200)); } } } @@ -73,6 +76,7 @@ impl Drop for DapSessionHandle { #[derive(Debug)] pub struct DapClientManager { pub(crate) registry: DebugAdapterRegistry, + pub(crate) process_registry: Arc>, pub(crate) sessions: Mutex>>, pub(crate) breakpoints: Mutex, pub(crate) watches: Mutex>>, @@ -85,8 +89,17 @@ pub struct DapClientManager { impl DapClientManager { /// Creates a manager around a populated adapter registry. pub fn new(registry: DebugAdapterRegistry) -> Self { + Self::with_process_registry(registry, Arc::new(Mutex::new(ProcessRegistry::new()))) + } + + /// Creates a manager that registers debug-adapter trees in a shared Process Registry. + pub fn with_process_registry( + registry: DebugAdapterRegistry, + process_registry: Arc>, + ) -> Self { Self { registry, + process_registry, sessions: Mutex::new(BTreeMap::new()), breakpoints: Mutex::new(BreakpointStore::new()), watches: Mutex::new(BTreeMap::new()), @@ -102,6 +115,11 @@ impl DapClientManager { &self.registry } + /// Returns the Process Registry used for debug-adapter Owned Processes. + pub fn process_registry(&self) -> Arc> { + Arc::clone(&self.process_registry) + } + /// Records a successful Debug Session start for last/recent replay. pub fn record_start( &self, @@ -805,11 +823,11 @@ impl DapClientManager { ); } - if let Some(child) = handle.child.as_ref() - && let Ok(mut child) = child.lock() + if let (Some(id), Some(registry)) = + (handle.owned_process_id, handle.process_registry.as_ref()) + && let Ok(mut registry) = registry.lock() { - let _ = child.kill(); - let _ = child.wait(); + let _ = registry.teardown_process(id, Duration::from_millis(200)); } if let Ok(mut store) = self.breakpoints.lock() { @@ -819,6 +837,21 @@ impl DapClientManager { Ok(handle.info.clone()) } + /// Stops every live Debug Session (Application Quit grace). + pub fn stop_all_sessions(&self) -> Result<(), DapClientError> { + let workspace_ids = { + let sessions = self + .sessions + .lock() + .map_err(|_| DapClientError::LockPoisoned)?; + sessions.keys().copied().collect::>() + }; + for workspace_id in workspace_ids { + let _ = self.stop_session(workspace_id); + } + Ok(()) + } + pub(crate) fn sync_all_breakpoints( &self, workspace_id: u64, @@ -928,7 +961,12 @@ impl DapClientManager { adapter: &DebugAdapterSpec, plan: DebugSessionPlan, ) -> Result { - let (writer, reader, child) = connect_transport(adapter, plan.transport())?; + let (writer, reader, owned) = connect_transport( + adapter, + plan.transport(), + &self.process_registry, + workspace_id, + )?; let writer: Arc>> = Arc::new(Mutex::new(writer)); let pending: Arc>> = Arc::new(Mutex::new(BTreeMap::new())); @@ -952,6 +990,10 @@ impl DapClientManager { ); let next_request_id = AtomicU64::new(1); + let (owned_process_id, process_registry) = match owned { + Some((id, registry)) => (Some(id), Some(registry)), + None => (None, None), + }; let mut handle = DapSessionHandle { info: DapSessionInfo { workspace_id, @@ -969,7 +1011,8 @@ impl DapClientManager { last_disconnect: Arc::clone(&last_disconnect), stop_state: Arc::clone(&stop_state), _reader: reader_handle, - child: child.map(Mutex::new), + owned_process_id, + process_registry, transport_log: Arc::clone(&self.transport_log), }; diff --git a/crates/editor-dap/src/client/types.rs b/crates/editor-dap/src/client/types.rs index d980f1a6..3824a387 100644 --- a/crates/editor-dap/src/client/types.rs +++ b/crates/editor-dap/src/client/types.rs @@ -6,7 +6,7 @@ use std::{ mem, net::TcpStream, path::{Path, PathBuf}, - process::{Child, Command, Stdio}, + process::{ChildStdin, ChildStdout}, sync::{ Arc, Mutex, atomic::{AtomicBool, Ordering}, @@ -20,6 +20,10 @@ use dap_types::{ VariablesArguments, requests::{Evaluate, Request as DapRequest, Scopes, StackTrace, Variables}, }; +use editor_jobs::{ + OwnedProcessId, ProcessLaunchSpec, ProcessLaunchStdio, ProcessRegistry, ProcessSupervisionMode, + WorkspaceId, +}; use serde::Deserialize; use serde_json::{Value, json}; @@ -36,9 +40,6 @@ pub(crate) const TCP_CONNECT_TIMEOUT: Duration = Duration::from_secs(5); pub(crate) const TCP_CONNECT_RETRY: Duration = Duration::from_millis(50); -#[cfg(windows)] -pub(crate) const CREATE_NO_WINDOW: u32 = 0x0800_0000; - /// Errors produced by the DAP client host. #[derive(Debug, Clone, PartialEq, Eq)] pub enum DapClientError { @@ -735,42 +736,45 @@ pub(crate) fn wait_for_initialized( } } -pub(crate) type TransportEnds = (Box, Box, Option); +pub(crate) type OwnedTransportProcess = (OwnedProcessId, Arc>); + +pub(crate) type TransportEnds = ( + Box, + Box, + Option, +); pub(crate) fn connect_transport( adapter: &DebugAdapterSpec, transport: &DebugAdapterTransport, + process_registry: &Arc>, + workspace_id: u64, ) -> Result { match transport { DebugAdapterTransport::Stdio => { - let mut child = spawn_adapter_command(adapter)?; - let stdin = child.stdin.take().ok_or_else(|| { - DapClientError::Protocol(format!( - "debug adapter `{}` is missing stdin pipe", - adapter.id() - )) - })?; - let stdout = child.stdout.take().ok_or_else(|| { - DapClientError::Protocol(format!( - "debug adapter `{}` is missing stdout pipe", - adapter.id() - )) - })?; - Ok((Box::new(stdin), Box::new(stdout), Some(child))) + let (owned_id, stdin, stdout) = + launch_dap_protocol_process(process_registry, adapter, workspace_id)?; + Ok(( + Box::new(stdin), + Box::new(stdout), + Some((owned_id, Arc::clone(process_registry))), + )) } DebugAdapterTransport::Tcp { host, port } => { // Empty program means connect-only (adapter already listening), used by tests // and remote adapters. - let child = if adapter.program().is_empty() { + let owned = if adapter.program().is_empty() { None } else { - Some(spawn_adapter_command(adapter)?) + let owned_id = + launch_dap_detached_process(process_registry, adapter, workspace_id)?; + Some((owned_id, Arc::clone(process_registry))) }; - let stream = connect_tcp(host, *port, child.is_some())?; + let stream = connect_tcp(host, *port, owned.is_some())?; let reader = stream .try_clone() .map_err(|error| DapClientError::Io(error.to_string()))?; - Ok((Box::new(stream), Box::new(reader), child)) + Ok((Box::new(stream), Box::new(reader), owned)) } } } @@ -811,36 +815,70 @@ pub(crate) fn connect_tcp( ))) } -pub(crate) fn configure_adapter_command(_command: &mut Command) { - #[cfg(windows)] - { - use std::os::windows::process::CommandExt as _; +pub(crate) fn launch_dap_protocol_process( + process_registry: &Arc>, + adapter: &DebugAdapterSpec, + workspace_id: u64, +) -> Result<(OwnedProcessId, ChildStdin, ChildStdout), DapClientError> { + let mut launched = launch_dap_spec( + process_registry, + adapter, + workspace_id, + ProcessLaunchStdio::Protocol, + )?; + let stdin = launched.stdin.take().ok_or_else(|| { + DapClientError::Protocol(format!( + "debug adapter `{}` is missing stdin pipe", + adapter.id() + )) + })?; + let stdout = launched.stdout.take().ok_or_else(|| { + DapClientError::Protocol(format!( + "debug adapter `{}` is missing stdout pipe", + adapter.id() + )) + })?; + Ok((launched.id, stdin, stdout)) +} - _command.creation_flags(CREATE_NO_WINDOW); - } +pub(crate) fn launch_dap_detached_process( + process_registry: &Arc>, + adapter: &DebugAdapterSpec, + workspace_id: u64, +) -> Result { + let launched = launch_dap_spec( + process_registry, + adapter, + workspace_id, + ProcessLaunchStdio::Null, + )?; + Ok(launched.id) } -pub(crate) fn spawn_adapter_command(adapter: &DebugAdapterSpec) -> Result { - let mut command = Command::new(adapter.program()); - configure_adapter_command(&mut command); - command - .args(adapter.args()) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()); +fn launch_dap_spec( + process_registry: &Arc>, + adapter: &DebugAdapterSpec, + workspace_id: u64, + stdio: ProcessLaunchStdio, +) -> Result { let mut env = Vec::new(); editor_tool_install::merge_effective_path(&mut env); - for (key, value) in env { - command.env(key, value); - } - match command.spawn() { - Ok(child) => Ok(child), - Err(error) => Err(DapClientError::AdapterMissing { + let mut spec = ProcessLaunchSpec::new(adapter.program(), adapter.args().to_vec()) + .with_stdio(stdio) + .with_mode(ProcessSupervisionMode::Background) + .with_workspace(WorkspaceId::from_raw(workspace_id)); + spec.env = env; + + let mut registry = process_registry + .lock() + .map_err(|_| DapClientError::LockPoisoned)?; + registry + .launch(spec) + .map_err(|error| DapClientError::AdapterMissing { adapter_id: adapter.id().to_owned(), program: adapter.program().to_owned(), detail: error.to_string(), - }), - } + }) } pub(crate) struct DapReaderSession { diff --git a/crates/editor-jobs/src/lib.rs b/crates/editor-jobs/src/lib.rs index 800f78e2..a090d76f 100644 --- a/crates/editor-jobs/src/lib.rs +++ b/crates/editor-jobs/src/lib.rs @@ -19,12 +19,12 @@ mod process_registry; pub use process_registry::{ LaunchedProcess, OwnedProcessId, ProcessLaunchSpec, ProcessLaunchStdio, ProcessRegistry, - ProcessRegistryError, ShareKey, WorkspaceId, owned_process_pid_alive, + ProcessRegistryError, ShareKey, WorkspaceId, language_server_share_key, + owned_process_pid_alive, }; /// Human-readable summary of this crate's responsibility. -pub const ROLE: &str = - "Asynchronous job scheduling, process supervision, Process Registry / Process Launch, and compilation task coordination."; +pub const ROLE: &str = "Asynchronous job scheduling, process supervision, Process Registry / Process Launch, and compilation task coordination."; /// Returns the responsibility summary for this crate. pub const fn role() -> &'static str { @@ -744,8 +744,8 @@ fn build_job_launch_spec( } }; #[cfg(windows)] - let resolved_program = - resolve_command_path(program, &env_pairs, runtime_env).unwrap_or_else(|| program.to_owned()); + let resolved_program = resolve_command_path(program, &env_pairs, runtime_env) + .unwrap_or_else(|| program.to_owned()); #[cfg(not(windows))] let resolved_program = resolve_command_path(program, &env_pairs, None).unwrap_or_else(|| program.to_owned()); diff --git a/crates/editor-jobs/src/process_registry.rs b/crates/editor-jobs/src/process_registry.rs index 9c052533..454b045f 100644 --- a/crates/editor-jobs/src/process_registry.rs +++ b/crates/editor-jobs/src/process_registry.rs @@ -6,8 +6,8 @@ use std::{ collections::{HashMap, HashSet}, fmt, - path::PathBuf, - process::{ChildStderr, ChildStdout, Command, Stdio}, + path::{Path, PathBuf}, + process::{ChildStderr, ChildStdin, ChildStdout, Command, Stdio}, thread, time::{Duration, Instant}, }; @@ -91,6 +91,8 @@ pub enum ProcessLaunchStdio { Null, /// Pipe stdout and stderr for callers that need to stream or capture output. Piped, + /// Pipe stdin and stdout for protocol children (LSP/DAP/ACP); discard stderr. + Protocol, } /// How Process Launch should start an Owned Process tree. @@ -172,17 +174,28 @@ impl ProcessLaunchSpec { } } -/// Result of Process Launch: registry id plus optional output pipes. +/// Result of Process Launch: registry id plus optional pipes. #[derive(Debug)] pub struct LaunchedProcess { /// Registry id for the Owned Process tree. pub id: OwnedProcessId, - /// Piped stdout when launched with [`ProcessLaunchStdio::Piped`]. + /// Piped stdin when launched with [`ProcessLaunchStdio::Protocol`]. + pub stdin: Option, + /// Piped stdout when launched with [`ProcessLaunchStdio::Piped`] or + /// [`ProcessLaunchStdio::Protocol`]. pub stdout: Option, /// Piped stderr when launched with [`ProcessLaunchStdio::Piped`]. pub stderr: Option, } +/// Builds a Share Key for a language server serving one on-disk root. +pub fn language_server_share_key(server_id: &str, root: Option<&Path>) -> ShareKey { + match root { + Some(root) => ShareKey::new(format!("lsp:{server_id}:{}", root.display())), + None => ShareKey::new(format!("lsp:{server_id}:global")), + } +} + /// Errors from Process Registry operations. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ProcessRegistryError { @@ -254,12 +267,13 @@ impl ProcessRegistry { entry.workspace_ids.extend(spec.workspace_ids); return Ok(LaunchedProcess { id: existing_id, + stdin: None, stdout: None, stderr: None, }); } - let (tree, stdout, stderr) = OwnedProcessTree::launch(&spec)?; + let (tree, stdin, stdout, stderr) = OwnedProcessTree::launch(&spec)?; let id = self.allocate_id(); let share_key = spec.share_key.clone(); if let Some(share_key) = share_key.clone() { @@ -276,6 +290,7 @@ impl ProcessRegistry { ); Ok(LaunchedProcess { id, + stdin, stdout, stderr, }) @@ -293,8 +308,8 @@ impl ProcessRegistry { where E: std::fmt::Display, { - let (payload, root_pid) = spawn() - .map_err(|error| ProcessRegistryError::Launch(error.to_string()))?; + let (payload, root_pid) = + spawn().map_err(|error| ProcessRegistryError::Launch(error.to_string()))?; let id = self.launch_interactive_root(root_pid, workspace_ids)?; Ok((id, payload)) } @@ -383,12 +398,35 @@ impl ProcessRegistry { Ok(()) } - /// Drops the platform kill-tree as if the owning UI process crashed, so - /// kill-on-job-close / equivalent should reap the tree without an orderly quit. - pub fn simulate_owner_crash( + /// Returns whether removing `workspace_id` would leave this Owned Process with + /// no Workspace tags (so Workspace Close would tear it down). + pub fn will_teardown_on_workspace_close( + &self, + id: OwnedProcessId, + workspace_id: WorkspaceId, + ) -> bool { + self.entries.get(&id).is_some_and(|entry| { + entry.workspace_ids.contains(&workspace_id) && entry.workspace_ids.len() == 1 + }) + } + + /// Adds a Workspace tag to an existing Owned Process. + pub fn tag_workspace( &mut self, id: OwnedProcessId, + workspace_id: WorkspaceId, ) -> Result<(), ProcessRegistryError> { + let entry = self + .entries + .get_mut(&id) + .ok_or(ProcessRegistryError::UnknownProcess(id))?; + entry.workspace_ids.insert(workspace_id); + Ok(()) + } + + /// Drops the platform kill-tree as if the owning UI process crashed, so + /// kill-on-job-close / equivalent should reap the tree without an orderly quit. + pub fn simulate_owner_crash(&mut self, id: OwnedProcessId) -> Result<(), ProcessRegistryError> { let Some(mut entry) = self.entries.remove(&id) else { return Err(ProcessRegistryError::UnknownProcess(id)); }; @@ -436,21 +474,41 @@ struct ExternalOwnedTree { root_pid: u32, } +type LaunchedTreeParts = ( + OwnedProcessTree, + Option, + Option, + Option, +); + impl OwnedProcessTree { - fn launch( - spec: &ProcessLaunchSpec, - ) -> Result<(Self, Option, Option), ProcessRegistryError> { + fn launch(spec: &ProcessLaunchSpec) -> Result { let (program, args) = supervised_command_for_launch(spec); let mut command = Command::new(&program); - command.args(&args).stdin(Stdio::null()); - match spec.stdio { + command.args(&args); + let (stdin, stdout, stderr) = match spec.stdio { ProcessLaunchStdio::Null => { - command.stdout(Stdio::null()).stderr(Stdio::null()); + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + (false, false, false) } ProcessLaunchStdio::Piped => { - command.stdout(Stdio::piped()).stderr(Stdio::piped()); + command + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + (false, true, true) } - } + ProcessLaunchStdio::Protocol => { + command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()); + (true, true, false) + } + }; if let Some(current_dir) = &spec.current_dir { command.current_dir(current_dir); } @@ -474,9 +532,22 @@ impl OwnedProcessTree { let mut child = builder .spawn() .map_err(|error| ProcessRegistryError::Launch(format!("failed to spawn: {error}")))?; - let stdout = child.inner().stdout.take(); - let stderr = child.inner().stderr.take(); - Ok((Self::Group(child), stdout, stderr)) + let stdin = if stdin { + child.inner().stdin.take() + } else { + None + }; + let stdout = if stdout { + child.inner().stdout.take() + } else { + None + }; + let stderr = if stderr { + child.inner().stderr.take() + } else { + None + }; + Ok((Self::Group(child), stdin, stdout, stderr)) } fn adopt_interactive_root(root_pid: u32) -> Result { diff --git a/crates/editor-jobs/src/process_registry_tests.rs b/crates/editor-jobs/src/process_registry_tests.rs index 4fddeaeb..855e0605 100644 --- a/crates/editor-jobs/src/process_registry_tests.rs +++ b/crates/editor-jobs/src/process_registry_tests.rs @@ -9,9 +9,11 @@ use std::{ use super::{ JobManager, JobSpec, OwnedProcessId, ProcessLaunchSpec, ProcessLaunchStdio, ProcessRegistry, - ProcessSupervisionMode, ShareKey, WorkspaceId, owned_process_pid_alive, + ProcessSupervisionMode, ShareKey, WorkspaceId, language_server_share_key, + owned_process_pid_alive, process_registry::{discover_volt_supervisor_exe, synthetic_sleep_command}, }; +use std::path::Path; fn must(result: Result) -> T { match result { @@ -170,7 +172,10 @@ fn process_launch_registers_alive_owned_process_tree() { let workspace = WorkspaceId::from_raw(1); let id = launch_id(&mut registry, sleep_spec(workspace)); - assert!(registry.is_alive(id), "launched owned process should be alive"); + assert!( + registry.is_alive(id), + "launched owned process should be alive" + ); let pid = registry .root_pid(id) .expect("registered owned process should expose a root pid"); @@ -193,12 +198,9 @@ fn process_launch_wraps_with_supervisor_when_volt_is_available() { let mut registry = ProcessRegistry::new(); let id = launch_id( &mut registry, - ProcessLaunchSpec::new( - synthetic_sleep_command(60).0, - synthetic_sleep_command(60).1, - ) - .with_workspace(WorkspaceId::from_raw(2)) - .with_process_supervisor_exe(supervisor), + ProcessLaunchSpec::new(synthetic_sleep_command(60).0, synthetic_sleep_command(60).1) + .with_workspace(WorkspaceId::from_raw(2)) + .with_process_supervisor_exe(supervisor), ); let pid = registry.root_pid(id).expect("root pid"); #[cfg(windows)] @@ -291,7 +293,9 @@ fn launch_interactive_root_registers_workspace_and_close_kills_tree() { must(registry.workspace_close(workspace, Duration::from_millis(200))); assert!(!registry.is_alive(id)); assert!( - wait_until(Duration::from_secs(3), || !owned_process_pid_alive(root_pid)), + wait_until(Duration::from_secs(3), || !owned_process_pid_alive( + root_pid + )), "interactive root {root_pid} must die on Workspace Close" ); } @@ -307,7 +311,9 @@ fn launch_interactive_session_runs_spawn_inside_process_launch() { assert_eq!(registry.root_pid(id), Some(reported_pid)); must(registry.application_quit(Duration::from_millis(200))); assert!( - wait_until(Duration::from_secs(3), || !owned_process_pid_alive(reported_pid)), + wait_until(Duration::from_secs(3), || !owned_process_pid_alive( + reported_pid + )), "interactive session root must die on Application Quit" ); } @@ -318,9 +324,8 @@ fn job_manager_launch_is_registry_owned_and_dies_on_workspace_close() { let mut jobs = JobManager::with_registry(Arc::clone(®istry)); let workspace = WorkspaceId::from_raw(65); let (program, args) = synthetic_sleep_command(60); - let handle = must(jobs.spawn( - JobSpec::command("owned-job", program, args).with_workspace(workspace), - )); + let handle = + must(jobs.spawn(JobSpec::command("owned-job", program, args).with_workspace(workspace))); assert!( wait_until(Duration::from_secs(2), || { registry @@ -396,9 +401,14 @@ fn workspace_close_kills_processes_tagged_for_that_workspace_only() { registry.is_alive(keep_id), "other workspace process must survive" ); - assert!(!registry.is_alive(close_id), "closed workspace process must die"); assert!( - wait_until(Duration::from_secs(2), || !owned_process_pid_alive(close_pid)), + !registry.is_alive(close_id), + "closed workspace process must die" + ); + assert!( + wait_until(Duration::from_secs(2), || !owned_process_pid_alive( + close_pid + )), "closed workspace root must not remain as an orphan" ); @@ -426,7 +436,10 @@ fn share_key_keeps_process_until_last_workspace_tag_drops() { let first_id = launch_id(&mut registry, first_spec); let second_id = launch_id(&mut registry, second_spec); - assert_eq!(first_id, second_id, "share key should reuse the owned process"); + assert_eq!( + first_id, second_id, + "share key should reuse the owned process" + ); let pid = registry.root_pid(first_id).expect("shared pid"); must(registry.workspace_close(first, Duration::from_millis(200))); @@ -499,3 +512,88 @@ fn owner_crash_leaves_no_orphans_for_launch_created_tree() { ); let _ = fs::remove_file(child_pid_file); } + +#[test] +fn protocol_stdio_launch_exposes_stdin_and_stdout() { + let mut registry = ProcessRegistry::new(); + let (program, args) = synthetic_sleep_command(60); + let launched = must( + registry.launch(with_supervisor( + ProcessLaunchSpec::new(program, args) + .with_workspace(WorkspaceId::from_raw(61)) + .with_stdio(ProcessLaunchStdio::Protocol), + )), + ); + assert!( + launched.stdin.is_some(), + "protocol launch must expose stdin" + ); + assert!( + launched.stdout.is_some(), + "protocol launch must expose stdout" + ); + assert!(launched.stderr.is_none(), "protocol launch discards stderr"); + assert!(registry.is_alive(launched.id)); + must(registry.application_quit(Duration::from_millis(200))); +} + +#[test] +fn language_tooling_shared_share_key_keeps_then_kills_on_last_close() { + let mut registry = ProcessRegistry::new(); + let first = WorkspaceId::from_raw(71); + let second = WorkspaceId::from_raw(72); + let share = language_server_share_key("rust-analyzer", Some(Path::new("/demo/root"))); + let (program, args) = synthetic_sleep_command(60); + + let first_id = launch_id( + &mut registry, + with_supervisor( + ProcessLaunchSpec::new(program.clone(), args.clone()) + .with_workspace(first) + .with_share_key(share.clone()) + .with_stdio(ProcessLaunchStdio::Protocol), + ), + ); + let second_id = launch_id( + &mut registry, + with_supervisor( + ProcessLaunchSpec::new(program, args) + .with_workspace(second) + .with_share_key(share) + .with_stdio(ProcessLaunchStdio::Protocol), + ), + ); + assert_eq!(first_id, second_id); + let pid = registry.root_pid(first_id).expect("shared pid"); + + must(registry.workspace_close(first, Duration::from_millis(200))); + assert!(registry.is_alive(first_id)); + assert!(owned_process_pid_alive(pid)); + + must(registry.workspace_close(second, Duration::from_millis(200))); + assert!(!registry.is_alive(first_id)); + assert!(wait_until(Duration::from_secs(2), || { + !owned_process_pid_alive(pid) + })); +} + +#[test] +fn language_tooling_single_tagged_helper_dies_on_quit() { + let mut registry = ProcessRegistry::new(); + let workspace = WorkspaceId::from_raw(81); + let (program, args) = synthetic_sleep_command(60); + let id = launch_id( + &mut registry, + with_supervisor( + ProcessLaunchSpec::new(program, args) + .with_workspace(workspace) + .with_stdio(ProcessLaunchStdio::Protocol), + ), + ); + let pid = registry.root_pid(id).expect("helper pid"); + must(registry.application_quit(Duration::from_millis(200))); + assert!(!registry.is_alive(id)); + assert!(wait_until(Duration::from_secs(2), || { + !owned_process_pid_alive(pid) + })); +} diff --git a/crates/editor-lsp/src/client/documents.rs b/crates/editor-lsp/src/client/documents.rs index 7a978bf7..460305b8 100644 --- a/crates/editor-lsp/src/client/documents.rs +++ b/crates/editor-lsp/src/client/documents.rs @@ -1,5 +1,7 @@ use std::{collections::BTreeSet, path::Path, sync::Arc}; +use editor_jobs::WorkspaceId; + use super::session::*; use super::types::*; @@ -22,7 +24,21 @@ impl LspClientManager { root: Option<&Path>, edits: Option<&[editor_buffer::TextEdit]>, ) -> Result, LspClientError> { - let sessions = self.ensure_sessions_for_path(path, root, None, false)?; + self.sync_buffer_with_edits_for_workspace(path, text, revision, root, edits, None) + } + + /// Syncs a buffer and tags live Sessions for the editor Workspace. + pub fn sync_buffer_with_edits_for_workspace( + &self, + path: &Path, + text: impl Into, + revision: u64, + root: Option<&Path>, + edits: Option<&[editor_buffer::TextEdit]>, + workspace_id: Option, + ) -> Result, LspClientError> { + let workspace_id = workspace_id.map(WorkspaceId::from_raw); + let sessions = self.ensure_sessions_for_path(path, root, None, false, workspace_id)?; self.sync_buffer_to_sessions(path, text.into(), revision, sessions, edits) } @@ -55,7 +71,7 @@ impl LspClientManager { session_root: Option<&Path>, edits: Option<&[editor_buffer::TextEdit]>, ) -> Result, LspClientError> { - let handle = self.ensure_session_handle(server_id, session_root)?; + let handle = self.ensure_session_handle(server_id, session_root, None)?; self.sync_buffer_to_sessions(path, text.into(), revision, vec![handle], edits) } diff --git a/crates/editor-lsp/src/client/manager.rs b/crates/editor-lsp/src/client/manager.rs index 5e0c9fd2..f0d45ecb 100644 --- a/crates/editor-lsp/src/client/manager.rs +++ b/crates/editor-lsp/src/client/manager.rs @@ -7,6 +7,8 @@ use std::{ }, }; +use editor_jobs::{ProcessRegistry, WorkspaceId}; + use crate::{LanguageServerRegistry, LspError}; use super::session::*; @@ -14,8 +16,17 @@ use super::types::*; impl LspClientManager { pub fn new(registry: LanguageServerRegistry) -> Self { + Self::with_process_registry(registry, Arc::new(Mutex::new(ProcessRegistry::new()))) + } + + /// Creates a manager that registers language-server trees in a shared Process Registry. + pub fn with_process_registry( + registry: LanguageServerRegistry, + process_registry: Arc>, + ) -> Self { Self { registry, + process_registry, state: Arc::new(Mutex::new(LspClientState::default())), transport_log: Arc::new(Mutex::new(LspTransportLog::new(TRANSPORT_LOG_MAX_ENTRIES))), notifications: Arc::new(Mutex::new(LspNotificationLog::new( @@ -33,6 +44,11 @@ impl LspClientManager { &self.registry } + /// Returns the Process Registry used for language-server Owned Processes. + pub fn process_registry(&self) -> Arc> { + Arc::clone(&self.process_registry) + } + pub(crate) fn live_session_for_server( &self, server_id: &str, @@ -172,7 +188,19 @@ impl LspClientManager { server_id: &str, root: Option<&Path>, ) -> Result<(), LspClientError> { - let _ = self.ensure_session_handle(server_id, root)?; + let _ = self.ensure_session_handle(server_id, root, None)?; + Ok(()) + } + + /// Ensures a live Session and tags it for the editor Workspace. + pub fn ensure_session_for_workspace( + &self, + server_id: &str, + root: Option<&Path>, + workspace_id: u64, + ) -> Result<(), LspClientError> { + let _ = + self.ensure_session_handle(server_id, root, Some(WorkspaceId::from_raw(workspace_id)))?; Ok(()) } @@ -180,6 +208,7 @@ impl LspClientManager { &self, server_id: &str, root: Option<&Path>, + workspace_id: Option, ) -> Result, LspClientError> { let key = SessionKey::new(server_id, root); { @@ -191,6 +220,9 @@ impl LspClientManager { if existing.is_disconnected() { state.sessions.remove(&key); } else { + if let Some(workspace_id) = workspace_id { + existing.attach_workspace_tag(workspace_id)?; + } return Ok(existing); } } @@ -215,6 +247,8 @@ impl LspClientManager { runtime_override, initialization_options_override, self.session_shared_state(), + Arc::clone(&self.process_registry), + workspace_id, )?; let mut state = self .state @@ -225,6 +259,79 @@ impl LspClientManager { Ok(handle) } + /// Protocol-shutdown Language Server Sessions that would die on this Workspace Close. + /// Shutdown runs on background threads so Workspace Close tree signaling can overlap. + pub fn prepare_workspace_close(&self, workspace_id: u64) -> Result<(), LspClientError> { + let workspace_id = WorkspaceId::from_raw(workspace_id); + let sessions = { + let state = self + .state + .lock() + .map_err(|_| LspClientError::Protocol("LSP state mutex poisoned".to_owned()))?; + state.sessions.values().cloned().collect::>() + }; + let to_shutdown = { + let registry = self.process_registry.lock().map_err(|_| { + LspClientError::Protocol("process registry mutex poisoned".to_owned()) + })?; + sessions + .into_iter() + .filter(|session| { + session.owned_process_id().is_some_and(|owned_id| { + registry.will_teardown_on_workspace_close(owned_id, workspace_id) + }) + }) + .collect::>() + }; + for session in to_shutdown { + std::thread::spawn(move || { + session.protocol_shutdown(); + }); + } + Ok(()) + } + + /// Removes Language Server Sessions whose Owned Processes are no longer registered. + pub fn reap_dead_sessions(&self) -> Result<(), LspClientError> { + let stale_keys = { + let state = self + .state + .lock() + .map_err(|_| LspClientError::Protocol("LSP state mutex poisoned".to_owned()))?; + let registry = self.process_registry.lock().map_err(|_| { + LspClientError::Protocol("process registry mutex poisoned".to_owned()) + })?; + state + .sessions + .iter() + .filter_map(|(key, session)| { + let owned_id = session.owned_process_id()?; + if registry.root_pid(owned_id).is_none() { + Some(key.clone()) + } else { + None + } + }) + .collect::>() + }; + self.shutdown_sessions(&stale_keys) + } + + /// Protocol-shutdown every live Language Server Session (Application Quit grace). + pub fn prepare_application_quit(&self) -> Result<(), LspClientError> { + let sessions = { + let state = self + .state + .lock() + .map_err(|_| LspClientError::Protocol("LSP state mutex poisoned".to_owned()))?; + state.sessions.values().cloned().collect::>() + }; + for session in sessions { + session.protocol_shutdown(); + } + Ok(()) + } + pub(crate) fn shutdown_sessions( &self, session_keys: &BTreeSet, @@ -233,16 +340,18 @@ impl LspClientManager { return Ok(()); } let mut dirty_paths = Vec::new(); + let mut removed = Vec::new(); { let mut state = self .state .lock() .map_err(|_| LspClientError::Protocol("LSP state mutex poisoned".to_owned()))?; for key in session_keys { - if let Some(session) = state.sessions.remove(key) - && let Ok(diagnostics) = session.diagnostics.lock() - { - dirty_paths.extend(diagnostics.keys().cloned()); + if let Some(session) = state.sessions.remove(key) { + if let Ok(diagnostics) = session.diagnostics.lock() { + dirty_paths.extend(diagnostics.keys().cloned()); + } + removed.push(session); } state.start_failures.remove(key); } @@ -252,6 +361,9 @@ impl LspClientManager { } } } + for session in removed { + session.protocol_shutdown(); + } if let Ok(mut dirty) = self.dirty_diagnostic_paths.lock() { dirty.extend(dirty_paths); } @@ -266,6 +378,7 @@ impl LspClientManager { root: Option<&Path>, preferred_server_id: Option<&str>, force_retry: bool, + workspace_id: Option, ) -> Result>, LspClientError> { let mut handles = if preferred_server_id.is_none() { self.tracked_sessions_for_path(path)? @@ -292,6 +405,13 @@ impl LspClientManager { for session in session_plans { let key = SessionKey::new(session.server_id(), session.root().map(PathBuf::as_path)); if handled_keys.contains(&key) { + if let Some(workspace_id) = workspace_id { + for handle in &handles { + if handle.key == key { + handle.attach_workspace_tag(workspace_id)?; + } + } + } continue; } let (existing, runtime_override, initialization_options_override, start_failed) = { @@ -318,6 +438,9 @@ impl LspClientManager { ) }; if let Some(existing) = existing { + if let Some(workspace_id) = workspace_id { + existing.attach_workspace_tag(workspace_id)?; + } handled_keys.insert(key); handles.push(existing); continue; @@ -330,6 +453,8 @@ impl LspClientManager { runtime_override, initialization_options_override, self.session_shared_state(), + Arc::clone(&self.process_registry), + workspace_id, ) { Ok(handle) => { self.state diff --git a/crates/editor-lsp/src/client/requests.rs b/crates/editor-lsp/src/client/requests.rs index d8a9ca40..355dc170 100644 --- a/crates/editor-lsp/src/client/requests.rs +++ b/crates/editor-lsp/src/client/requests.rs @@ -8,6 +8,7 @@ use std::{ }; use editor_buffer::{TextPoint, TextRange}; +use editor_jobs::WorkspaceId; use lsp_types::TextDocumentSyncKind; use serde_json::Value; @@ -304,7 +305,26 @@ impl LspClientManager { server_id: &str, edits: Option<&[editor_buffer::TextEdit]>, ) -> Result, LspClientError> { - let sessions = self.ensure_sessions_for_path(path, root, Some(server_id), true)?; + self.start_buffer_server_with_edits_for_workspace( + path, text, revision, root, server_id, edits, None, + ) + } + + /// Starts/syncs onto an exact server and tags the Session for the editor Workspace. + #[allow(clippy::too_many_arguments)] + pub fn start_buffer_server_with_edits_for_workspace( + &self, + path: &Path, + text: impl Into, + revision: u64, + root: Option<&Path>, + server_id: &str, + edits: Option<&[editor_buffer::TextEdit]>, + workspace_id: Option, + ) -> Result, LspClientError> { + let workspace_id = workspace_id.map(WorkspaceId::from_raw); + let sessions = + self.ensure_sessions_for_path(path, root, Some(server_id), true, workspace_id)?; self.sync_buffer_to_sessions(path, text.into(), revision, sessions, edits) } @@ -583,7 +603,9 @@ impl LspClientManager { Ok(Arc::new(LspSessionHandle { key: SessionKey::new(server_id, session.root().map(PathBuf::as_path)), session, - child: Mutex::new(child), + child: Some(Mutex::new(child)), + owned_process_id: None, + process_registry: None, writer: Arc::new(Mutex::new(writer)), pending: Arc::new(Mutex::new(BTreeMap::new())), diagnostics: Arc::new(Mutex::new(diagnostics_by_path)), diff --git a/crates/editor-lsp/src/client/session.rs b/crates/editor-lsp/src/client/session.rs index 612ebd09..8d4fb456 100644 --- a/crates/editor-lsp/src/client/session.rs +++ b/crates/editor-lsp/src/client/session.rs @@ -12,6 +12,7 @@ use std::{ }; use editor_buffer::{TextPoint, TextRange}; +use editor_jobs::{OwnedProcessId, ProcessRegistry, WorkspaceId}; use lsp_types::{ ClientInfo, CompletionParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentFormattingParams, @@ -22,11 +23,12 @@ use lsp_types::{ WorkspaceFolder, notification::{ DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, DidSaveTextDocument, - Initialized, Notification, + Exit, Initialized, Notification, }, request::{ CodeActionRequest, Completion, Formatting, GotoDefinition, GotoImplementation, - HoverRequest, Initialize, RangeFormatting, References, Request, SignatureHelpRequest, + HoverRequest, Initialize, RangeFormatting, References, Request, Shutdown, + SignatureHelpRequest, }, }; use serde::Serialize; @@ -39,7 +41,10 @@ use super::types::*; pub(crate) struct LspSessionHandle { pub(crate) key: SessionKey, pub(crate) session: LanguageServerSession, - pub(crate) child: Mutex, + /// Inert/memory test children only. Live Process Launch sessions leave this empty. + pub(crate) child: Option>, + pub(crate) owned_process_id: Option, + pub(crate) process_registry: Option>>, pub(crate) writer: Arc>, pub(crate) pending: PendingResponseMap, pub(crate) diagnostics: DiagnosticsByPath, @@ -74,7 +79,17 @@ impl Drop for LspSessionHandle { &self.key.server_id, "terminating language server process", ); - if let Ok(mut child) = self.child.lock() { + self.disconnected.store(true, Ordering::Release); + if let (Some(id), Some(registry)) = + (self.owned_process_id.take(), self.process_registry.take()) + && let Ok(mut registry) = registry.lock() + { + let _ = registry.teardown_process(id, Duration::from_millis(200)); + return; + } + if let Some(child) = self.child.as_ref() + && let Ok(mut child) = child.lock() + { let _ = child.kill(); let _ = child.wait(); } @@ -87,6 +102,8 @@ impl LspSessionHandle { runtime_override: Option, initialization_options_override: Option, shared: LspSessionSharedState, + process_registry: Arc>, + workspace_id: Option, ) -> Result, LspClientError> { let LspSessionSharedState { transport_log, @@ -100,11 +117,17 @@ impl LspSessionHandle { let launch_args = launch.args().to_vec(); let launch_cwd = launch.cwd().cloned(); let launch_env = launch.env().to_vec(); - let mut child = spawn_lsp_command( - &launch_program, - &launch_args, - launch_cwd.as_deref(), - &launch_env, + let (owned_process_id, stdin, stdout, pid) = launch_lsp_owned_process( + &process_registry, + LspLaunchRequest { + program: &launch_program, + args: &launch_args, + cwd: launch_cwd.as_deref(), + env: &launch_env, + server_id: session.server_id(), + root: session.root().map(|path| path.as_path()), + workspace_id, + }, ) .map_err(|error| { LspClientError::Protocol(format!( @@ -112,18 +135,6 @@ impl LspSessionHandle { session.server_id() )) })?; - let stdin = child.stdin.take().ok_or_else(|| { - LspClientError::Protocol(format!( - "language server `{}` is missing stdin pipe", - session.server_id() - )) - })?; - let stdout = child.stdout.take().ok_or_else(|| { - LspClientError::Protocol(format!( - "language server `{}` is missing stdout pipe", - session.server_id() - )) - })?; let key = SessionKey::new(session.server_id(), session.root().map(PathBuf::as_path)); let writer = Arc::new(Mutex::new(stdin)); @@ -138,11 +149,12 @@ impl LspSessionHandle { initialization_options_override.as_ref(), ); let disconnected = Arc::new(AtomicBool::new(false)); - let pid = child.id(); let handle = Arc::new(Self { key, session, - child: Mutex::new(child), + child: None, + owned_process_id: Some(owned_process_id), + process_registry: Some(process_registry), writer: Arc::clone(&writer), pending: Arc::clone(&pending), diagnostics: Arc::clone(&diagnostics), @@ -211,6 +223,37 @@ impl LspSessionHandle { Ok(handle) } + pub(crate) fn attach_workspace_tag( + &self, + workspace_id: WorkspaceId, + ) -> Result<(), LspClientError> { + let Some(id) = self.owned_process_id else { + return Ok(()); + }; + let Some(registry) = &self.process_registry else { + return Ok(()); + }; + let mut registry = registry + .lock() + .map_err(|_| LspClientError::Protocol("process registry mutex poisoned".to_owned()))?; + registry + .tag_workspace(id, workspace_id) + .map_err(|error| LspClientError::Protocol(error.to_string())) + } + + pub(crate) fn protocol_shutdown(&self) { + if self.disconnected.load(Ordering::Acquire) { + return; + } + let _ = self.request_typed::(()); + let _ = self.notify_typed::(()); + self.disconnected.store(true, Ordering::Release); + } + + pub(crate) fn owned_process_id(&self) -> Option { + self.owned_process_id + } + pub(crate) fn server_id(&self) -> &str { self.session.server_id() } diff --git a/crates/editor-lsp/src/client/tests.rs b/crates/editor-lsp/src/client/tests.rs index 1d9c28d5..f170c585 100644 --- a/crates/editor-lsp/src/client/tests.rs +++ b/crates/editor-lsp/src/client/tests.rs @@ -1415,7 +1415,9 @@ fn test_session_handle_in_workspace( Arc::new(LspSessionHandle { key: SessionKey::new(server_id, session.root().map(PathBuf::as_path)), session, - child: Mutex::new(child), + child: Some(Mutex::new(child)), + owned_process_id: None, + process_registry: None, writer: Arc::new(Mutex::new(writer)), pending: Arc::new(Mutex::new(BTreeMap::new())), diagnostics: Arc::new(Mutex::new(diagnostics_by_path)), diff --git a/crates/editor-lsp/src/client/types.rs b/crates/editor-lsp/src/client/types.rs index b5f71fb5..ac81318c 100644 --- a/crates/editor-lsp/src/client/types.rs +++ b/crates/editor-lsp/src/client/types.rs @@ -2,7 +2,7 @@ use std::{ collections::{BTreeMap, BTreeSet}, io::{BufRead, BufReader, Read, Write}, path::{Path, PathBuf}, - process::{Child, ChildStdin, Command, Stdio}, + process::{Child, ChildStdin, ChildStdout, Command, Stdio}, sync::{ Arc, Mutex, atomic::{AtomicBool, AtomicU64, Ordering}, @@ -13,7 +13,10 @@ use std::{ }; use editor_buffer::{TextPoint, TextRange}; -use editor_jobs::{ProcessSupervisionMode, supervised_command_if_resolved}; +use editor_jobs::{ + OwnedProcessId, ProcessLaunchSpec, ProcessLaunchStdio, ProcessRegistry, ProcessSupervisionMode, + WorkspaceId, language_server_share_key, +}; use lsp_types::{ ClientCapabilities, CodeActionContext, CodeActionParams, CodeActionTriggerKind, Diagnostic as LspDiagnostic, DiagnosticSeverity as LspDiagnosticSeverity, Documentation, @@ -1004,6 +1007,7 @@ impl From for LspClientError { #[derive(Debug, Clone)] pub struct LspClientManager { pub(crate) registry: LanguageServerRegistry, + pub(crate) process_registry: Arc>, pub(crate) state: Arc>, pub(crate) transport_log: TransportLog, pub(crate) notifications: NotificationLog, @@ -1313,89 +1317,118 @@ pub(crate) fn configure_lsp_command(_command: &mut Command) { } } -pub(crate) fn spawn_lsp_command( - program: &str, - args: &[String], - cwd: Option<&Path>, - env: &[(String, String)], -) -> std::io::Result { - #[cfg(not(windows))] - let spawn_result = build_lsp_command(program, args, cwd, env, None).spawn(); +pub(crate) struct LspLaunchRequest<'a> { + pub(crate) program: &'a str, + pub(crate) args: &'a [String], + pub(crate) cwd: Option<&'a Path>, + pub(crate) env: &'a [(String, String)], + pub(crate) server_id: &'a str, + pub(crate) root: Option<&'a Path>, + pub(crate) workspace_id: Option, +} + +pub(crate) fn launch_lsp_owned_process( + registry: &Arc>, + request: LspLaunchRequest<'_>, +) -> Result<(OwnedProcessId, ChildStdin, ChildStdout, u32), String> { + let share_key = language_server_share_key(request.server_id, request.root); + let mut attempts = Vec::new(); + attempts.push((request.program.to_owned(), request.env.to_vec())); - #[cfg(windows)] - let mut spawn_result = build_lsp_command(program, args, cwd, env, None).spawn(); #[cfg(windows)] { - let should_retry = matches!( - &spawn_result, - Err(error) if windows_should_retry_spawn_error(error) - ); - if should_retry { - for candidate in windows_launch_program_candidates(program) { - spawn_result = build_lsp_command(&candidate, args, cwd, env, None).spawn(); - match &spawn_result { - Ok(_) => break, - Err(error) if windows_should_retry_spawn_error(error) => {} - Err(_) => break, - } + for candidate in windows_launch_program_candidates(request.program) { + attempts.push((candidate, request.env.to_vec())); + } + if let Some(fnm_env) = windows_fnm_environment(request.cwd, request.env) { + for candidate in windows_fnm_launch_program_candidates(request.program, &fnm_env) { + let mut merged = request.env.to_vec(); + merged.extend(fnm_env.iter().cloned()); + attempts.push((candidate, merged)); } } - let should_retry_with_fnm = matches!( - &spawn_result, - Err(error) if windows_should_retry_spawn_error(error) - ); - if should_retry_with_fnm && let Some(fnm_env) = windows_fnm_environment(cwd, env) { - for candidate in windows_fnm_launch_program_candidates(program, &fnm_env) { - spawn_result = - build_lsp_command(&candidate, args, cwd, env, Some(&fnm_env)).spawn(); - match &spawn_result { - Ok(_) => break, - Err(error) if windows_should_retry_spawn_error(error) => {} - Err(_) => break, - } + if let Some(nvm_env) = windows_nvm_environment(request.cwd, request.env) { + for candidate in windows_nvm_launch_program_candidates(request.program, &nvm_env) { + let mut merged = request.env.to_vec(); + merged.extend(nvm_env.iter().cloned()); + attempts.push((candidate, merged)); } } - let should_retry_with_nvm = matches!( - &spawn_result, - Err(error) if windows_should_retry_spawn_error(error) - ); - if should_retry_with_nvm && let Some(nvm_env) = windows_nvm_environment(cwd, env) { - for candidate in windows_nvm_launch_program_candidates(program, &nvm_env) { - spawn_result = - build_lsp_command(&candidate, args, cwd, env, Some(&nvm_env)).spawn(); - match &spawn_result { - Ok(_) => break, - Err(error) if windows_should_retry_spawn_error(error) => {} - Err(_) => break, - } + } + + let mut last_error = None; + for (candidate, attempt_env) in attempts { + match launch_lsp_attempt( + registry, + &candidate, + request.args, + request.cwd, + &attempt_env, + share_key.clone(), + request.workspace_id, + ) { + Ok(result) => return Ok(result), + Err(error) => { + last_error = Some(error); } } } - spawn_result + Err(last_error.unwrap_or_else(|| "failed to launch language server".to_owned())) } +fn launch_lsp_attempt( + registry: &Arc>, + program: &str, + args: &[String], + cwd: Option<&Path>, + env: &[(String, String)], + share_key: editor_jobs::ShareKey, + workspace_id: Option, +) -> Result<(OwnedProcessId, ChildStdin, ChildStdout, u32), String> { + let mut env = env.to_vec(); + editor_tool_install::merge_effective_path(&mut env); + let mut spec = ProcessLaunchSpec::new(program, args.to_vec()) + .with_stdio(ProcessLaunchStdio::Protocol) + .with_mode(ProcessSupervisionMode::Background) + .with_share_key(share_key); + if let Some(workspace_id) = workspace_id { + spec = spec.with_workspace(workspace_id); + } + if let Some(cwd) = cwd { + spec = spec.with_current_dir(cwd); + } + spec.env = env; + + let mut registry = registry + .lock() + .map_err(|_| "process registry mutex poisoned".to_owned())?; + let mut launched = registry.launch(spec).map_err(|error| error.to_string())?; + let stdin = launched + .stdin + .take() + .ok_or_else(|| "language server is missing stdin pipe".to_owned())?; + let stdout = launched + .stdout + .take() + .ok_or_else(|| "language server is missing stdout pipe".to_owned())?; + let pid = registry + .root_pid(launched.id) + .ok_or_else(|| "language server owned process missing root pid".to_owned())?; + Ok((launched.id, stdin, stdout, pid)) +} + +#[cfg(all(windows, test))] pub(crate) fn build_lsp_command( program: &str, args: &[String], cwd: Option<&Path>, env: &[(String, String)], - #[cfg(windows)] runtime_env: Option<&[(String, String)]>, - #[cfg(not(windows))] _runtime_env: Option<&[(String, String)]>, + runtime_env: Option<&[(String, String)]>, ) -> Command { - let (program, args) = supervised_command_if_resolved( - program, - args, - env, - #[cfg(windows)] - runtime_env, - #[cfg(not(windows))] - None, - ProcessSupervisionMode::Background, - ); - let mut command = Command::new(&program); + let mut command = Command::new(program); configure_lsp_command(&mut command); command - .args(&args) + .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()); @@ -1404,14 +1437,11 @@ pub(crate) fn build_lsp_command( } let mut env = env.to_vec(); editor_tool_install::merge_effective_path(&mut env); - #[cfg(windows)] if let Some(runtime_env) = runtime_env { apply_windows_runtime_environment(&mut command, &env, runtime_env); } else { apply_command_environment(&mut command, &env); } - #[cfg(not(windows))] - apply_command_environment(&mut command, &env); command } @@ -1437,7 +1467,7 @@ pub(crate) fn windows_launch_program_candidates(program: &str) -> Vec { candidates } -#[cfg(windows)] +#[cfg(all(windows, test))] pub(crate) fn windows_should_retry_spawn_error(error: &std::io::Error) -> bool { error.kind() == std::io::ErrorKind::NotFound || error.raw_os_error() == Some(193) } @@ -1630,7 +1660,7 @@ pub(crate) fn parse_windows_cmd_environment(output: &str) -> Option agent_client_protocol::Result { + let workspace_id = { + let state = self.state.borrow(); + state + .sessions + .get(&args.session_id) + .map(|session| session.workspace_id) + } + .ok_or_else(|| Error::resource_not_found(None))?; let env = args .env .iter() .map(|variable| (variable.name.clone(), variable.value.clone())) .collect::>(); - let mut child = spawn_background_command( + let process_registry = self.state.borrow().process_registry.clone(); + let mut launched = launch_owned_background_command( + &process_registry, + editor_jobs::WorkspaceId::from_raw(workspace_id.get()), &args.command, &args.args, args.cwd.as_deref().map(Path::new), @@ -136,13 +147,13 @@ impl Client for AcpClient { BackgroundCommandPipes::TERMINAL, ) .await - .map_err(|error| Error::internal_error().data(error.to_string()))?; + .map_err(|error| Error::internal_error().data(error))?; let output = Rc::new(RefCell::new(String::new())); let exit_status = Rc::new(RefCell::new(None)); - if let Some(stdout) = child.stdout.take() { + if let Some(stdout) = launched.stdout.take() { spawn_terminal_reader(output.clone(), stdout); } - if let Some(stderr) = child.stderr.take() { + if let Some(stderr) = launched.stderr.take() { spawn_terminal_reader(output.clone(), stderr); } let terminal_id = TerminalId::new(format!( @@ -156,7 +167,7 @@ impl Client for AcpClient { output, exit_status, output_limit: args.output_byte_limit, - child, + owned_process_id: launched.owned_process_id, }, ); Ok(CreateTerminalResponse::new(terminal_id)) @@ -167,14 +178,15 @@ impl Client for AcpClient { args: TerminalOutputRequest, ) -> agent_client_protocol::Result { let mut state = self.state.borrow_mut(); + let process_registry = state.process_registry.clone(); let terminal = state .terminals .get_mut(&args.terminal_id) .ok_or_else(|| Error::resource_not_found(None))?; if terminal.exit_status.borrow().is_none() - && let Ok(Some(status)) = terminal.child.try_wait() + && let Some(code) = try_owned_process_exit(&process_registry, terminal.owned_process_id) { - let exit = TerminalExitStatus::new().exit_code(status.code().map(|code| code as u32)); + let exit = TerminalExitStatus::new().exit_code(Some(code as u32)); *terminal.exit_status.borrow_mut() = Some(exit); } let output = terminal.output.borrow().clone(); @@ -190,16 +202,16 @@ impl Client for AcpClient { &self, args: WaitForTerminalExitRequest, ) -> agent_client_protocol::Result { - let terminal = self.state.borrow_mut().terminals.remove(&args.terminal_id); - let Some(mut terminal) = terminal else { - return Err(Error::resource_not_found(None)); + let (terminal, process_registry) = { + let mut state = self.state.borrow_mut(); + let terminal = state + .terminals + .remove(&args.terminal_id) + .ok_or_else(|| Error::resource_not_found(None))?; + (terminal, state.process_registry.clone()) }; - let status = terminal - .child - .wait() - .await - .map_err(|error| Error::internal_error().data(error.to_string()))?; - let exit = TerminalExitStatus::new().exit_code(status.code().map(|code| code as u32)); + let code = wait_owned_process_exit(&process_registry, terminal.owned_process_id).await; + let exit = TerminalExitStatus::new().exit_code(code.map(|code| code as u32)); *terminal.exit_status.borrow_mut() = Some(exit.clone()); let terminal_id = args.terminal_id.clone(); self.state @@ -214,8 +226,9 @@ impl Client for AcpClient { args: ReleaseTerminalRequest, ) -> agent_client_protocol::Result { let terminal = self.state.borrow_mut().terminals.remove(&args.terminal_id); - if let Some(mut terminal) = terminal { - let _ = terminal.child.kill().await; + if let Some(terminal) = terminal { + let process_registry = self.state.borrow().process_registry.clone(); + teardown_owned_process(&process_registry, terminal.owned_process_id); } Ok(ReleaseTerminalResponse::new()) } @@ -225,10 +238,11 @@ impl Client for AcpClient { args: KillTerminalRequest, ) -> agent_client_protocol::Result { let terminal = self.state.borrow_mut().terminals.remove(&args.terminal_id); - let Some(mut terminal) = terminal else { + let Some(terminal) = terminal else { return Err(Error::resource_not_found(None)); }; - let _ = terminal.child.kill().await; + let process_registry = self.state.borrow().process_registry.clone(); + teardown_owned_process(&process_registry, terminal.owned_process_id); self.state .borrow_mut() .terminals diff --git a/crates/editor-sdl/src/shell/acp/launch.rs b/crates/editor-sdl/src/shell/acp/launch.rs index 64a46d93..572392f1 100644 --- a/crates/editor-sdl/src/shell/acp/launch.rs +++ b/crates/editor-sdl/src/shell/acp/launch.rs @@ -1,9 +1,13 @@ use std::{ env, path::{Path, PathBuf}, + sync::{Arc, Mutex}, }; -use editor_jobs::{ProcessSupervisionMode, supervised_command_if_resolved}; +use editor_jobs::{ + OwnedProcessId, ProcessLaunchSpec, ProcessLaunchStdio, ProcessRegistry, ProcessSupervisionMode, + WorkspaceId as ProcessWorkspaceId, +}; use tokio::process::Command; #[cfg(windows)] @@ -29,7 +33,7 @@ impl BackgroundCommandPipes { pub(crate) const ACP_CLIENT: Self = Self { stdin: true, stdout: true, - stderr: true, + stderr: false, }; pub(crate) const TERMINAL: Self = Self { @@ -37,127 +41,166 @@ impl BackgroundCommandPipes { stdout: true, stderr: true, }; + + pub(crate) fn to_launch_stdio(self) -> ProcessLaunchStdio { + if self.stdin && self.stdout && !self.stderr { + ProcessLaunchStdio::Protocol + } else if self.stdin || self.stdout || self.stderr { + ProcessLaunchStdio::Piped + } else { + ProcessLaunchStdio::Null + } + } +} + +pub(crate) struct OwnedBackgroundLaunch { + pub(crate) owned_process_id: OwnedProcessId, + pub(crate) stdin: Option, + pub(crate) stdout: Option, + pub(crate) stderr: Option, } -pub(crate) async fn spawn_background_command( +pub(crate) async fn launch_owned_background_command( + process_registry: &Arc>, + workspace_id: ProcessWorkspaceId, program: &str, args: &[String], cwd: Option<&Path>, env: &[(String, String)], pipes: BackgroundCommandPipes, -) -> std::io::Result { - let mut spawn_result = build_background_command(program, args, cwd, env, pipes, None).spawn(); - - let should_retry = matches!( - &spawn_result, - Err(error) if background_spawn_should_retry(error) - ); - if should_retry { - for candidate in background_command_candidates(program, env, None) { - spawn_result = - build_background_command(&candidate, args, cwd, env, pipes, None).spawn(); - match &spawn_result { - Ok(_) => break, - Err(error) if background_spawn_should_retry(error) => {} - Err(_) => break, +) -> Result { + let stdio = pipes.to_launch_stdio(); + let mut attempts: Vec<(String, Vec<(String, String)>)> = Vec::new(); + attempts.push((program.to_owned(), env.to_vec())); + + for candidate in background_command_candidates(program, env, None) { + attempts.push((candidate, env.to_vec())); + } + + let mut last_error = None; + for (candidate, attempt_env) in &attempts { + match try_owned_launch( + process_registry, + workspace_id, + candidate, + args, + cwd, + attempt_env, + stdio, + ) { + Ok(launched) => return Ok(launched), + Err(error) if owned_launch_should_retry(&error) => { + last_error = Some(error); } + Err(error) => return Err(error), } } - let should_retry = matches!( - &spawn_result, - Err(error) if background_spawn_should_retry(error) - ); - let launch_env = if should_retry { - refreshed_launch_environment(cwd).await - } else { - None - }; - if let Some(launch_env) = launch_env.as_deref() { - for candidate in background_command_candidates(program, env, Some(launch_env)) { - spawn_result = - build_background_command(&candidate, args, cwd, env, pipes, Some(launch_env)) - .spawn(); - match &spawn_result { - Ok(_) => break, - Err(error) if background_spawn_should_retry(error) => {} - Err(_) => break, + if let Some(launch_env) = refreshed_launch_environment(cwd).await { + for candidate in background_command_candidates(program, env, Some(&launch_env)) { + let mut merged = launch_env.clone(); + merged.extend(env.iter().cloned()); + match try_owned_launch( + process_registry, + workspace_id, + &candidate, + args, + cwd, + &merged, + stdio, + ) { + Ok(launched) => return Ok(launched), + Err(error) if owned_launch_should_retry(&error) => { + last_error = Some(error); + } + Err(error) => return Err(error), } } - } - #[cfg(windows)] - { - let should_retry_with_node_manager = matches!( - &spawn_result, - Err(error) if background_spawn_should_retry(error) - ); - if should_retry_with_node_manager - && let Some(node_manager_env) = - windows_node_manager_environment(cwd, env, launch_env.as_deref()).await + #[cfg(windows)] + if let Some(node_manager_env) = + windows_node_manager_environment(cwd, env, Some(&launch_env)).await { for candidate in background_command_candidates(program, &[], Some(&node_manager_env)) { - spawn_result = build_background_command( + match try_owned_launch( + process_registry, + workspace_id, &candidate, args, cwd, - &[], - pipes, - Some(&node_manager_env), - ) - .spawn(); - match &spawn_result { - Ok(_) => break, - Err(error) if background_spawn_should_retry(error) => {} - Err(_) => break, + &node_manager_env, + stdio, + ) { + Ok(launched) => return Ok(launched), + Err(error) if owned_launch_should_retry(&error) => { + last_error = Some(error); + } + Err(error) => return Err(error), } } } } - spawn_result + Err(last_error.unwrap_or_else(|| format!("failed to launch `{program}`"))) } -pub(crate) fn build_background_command( +fn try_owned_launch( + process_registry: &Arc>, + workspace_id: ProcessWorkspaceId, program: &str, args: &[String], cwd: Option<&Path>, env: &[(String, String)], - pipes: BackgroundCommandPipes, - launch_env: Option<&[(String, String)]>, -) -> Command { - let (program, args) = supervised_command_if_resolved( - program, - args, - env, - launch_env, - ProcessSupervisionMode::Background, - ); - let mut command = Command::new(&program); - configure_background_command(&mut command); - command.args(&args); - apply_background_pipes(&mut command, pipes); + stdio: ProcessLaunchStdio, +) -> Result { + let mut env = env.to_vec(); + editor_tool_install::merge_effective_path(&mut env); + let mut spec = ProcessLaunchSpec::new(program, args.to_vec()) + .with_stdio(stdio) + .with_mode(ProcessSupervisionMode::Background) + .with_workspace(workspace_id); if let Some(cwd) = cwd { - command.current_dir(cwd); - } - if let Some(launch_env) = launch_env { - apply_launch_environment(&mut command, env, launch_env); - } else { - apply_command_environment(&mut command, env); - } - command + spec = spec.with_current_dir(cwd); + } + spec.env = env; + + let mut registry = process_registry + .lock() + .map_err(|_| "process registry mutex poisoned".to_owned())?; + let mut launched = registry.launch(spec).map_err(|error| error.to_string())?; + let stdin = launched + .stdin + .take() + .map(tokio::process::ChildStdin::from_std) + .transpose() + .map_err(|error| error.to_string())?; + let stdout = launched + .stdout + .take() + .map(tokio::process::ChildStdout::from_std) + .transpose() + .map_err(|error| error.to_string())?; + let stderr = launched + .stderr + .take() + .map(tokio::process::ChildStderr::from_std) + .transpose() + .map_err(|error| error.to_string())?; + Ok(OwnedBackgroundLaunch { + owned_process_id: launched.id, + stdin, + stdout, + stderr, + }) } -pub(crate) fn apply_background_pipes(command: &mut Command, pipes: BackgroundCommandPipes) { - if pipes.stdin { - command.stdin(std::process::Stdio::piped()); - } - if pipes.stdout { - command.stdout(std::process::Stdio::piped()); - } - if pipes.stderr { - command.stderr(std::process::Stdio::piped()); - } +fn owned_launch_should_retry(error: &str) -> bool { + let lower = error.to_ascii_lowercase(); + lower.contains("not found") + || lower.contains("cannot find") + || lower.contains("the system cannot find") + || lower.contains("os error 2") + || lower.contains("os error 193") } pub(crate) fn apply_command_environment(command: &mut Command, env: &[(String, String)]) { @@ -312,16 +355,6 @@ pub(crate) fn is_launch_candidate(candidate: &Path) -> bool { .unwrap_or(false) } -#[cfg(windows)] -pub(crate) fn background_spawn_should_retry(error: &std::io::Error) -> bool { - error.kind() == std::io::ErrorKind::NotFound || error.raw_os_error() == Some(193) -} - -#[cfg(not(windows))] -pub(crate) fn background_spawn_should_retry(error: &std::io::Error) -> bool { - error.kind() == std::io::ErrorKind::NotFound -} - #[cfg(windows)] pub(crate) async fn windows_node_manager_environment( cwd: Option<&Path>, diff --git a/crates/editor-sdl/src/shell/acp/manager.rs b/crates/editor-sdl/src/shell/acp/manager.rs index da3a2f09..006b7fec 100644 --- a/crates/editor-sdl/src/shell/acp/manager.rs +++ b/crates/editor-sdl/src/shell/acp/manager.rs @@ -9,6 +9,7 @@ use agent_client_protocol::{ SessionModeId, SessionModeState, SessionModelState, }; use base64::Engine as _; +use editor_jobs::ProcessRegistry; use editor_picker::PickerResultOrder; use editor_plugin_api::AcpClient as AcpClientConfig; @@ -352,9 +353,9 @@ pub(crate) struct AcpManager { } impl AcpManager { - pub(crate) fn new() -> Result { + pub(crate) fn new(process_registry: Arc>) -> Result { let (event_tx, event_rx) = mpsc::channel(); - let runtime = AcpRuntime::new(event_tx)?; + let runtime = AcpRuntime::new(event_tx, process_registry)?; Ok(Self { runtime, events: event_rx, @@ -528,6 +529,7 @@ impl AcpManager { if let Err(error) = self.runtime.send(AcpCommand::Connect { config: client, workspace_root, + workspace_id, buffer_id, }) { self.pending_clients.remove(&buffer_id); diff --git a/crates/editor-sdl/src/shell/acp/runtime.rs b/crates/editor-sdl/src/shell/acp/runtime.rs index 4e3bb912..3fef009f 100644 --- a/crates/editor-sdl/src/shell/acp/runtime.rs +++ b/crates/editor-sdl/src/shell/acp/runtime.rs @@ -3,8 +3,9 @@ use std::{ collections::{HashMap, VecDeque}, path::{Path, PathBuf}, rc::Rc, - sync::mpsc, + sync::{Arc, Mutex, mpsc}, thread, + time::Duration, }; use agent_client_protocol::{Agent, ClientSideConnection}; @@ -17,9 +18,9 @@ use agent_client_protocol::{ SessionModeState, SessionModelState, SetSessionConfigOptionRequest, SetSessionModeRequest, SetSessionModelRequest, StopReason, TerminalExitStatus, TerminalId, ToolCall, ToolCallUpdate, }; +use editor_jobs::{OwnedProcessId, ProcessRegistry, WorkspaceId as ProcessWorkspaceId}; use editor_plugin_api::AcpClient as AcpClientConfig; use tokio::{ - io::{AsyncBufReadExt, BufReader}, sync::{mpsc as tokio_mpsc, oneshot}, task::LocalSet, }; @@ -291,6 +292,7 @@ pub(crate) enum AcpCommand { Connect { config: AcpClientConfig, workspace_root: PathBuf, + workspace_id: WorkspaceId, buffer_id: BufferId, }, Prompt { @@ -345,9 +347,12 @@ pub(crate) struct AcpRuntime { } impl AcpRuntime { - pub(crate) fn new(event_tx: mpsc::Sender) -> Result { + pub(crate) fn new( + event_tx: mpsc::Sender, + process_registry: Arc>, + ) -> Result { let (sender, receiver) = tokio_mpsc::unbounded_channel(); - thread::spawn(move || run_acp_runtime(receiver, event_tx)); + thread::spawn(move || run_acp_runtime(receiver, event_tx, process_registry)); Ok(Self { sender }) } @@ -361,6 +366,7 @@ impl AcpRuntime { pub(crate) fn run_acp_runtime( receiver: tokio_mpsc::UnboundedReceiver, event_tx: mpsc::Sender, + process_registry: Arc>, ) { let runtime = match tokio::runtime::Builder::new_current_thread() .enable_all() @@ -370,7 +376,10 @@ pub(crate) fn run_acp_runtime( Err(_) => return, }; let local = LocalSet::new(); - let state = Rc::new(RefCell::new(AcpRuntimeState::new(event_tx))); + let state = Rc::new(RefCell::new(AcpRuntimeState::new( + event_tx, + process_registry, + ))); local.block_on(&runtime, async move { acp_runtime_loop(state, receiver).await; }); @@ -385,12 +394,19 @@ pub(crate) async fn acp_runtime_loop( AcpCommand::Connect { config, workspace_root, + workspace_id, buffer_id, } => { let state = state.clone(); tokio::task::spawn_local(async move { - if let Err(error) = - connect_acp_client(state.clone(), config, workspace_root, buffer_id).await + if let Err(error) = connect_acp_client( + state.clone(), + config, + workspace_root, + workspace_id, + buffer_id, + ) + .await { send_client_failure(&state, buffer_id, error); } @@ -520,6 +536,7 @@ pub(crate) async fn connect_acp_client( state: Rc>, config: AcpClientConfig, workspace_root: PathBuf, + workspace_id: WorkspaceId, buffer_id: BufferId, ) -> Result<(), String> { let cwd = config @@ -527,7 +544,10 @@ pub(crate) async fn connect_acp_client( .as_deref() .map(Path::new) .unwrap_or(workspace_root.as_path()); - let mut child = spawn_background_command( + let process_registry = state.borrow().process_registry.clone(); + let launched = launch_owned_background_command( + &process_registry, + ProcessWorkspaceId::from_raw(workspace_id.get()), &config.command, &config.args, Some(cwd), @@ -536,20 +556,12 @@ pub(crate) async fn connect_acp_client( ) .await .map_err(|error| format!("failed to start ACP client: {error}"))?; - let stdin = child + let stdin = launched .stdin - .take() .ok_or_else(|| "ACP client stdin unavailable".to_owned())?; - let stdout = child + let stdout = launched .stdout - .take() .ok_or_else(|| "ACP client stdout unavailable".to_owned())?; - if let Some(stderr) = child.stderr.take() { - let state = state.clone(); - tokio::task::spawn_local(async move { - drain_stderr(state, buffer_id, stderr).await; - }); - } let client = Rc::new(AcpClient::new(state.clone())); let (connection, io_task) = @@ -596,7 +608,8 @@ pub(crate) async fn connect_acp_client( session_id.clone(), AcpSession { connection: Rc::new(connection), - child, + owned_process_id: launched.owned_process_id, + workspace_id, }, ); state.borrow().emit(AcpEvent::Connected { @@ -789,8 +802,8 @@ pub(crate) async fn disconnect_acp_session( let mut state = state.borrow_mut(); state.sessions.remove(&session_id) }; - if let Some(mut session) = session { - let _ = session.child.kill().await; + if let Some(session) = session { + teardown_owned_process(&state.borrow().process_registry, session.owned_process_id); } resolve_all_pending_permissions(&state, &session_id); state.borrow().emit(AcpEvent::Disconnected { @@ -913,28 +926,39 @@ pub(crate) fn choose_permission_outcome( .unwrap_or(RequestPermissionOutcome::Cancelled) } -pub(crate) async fn drain_stderr( - state: Rc>, - buffer_id: BufferId, - stderr: tokio::process::ChildStderr, +pub(crate) fn teardown_owned_process( + process_registry: &Arc>, + owned_process_id: OwnedProcessId, ) { - let mut reader = BufReader::new(stderr); - let mut line = String::new(); + if let Ok(mut registry) = process_registry.lock() { + let _ = registry.teardown_process(owned_process_id, Duration::from_millis(200)); + } +} + +pub(crate) fn try_owned_process_exit( + process_registry: &Arc>, + owned_process_id: OwnedProcessId, +) -> Option { + let mut registry = process_registry.lock().ok()?; + let code = registry.exit_code(owned_process_id)?; + let _ = registry.reclaim(owned_process_id); + Some(code) +} + +pub(crate) async fn wait_owned_process_exit( + process_registry: &Arc>, + owned_process_id: OwnedProcessId, +) -> Option { loop { - line.clear(); - match reader.read_line(&mut line).await { - Ok(0) => break, - Ok(_) => { - let message = line.trim_end().to_owned(); - if !message.is_empty() { - send_client_log(&state, buffer_id, message); - } - } - Err(error) => { - send_client_log(&state, buffer_id, format!("ACP stderr error: {error}")); - break; + { + let mut registry = process_registry.lock().ok()?; + if let Some(code) = registry.exit_code(owned_process_id) { + let _ = registry.reclaim(owned_process_id); + return Some(code); } + registry.root_pid(owned_process_id)?; } + tokio::time::sleep(Duration::from_millis(25)).await; } } @@ -970,6 +994,7 @@ pub(crate) fn send_session_lines( } pub(crate) struct AcpRuntimeState { + pub(crate) process_registry: Arc>, pub(crate) sessions: HashMap, pub(crate) terminals: HashMap, pub(crate) pending_permissions: VecDeque, @@ -978,8 +1003,12 @@ pub(crate) struct AcpRuntimeState { } impl AcpRuntimeState { - pub(crate) fn new(event_tx: mpsc::Sender) -> Self { + pub(crate) fn new( + event_tx: mpsc::Sender, + process_registry: Arc>, + ) -> Self { Self { + process_registry, sessions: HashMap::new(), terminals: HashMap::new(), pending_permissions: VecDeque::new(), @@ -997,7 +1026,8 @@ impl AcpRuntimeState { pub(crate) struct AcpSession { pub(crate) connection: Rc, - pub(crate) child: tokio::process::Child, + pub(crate) owned_process_id: OwnedProcessId, + pub(crate) workspace_id: WorkspaceId, } pub(crate) struct PendingPermission { @@ -1011,5 +1041,5 @@ pub(crate) struct AcpTerminal { pub(crate) output: Rc>, pub(crate) exit_status: Rc>>, pub(crate) output_limit: Option, - pub(crate) child: tokio::process::Child, + pub(crate) owned_process_id: OwnedProcessId, } diff --git a/crates/editor-sdl/src/shell/acp/session.rs b/crates/editor-sdl/src/shell/acp/session.rs index 9c761087..051900e5 100644 --- a/crates/editor-sdl/src/shell/acp/session.rs +++ b/crates/editor-sdl/src/shell/acp/session.rs @@ -12,7 +12,8 @@ use super::manager::*; use super::runtime::*; pub(crate) fn init_acp_manager(runtime: &mut EditorRuntime) -> Result<(), ShellError> { - let manager = AcpManager::new().map_err(ShellError::Runtime)?; + let process_registry = process_registry_service(runtime).map_err(ShellError::Runtime)?; + let manager = AcpManager::new(process_registry).map_err(ShellError::Runtime)?; runtime.services_mut().insert(Arc::new(Mutex::new(manager))); Ok(()) } diff --git a/crates/editor-sdl/src/shell/command_stream.rs b/crates/editor-sdl/src/shell/command_stream.rs index 3ad1bce8..0c03c72d 100644 --- a/crates/editor-sdl/src/shell/command_stream.rs +++ b/crates/editor-sdl/src/shell/command_stream.rs @@ -6,9 +6,7 @@ use super::{ }, *, }; -use editor_jobs::{ - JobManager, JobSpec, ProcessSupervisionMode, enrich_env_with_node_manager, -}; +use editor_jobs::{JobManager, JobSpec, ProcessSupervisionMode, enrich_env_with_node_manager}; use std::{ collections::BTreeMap, io::{BufReader, Read}, @@ -796,8 +794,8 @@ fn run_streamed_command( notify_on_failure, } = request; let env = enrich_env_with_node_manager(Some(&cwd), env); - let resolved_program = editor_jobs::resolve_command_path(&program, &env, None) - .unwrap_or(program); + let resolved_program = + editor_jobs::resolve_command_path(&program, &env, None).unwrap_or(program); let mut launch_spec = editor_jobs::ProcessLaunchSpec::new(resolved_program, args) .with_workspace(workspace_id) .with_mode(ProcessSupervisionMode::Background) diff --git a/crates/editor-sdl/src/shell/compile.rs b/crates/editor-sdl/src/shell/compile.rs index 9df4a346..0b4e9ad7 100644 --- a/crates/editor-sdl/src/shell/compile.rs +++ b/crates/editor-sdl/src/shell/compile.rs @@ -449,17 +449,18 @@ fn replace_runtime_user_library( lsp_registry .register_all(user_library.language_servers()) .map_err(|error| error.to_string())?; - runtime - .services_mut() - .insert(Arc::new(LspClientManager::new(lsp_registry))); + let process_registry = shared_process_registry(runtime)?; + runtime.services_mut().insert(Arc::new( + LspClientManager::with_process_registry(lsp_registry, Arc::clone(&process_registry)), + )); let mut dap_registry = DebugAdapterRegistry::new(); dap_registry .register_all(user_library.debug_adapters()) .map_err(|error| error.to_string())?; - runtime - .services_mut() - .insert(Arc::new(DapClientManager::new(dap_registry))); + runtime.services_mut().insert(Arc::new( + DapClientManager::with_process_registry(dap_registry, process_registry), + )); let mut syntax_registry = SyntaxRegistry::new(); syntax_registry diff --git a/crates/editor-sdl/src/shell/lsp_commands.rs b/crates/editor-sdl/src/shell/lsp_commands.rs index 5f635a59..d615ead9 100644 --- a/crates/editor-sdl/src/shell/lsp_commands.rs +++ b/crates/editor-sdl/src/shell/lsp_commands.rs @@ -426,6 +426,7 @@ fn schedule_immediate_lsp_sync( revision: context.revision, text: TextBuffer::from_text(&context.text).snapshot(), root: context.root.clone(), + workspace_id: Some(context.workspace_id.get()), lsp_client, preferred_server_id: preferred_server_id.map(str::to_owned), edits, diff --git a/crates/editor-sdl/src/shell/refresh.rs b/crates/editor-sdl/src/shell/refresh.rs index c0e83b3a..c8c39cef 100644 --- a/crates/editor-sdl/src/shell/refresh.rs +++ b/crates/editor-sdl/src/shell/refresh.rs @@ -296,11 +296,17 @@ fn schedule_pending_lsp_syncs( { return Ok(None); } + let workspace_id = ui + .workspace_views + .iter() + .find(|(_, view)| view.buffer_ids.contains(&buffer.id())) + .map(|(workspace_id, _)| workspace_id.get()); Ok(Some(LspSyncWorkerRequest { path: path.clone(), revision, text: buffer.text.snapshot(), root, + workspace_id, lsp_client: lsp_client.clone(), preferred_server_id: None, edits: lsp_edits_since_last_sync(&lsp_client, &path, &buffer.text), diff --git a/crates/editor-sdl/src/shell/run_loop.rs b/crates/editor-sdl/src/shell/run_loop.rs index 267856e6..14713e19 100644 --- a/crates/editor-sdl/src/shell/run_loop.rs +++ b/crates/editor-sdl/src/shell/run_loop.rs @@ -684,10 +684,20 @@ pub fn run_demo_shell(config: ShellConfig) -> Result { match frame_result { Ok(FrameOutcome::Quit) => { - if let Ok(registry) = process_registry_service(&state.runtime) { - if let Ok(mut registry) = registry.lock() { - let _ = registry.application_quit(OWNED_PROCESS_TEARDOWN_GRACE); - } + if let Some(lsp_client) = + state.runtime.services().get::>().cloned() + { + let _ = lsp_client.prepare_application_quit(); + } + if let Some(dap_client) = + state.runtime.services().get::>().cloned() + { + let _ = dap_client.stop_all_sessions(); + } + if let Ok(registry) = process_registry_service(&state.runtime) + && let Ok(mut registry) = registry.lock() + { + let _ = registry.application_quit(OWNED_PROCESS_TEARDOWN_GRACE); } return Ok(build_shell_summary( &mut state, diff --git a/crates/editor-sdl/src/shell/save.rs b/crates/editor-sdl/src/shell/save.rs index 96c245b7..1115099b 100644 --- a/crates/editor-sdl/src/shell/save.rs +++ b/crates/editor-sdl/src/shell/save.rs @@ -184,10 +184,9 @@ fn close_lsp_buffers_for_workspace( let Some(lsp_client) = lsp_client else { return Ok(()); }; - let (paths, workspace_root) = { + let paths = { let ui = shell_ui(runtime)?; - let paths = ui - .workspace_views + ui.workspace_views .get(&workspace_id) .map(|view| { view.buffer_ids @@ -198,13 +197,7 @@ fn close_lsp_buffers_for_workspace( }) .collect::>() }) - .unwrap_or_default(); - let workspace_root = runtime - .model() - .workspace(workspace_id) - .ok() - .and_then(|workspace| workspace.root().map(Path::to_path_buf)); - (paths, workspace_root) + .unwrap_or_default() }; for path in paths { cancel_lsp_sync_for_path(runtime, &path)?; @@ -213,7 +206,7 @@ fn close_lsp_buffers_for_workspace( .map_err(|error| error.to_string())?; } lsp_client - .stop_sessions_for_root(workspace_root.as_deref()) + .prepare_workspace_close(workspace_id.get()) .map_err(|error| error.to_string())?; Ok(()) } diff --git a/crates/editor-sdl/src/shell/syntax_refresh.rs b/crates/editor-sdl/src/shell/syntax_refresh.rs index 4b52dcd6..8fe72025 100644 --- a/crates/editor-sdl/src/shell/syntax_refresh.rs +++ b/crates/editor-sdl/src/shell/syntax_refresh.rs @@ -66,16 +66,17 @@ fn install_optional_runtime_services( lsp_registry .register_all(user_library.language_servers()) .map_err(|error| ShellError::Runtime(error.to_string()))?; - runtime - .services_mut() - .insert(Arc::new(LspClientManager::new(lsp_registry))); + let process_registry = shared_process_registry(runtime).map_err(ShellError::Runtime)?; + runtime.services_mut().insert(Arc::new( + LspClientManager::with_process_registry(lsp_registry, Arc::clone(&process_registry)), + )); let mut dap_registry = DebugAdapterRegistry::new(); dap_registry .register_all(user_library.debug_adapters()) .map_err(|error| ShellError::Runtime(error.to_string()))?; - runtime - .services_mut() - .insert(Arc::new(DapClientManager::new(dap_registry))); + runtime.services_mut().insert(Arc::new( + DapClientManager::with_process_registry(dap_registry, process_registry), + )); let mut syntax_registry = SyntaxRegistry::new(); syntax_registry .register_all(user_library.syntax_languages()) diff --git a/crates/editor-sdl/src/shell/terminal.rs b/crates/editor-sdl/src/shell/terminal.rs index 5332c533..04c9cc59 100644 --- a/crates/editor-sdl/src/shell/terminal.rs +++ b/crates/editor-sdl/src/shell/terminal.rs @@ -80,9 +80,9 @@ impl TerminalBufferState { &mut self, buffer_id: BufferId, ) -> Option<(LiveTerminalSession, Option)> { - self.sessions.remove(&buffer_id).map(|tracked| { - (tracked.session, tracked.owned_process_id) - }) + self.sessions + .remove(&buffer_id) + .map(|tracked| (tracked.session, tracked.owned_process_id)) } pub(super) fn buffer_ids(&self) -> Vec { @@ -675,7 +675,8 @@ pub(super) fn close_terminal_buffer( if let Ok(buffer) = shell_buffer_mut(runtime, buffer_id) { buffer.clear_terminal_render(); } - let Some((mut session, owned_process_id)) = terminal_buffer_state_mut(runtime)?.remove(buffer_id) + let Some((mut session, owned_process_id)) = + terminal_buffer_state_mut(runtime)?.remove(buffer_id) else { return Ok(()); }; @@ -705,6 +706,12 @@ pub(super) fn process_registry_service( .ok_or_else(|| "process registry service missing".to_owned()) } +pub(super) fn shared_process_registry( + runtime: &EditorRuntime, +) -> Result>, String> { + process_registry_service(runtime) +} + pub(super) const OWNED_PROCESS_TEARDOWN_GRACE: Duration = Duration::from_millis(200); pub(super) fn terminal_key_for_event(keycode: Keycode, keymod: Mod) -> Option { diff --git a/crates/editor-sdl/src/shell/tests/debug.rs b/crates/editor-sdl/src/shell/tests/debug.rs index e6c87ea4..a8149639 100644 --- a/crates/editor-sdl/src/shell/tests/debug.rs +++ b/crates/editor-sdl/src/shell/tests/debug.rs @@ -782,10 +782,14 @@ fn dap_start_opens_adapter_picker_ordered_by_preference() -> Result<(), String> .with_preference(100), ) .map_err(|e| e.to_string())?; + let process_registry = shared_process_registry(&state.runtime)?; state .runtime .services_mut() - .insert(Arc::new(DapClientManager::new(registry))); + .insert(Arc::new(DapClientManager::with_process_registry( + registry, + process_registry, + ))); start_dap_for_active_workspace(&mut state.runtime, None)?; let picker = shell_ui(&state.runtime)? diff --git a/crates/editor-sdl/src/shell/tests/helpers.rs b/crates/editor-sdl/src/shell/tests/helpers.rs index abcfb3f5..806996a3 100644 --- a/crates/editor-sdl/src/shell/tests/helpers.rs +++ b/crates/editor-sdl/src/shell/tests/helpers.rs @@ -638,7 +638,10 @@ pub(super) fn install_test_lsp_manager( )) .map_err(|error| error.to_string())?; } - let manager = Arc::new(LspClientManager::new(registry)); + let manager = Arc::new(LspClientManager::with_process_registry( + registry, + shared_process_registry(runtime)?, + )); runtime.services_mut().insert(Arc::clone(&manager)); Ok(manager) } @@ -2402,8 +2405,12 @@ pub(super) fn install_fake_tcp_dap_manager( .with_preference(10), ) .map_err(|e| e.to_string())?; + let process_registry = shared_process_registry(runtime)?; runtime .services_mut() - .insert(Arc::new(DapClientManager::new(registry))); + .insert(Arc::new(DapClientManager::with_process_registry( + registry, + process_registry, + ))); Ok((port, handle)) } diff --git a/crates/editor-sdl/src/shell/tests/misc.rs b/crates/editor-sdl/src/shell/tests/misc.rs index 62863dae..e4763d12 100644 --- a/crates/editor-sdl/src/shell/tests/misc.rs +++ b/crates/editor-sdl/src/shell/tests/misc.rs @@ -1074,11 +1074,13 @@ fn buffer_save_still_writes_when_format_on_save_fails() -> Result<(), String> { open_workspace_from_project(&mut state.runtime, "format-failure", &root)?; let buffer_id = open_workspace_file(&mut state.runtime, &path)?; + let process_registry = shared_process_registry(&state.runtime)?; state .runtime .services_mut() - .insert(Arc::new(LspClientManager::new( + .insert(Arc::new(LspClientManager::with_process_registry( LanguageServerRegistry::new(), + process_registry, ))); state .runtime diff --git a/crates/editor-sdl/src/shell/workers_autocomplete.rs b/crates/editor-sdl/src/shell/workers_autocomplete.rs index 07b258d9..81654937 100644 --- a/crates/editor-sdl/src/shell/workers_autocomplete.rs +++ b/crates/editor-sdl/src/shell/workers_autocomplete.rs @@ -35,6 +35,7 @@ struct LspSyncWorkerRequest { revision: u64, text: TextSnapshot, root: Option, + workspace_id: Option, lsp_client: Arc, preferred_server_id: Option, edits: Option>, @@ -199,20 +200,24 @@ impl LspSyncWorkerState { } for request in latest_by_path.into_values() { let sync_result = match request.preferred_server_id.as_deref() { - Some(server_id) => request.lsp_client.start_buffer_server_with_edits( - &request.path, - request.text.text(), - request.revision, - request.root.as_deref(), - server_id, - request.edits.as_deref(), - ), - None => request.lsp_client.sync_buffer_with_edits( + Some(server_id) => request + .lsp_client + .start_buffer_server_with_edits_for_workspace( + &request.path, + request.text.text(), + request.revision, + request.root.as_deref(), + server_id, + request.edits.as_deref(), + request.workspace_id, + ), + None => request.lsp_client.sync_buffer_with_edits_for_workspace( &request.path, request.text.text(), request.revision, request.root.as_deref(), request.edits.as_deref(), + request.workspace_id, ), }; let error = sync_result.err().map(|error| error.to_string()); diff --git a/crates/editor-sdl/src/shell/workspace.rs b/crates/editor-sdl/src/shell/workspace.rs index 531c91a9..eeeb3b41 100644 --- a/crates/editor-sdl/src/shell/workspace.rs +++ b/crates/editor-sdl/src/shell/workspace.rs @@ -971,6 +971,9 @@ pub(crate) fn delete_runtime_workspace( let window_id = active_window_id(runtime)?; close_lsp_buffers_for_workspace(runtime, workspace_id)?; + if let Some(dap_client) = runtime.services().get::>().cloned() { + let _ = dap_client.stop_session(workspace_id.get()); + } close_terminal_buffers_for_workspace(runtime, workspace_id)?; acp::close_acp_workspace_buffers(runtime, workspace_id)?; if let Ok(registry) = process_registry_service(runtime) { @@ -984,6 +987,11 @@ pub(crate) fn delete_runtime_workspace( ) .map_err(|error| error.to_string())?; } + if let Some(lsp_client) = runtime.services().get::>().cloned() { + lsp_client + .reap_dead_sessions() + .map_err(|error| error.to_string())?; + } let removed = runtime .model_mut() .close_workspace(workspace_id) diff --git a/graphify-out/.graphify_labels.json b/graphify-out/.graphify_labels.json index 1ee8d887..824784a3 100644 --- a/graphify-out/.graphify_labels.json +++ b/graphify-out/.graphify_labels.json @@ -3,18 +3,18 @@ "1": "LspSessionHandle", "2": "SyntaxRegistry", "3": "editor-sdl/src/tests.rs", - "4": "acp_view.rs", + "4": "buffer_types.rs", "5": "user/lib.rs", "6": "LanguageConfiguration", - "7": "state_with_user_library", + "7": "misc.rs", "8": "helpers.rs", "9": "theme.rs", "10": "draw.rs", - "11": "RepositoryFilesError", + "11": "parse_status", "12": "sdk/src/lib.rs", "13": "ShellBuffer", "14": "editor-dap/src/config.rs", - "15": "LiveTerminalSession", + "15": "LiveTerminalConfig", "16": "editor-fs/src/lib.rs", "17": "GitStatusSnapshot", "18": "editor-issues/src/lib.rs", @@ -43,7 +43,7 @@ "41": "editor-syntax/src/tests.rs", "42": "fringe.rs", "43": "dap_commands.rs", - "44": "browser_host.rs", + "44": ".new", "45": "editor-markdown/src/lib.rs", "46": "project_discovery.rs", "47": "shell/workspace.rs", @@ -54,7 +54,7 @@ "52": "Self", "53": "InputField", "54": "lsp.rs", - "55": "ModelineSegment", + "55": "db_commands.rs", "56": "PickerOverlay", "57": "AcpManager", "58": "probe.rs", @@ -67,32 +67,32 @@ "65": "EditorRuntime", "66": "TextBuffer", "67": "state.rs", - "68": "shell_ui_mut", + "68": "active_shell_buffer_mut", "69": "clipboard.rs", - "70": "buffer_types.rs", + "70": "PluginTextPaneState", "71": "ShellBuffer", - "72": "status.rs", + "72": "LineSyntaxSpan", "73": "PluginPackage", - "74": "PluginBufferSection", - "75": "PaneId", + "74": "PluginBuffer", + "75": "status.rs", "76": "directory.rs", "77": "editor-jobs/src/lib.rs", "78": "workspace_search.rs", "79": "shell/terminal.rs", "80": "User Packages", "81": "DapClientManager", - "82": "git_command_output", + "82": "state_with_user_library", "83": "repository_files.rs", "84": "tool_install.rs", "85": "InstallCommand", "86": "From", "87": "shell_buffer_mut", "88": "diagnostics.rs", - "89": ".handle_text_input_inner", + "89": "ctrl_mod", "90": "resolve_picker_extra", "91": "load_auto_loaded_packages", - "92": "CommandRegistry", - "93": "shell_ui", + "92": "CommandSource", + "93": "install_acp_test_buffer", "94": "PathPattern", "95": "workspace_nav_tests.rs", "96": "shell/text_layout.rs", @@ -102,110 +102,110 @@ "100": "editor-dap/src/client/tests.rs", "101": "theme_reload.rs", "102": "telemetry.rs", - "103": "FontSet", + "103": "AcpPaneState", "104": "registered_queries.rs", "105": "ToolInstallError", "106": "shell/browser.rs", "107": "src/types.rs", "108": "paths_logs.rs", "109": "shell/picker.rs", - "110": "idle.rs", + "110": "ping_shell_wakeup", "111": "git.rs", "112": "load", "113": "git_root", "114": "PluginKeyBinding", "115": "Section", - "116": "process.rs", + "116": "remote.rs", "117": "engines.rs", "118": "RVec", "119": "launch.rs", - "120": "remote.rs", - "121": "terminal_ui.rs", + "120": "ProcessRegistry", + "121": "save.rs", "122": "editor-buffer/src/tests.rs", "123": "lsp_commands.rs", - "124": "rainbow_parens.rs", - "125": "LspLogEntry", + "124": "UserLibrary", + "125": "browser_host.rs", "126": "StoredBreakpoint", - "127": "shell/compile.rs", + "127": ".handle_event", "128": "TextBuffer", "129": "editor-lsp/src/lib_tests.rs", "130": "user/config.rs", "131": "ServiceRegistry", "132": ".new", - "133": "abi.rs", + "133": "AbiPaneConfig", "134": "syntax_registry_mut", "135": "ShellState", "136": "oh-my-githubcopilot (OMG) - Intelligent Multi-Agent Orchestration", - "137": "Vec", + "137": "shell/compile.rs", "138": "DbSessionId", "139": "PixelRect", "140": "AcpClient", "141": "CommandLineOverlay", "142": "Vec", - "143": "commit_git_buffer", + "143": "compute_buffer_syntax", "144": "acp/tests.rs", "145": "shell_user_library", "146": "volt/build.rs", - "147": "editor-icons/src/lib.rs", + "147": "treesittercontext_ghosttext.rs", "148": "editor-fs/src/tests.rs", "149": "input.rs", - "150": "save.rs", + "150": "shell_ui_mut", "151": "query.rs", "152": "user/oil.rs", "153": "shell/dap.rs", - "154": "ROption", - "155": "Diagnostic", + "154": "AbiStatuslineContext", + "155": "objects.rs", "156": "WorkspaceConfigurationValue", "157": "user/workspace.rs", - "158": "normal_nav.rs", + "158": "role", "159": "Copilot instructions for `volt`", "160": "TerminalRenderSnapshot", "161": "DapStoppedSnapshot", "162": "InstallRecipe", "163": "DbService", - "164": "input_shortcuts.rs", - "165": "plugin_section_buffer_layout", + "164": "install.rs", + "165": "render/layout.rs", "166": "String", "167": "user/terminal.rs", "168": "corpus_inventory.rs", "169": "FontSet<'ttf>", - "170": "render_workspace_dock", + "170": ".render", "171": "rainbow_paren.rs", "172": "user/dap.rs", "173": "workers.rs", - "174": "EditorModel", + "174": "WorkspaceId", "175": "user/browser.rs", "176": "worktree.rs", "177": "editor-lsp/src/client/types.rs", "178": "`user`", "179": "render_overlays.rs", - "180": "load_font_set", - "181": "PluginTextPaneState", + "180": "process_registry_tests.rs", + "181": "compile_reload.rs", "182": "Quickfix List PRD", "183": "User-Owned Extension Surfaces Migration PRD", "184": "Building locally", "185": "GitEditorState", - "186": "LineSyntaxSpan", - "187": "String", - "188": ".new", - "189": "LspNotificationSnapshot", - "190": "LiveTerminalError", + "186": "Diagnostic", + "187": "toggle_line_comments_in_range", + "188": "JobError", + "189": "process_supervisor.rs", + "190": "LiveTerminalSession", "191": "markdown_table.rs", "192": "Database Explorer PRD", "193": "markdown_table_event_dimensions", "194": "ShellConfig", "195": "PickerSession", "196": "ShellBuffer", - "197": "LiveTerminalConfig", + "197": "src/session.rs", "198": "render_buffer", - "199": "TextPoint", + "199": "UndoTree", "200": "lang/markdown.rs", - "201": ".text", + "201": ".new", "202": "split_layout.rs", "203": "PersistedProject", "204": "buffer_footer_layout_with_command_line", "205": "Self", - "206": "LspCompletionItem", + "206": "DapSessionHandle", "207": "AbiGitFeatureSpec", "208": "0004-markdown-pretty-pipeline.md", "209": "Option", @@ -216,25 +216,25 @@ "214": "logo.rs", "215": "highlight.rs", "216": "Language", - "217": "Self", + "217": "AutocompleteOverlay", "218": "Domain Docs", "219": "Issue tracker: GitHub", - "220": "Option", + "220": "PluginCommand", "221": "runtime.rs", "222": "idle_wait.rs", "223": "volt/src/main.rs", - "224": "refresh_theme_registry_if_needed", - "225": "apply_lsp_notifications", - "226": "LanguageInstallPlan", - "227": "TextRange", - "228": "UserLibrary", - "229": "main", - "230": "draw_primary_ligature_texture_if_available", - "231": "treesittercontext_ghosttext.rs", - "232": ".should_follow_output", - "233": "treesittercontext_shared.rs", + "224": "render_chrome.rs", + "225": "StreamedCommandExitAction", + "226": "TerminalCursorSnapshot", + "227": "String", + "228": "LspLogSnapshot", + "229": "editor-lsp/src/registry.rs", + "230": "StatuslineContext", + "231": "bash.rs", + "232": ".acp_output_viewport_lines", + "233": "feature_spec", "234": "String", - "235": "render_chrome.rs", + "235": "clojure.rs", "236": "AbiSectionTree", "237": "editor-git/src/tests.rs", "238": "ligatures.rs", @@ -243,7 +243,7 @@ "241": "git_probe_snapshot", "242": "workspace_roots.rs", "243": "LanguageServerSession", - "244": "acp.rs", + "244": "AcpPickerItemSpec", "245": "0002-lsp-stop-restart-session-picker.md", "246": "0003-external-command-stream-default.md", "247": "MarkdownPrettyPlanCache", @@ -253,23 +253,23 @@ "251": "LspCodeAction", "252": "editor-lsp/src/client/tests.rs", "253": "notification_overlay_layouts", - "254": "CommandSource", - "255": ".default", - "256": "first_project_discovery_root", + "254": "elixir.rs", + "255": "java.rs", + "256": "perl.rs", "257": "0005-dap-session-and-client.md", - "258": "headerline_lines", - "259": "AbiColor", + "258": "php.rs", + "259": "JobSpec", "260": "patch_dap_hooks.py", "261": "patch_shell_dap_pending.py", "262": "patch_shell_dap_start.py", "263": "patch_shell_hooks.py", "264": "KeymapError", - "265": "workspace_pane_rects", - "266": "Option", + "265": "r.rs", + "266": "solidity.rs", "267": "0001-csharp-ls-one-session-per-solution.md", "268": "triage-labels.md", - "269": "GhostTextContext", - "270": "xml.rs", + "269": "swift.rs", + "270": "LspLogEntry", "271": "editor-icons/nerd_font_symbols/cod.rs", "272": "editor-icons/nerd_font_symbols/dev.rs", "273": "editor-icons/nerd_font_symbols/fa.rs", @@ -284,8 +284,8 @@ "282": "editor-icons/nerd_font_symbols/seti.rs", "283": "editor-icons/nerd_font_symbols/weather.rs", "284": "editor-sdl/src/lib.rs", - "285": "clojure.rs", - "286": "graphql.rs", + "285": "default_volt_state_dir", + "286": "hover_ui.rs", "287": "ISS-003-add-when-asm-is-added.md", "288": "ISS-004-can-t-get-this-to-work-goal-is-to-indent-at-the.md", "289": "ISS-005-missing-local-reference-usage-tuned-for-each-rel.md", @@ -334,35 +334,34 @@ "332": "user/nerd_font_symbols/seti.rs", "333": "user/nerd_font_symbols/weather.rs", "334": "editor-jobs/src/tests.rs", - "335": ".ensure_sessions_for_path", - "336": "hcl.rs", - "337": "src/session.rs", + "335": "LspClientManager", + "336": "graphql.rs", + "337": "render_tests.rs", "338": "acp_chat.rs", "339": "0006-language-server-and-debug-adapter-install.md", - "340": "comments.rs", - "341": "java.rs", + "340": "editor-core/src/lib_tests.rs", + "341": "LspNotificationSnapshot", "342": "open_acp_client_with_config", "343": "kotlin.rs", - "344": "lua.rs", + "344": "locate.rs", "345": "TempFile", "346": "nix.rs", "347": "init_repo", "348": "keymap.rs", "349": ".from_connection_string", - "350": "perl.rs", - "351": "php.rs", + "350": "JobResult", + "351": "autocomplete_tokens_tests.rs", "352": "fill_rounded_corner_canvas", - "353": "proto.rs", + "353": "AbiLanguageConfiguration", "354": "vim_search.rs", - "355": "r.rs", + "355": "AbiKeymapConfig", "356": "command_stream_tests.rs", - "357": "config_tests.rs", - "358": "ruby.rs", - "359": "scala.rs", + "357": "ancestor_contexts_for_cursor", + "358": "RuntimePopupSnapshot", + "359": "panic_payload_message", "360": "log.rs", "361": ".sync_buffer_to_sessions", - "362": "autocomplete_tokens_tests.rs", - "363": "solidity.rs", + "362": "I", "364": "plan_markdown_pretty_ephemeral", "365": "build_plan", "366": "MarkdownPrettyPlan", @@ -370,46 +369,36 @@ "368": "must", "369": "transport.rs", "370": "DapStackFrameInfo", - "371": "user/workspace_dock.rs", - "372": ".new", + "371": "AbiOilDefaults", + "372": ".next_token", "373": "package", - "374": "Path", + "374": "TextPoint", "375": "must", "376": "Vec", - "377": "package", + "377": "GhostTextContext", "378": "run_demo_shell", "379": "must", - "380": "ancestor_contexts_for_cursor", - "381": "bash.rs", - "382": ".load_from_file", "383": "keys.rs", - "384": "elixir.rs", + "384": "config_tests.rs", "385": "breakpoints_tests.rs", "386": "setup_standalone_user_repository", - "387": "locate.rs", "388": "Result", - "389": "BufferContextOverlaySnapshot", - "390": "syntax_languages", + "390": "ISS-owned-process-lifecycle-spec.md", "391": "common.rs", "392": "DapLogEntry", "393": "idle_pacing.rs", - "394": "VimActionContext", + "394": "UserLibraryModule", "395": "OilDefaultsSection", "396": "WorkspaceDockConfig", "397": "show_paren.rs", "398": "editor-path/src/tests.rs", "399": "browser_tests.rs", - "400": "latex.rs", "401": ".oil_directory_sections", - "402": ".acp_clients", - "403": "UserLibraryModule", - "404": "package", - "405": ".icon_symbols", "406": "src/text.rs", "407": "text_tests.rs", - "408": "swift.rs", - "409": "PaneConfig", - "410": "spawn_reader_thread", + "408": "hcl.rs", + "409": "01-process-registry-and-hybrid-launch.md", + "410": "PathBuf", "411": "compile_reload_tests.rs", "412": "lsp_diagnostic_scope_tests.rs", "413": "sections_tests.rs", @@ -433,12 +422,17 @@ "431": "shell_state_render.rs", "432": "shell_state_vim.rs", "433": "statusline.rs", - "434": "DapSessionHandle", - "435": ".oil_defaults", - "436": ".show_paren_config", - "437": "syntax_language", - "438": ".acp_rebuild_plan_view", - "439": ".terminal_config", - "440": ".themes", - "441": "AbiPickerTruncateStrategy" + "434": "evaluate_expression", + "435": "02-body.md", + "436": "02-shells-and-jobs-owned.md", + "437": "ruby.rs", + "438": "03-body.md", + "439": "03-language-tooling-and-helpers-owned.md", + "440": "04-body.md", + "442": "04-spawn-path-contract-and-release-verification.md", + "445": "cargo", + "449": ".text", + "451": "DapThreadInfo", + "452": "0008-user-source-tree-and-plugin-sdk-boundary.md", + "453": "0007-owned-process-hybrid-supervision.md" } diff --git a/graphify-out/.graphify_labels.json.sig b/graphify-out/.graphify_labels.json.sig index f41e97f7..1e9fd9ad 100644 --- a/graphify-out/.graphify_labels.json.sig +++ b/graphify-out/.graphify_labels.json.sig @@ -1 +1 @@ -{"0": "0948024f7ee58cac", "1": "51939d5b8cd2beb4", "2": "5aa199fd07b8af03", "3": "d19db4200a5db37c", "4": "f7e310978fc57514", "5": "169f1815bc66813a", "6": "b882fb3823a5cdc9", "7": "ce4aea2f7591dcfc", "8": "b76fe91c159c6fec", "9": "4651091ee501f8e2", "10": "0e016e1911ad2dcf", "11": "82200bb7944e7456", "12": "a7e1fb887b34c5ce", "13": "5f5bd580e5d8810c", "14": "dd088d5e180683f8", "15": "c1e22549ae465033", "16": "023763f94eb1e54d", "17": "741d4e97d89680a7", "18": "add961a2b012f5b8", "19": "dc5c93900a7a06c3", "20": "bb752860993b0e2b", "21": "97259e9b59c4b7f9", "22": "49074f51860bb0c0", "23": "d7c31f09bc9f6bf6", "24": "f569f9acddbf131e", "25": "5e07bedf4cb76ac2", "26": "ac60c27ac3fc6bdf", "27": "80d14b6d5b4b9fec", "28": "7351852156b89e42", "29": "e0e5e6a26fe72cc9", "30": "7472d364cb806959", "31": "690cb109e71f4a04", "32": "60eb902c64ff12b5", "33": "e2fb7bbf6a76805b", "34": "6d75be2c16de734b", "35": "fd92615b0a0fdcad", "36": "55285f9512a2a264", "37": "5650730b8781bdd7", "38": "d3bdf975858b58be", "39": "d4a7ab7a3752559a", "40": "aa5c43455ec8247d", "41": "4ee92814eb363062", "42": "e29258ad032ee179", "43": "64a1fa8747b3e7ba", "44": "ea624b19b75bfc9c", "45": "3aad37cf0734add1", "46": "97a32c8b43c462e6", "47": "1c96ba3124c1fb35", "48": "627c81cdc669ee3c", "49": "851024bdeca64a01", "50": "932cdf7acff323b1", "51": "921a0af54fd696e1", "52": "c0c0810d651d15de", "53": "7b8f1ef989e568e7", "54": "886e9f8aa824945b", "55": "f328a4f05a8ec87a", "56": "7cf2b4ea9afcea0e", "57": "a4dd2fd6078f3468", "58": "d3b590e59a673e91", "59": "0084643179b771ea", "60": "2cdc816262f08c67", "61": "6798f176eb972a62", "62": "38f06dbcdb7b6e7e", "63": "0254213650b3058f", "64": "afa582cec7787125", "65": "7ec81d02b8caf0ac", "66": "fcfbf9aadf449d2f", "67": "b67106a4ada2b7cc", "68": "e70fcd3ec7564a43", "69": "a55ea931a8a6c2db", "70": "4eabec82622ebe56", "71": "d8c88c13a429c239", "72": "88563051acbb6461", "73": "8b69045324cfa440", "74": "9e64e01262a3dbc8", "75": "b9e1fc5c902fc7f6", "76": "9e7c339cfcbaa1f3", "77": "3a6bc5a30b60baf3", "78": "1171718e134d714b", "79": "47407691def38b94", "80": "ac917a3a99722efa", "81": "2a1cd795e2fafc59", "82": "09b1b6cf600d34a3", "83": "b964229278289d7f", "84": "cf750b61304364ca", "85": "c055b20f6031414a", "86": "b8ed28965ecedde8", "87": "14214bde6c2ebaec", "88": "e2b4925dad31b8fa", "89": "5bd1584d43c18ade", "90": "f78b3d99ba1d77b0", "91": "4b4b3bf26fd47b76", "92": "65956e73a0842dd4", "93": "e3759d9521d6074d", "94": "4b769d940f83c09d", "95": "d223aee991525bea", "96": "74272be1b6c281fb", "97": "bcad64381b617268", "98": "8f12a30ff3eec134", "99": "75e832df813db04c", "100": "caae9f71a8460ab9", "101": "2f4fbb0330e06169", "102": "a91cf11e858c8a6a", "103": "db9026df361c7f73", "104": "a60bd8580ba52eea", "105": "74281b0ea05bf7a5", "106": "d4374bfbb0992394", "107": "c29e61e22fc56249", "108": "99fd49ef0f56d16f", "109": "c1115d8c2668c3c2", "110": "644b7a41321d905b", "111": "9b0d0f6c2b42ae6d", "112": "4147232fc08b38d6", "113": "96c27a26db4af9a9", "114": "1c042a93d8eece37", "115": "b7ff25f1f04551de", "116": "e0c8966f63bc6a53", "117": "16cfe7c33ae31e7f", "118": "fd351c0266325130", "119": "4098b23250552649", "120": "31cf1c23d71473d9", "121": "87629a56a86720fc", "122": "84b6a4a25fa994d9", "123": "f609c66136af757c", "124": "5c72c5b179643280", "125": "5dcb33adc8d9587a", "126": "179c0bb2314c5584", "127": "0fcdff62f37c4a96", "128": "31b7252e54088f0f", "129": "6b776c8ad1d6969e", "130": "4cf623550b8bef8d", "131": "bcc09eeb4abab037", "132": "ba75f40f64973ccc", "133": "a7ddffab10dc629d", "134": "47d79ba1a2f64cf9", "135": "9b26e980c79c0b03", "136": "02e7a3b65e47b0c8", "137": "1a886eb1275eaeb1", "138": "7d113dde7acabe0f", "139": "aff6e005cc947780", "140": "98dffac0d7426587", "141": "b540cb45d4694cae", "142": "711388d88f6428f5", "143": "04128cd5b689a2b2", "144": "fc523a1fb5c7de90", "145": "45a104a1323143fb", "146": "49ea01a9d69081de", "147": "5545b0278a0edb6d", "148": "c0985ccc4f5d693f", "149": "0ee43646cd414749", "150": "f142366ed76c97f2", "151": "c92f4118d89ef8b5", "152": "c31a94fd73e2df53", "153": "e9b9b313b29808dd", "154": "77202ef0c472841c", "155": "d9e3a456eb76fc25", "156": "5c49c30f959b6b7b", "157": "18ebfad179373909", "158": "52b13f2b5c4d5192", "159": "bca8387e0bcaaeb7", "160": "433b64c7d12786ae", "161": "d77cd29534a55726", "162": "86f3e86aa74d5a20", "163": "bfc47ef6cbffcb37", "164": "c191030a18c9d797", "165": "ced8654ad333c4af", "166": "e5d682eec6dbbc02", "167": "dce25438c2126675", "168": "e91350ddec08bea1", "169": "3581fd8e9bbf3ee9", "170": "c77d41c2c275e9d7", "171": "d49c60a47aad6795", "172": "ffb083de194db126", "173": "f2179bd352214f1f", "174": "449e901349d75825", "175": "64a601486823a3e3", "176": "7fe6bd0df70f1f50", "177": "d832b98393dfb32e", "178": "d2b424774567b28d", "179": "cc5ddad816066509", "180": "28c04ce123aa3ec8", "181": "eb091c3124143aab", "182": "fec86fa05950c454", "183": "b9e0a8f8b26f894c", "184": "db66a3e16d370828", "185": "9a61d830348d77d5", "186": "c742987f772205af", "187": "44bcef9c5b8540d8", "188": "ef149c17fc8d5299", "189": "dc4ae95a14f129c6", "190": "4fc39a25799bfdcc", "191": "2fd68c8da5b06d9a", "192": "629dc11623b5da3f", "193": "e0a39a19c41e51ca", "194": "c90eb06312a9f397", "195": "e8504dba4ba24246", "196": "d8d8a07ff63969a1", "197": "60274e229a5fba11", "198": "d498949735555a3d", "199": "6d0881056a35e091", "200": "2fdabb782b516c1c", "201": "ba01fe5c812374e2", "202": "58f21bc3723df194", "203": "b29825a1c7319224", "204": "afbf3077d928c4ee", "205": "83aa2b31cefce9eb", "206": "978b45082b4d542f", "207": "a1ed74035008104e", "208": "162c08a0cef33964", "209": "28014b0fdb5aeb44", "210": "759234203863db88", "211": "b5cf901b680ab12b", "212": "af625befc3313070", "213": "7cbca96d284c27c0", "214": "ae16609978083ef2", "215": "c3620585a9cc59a1", "216": "bcd6be729a17e613", "217": "68edd2df5991c013", "218": "99128a4c2f791d05", "219": "4769e855eebe2afb", "220": "d7b0692ee0dd0aa0", "221": "c55de190280c86e9", "222": "1424fb48825a5161", "223": "87cc4c2b3c2ca1c4", "224": "325858fb709ce0b0", "225": "c4b8f87d36eff299", "226": "fb72b0c583fb52f8", "227": "01579684be9e7ef6", "228": "02563624c335532f", "229": "27d83fda6ac77e0d", "230": "d80af81b7d5c4055", "231": "1dba280aa7e5508a", "232": "be509bfb46e4eee2", "233": "0c1b9030f5f11c45", "234": "5f705c2cc1b6b70a", "235": "cd053acb84e23b65", "236": "a70265448f926c03", "237": "a2aa5aee31830a3c", "238": "5932b34719b2283c", "239": "2daacbd6d94fe64e", "240": "f34992e89ea38f76", "241": "3754d6a6a560b666", "242": "2165dc950d50db71", "243": "c50bc6402bc3300b", "244": "4c254732787a5ab4", "245": "bf74a7e804f197c0", "246": "6f701b02723d754d", "247": "4258c183b5b468a5", "248": "a45b9d8e7f895856", "249": "e30881e15f1e6374", "250": "53fd726a6acd87d7", "251": "ecc130c7e7e207cc", "252": "59bd4aa5ca6b2cdb", "253": "8fd438fcebe29973", "254": "1684a5e63d1ef742", "255": "d7fe18e48db61207", "256": "ba2d97ce6314256c", "257": "49557c247ddde5da", "258": "80b640f27ba11b08", "259": "dabcfd90ad594e5d", "260": "b65b99ae3a432962", "261": "4e8367fa83bfecca", "262": "eca67ee50d73da6e", "263": "bbcd7af61baa3e19", "264": "c53f33a78377dabb", "265": "a522c397acb5eac6", "266": "fa3d3ae27f77f589", "267": "f8921854b28185e8", "268": "209855848c5ac45f", "269": "03b075b8548a07d5", "270": "f0223eab5d36663a", "271": "cf0bc8bfb4ce0975", "272": "627a95332b8d2596", "273": "762cc514365b26a6", "274": "4111370da151ea78", "275": "edd3ac0904a8bc2d", "276": "473bf6fd096071b0", "277": "62340eb1a3abd53f", "278": "7c099e04903378ea", "279": "96720ba49c92ab73", "280": "ff793b0ddb910a70", "281": "99165c81d92d0521", "282": "1ff065bee98c0dbd", "283": "bbc1ad61f2e3b7b9", "284": "059c7fc7c02e9d3b", "285": "cdc6155dd3151719", "286": "11c7d2f9482af96e", "287": "ad72e552f183829d", "288": "7f11400fadefbfe4", "289": "244f6c2e41c4799f", "290": "ebe03b88d556c35a", "291": "25490252597edb7a", "292": "9f2a104004252a97", "293": "5f23239ef8faa13f", "294": "cea19eab239e2aaf", "295": "cde9b239fcc78f76", "296": "b27eb6b5adc93847", "297": "d922fe102fa36f44", "298": "42454d71689b610e", "299": "0107b6cf567af08c", "300": "1996a2a9a0b10459", "301": "d254143939b6eb90", "302": "5674792c9c6688e8", "303": "b578c0815e8125ce", "304": "d7775ee93e8ea3f1", "305": "e1be1aa37f0e3551", "306": "2e81ab5f595ef1b3", "307": "82910c393ce8ffa5", "308": "d98457ff8c6148ca", "309": "7db24f47975a8b9e", "310": "766f40cf24042c7c", "311": "6875a76af5025449", "312": "3bb3e237538b4ee9", "313": "680da1c16d2c4f46", "314": "bd5d434d9ea4f2de", "315": "f81ced778f59cb76", "316": "15de83be968af9af", "317": "692c9eb8164986cb", "318": "a8e386adf4059d17", "319": "c0f2304e66bb39c6", "320": "7759de3d6f0a46b9", "321": "237d73eea7a0d218", "322": "b38fcd665d7f970a", "323": "65959ab2b752daf5", "324": "60ff2d128a681390", "325": "d65450f6354d067c", "326": "394204acc09330f1", "327": "506142a8928e476f", "328": "c3407875a39813bf", "329": "47938237bf05dda8", "330": "72fbb82eb4364336", "331": "8c3b4c761ac5824f", "332": "0984789cdfb63d5f", "333": "5a476bf8871981aa", "334": "8d1727deb2f4bb02", "335": "f9c67b2e2df5177b", "336": "7d9953838613cd02", "337": "a2d2e886d1924676", "338": "1a17c5edd37a0408", "339": "4edfd069c218ba5d", "340": "e1ffd6125152f65d", "341": "fb05e25ac530ba93", "342": "47073d9a3b79b3f3", "343": "b16de6b7087633c9", "344": "80ac72e6b71df371", "345": "58a7a63487e3c4eb", "346": "39c68bd1f268f789", "347": "06e92a0d442f5786", "348": "dbe116ae3231eb3e", "349": "db0fd45eea919f1d", "350": "04e95e7614afc045", "351": "202176f882b0d35f", "352": "60736555d392c5fd", "353": "a54b097b15e47fc1", "354": "c25260333da788ee", "355": "bb96a98ebe5aa51f", "356": "3ff9a5fbc01bb156", "357": "327b16d7b713c87e", "358": "806bd6e003c0021e", "359": "0719de098e7aac23", "360": "f77aff29adaa8d6b", "361": "19b8a664b4cbd538", "362": "029e8f44b2f9b0ec", "363": "e54697d8445610d3", "364": "3b6d0caf8b209650", "365": "0236b59a1298dc2b", "366": "062fa28f19770c77", "367": "2b0a5d390cc4550c", "368": "dcce48534c8497f2", "369": "0612368a28842c70", "370": "5b1126c4aca5162d", "371": "25fac052dfb5ddb2", "372": "865b1529f85d2ac6", "373": "8e722643c16d280a", "374": "54bb709e37167eb3", "375": "4b0af5073ea50781", "376": "08a7ac5ff20d7eb4", "377": "b50c8815481c7a97", "378": "11940204ca53f346", "379": "698bc594738eebc4", "380": "ee1fb8af0d08bcd9", "381": "e4dd1368651b21d6", "382": "8a07461848346df2", "383": "bc830528dfd60e93", "384": "1bd361590b9942e1", "385": "8c50dccc26c9323b", "386": "e7e9e55b730daaf0", "387": "54e78054d89c2d20", "388": "2541efcbcfcad0c5", "389": "dbca0af580c8c388", "390": "68d3f8593bce3f3d", "391": "4d60126b0e5b6e04", "392": "960736b63c325b57", "393": "bddf14c11d11fa69", "394": "b8444598eba429a7", "395": "1032b2854ca5bfd7", "396": "4833dd793d5e5a72", "397": "4521f2458da246a7", "398": "79555d8323702c30", "399": "765350cf8c65fa12", "400": "e84a3ae8c76561e0", "401": "b0402f15a9f6b9cf", "402": "2be8ec979bad54b4", "403": "b61f5f69bbc82e2f", "404": "3ebebd7d7efd18ee", "405": "6710b3f7c0d5e89c", "406": "30c2c5cb6b2fe764", "407": "9031876df49f1151", "408": "49f41dbda7d0d016", "409": "17f5a468468d9c9e", "410": "4b78560cc68460cd", "411": "ae631c1b906b5c6a", "412": "35b2122ada40e959", "413": "8166242df714b1a7", "414": "42e906e992eafa5f", "415": "49edb36c92cbfa8c", "416": "033043a5738816e4", "417": "1186823d541cb0d0", "418": "d743a8c48927e0f3", "419": "464b536a22a13596", "420": "56cae5efb189df52", "421": "9fee73c06119763d", "422": "e6023f59e2c37c7b", "423": "9a76a37e7fe8ce72", "424": "c33b9bca5e6a0976", "425": "75925fa2acadea26", "426": "a65a7f609064d46a", "427": "bc3d602a8638dc0e", "428": "0f6e5955590e19bb", "429": "54516586f80f04bf", "430": "f33d3b00e05cb772", "431": "c54adfb9427136c2", "432": "2872c063e16ec613", "433": "203b07e16518301c", "434": "5ddc0893e9ebe671", "435": "a1beacf701007690", "436": "41e916a2dcce35f7", "437": "211ae45fb6073abd", "438": "00d3e8457cdaa053", "439": "799da063135aadce", "440": "b583c18aeb131f13", "441": "9a5f3b19b0bd3f9e"} \ No newline at end of file +{"0": "3f4976f4d8db8860", "1": "3e64d17db1ae7f6c", "2": "281ada3e5406800e", "3": "d19db4200a5db37c", "4": "22fdfb0949abc541", "5": "c09a1f883006b94d", "6": "4e9cdeba3311a3bd", "7": "066236d232182752", "8": "67a5d8a564726f83", "9": "4651091ee501f8e2", "10": "b3a46d54bdad979d", "11": "f7c6e5573e7d82b4", "12": "6f85c353a3903512", "13": "5b69a04679558378", "14": "f615fd56595dab72", "15": "b7f54ca07c32afb3", "16": "b12da77d4530f241", "17": "013aac287d0f8672", "18": "add961a2b012f5b8", "19": "cd41963884a4c1dd", "20": "54c199a177d8ed3e", "21": "69f761bde51cb0a0", "22": "53226cb10750e5e1", "23": "66755779f2fc75f9", "24": "f569f9acddbf131e", "25": "614864be9351a97d", "26": "ac60c27ac3fc6bdf", "27": "d12c8a5756f21f31", "28": "1591be97c3fedce6", "29": "3d6ebea8b1d1acf3", "30": "7472d364cb806959", "31": "2c2c44e27f9caaab", "32": "f4843667934b9836", "33": "ff38a05a78c4d6fc", "34": "066183dfae5aca38", "35": "fd92615b0a0fdcad", "36": "55285f9512a2a264", "37": "5650730b8781bdd7", "38": "d3bdf975858b58be", "39": "48c1c85b80b8e6e3", "40": "aa5c43455ec8247d", "41": "4ee92814eb363062", "42": "e29258ad032ee179", "43": "66b71716a08764e4", "44": "fafa7c4954fb1f09", "45": "e05a958e86ba0581", "46": "c00e78f3c05a2e4c", "47": "d302ba0c297c1e63", "48": "627c81cdc669ee3c", "49": "b5948ef798ecf648", "50": "21165e15d42bc9b5", "51": "1fd515ad5d9f97a8", "52": "ba541bef13f581a6", "53": "25c5270f9ad059d8", "54": "886e9f8aa824945b", "55": "b193eb5c8058594f", "56": "b4904e355b460192", "57": "8f5949033b221082", "58": "d3b590e59a673e91", "59": "567bcc00fe0aa308", "60": "2cdc816262f08c67", "61": "6798f176eb972a62", "62": "6cd876b3e83f8aac", "63": "d5271105e850098d", "64": "d21251e7137d333f", "65": "e77e0f8f0a7eda32", "66": "2c8b70a78b756c67", "67": "43dcfaa415cc414a", "68": "705e0ad7d68b999a", "69": "42e2b5fc6f1b88bb", "70": "9db58ca77f5640e8", "71": "d8c88c13a429c239", "72": "a000f36efce67df7", "73": "da034500d7f7f81b", "74": "e4f8b5caf539969d", "75": "a44c30bfcc82d9fe", "76": "9665764e8fea942d", "77": "148f64dd32f098ba", "78": "5102d56fddd33129", "79": "b9bfa1ba2b053aa4", "80": "ac917a3a99722efa", "81": "22a16c112743ccba", "82": "3724d3c543752216", "83": "39b02c556595d99f", "84": "cf750b61304364ca", "85": "c055b20f6031414a", "86": "c67ace32135df5d5", "87": "38399f978c7d93aa", "88": "e2b4925dad31b8fa", "89": "48bc71fe6e73dcc7", "90": "ec7dbaf065f9a194", "91": "017abbfd2365dc94", "92": "9cb26b43935a8ad1", "93": "044aad6c83b70433", "94": "4b769d940f83c09d", "95": "d223aee991525bea", "96": "1b7b5ec225e4281d", "97": "b66cb1358b4458b7", "98": "044c0afbcddca7ee", "99": "75e832df813db04c", "100": "caae9f71a8460ab9", "101": "957bbf599db933a8", "102": "93f05403ef1fa90e", "103": "2f25598842569095", "104": "a60bd8580ba52eea", "105": "74281b0ea05bf7a5", "106": "d6e463c5feff097f", "107": "08ac45091c7e12fb", "108": "9af29582962bda37", "109": "b48e1188b9f552d8", "110": "dbc719acc1d16565", "111": "4ab6171d6e26eb13", "112": "4147232fc08b38d6", "113": "27498cdc42681e5e", "114": "515ea49db0d6e682", "115": "b7ff25f1f04551de", "116": "b4c0c5938ddc399f", "117": "bcd78ca1a087a357", "118": "065e89e352b8abdd", "119": "b8c816bb2fd47c2a", "120": "f49af7b5a6f28b31", "121": "7aa70b22ed36d007", "122": "84b6a4a25fa994d9", "123": "4433e21e11772a81", "124": "9b9808e040784ee0", "125": "7b99e7e269ccbc1b", "126": "179c0bb2314c5584", "127": "594ac78c6788c961", "128": "ec1fad618b538090", "129": "6b776c8ad1d6969e", "130": "4cf623550b8bef8d", "131": "a4bc3e073f04928f", "132": "d3d847c17d56188b", "133": "70cff248010058ca", "134": "47d79ba1a2f64cf9", "135": "01d37fe01bfc75fb", "136": "02e7a3b65e47b0c8", "137": "6dda60a1cc59379e", "138": "7d113dde7acabe0f", "139": "7f2ce545c963bc52", "140": "1c0773ff89c4dec3", "141": "b540cb45d4694cae", "142": "711388d88f6428f5", "143": "c32c5d4b65ee8d34", "144": "248dd1f2c08f3a42", "145": "7d322c58282aedf8", "146": "49ea01a9d69081de", "147": "620e9d272c62d0c7", "148": "11eca4382ed41ddf", "149": "0ee43646cd414749", "150": "09d6b3c367d3d341", "151": "c92f4118d89ef8b5", "152": "c816c90ade7a759d", "153": "e9b9b313b29808dd", "154": "914352488aaaa300", "155": "ebea979ceccb0901", "156": "1f72b9e935284e44", "157": "e6689911131584de", "158": "88b43059e7ea141a", "159": "bca8387e0bcaaeb7", "160": "bf5cb15533c466e8", "161": "833a71da1bd7144c", "162": "86f3e86aa74d5a20", "163": "5691e1b2de257db6", "164": "407ae1e0897a549a", "165": "43b0e578bf3d9bb1", "166": "e5d682eec6dbbc02", "167": "fde9170355454e96", "168": "e91350ddec08bea1", "169": "a24fde59c07daafe", "170": "22f990a83026b017", "171": "36234cc4d8d7d225", "172": "ffb083de194db126", "173": "9b58df26adbf4aff", "174": "adc549368b9b8e2f", "175": "8f2ecda5b2fd5ad2", "176": "7fe6bd0df70f1f50", "177": "0a915ecceaa89ca1", "178": "d2b424774567b28d", "179": "33330e2b493bb499", "180": "1410ce3c21359377", "181": "bb508e88a6dc86e1", "182": "fec86fa05950c454", "183": "b9e0a8f8b26f894c", "184": "db66a3e16d370828", "185": "9a61d830348d77d5", "186": "e8129848d0c3b236", "187": "cc24e3afc3ae9354", "188": "9661b28f3d268550", "189": "742fd880743cf48c", "190": "44b50097a6d7fc31", "191": "2fd68c8da5b06d9a", "192": "629dc11623b5da3f", "193": "e0a39a19c41e51ca", "194": "c90eb06312a9f397", "195": "5a82b4c89733b8f8", "196": "d8d8a07ff63969a1", "197": "c753387ca9eacd62", "198": "82a654ba730f5f11", "199": "36470c7c8b6a4590", "200": "2fdabb782b516c1c", "201": "29d5c475312dcb65", "202": "58f21bc3723df194", "203": "c9755873cf95b9e5", "204": "a4fc4922f5066bac", "205": "33b84e8889db94e5", "206": "08c73e5f71189d7d", "207": "a1ed74035008104e", "208": "162c08a0cef33964", "209": "bd9a770c67021ade", "210": "759234203863db88", "211": "b5cf901b680ab12b", "212": "161c4c6d07062be5", "213": "7cbca96d284c27c0", "214": "a8aba82d3310d2e6", "215": "c3620585a9cc59a1", "216": "e06d9d1f1b2257db", "217": "8228f774cabf3d76", "218": "99128a4c2f791d05", "219": "4769e855eebe2afb", "220": "09554b9683d6424b", "221": "40bd4f31591e6952", "222": "1424fb48825a5161", "223": "0e8fac7df984de37", "224": "cd053acb84e23b65", "225": "317135dcbec2f3e1", "226": "89e6566098c8ea63", "227": "568a87df2215d677", "228": "c19c8cb81c8b49ca", "229": "4eced4976accbba3", "230": "a0f66d67010d64b4", "231": "e4dd1368651b21d6", "232": "92a78b2d2d53fabe", "233": "c8e17108628f631d", "234": "5f705c2cc1b6b70a", "235": "cdc6155dd3151719", "236": "552b7486c42042f1", "237": "a2aa5aee31830a3c", "238": "5932b34719b2283c", "239": "fec62c693c523b48", "240": "c1df4b1b7315a1ed", "241": "3754d6a6a560b666", "242": "2165dc950d50db71", "243": "f211b29b83e4e005", "244": "d8768b5bcae408f0", "245": "bf74a7e804f197c0", "246": "6f701b02723d754d", "247": "4258c183b5b468a5", "248": "1fa045da54ece8e0", "249": "e30881e15f1e6374", "250": "eb64f522ac3b2155", "251": "c8dcda821c0e1d30", "252": "386ac536d33c1af2", "253": "be71423ba2020a80", "254": "1bd361590b9942e1", "255": "fb05e25ac530ba93", "256": "04e95e7614afc045", "257": "49557c247ddde5da", "258": "202176f882b0d35f", "259": "14e8aa10f1e38c99", "260": "b65b99ae3a432962", "261": "4e8367fa83bfecca", "262": "eca67ee50d73da6e", "263": "bbcd7af61baa3e19", "264": "f905121f521ac859", "265": "bb96a98ebe5aa51f", "266": "e54697d8445610d3", "267": "f8921854b28185e8", "268": "209855848c5ac45f", "269": "49f41dbda7d0d016", "270": "365a7a6906979008", "271": "cf0bc8bfb4ce0975", "272": "627a95332b8d2596", "273": "762cc514365b26a6", "274": "4111370da151ea78", "275": "edd3ac0904a8bc2d", "276": "473bf6fd096071b0", "277": "62340eb1a3abd53f", "278": "7c099e04903378ea", "279": "96720ba49c92ab73", "280": "ff793b0ddb910a70", "281": "99165c81d92d0521", "282": "1ff065bee98c0dbd", "283": "bbc1ad61f2e3b7b9", "284": "059c7fc7c02e9d3b", "285": "bfb52ff9be177ae4", "286": "bda62ced437d3e4a", "287": "ad72e552f183829d", "288": "7f11400fadefbfe4", "289": "244f6c2e41c4799f", "290": "ebe03b88d556c35a", "291": "25490252597edb7a", "292": "9f2a104004252a97", "293": "5f23239ef8faa13f", "294": "cea19eab239e2aaf", "295": "cde9b239fcc78f76", "296": "b27eb6b5adc93847", "297": "d922fe102fa36f44", "298": "42454d71689b610e", "299": "0107b6cf567af08c", "300": "1996a2a9a0b10459", "301": "d254143939b6eb90", "302": "5674792c9c6688e8", "303": "b578c0815e8125ce", "304": "d7775ee93e8ea3f1", "305": "e1be1aa37f0e3551", "306": "2e81ab5f595ef1b3", "307": "82910c393ce8ffa5", "308": "d98457ff8c6148ca", "309": "7db24f47975a8b9e", "310": "766f40cf24042c7c", "311": "6875a76af5025449", "312": "3bb3e237538b4ee9", "313": "680da1c16d2c4f46", "314": "bd5d434d9ea4f2de", "315": "f81ced778f59cb76", "316": "15de83be968af9af", "317": "692c9eb8164986cb", "318": "a8e386adf4059d17", "319": "c0f2304e66bb39c6", "320": "7759de3d6f0a46b9", "321": "237d73eea7a0d218", "322": "b38fcd665d7f970a", "323": "65959ab2b752daf5", "324": "60ff2d128a681390", "325": "d65450f6354d067c", "326": "394204acc09330f1", "327": "506142a8928e476f", "328": "c3407875a39813bf", "329": "47938237bf05dda8", "330": "72fbb82eb4364336", "331": "8c3b4c761ac5824f", "332": "0984789cdfb63d5f", "333": "5a476bf8871981aa", "334": "499ef3846fb16967", "335": "73547f7b46118664", "336": "11c7d2f9482af96e", "337": "0c6a9f7d80f73cfa", "338": "9d717c179bf38290", "339": "4edfd069c218ba5d", "340": "de34d0925d1eef34", "341": "dc4ae95a14f129c6", "342": "90bb018e41e0d3f3", "343": "b16de6b7087633c9", "344": "54e78054d89c2d20", "345": "58a7a63487e3c4eb", "346": "39c68bd1f268f789", "347": "06e92a0d442f5786", "348": "dbe116ae3231eb3e", "349": "db0fd45eea919f1d", "350": "1219cdd14b52a1c6", "351": "029e8f44b2f9b0ec", "352": "60736555d392c5fd", "353": "c327e0a45922c561", "354": "c25260333da788ee", "355": "93445cefeabfd369", "356": "3ff9a5fbc01bb156", "357": "ee1fb8af0d08bcd9", "358": "77351721bf8a4264", "359": "7d258794bce3a51b", "360": "7c3e5b6d9c679819", "361": "6094d6dec4a915b9", "362": "0226a53ba96dbea7", "364": "3b6d0caf8b209650", "365": "0236b59a1298dc2b", "366": "57d7d638ad600d41", "367": "2b0a5d390cc4550c", "368": "dcce48534c8497f2", "369": "0612368a28842c70", "370": "5b1126c4aca5162d", "371": "575a399050e49928", "372": "9fb06f37a0d0f544", "373": "8e722643c16d280a", "374": "27663c42c80acde8", "375": "4b0af5073ea50781", "376": "6ae651af51c5bf10", "377": "c19a25360dbe592a", "378": "11940204ca53f346", "379": "698bc594738eebc4", "383": "31651edb8e2ff8b2", "384": "327b16d7b713c87e", "385": "8c50dccc26c9323b", "386": "e7e9e55b730daaf0", "388": "2541efcbcfcad0c5", "390": "ea32f5c5e7712df4", "391": "27e14af9587965ed", "392": "960736b63c325b57", "393": "ea35f6cc897d144d", "394": "285ace6d9fc3573f", "395": "1032b2854ca5bfd7", "396": "d654fcd02124e6b7", "397": "4521f2458da246a7", "398": "79555d8323702c30", "399": "765350cf8c65fa12", "401": "b0402f15a9f6b9cf", "406": "30c2c5cb6b2fe764", "407": "9031876df49f1151", "408": "7d9953838613cd02", "409": "38349c271babfd62", "410": "673d89c7831f5943", "411": "ae631c1b906b5c6a", "412": "35b2122ada40e959", "413": "8166242df714b1a7", "414": "42e906e992eafa5f", "415": "49edb36c92cbfa8c", "416": "033043a5738816e4", "417": "1186823d541cb0d0", "418": "d743a8c48927e0f3", "419": "464b536a22a13596", "420": "56cae5efb189df52", "421": "9fee73c06119763d", "422": "e6023f59e2c37c7b", "423": "9a76a37e7fe8ce72", "424": "c33b9bca5e6a0976", "425": "75925fa2acadea26", "426": "a65a7f609064d46a", "427": "bc3d602a8638dc0e", "428": "0f6e5955590e19bb", "429": "54516586f80f04bf", "430": "f33d3b00e05cb772", "431": "c54adfb9427136c2", "432": "2872c063e16ec613", "433": "203b07e16518301c", "434": "18b66e813851cf0c", "435": "bbad4d418ce6f790", "436": "d2bf391694e49c2d", "437": "806bd6e003c0021e", "438": "4231fd6d2715f54d", "439": "714b118fee403399", "440": "03df858b8c46da35", "442": "0174e316548a40bf", "445": "2c43cfe63a49533a", "449": "ba01fe5c812374e2", "451": "6811fb8bc77d848e", "452": "bfe55164ccc393aa", "453": "f2f83d9fccda45c5"} \ No newline at end of file diff --git a/graphify-out/GRAPH_REPORT.md b/graphify-out/GRAPH_REPORT.md index 299fed8c..e3a9e7b1 100644 --- a/graphify-out/GRAPH_REPORT.md +++ b/graphify-out/GRAPH_REPORT.md @@ -1,16 +1,16 @@ -# Graph Report - volt (2026-09-11) +# Graph Report - volt (2026-09-12) ## Corpus Check -- 469 files · ~684,892 words +- 481 files · ~698,812 words - Verdict: corpus is large enough that graph structure adds value. ## Summary -- 12563 nodes · 41495 edges · 442 communities (420 shown, 22 thin omitted) -- Extraction: 85% EXTRACTED · 15% INFERRED · 0% AMBIGUOUS · INFERRED: 6069 edges (avg confidence: 0.8) +- 12749 nodes · 42113 edges · 436 communities (415 shown, 21 thin omitted) +- Extraction: 86% EXTRACTED · 14% INFERRED · 0% AMBIGUOUS · INFERRED: 6093 edges (avg confidence: 0.8) - Token cost: 0 input · 0 output ## Graph Freshness -- Built from commit: `f2933774` +- Built from commit: `60add880` - Run `git rev-parse HEAD` and compare to check if the graph is stale. - Run `graphify update .` after code changes (no API cost). @@ -19,18 +19,18 @@ - LspSessionHandle - SyntaxRegistry - editor-sdl/src/tests.rs -- acp_view.rs +- buffer_types.rs - user/lib.rs - LanguageConfiguration -- state_with_user_library +- misc.rs - helpers.rs - theme.rs - draw.rs -- RepositoryFilesError +- parse_status - sdk/src/lib.rs - ShellBuffer - editor-dap/src/config.rs -- LiveTerminalSession +- LiveTerminalConfig - editor-fs/src/lib.rs - GitStatusSnapshot - editor-issues/src/lib.rs @@ -59,7 +59,7 @@ - editor-syntax/src/tests.rs - fringe.rs - dap_commands.rs -- browser_host.rs +- .new - editor-markdown/src/lib.rs - project_discovery.rs - shell/workspace.rs @@ -70,7 +70,7 @@ - Self - InputField - lsp.rs -- ModelineSegment +- db_commands.rs - PickerOverlay - AcpManager - probe.rs @@ -83,32 +83,32 @@ - EditorRuntime - TextBuffer - state.rs -- shell_ui_mut +- active_shell_buffer_mut - clipboard.rs -- buffer_types.rs +- PluginTextPaneState - ShellBuffer -- status.rs +- LineSyntaxSpan - PluginPackage -- PluginBufferSection -- PaneId +- PluginBuffer +- status.rs - directory.rs - editor-jobs/src/lib.rs - workspace_search.rs - shell/terminal.rs - User Packages - DapClientManager -- git_command_output +- state_with_user_library - repository_files.rs - tool_install.rs - InstallCommand - From - shell_buffer_mut - diagnostics.rs -- .handle_text_input_inner +- ctrl_mod - resolve_picker_extra - load_auto_loaded_packages -- CommandRegistry -- shell_ui +- CommandSource +- install_acp_test_buffer - PathPattern - workspace_nav_tests.rs - shell/text_layout.rs @@ -118,109 +118,109 @@ - editor-dap/src/client/tests.rs - theme_reload.rs - telemetry.rs -- FontSet +- AcpPaneState - registered_queries.rs - ToolInstallError - shell/browser.rs - src/types.rs - paths_logs.rs - shell/picker.rs -- idle.rs +- ping_shell_wakeup - git.rs - load - git_root - PluginKeyBinding - Section -- process.rs +- remote.rs - engines.rs - RVec - launch.rs -- remote.rs -- terminal_ui.rs +- ProcessRegistry +- save.rs - lsp_commands.rs -- rainbow_parens.rs -- LspLogEntry +- UserLibrary +- browser_host.rs - StoredBreakpoint -- shell/compile.rs +- .handle_event - TextBuffer - editor-lsp/src/lib_tests.rs - user/config.rs - ServiceRegistry - .new -- abi.rs +- AbiPaneConfig - syntax_registry_mut - ShellState - oh-my-githubcopilot (OMG) - Intelligent Multi-Agent Orchestration -- Vec +- shell/compile.rs - DbSessionId - PixelRect - AcpClient - CommandLineOverlay - Vec -- commit_git_buffer +- compute_buffer_syntax - acp/tests.rs - shell_user_library - volt/build.rs -- editor-icons/src/lib.rs +- treesittercontext_ghosttext.rs - editor-fs/src/tests.rs - input.rs -- save.rs +- shell_ui_mut - query.rs - user/oil.rs - shell/dap.rs -- ROption -- Diagnostic +- AbiStatuslineContext +- objects.rs - WorkspaceConfigurationValue - user/workspace.rs -- normal_nav.rs +- role - Copilot instructions for `volt` - TerminalRenderSnapshot - DapStoppedSnapshot - InstallRecipe - DbService -- input_shortcuts.rs -- plugin_section_buffer_layout +- install.rs +- render/layout.rs - String - user/terminal.rs - corpus_inventory.rs - FontSet<'ttf> -- render_workspace_dock +- .render - rainbow_paren.rs - user/dap.rs - workers.rs -- EditorModel +- WorkspaceId - user/browser.rs - worktree.rs - editor-lsp/src/client/types.rs - `user` - render_overlays.rs -- load_font_set -- PluginTextPaneState +- process_registry_tests.rs +- compile_reload.rs - Quickfix List PRD - User-Owned Extension Surfaces Migration PRD - Building locally - GitEditorState -- LineSyntaxSpan -- String -- .new -- LspNotificationSnapshot -- LiveTerminalError +- Diagnostic +- toggle_line_comments_in_range +- JobError +- process_supervisor.rs +- LiveTerminalSession - markdown_table.rs - Database Explorer PRD - markdown_table_event_dimensions - ShellConfig - PickerSession - ShellBuffer -- LiveTerminalConfig +- src/session.rs - render_buffer -- TextPoint +- UndoTree - lang/markdown.rs -- .text +- .new - split_layout.rs - PersistedProject - buffer_footer_layout_with_command_line - Self -- LspCompletionItem +- DapSessionHandle - AbiGitFeatureSpec - 0004-markdown-pretty-pipeline.md - Option @@ -231,24 +231,24 @@ - logo.rs - highlight.rs - Language -- Self +- AutocompleteOverlay - Domain Docs - Issue tracker: GitHub -- Option +- PluginCommand - runtime.rs - volt/src/main.rs -- refresh_theme_registry_if_needed -- apply_lsp_notifications -- LanguageInstallPlan -- TextRange -- UserLibrary -- main -- draw_primary_ligature_texture_if_available -- treesittercontext_ghosttext.rs -- .should_follow_output -- treesittercontext_shared.rs -- String - render_chrome.rs +- StreamedCommandExitAction +- TerminalCursorSnapshot +- String +- LspLogSnapshot +- editor-lsp/src/registry.rs +- StatuslineContext +- bash.rs +- .acp_output_viewport_lines +- feature_spec +- String +- clojure.rs - AbiSectionTree - editor-git/src/tests.rs - ligatures.rs @@ -257,7 +257,7 @@ - git_probe_snapshot - workspace_roots.rs - LanguageServerSession -- acp.rs +- AcpPickerItemSpec - 0002-lsp-stop-restart-session-picker.md - 0003-external-command-stream-default.md - MarkdownPrettyPlanCache @@ -267,51 +267,50 @@ - LspCodeAction - editor-lsp/src/client/tests.rs - notification_overlay_layouts -- CommandSource -- .default -- first_project_discovery_root +- elixir.rs +- java.rs +- perl.rs - 0005-dap-session-and-client.md -- headerline_lines -- AbiColor +- php.rs +- JobSpec - KeymapError -- workspace_pane_rects -- Option +- r.rs +- solidity.rs - 0001-csharp-ls-one-session-per-solution.md - triage-labels.md -- GhostTextContext -- xml.rs -- clojure.rs -- graphql.rs +- swift.rs +- LspLogEntry +- default_volt_state_dir +- hover_ui.rs - editor-jobs/src/tests.rs -- .ensure_sessions_for_path -- hcl.rs -- src/session.rs +- LspClientManager +- graphql.rs +- render_tests.rs - acp_chat.rs - 0006-language-server-and-debug-adapter-install.md -- comments.rs -- java.rs +- editor-core/src/lib_tests.rs +- LspNotificationSnapshot - open_acp_client_with_config - kotlin.rs -- lua.rs +- locate.rs - TempFile - nix.rs - init_repo - keymap.rs - .from_connection_string -- perl.rs -- php.rs +- JobResult +- autocomplete_tokens_tests.rs - fill_rounded_corner_canvas -- proto.rs +- AbiLanguageConfiguration - vim_search.rs -- r.rs +- AbiKeymapConfig - command_stream_tests.rs -- config_tests.rs -- ruby.rs -- scala.rs +- ancestor_contexts_for_cursor +- RuntimePopupSnapshot +- panic_payload_message - log.rs - .sync_buffer_to_sessions -- autocomplete_tokens_tests.rs -- solidity.rs +- I - plan_markdown_pretty_ephemeral - build_plan - MarkdownPrettyPlan @@ -319,53 +318,47 @@ - must - transport.rs - DapStackFrameInfo -- user/workspace_dock.rs -- .new +- AbiOilDefaults +- .next_token - package -- Path +- TextPoint - must - Vec -- package +- GhostTextContext - run_demo_shell - must -- ancestor_contexts_for_cursor -- bash.rs -- .load_from_file -- keys.rs -- elixir.rs +- config_tests.rs - setup_standalone_user_repository -- locate.rs - Result -- BufferContextOverlaySnapshot -- syntax_languages +- ISS-owned-process-lifecycle-spec.md - common.rs - DapLogEntry - idle_pacing.rs -- VimActionContext +- UserLibraryModule - OilDefaultsSection - WorkspaceDockConfig - show_paren.rs -- latex.rs - .oil_directory_sections -- .acp_clients -- UserLibraryModule -- package -- .icon_symbols - src/text.rs -- swift.rs -- PaneConfig -- spawn_reader_thread -- DapSessionHandle -- .oil_defaults -- .show_paren_config -- syntax_language -- .acp_rebuild_plan_view -- .terminal_config -- .themes -- AbiPickerTruncateStrategy +- hcl.rs +- 01-process-registry-and-hybrid-launch.md +- PathBuf +- evaluate_expression +- 02-body.md +- 02-shells-and-jobs-owned.md +- ruby.rs +- 03-body.md +- 03-language-tooling-and-helpers-owned.md +- 04-body.md +- 04-spawn-path-contract-and-release-verification.md +- cargo +- .text +- DapThreadInfo +- 0008-user-source-tree-and-plugin-sdk-boundary.md +- 0007-owned-process-hybrid-supervision.md ## God Nodes (most connected - your core abstractions) -1. `EditorRuntime` - 915 edges +1. `EditorRuntime` - 917 edges 2. `shell_ui_mut()` - 423 edges 3. `shell_ui()` - 293 edges 4. `shell_buffer_mut()` - 224 edges @@ -395,147 +388,147 @@ - 3-file cycle: `crates/editor-sdl/src/shell/git/fringe.rs -> crates/editor-sdl/src/shell/git/process.rs -> crates/editor-sdl/src/shell/git/status.rs -> crates/editor-sdl/src/shell/git/fringe.rs` - 4-file cycle: `crates/editor-sdl/src/shell/git/fringe.rs -> crates/editor-sdl/src/shell/git/process.rs -> crates/editor-sdl/src/shell/git/remote.rs -> crates/editor-sdl/src/shell/git/status.rs -> crates/editor-sdl/src/shell/git/fringe.rs` -## Communities (442 total, 22 thin omitted) +## Communities (436 total, 21 thin omitted) ### Community 0 - "DebugConfiguration" Cohesion: 0.06 -Nodes (29): attach_arguments(), launch_arguments(), DapError, DebugAdapterRegistry, DebugAdapterRootStrategy, DebugAdapterSpec, DebugAdapterTransport, DebugConfiguration (+21 more) +Nodes (27): DapError, DebugAdapterRegistry, DebugAdapterRootStrategy, DebugAdapterSpec, DebugAdapterTransport, DebugConfiguration, DebugRequestKind, DebugSessionPlan (+19 more) ### Community 1 - "LspSessionHandle" -Cohesion: 0.05 -Nodes (51): ClientCapabilities, CodeActionParams, LspSessionHandle, Arc, AtomicBool, AtomicU64, BTreeMap, BTreeSet (+43 more) +Cohesion: 0.07 +Nodes (36): LspSessionHandle, AtomicBool, AtomicU64, BTreeMap, BTreeSet, Child, ChildStdin, Debug (+28 more) ### Community 2 - "SyntaxRegistry" -Cohesion: 0.09 -Nodes (30): SyntaxText, Display, Error, Tree, SyntaxError, SyntaxParseSession, text_point_to_tree_sitter_point(), DeferredQuery (+22 more) +Cohesion: 0.08 +Nodes (34): SyntaxText, Display, Error, Tree, SyntaxError, SyntaxNodeContext, SyntaxParseSession, text_point_to_tree_sitter_point() (+26 more) ### Community 3 - "editor-sdl/src/tests.rs" Cohesion: 0.12 Nodes (78): autocomplete_closes_when_no_results_remain(), autocomplete_opens_while_typing_buffer_tokens(), autocomplete_trigger_updates_and_accepts_buffer_tokens(), begin_project_discovery_test(), ctrl_n_and_ctrl_p_cycle_autocomplete_without_inserting_text(), ctrl_space_triggers_autocomplete_without_inserting_space(), discovery_fixture(), file_buffer_reload_refreshes_clean_open_buffers_after_disk_change() (+70 more) -### Community 4 - "acp_view.rs" -Cohesion: 0.09 -Nodes (54): acp_build_output_lines(), acp_build_plan_lines(), acp_decode_image(), acp_diff_display_lines(), acp_icon_segment(), acp_mark_chat(), acp_mark_gutter(), acp_multiline_text_lines() (+46 more) +### Community 4 - "buffer_types.rs" +Cohesion: 0.08 +Nodes (64): acp_build_output_lines(), acp_build_plan_lines(), acp_decode_image(), acp_diff_display_lines(), acp_icon_segment(), acp_mark_chat(), acp_mark_gutter(), acp_multiline_text_lines() (+56 more) ### Community 5 - "user/lib.rs" Cohesion: 0.03 -Nodes (114): bundled_highlight_query(), cached_syntax_languages(), capture_requires_theme_token(), debug_adapters(), every_installed_grammar_highlight_query_compiles(), exported_acp_client_by_id(), exported_acp_clients(), exported_acp_picker_items() (+106 more) +Nodes (95): bundled_highlight_query(), cached_syntax_languages(), capture_requires_theme_token(), debug_adapters(), every_installed_grammar_highlight_query_compiles(), exported_acp_client_by_id(), exported_acp_clients(), exported_autocomplete_providers() (+87 more) ### Community 6 - "LanguageConfiguration" -Cohesion: 0.08 -Nodes (21): normalize_unique_entries(), I, Vec, CaptureThemeMapping, GrammarRecompileFailure, GrammarRecompileReport, LanguageConfiguration, LanguageLoader (+13 more) +Cohesion: 0.05 +Nodes (28): normalize_unique_entries(), I, Vec, windows_msvc_target_triple(), CaptureThemeMapping, GrammarRecompileFailure, GrammarRecompileReport, GrammarSource (+20 more) -### Community 7 - "state_with_user_library" +### Community 7 - "misc.rs" Cohesion: 0.05 -Nodes (145): start_dap_for_active_workspace(), stop_dap_for_active_workspace(), open_workspace_file(), apply_pending_lsp_state(), dap_heuristic_compile_opens_confirm_picker(), dap_continue_to_exit_tears_down_debug_layout(), dap_default_workspace_skips_deep_inference(), dap_locals_and_watches_expand_structured_variables() (+137 more) +Nodes (99): open_workspace_file(), accept_autocomplete(), install_test_lsp_manager(), LspClientManager, state_with_workspace_dock_config(), unique_temp_dir(), write_test_pdf(), accept_autocomplete_avoids_double_dot_when_lsp_insert_includes_trigger() (+91 more) ### Community 8 - "helpers.rs" Cohesion: 0.08 -Nodes (105): acp_at_symbol_opens_git_file_picker_and_return_inserts_mention(), buffer_save_does_not_synchronously_refresh_git_status_buffers(), fetch_git_prune_is_silent_command_without_popup(), git_editor_confirm_preserves_crlf_line_endings(), git_editor_confirm_writes_file_and_signals_stub(), git_pull_upstream_streams_into_popup_buffer(), git_push_upstream_streams_into_popup_buffer_and_refreshes_status(), git_status_buffer_supports_first_commit_on_fresh_repo() (+97 more) +Nodes (107): acp_at_symbol_opens_git_file_picker_and_return_inserts_mention(), buffer_save_does_not_synchronously_refresh_git_status_buffers(), fetch_git_prune_is_silent_command_without_popup(), git_editor_confirm_preserves_crlf_line_endings(), git_editor_confirm_writes_file_and_signals_stub(), git_pull_upstream_streams_into_popup_buffer(), git_push_upstream_streams_into_popup_buffer_and_refreshes_status(), git_status_buffer_supports_first_commit_on_fresh_repo() (+99 more) ### Community 9 - "theme.rs" Cohesion: 0.13 Nodes (50): apply_language_options_table(), apply_options_table(), assert_bundled_theme_omits_shared_sections(), assert_bundled_theme_uses_pallet_colors(), bundled_shared_theme_config(), bundled_theme_sources(), bundled_themes_define_defaults_for_all_compiled_languages(), bundled_themes_use_pallet_sections_and_token_references() (+42 more) ### Community 10 - "draw.rs" -Cohesion: 0.09 -Nodes (49): AcpBufferDraw, AcpPaneDraw, AcpPrefixDraw, BrowserBufferDraw, BrowserSyncView, BufferChrome, BufferChrome<'a>, BufferDecorations (+41 more) +Cohesion: 0.08 +Nodes (54): BrowserBufferLayout, AcpBufferDraw, AcpPaneDraw, AcpPrefixDraw, BrowserBufferDraw, BrowserSyncView, BufferChrome, BufferChrome<'a> (+46 more) -### Community 11 - "RepositoryFilesError" -Cohesion: 0.33 -Nodes (6): GitStatusError, RepositoryFilesError, Display, Error, Formatter, Result +### Community 11 - "parse_status" +Cohesion: 0.22 +Nodes (10): GitStatusError, parse_header(), parse_status(), RepositoryFilesError, Display, Error, Formatter, Result (+2 more) ### Community 12 - "sdk/src/lib.rs" -Cohesion: 0.06 -Nodes (53): AcpClient, AutocompleteProvider, AutocompleteProviderItem, BrowserFeatureSpec, ContextHelpEntry, ContextHelpSpec, DbFeatureSpec, decode_modeline() (+45 more) +Cohesion: 0.05 +Nodes (56): AcpClient, AutocompleteProvider, AutocompleteProviderItem, BrowserFeatureSpec, ContextHelpSpec, DbFeatureSpec, decode_modeline(), decode_modeline_segment() (+48 more) ### Community 13 - "ShellBuffer" -Cohesion: 0.04 -Nodes (17): WrapCacheInlineEdit, WrapCacheInsertPlan, WrapCacheLineSplice, SectionedBufferState, BTreeMap, GitStatusSnapshot, IndexedSyntaxLines, LspDiagnostic (+9 more) +Cohesion: 0.03 +Nodes (23): WrapCacheInlineEdit, WrapCacheInsertPlan, WrapCacheLineSplice, SectionedBufferState, BTreeMap, Error, GitStatusSnapshot, IndexedSyntaxLines (+15 more) ### Community 14 - "editor-dap/src/config.rs" Cohesion: 0.10 Nodes (36): collect_configuration_candidates(), configuration_holes(), DapConfigError, DebugConfigurationCandidate, DebugConfigurationSource, DebugInferContext, DebugStartHistory, DebugStartRecord (+28 more) -### Community 15 - "LiveTerminalSession" -Cohesion: 0.08 -Nodes (21): AlacrittyEvent, Self, LiveTerminalSession, QueuedEventListener, Arc, Debug, Drop, Fn (+13 more) +### Community 15 - "LiveTerminalConfig" +Cohesion: 0.10 +Nodes (22): AlacrittyEvent, terminal_tty_options(), tty_process_id(), LiveTerminalConfig, QueuedEventListener, Arc, Debug, Fn (+14 more) ### Community 16 - "editor-fs/src/lib.rs" -Cohesion: 0.09 +Cohesion: 0.10 Nodes (36): compact_project_path(), default_worktree_common_dir(), detect_project_kind(), DirectoryBuffer, DirectoryEntry, DirectoryEntryKind, discover_projects(), discover_projects_in() (+28 more) ### Community 17 - "GitStatusSnapshot" -Cohesion: 0.07 -Nodes (20): configure_background_command(), detect_in_progress(), GitLogEntry, GitStashEntry, GitStatusSnapshot, parse_header(), parse_stash_list(), parse_status() (+12 more) +Cohesion: 0.08 +Nodes (15): detect_in_progress(), GitLogEntry, GitStashEntry, GitStatusSnapshot, parse_log_oneline(), parse_stash_list(), RepositoryStatus, AsRef (+7 more) ### Community 18 - "editor-issues/src/lib.rs" Cohesion: 0.05 Nodes (112): board_issues(), capture_file(), CaptureItem, CaptureReport, civil_from_days(), CodeReference, comment_prefix_for_path(), confirm_rewrite_applied() (+104 more) ### Community 19 - "DynamicUserLibrary" -Cohesion: 0.03 -Nodes (31): CompilationState, DynamicUserLibrary, LspState, AcpClient, AutocompleteProvider, BrowserFeatureSpec, ContextHelpSpec, DbFeatureSpec (+23 more) +Cohesion: 0.04 +Nodes (23): CompilationState, DynamicUserLibrary, AcpClient, BrowserFeatureSpec, DbFeatureSpec, GitFeatureSpec, GitStatusPrefix, IconFontSymbol (+15 more) ### Community 20 - "HookBus" Cohesion: 0.07 -Nodes (23): HookBus, HookDefinition, HookError, HookEvent, HookSubscription, BTreeMap, BufferId, Default (+15 more) +Nodes (22): HookBus, HookDefinition, HookError, HookEvent, HookSubscription, BTreeMap, BufferId, Default (+14 more) ### Community 21 - "register_shell_hooks" Cohesion: 0.07 -Nodes (110): active_buffer_event_context(), active_buffer_revision_key(), active_shell_buffer_has_input(), active_shell_buffer_id(), active_shell_buffer_is_terminal(), active_shell_buffer_read_only(), active_shell_workspace_id(), apply_sqls_workspace_settings_for_active_buffer_context() (+102 more) +Nodes (99): active_buffer_event_context(), active_buffer_revision_key(), active_shell_buffer_has_input(), active_shell_buffer_id(), active_shell_buffer_is_terminal(), active_shell_buffer_read_only(), active_shell_buffer_vim_targets_input(), active_shell_workspace_id() (+91 more) ### Community 22 - "KeymapScope" Cohesion: 0.13 -Nodes (21): BindingKey, ChordModifier, KeyBinding, KeymapRegistry, KeymapScope, KeymapVimMode, normalize_chord(), normalize_chord_token() (+13 more) +Nodes (20): BindingKey, ChordModifier, KeyBinding, KeymapRegistry, KeymapScope, KeymapVimMode, normalize_chord(), normalize_chord_token() (+12 more) ### Community 23 - "calculator.rs" -Cohesion: 0.09 -Nodes (25): buffer_sections(), calculator_buffer_sections_start_with_single_output_row(), calculator_evaluate_command_emits_generic_plugin_evaluate_hook(), calculator_package_binds_ctrl_c_ctrl_c(), calculator_package_binds_ctrl_tab_to_switch_panes(), calculator_package_declares_its_buffer_through_package_metadata(), calculator_package_exports_open_and_evaluate_commands(), calculator_package_has_no_hook_declarations() (+17 more) +Cohesion: 0.08 +Nodes (29): autocomplete_provider(), buffer_sections(), calculator_autocomplete_provider_scopes_manual_items_to_calculator_buffers(), calculator_buffer_sections_start_with_single_output_row(), calculator_evaluate_command_emits_generic_plugin_evaluate_hook(), calculator_hover_provider_exports_function_and_constant_topics(), calculator_package_binds_ctrl_c_ctrl_c(), calculator_package_binds_ctrl_tab_to_switch_panes() (+21 more) ### Community 24 - "paths.rs" Cohesion: 0.19 Nodes (19): ToolKind, apply_install_bins_to_process_path(), bin_dir(), effective_path(), effective_path_env(), ensure_install_layout(), merge_effective_path(), package_dir() (+11 more) ### Community 25 - "editor-dap/src/client/types.rs" -Cohesion: 0.08 -Nodes (42): Arguments, Response, configure_adapter_command(), connect_tcp(), connect_transport(), DapLogDirection, DapReaderSession, DapSessionEvent (+34 more) +Cohesion: 0.07 +Nodes (54): Arguments, DebugAdapterSpec, Response, attach_arguments(), connect_tcp(), connect_transport(), DapLogDirection, DapReaderSession (+46 more) ### Community 26 - "window_effects.rs" Cohesion: 0.08 Nodes (56): apply_transparency(), apply_window_effects(), apply_window_effects_to_target(), apply_window_transparency(), clear_transparency(), clear_window_transparency(), configure_window_opacity_driver(), current_window_opacity_mode() (+48 more) ### Community 27 - "refresh_pending_streamed_commands" -Cohesion: 0.12 -Nodes (42): configure_background_command(), Command, append_streamed_command_error(), append_streamed_command_header(), append_streamed_command_lines(), continue_streamed_command_popup(), detect_build_command(), drain_completed_output_lines() (+34 more) +Cohesion: 0.17 +Nodes (33): append_streamed_command_error(), append_streamed_command_header(), append_streamed_command_lines(), continue_streamed_command_popup(), detect_build_command(), drain_completed_output_lines(), ExternalCommandInvocation, ExternalCommandResult (+25 more) ### Community 28 - "editor-render/src/lib.rs" -Cohesion: 0.10 -Nodes (40): centered_rect(), default_font_candidates(), find_font_by_name(), find_system_monospace_font(), font_data_matches_name(), font_match_sort_key(), font_name_matches(), font_style_rank() (+32 more) +Cohesion: 0.07 +Nodes (47): centered_rect(), default_font_candidates(), find_font_by_name(), find_system_monospace_font(), font_data_matches_name(), font_match_sort_key(), font_name_matches(), font_style_rank() (+39 more) ### Community 29 - "HoverOverlay" -Cohesion: 0.07 -Nodes (27): test_notification_update(), AutocompleteOverlay, AutocompleteRegistry, HoverOverlay, HoverProviderContent, HoverProviderKind, HoverProviderSpec, HoverRegistry (+19 more) +Cohesion: 0.09 +Nodes (24): test_notification_update(), AutocompleteRegistry, HoverOverlay, HoverProviderContent, HoverProviderKind, HoverProviderSpec, HoverRegistry, NotificationAction (+16 more) ### Community 30 - "ThemeRegistry" Cohesion: 0.08 Nodes (20): Color, BTreeMap, Display, Error, Formatter, From, I, Into (+12 more) ### Community 31 - "PickerItemSpec" -Cohesion: 0.08 -Nodes (40): acp_client_picker_items(), buffer_close_picker_items(), buffer_picker_detail(), buffer_picker_items(), buffer_picker_label(), buffer_picker_shows_file_name_first_and_keeps_path_search(), command_picker_items(), height_fraction() (+32 more) +Cohesion: 0.06 +Nodes (48): BoundedSourceLibrary, Option, Vec, acp_client_picker_items(), buffer_close_picker_items(), buffer_picker_detail(), buffer_picker_items(), buffer_picker_label() (+40 more) ### Community 32 - "font_atlas.rs" Cohesion: 0.12 -Nodes (16): ascii_ligature_byte_ranges_isolate_inline_operator_in_mixed_text(), emoji_raster_font_rasterizes_simple_emoji(), ligature_shaping_accepts_same_length_contextual_substitutions(), ligature_shaping_collapses_material_icon_label_when_enabled(), load_font_set_normalizes_icon_raster_sizes_to_primary_line_height(), material_icons_rasterize_from_nfm_with_fontdue(), Result, String (+8 more) +Nodes (17): shape_ascii_ligature_run_with_face(), ascii_ligature_byte_ranges_isolate_inline_operator_in_mixed_text(), emoji_raster_font_rasterizes_simple_emoji(), ligature_shaping_accepts_same_length_contextual_substitutions(), ligature_shaping_collapses_material_icon_label_when_enabled(), load_font_set_normalizes_icon_raster_sizes_to_primary_line_height(), material_icons_rasterize_from_nfm_with_fontdue(), Result (+9 more) ### Community 33 - "open_image_workspace_file" -Cohesion: 0.27 -Nodes (18): ImageBufferFormat, active_dashboard_editor_buffer(), apply_undo_tree_node(), create_workspace_file_from_query(), file_open_detail(), image_format_for_path(), load_workspace_file_into_db_editor(), open_image_workspace_file() (+10 more) +Cohesion: 0.29 +Nodes (17): ImageBufferFormat, active_dashboard_editor_buffer(), apply_undo_tree_node(), create_workspace_file_from_query(), file_open_detail(), image_format_for_path(), load_workspace_file_into_db_editor(), open_image_workspace_file() (+9 more) ### Community 34 - "shell/pdf.rs" Cohesion: 0.07 -Nodes (53): ImageBufferMode, ImageBufferState, DecodedImage, is_pdf_path(), latex_escape_text(), load_pdf_buffer_state(), open_pdf_workspace_file(), pdf_buffer_lines() (+45 more) +Nodes (52): ImageBufferMode, ImageBufferState, DecodedImage, is_pdf_path(), latex_escape_text(), load_pdf_buffer_state(), open_pdf_workspace_file(), pdf_buffer_lines() (+44 more) ### Community 35 - "AutocompleteProviderConfig" Cohesion: 0.22 @@ -554,8 +547,8 @@ Cohesion: 0.15 Nodes (9): copyButtons, copyText(), initAutoCodeCopy(), navLinks, navToggle, pageSidebar, revealItems, sections (+1 more) ### Community 39 - "DynamicUserLibrary" -Cohesion: 0.04 -Nodes (23): DynamicUserLibrary, AcpClient, BrowserFeatureSpec, DbFeatureSpec, GitFeatureSpec, GitStatusPrefix, IconFontSymbol, KeymapConfig (+15 more) +Cohesion: 0.03 +Nodes (41): DynamicUserLibrary, AcpClient, Arc, AutocompleteProvider, BrowserFeatureSpec, ContextHelpSpec, DbFeatureSpec, DebugAdapterSpec (+33 more) ### Community 40 - "Self" Cohesion: 0.12 @@ -570,72 +563,72 @@ Cohesion: 0.05 Nodes (62): apply_git_fringe_hunk(), build_git_fringe_snapshot_with_cache(), build_git_summary_snapshot(), classify_head_blob(), FringeDiffOp, git_fringe_snapshot_from_texts(), GitFringeKind, GitFringeSnapshot (+54 more) ### Community 43 - "dap_commands.rs" -Cohesion: 0.10 -Nodes (84): add_dap_expression(), apply_dap_breakpoint_extra(), apply_dap_continued_ui(), apply_dap_locals_edits(), apply_dap_stopped_ui(), buffer_is_dap_layout_side(), continue_dap_start(), dap_breakpoint_cursor_target() (+76 more) +Cohesion: 0.11 +Nodes (83): add_dap_expression(), apply_dap_breakpoint_extra(), apply_dap_continued_ui(), apply_dap_locals_edits(), apply_dap_stopped_ui(), buffer_is_dap_layout_side(), continue_dap_start(), dap_breakpoint_cursor_target() (+75 more) -### Community 44 - "browser_host.rs" -Cohesion: 0.08 -Nodes (41): allow_browser_drag_drop(), browser_additional_args(), browser_additional_args_from_env(), browser_additional_args_from_env_appends_custom_args(), browser_additional_args_from_env_appends_web_security_bypass(), browser_host_event_for_ipc(), browser_host_ipc_event_ignores_unknown_messages(), browser_host_ipc_event_routes_focus_parent_requests() (+33 more) +### Community 44 - ".new" +Cohesion: 0.14 +Nodes (22): browser_host_event_for_ipc(), browser_navigation_retry_required(), BrowserHostEvent, BrowserHostService, BrowserInstance, BrowserLocationUpdate, DesktopBrowserHostService, optional_non_empty_text() (+14 more) ### Community 45 - "editor-markdown/src/lib.rs" Cohesion: 0.15 -Nodes (34): apply_link_pretty(), apply_structure_node(), atx_heading_marker(), conceal_line_text(), ConcealRange, default_icon_map(), is_thematic_break(), line_icon_prefix() (+26 more) +Nodes (36): apply_link_pretty(), apply_structure_node(), atx_heading_marker(), conceal_line_text(), ConcealRange, default_icon_map(), ImageDestination, is_thematic_break() (+28 more) ### Community 46 - "project_discovery.rs" Cohesion: 0.09 -Nodes (48): Condvar, ProjectSearchRoot, apply_scan_result(), background_tick_state(), CacheInner, cancel_project_discovery_scan(), candidate_roots_equal(), current_project_discovery_snapshot() (+40 more) +Nodes (49): Condvar, ProjectSearchRoot, apply_scan_result(), background_tick_state(), CacheInner, cancel_project_discovery_scan(), candidate_roots_equal(), current_project_discovery_snapshot() (+41 more) ### Community 47 - "shell/workspace.rs" Cohesion: 0.10 -Nodes (73): workspace_marked_slot_jump_switches_open_opens_closed_and_handles_empty_missing(), WindowMoveDirection, acp_dock_buffer_label(), active_project_workspace_root(), active_runtime_buffer(), active_window_id(), active_workspace_root(), canonicalize_project_root_path() (+65 more) +Nodes (69): workspace_marked_slot_jump_switches_open_opens_closed_and_handles_empty_missing(), MarkListState, WindowMoveDirection, acp_dock_buffer_label(), active_project_workspace_root(), active_runtime_buffer(), active_window_id(), active_workspace_root() (+61 more) ### Community 48 - "TextEdit" Cohesion: 0.17 Nodes (30): TextEdit, add_counts(), add_delta(), apply_edits_to_counts(), AutocompleteTokenCache, AutocompleteTokenScan, AutocompleteTokenScanKind, char_at_byte() (+22 more) ### Community 49 - "LspClientError" -Cohesion: 0.11 -Nodes (16): LspClientManager, Arc, BTreeSet, F, Into, Option, Path, PathBuf (+8 more) +Cohesion: 0.10 +Nodes (19): LspClientManager, Option, Result, LspClientManager, Arc, BTreeSet, F, Into (+11 more) ### Community 50 - "workers_autocomplete.rs" Cohesion: 0.08 -Nodes (61): DbAutocompleteCandidate, autocomplete_query_allows_empty_member_access_after_dot_and_arrow(), AutocompleteEntry, AutocompleteProviderSpec, AutocompleteQuery, AutocompleteProviderItem, AutocompleteProviderKind, attach_token_count_edits() (+53 more) +Nodes (57): AutocompleteEntry, AutocompleteProviderSpec, AutocompleteQuery, AutocompleteProviderItem, AutocompleteProviderKind, attach_token_count_edits(), autocomplete_entries(), autocomplete_request_for_buffer() (+49 more) ### Community 51 - "HeaderlineTestUserLibrary" -Cohesion: 0.07 -Nodes (7): AtomicUsize, HeaderlineTestUserLibrary, Arc, Default, LigatureConfig, MarkdownPrettyConfig, OilKeybindings +Cohesion: 0.04 +Nodes (34): AtomicUsize, buffer_autocomplete_request(), HeaderlineTestUserLibrary, render_shell_state_scene_with_docked_runtime_popup(), render_shell_state_scene_with_notification_overlay(), AcpClient, Arc, AutocompleteProvider (+26 more) ### Community 52 - "Self" Cohesion: 0.06 -Nodes (16): AcpActionSpec, AcpPickerContext, AcpPickerItemSpec, AcpPickerKind, AcpPickerOption, DbActionSpec, DbBrowserItemContext, picker_provider_spec_accepts_extra_keybinds() (+8 more) +Nodes (17): default_action(), hook_command(), AcpActionSpec, AcpPickerOption, ContextHelpEntry, DbActionSpec, DbBrowserItemContext, DbBrowserItemKind (+9 more) ### Community 53 - "InputField" -Cohesion: 0.08 -Nodes (7): InputField, Into, Option, Self, String, TextBuffer, Vec +Cohesion: 0.07 +Nodes (12): BufferContextOverlayCacheKey, BufferContextOverlaySnapshot, InlineCompletionState, InputField, BTreeMap, Into, Option, Self (+4 more) ### Community 54 - "lsp.rs" Cohesion: 0.17 Nodes (23): auto_start_binding_details(), auto_start_bindings_match_registered_server_path_matchers(), clojure_lsp_recipe(), copilot_language_server(), csharp_workspace_configuration_remains_well_formed_when_present(), has_command(), install_recipe_for_language_server(), language_servers() (+15 more) -### Community 55 - "ModelineSegment" -Cohesion: 0.18 -Nodes (27): autocomplete_docs_line_count(), autocomplete_docs_lines(), autocomplete_docs_panel_width(), autocomplete_preview_lines(), centered_image_draw_rect(), image_buffer_viewport_rect(), render_footer_separator(), render_image_buffer_body() (+19 more) +### Community 55 - "db_commands.rs" +Cohesion: 0.17 +Nodes (53): buffer_is_db_browser(), buffer_is_db_dashboard(), buffer_is_db_sidebar(), activate_db_browser_line(), active_or_open_dashboard_buffer(), apply_db_browser_view(), apply_db_browser_view_to_section(), apply_db_results_to_output_section() (+45 more) ### Community 56 - "PickerOverlay" Cohesion: 0.05 -Nodes (16): DirectoryPrefixState, MarkListState, PickerKind, PickerOverlay, QuickfixEntry, QuickfixState, BTreeSet, IndexedSyntaxLines (+8 more) +Nodes (24): absolute_path_hint(), DebugLayoutState, DirectoryPrefixState, DismissedPopupState, GitBranchActionKind, GitCommitActionKind, KeySequenceState, PaneSplitDirection (+16 more) ### Community 57 - "AcpManager" Cohesion: 0.08 -Nodes (37): AcpUiAction, CompletionTrigger, pending_slash_trigger(), PendingSlashTrigger, acp_file_uri(), AcpManager, compose_acp_prompt_blocks(), file_mention_content_block() (+29 more) +Nodes (39): AcpUiAction, CompletionTrigger, pending_slash_trigger(), PendingSlashTrigger, acp_file_uri(), AcpManager, compose_acp_prompt_blocks(), file_mention_content_block() (+31 more) ### Community 58 - "probe.rs" Cohesion: 0.11 Nodes (42): CachedProbe, compute_identity_snapshot(), fallback_rev_parse(), fill_numstat(), git_probe_snapshot_with_numstat(), GitProbeSnapshot, HeadParse, identity_revision() (+34 more) ### Community 59 - "TextSnapshot" -Cohesion: 0.08 -Nodes (19): Range, advance_point_by_text(), HighlightDocument, Item, Iterator, Option, Range, Rope (+11 more) +Cohesion: 0.11 +Nodes (11): HighlightDocument, Item, Option, Rope, String, TextBuffer, Vec, TextSnapshot (+3 more) ### Community 60 - "build_output.rs" Cohesion: 0.18 @@ -646,88 +639,92 @@ Cohesion: 0.20 Nodes (22): KeySequenceOptions, KeySequencePush, KeySequenceTick, normalize_token(), PendingKeySequence, push_key_sequence(), resolve_tokens(), retain_live_pending() (+14 more) ### Community 62 - "textures.rs" -Cohesion: 0.13 -Nodes (40): PrimaryTextRenderMode, PrimaryTextRun, shaped_run_preserves_monospace_layout(), shaped_run_uses_cell_grid(), ShapedGlyph, ShapedRun, adjusted_contextual_ligature_pixel_size(), ascii_ligature_byte_ranges_with_face() (+32 more) +Cohesion: 0.06 +Nodes (80): FontSet, PrimaryTextRenderMode, PrimaryTextRun, shaped_run_preserves_monospace_layout(), shaped_run_uses_cell_grid(), ShapedGlyph, ShapedRun, adjusted_contextual_ligature_pixel_size() (+72 more) ### Community 63 - "ShellError" -Cohesion: 0.06 -Nodes (34): Display, Error, From, ShellError, Self, shell_docks_layout(), key_sequence_options(), keymap_vim_mode() (+26 more) +Cohesion: 0.07 +Nodes (26): Display, Error, From, ShellError, browser_sync_plan(), workspace_pane_rects(), popup_window_height(), popup_content_rect() (+18 more) ### Community 64 - "HighlightWindow" Cohesion: 0.08 Nodes (50): B, apply_text_edits_to_span(), buffer_text_for_byte_range(), changed_range_windows(), collect_injection_regions(), collect_structure_nodes(), create_parser(), highlight_inline_language_per_line() (+42 more) ### Community 65 - "EditorRuntime" -Cohesion: 0.08 -Nodes (99): EditorRuntime, Default, buffer_is_git_status(), active_git_status_command_context(), clear_key_sequence(), ensure_no_rebase_in_progress(), ensure_rebase_in_progress(), git_status_apply_commit_command() (+91 more) +Cohesion: 0.07 +Nodes (101): EditorRuntime, Default, Self, runtime_descriptor(), RuntimeDescriptor, runtime_descriptor_matches_expected_foundation_values(), active_git_status_command_context(), ensure_no_rebase_in_progress() (+93 more) ### Community 66 - "TextBuffer" -Cohesion: 0.06 -Nodes (22): EditRecord, AsRef, Default, Fn, Into, Option, Path, PathBuf (+14 more) +Cohesion: 0.08 +Nodes (8): Default, Fn, Option, String, Vec, TextBuffer, BufferStats, is_sentence_closer() ### Community 67 - "state.rs" -Cohesion: 0.09 -Nodes (24): reverse_find_kind(), BlockInsertState, DirectoryYankEntry, LastFind, LastSearch, BTreeMap, BufferId, Default (+16 more) +Cohesion: 0.07 +Nodes (34): clear_key_sequence(), active_workspace_has_debug_session(), key_sequence_options(), keymap_vim_mode(), Option, Result, Vec, ShellState (+26 more) -### Community 68 - "shell_ui_mut" -Cohesion: 0.04 -Nodes (188): active_shell_buffer_mut(), active_shell_buffer_vim_targets_input(), buffer_is_directory(), shell_ui_mut(), Cow, yank_to_clipboard_text(), Result, String (+180 more) +### Community 68 - "active_shell_buffer_mut" +Cohesion: 0.06 +Nodes (118): active_shell_buffer_mut(), add_next_multicursor_match(), add_previous_multicursor_match(), advance_point_by_text(), apply_multicursor_delete(), apply_multicursor_insert_text(), apply_multicursor_motion(), apply_multicursor_operator_motion() (+110 more) ### Community 69 - "clipboard.rs" -Cohesion: 0.12 -Nodes (34): ClipboardUtil, clipboard_data_for_mime(), clipboard_image_from_path(), clipboard_image_from_path_text(), clipboard_image_from_uri_list(), clipboard_text_for_mime(), clipboard_video_ready(), ClipboardContext (+26 more) +Cohesion: 0.11 +Nodes (36): ClipboardUtil, clipboard_data_for_mime(), clipboard_image_from_path(), clipboard_image_from_path_text(), clipboard_image_from_uri_list(), clipboard_text_for_mime(), clipboard_video_ready(), ClipboardContext (+28 more) -### Community 70 - "buffer_types.rs" -Cohesion: 0.06 -Nodes (41): acp_output_header_title(), acp_pane_content_rows(), acp_pane_cursor_visual_row(), acp_pane_line_index_for_visual_row(), acp_pane_max_scroll_visual_row(), acp_pane_point_for_visual_row(), acp_pane_total_visual_rows(), acp_rendered_line_row_count() (+33 more) +### Community 70 - "PluginTextPaneState" +Cohesion: 0.07 +Nodes (21): AcpBufferState, AcpPane, PluginSectionBufferState, PluginTextPaneState, Arc, Box, BTreeMap, BufferId (+13 more) ### Community 71 - "ShellBuffer" Cohesion: 0.05 Nodes (4): Option, String, ShellBuffer, line_wrap_row_count() -### Community 72 - "status.rs" -Cohesion: 0.18 -Nodes (30): find_paren_number_range(), format_section_line(), git_status_command_name(), git_status_commit_item_spans(), git_status_commit_message_spans(), git_status_entry_item_spans(), git_status_entry_token(), git_status_entry_token_from_icon() (+22 more) +### Community 72 - "LineSyntaxSpan" +Cohesion: 0.10 +Nodes (51): cell_theme_token(), connection_line_spans(), db_browser_line_spans(), db_results_error_spans(), db_results_line_spans(), db_results_syntax_lines(), db_results_table_row_spans(), db_schema_column_spans() (+43 more) ### Community 73 - "PluginPackage" +Cohesion: 0.04 +Nodes (69): package(), package(), package(), package_exports_image_commands(), package_exports_image_keybindings(), package(), LanguageConfiguration, syntax_language() (+61 more) + +### Community 74 - "PluginBuffer" Cohesion: 0.06 -Nodes (43): package(), package(), package(), LanguageConfiguration, syntax_language(), package(), LanguageConfiguration, syntax_language() (+35 more) +Nodes (13): dashboard_sections(), sidebar_sections(), DbBrowserKind, PickerKeybindingContext, plugin_buffer_sections_can_declare_nested_layout_tree(), PluginBuffer, PluginBufferLayout, PluginBufferLayoutAxis (+5 more) -### Community 74 - "PluginBufferSection" -Cohesion: 0.07 -Nodes (12): dashboard_sections(), sidebar_sections(), DbBrowserKind, PickerKeybindingContext, plugin_buffer_sections_can_declare_nested_layout_tree(), PluginBufferLayout, PluginBufferLayoutAxis, PluginBufferLayoutNode (+4 more) +### Community 75 - "status.rs" +Cohesion: 0.13 +Nodes (34): buffer_is_git_status(), handle_git_status_chord(), cancel_git_commit_buffer(), commit_git_buffer(), git_commit_message(), git_commit_temp_path(), open_git_commit_buffer(), BufferId (+26 more) ### Community 76 - "directory.rs" Cohesion: 0.11 Nodes (55): apply_directory_edit_actions(), apply_directory_edit_queue(), apply_directory_listing_patch(), apply_directory_state(), copy_directory_recursive(), copy_directory_yank_entries(), copy_directory_yank_entries_copies_files_and_directories(), create_dir_action_creates_empty_directory() (+47 more) ### Community 77 - "editor-jobs/src/lib.rs" -Cohesion: 0.05 -Nodes (80): apply_command_environment(), apply_windows_runtime_environment(), build_job_command(), command_candidate_names(), CompilationResult, CompilationRunner, configure_background_command(), default_process_supervisor_executable() (+72 more) +Cohesion: 0.18 +Nodes (34): apply_command_environment(), build_job_launch_spec(), command_candidate_names(), configure_background_command(), default_process_supervisor_executable(), enrich_env_with_node_manager(), environment_value(), explicit_windows_env_value() (+26 more) ### Community 78 - "workspace_search.rs" Cohesion: 0.08 -Nodes (66): collect_search_output_stops_after_limit(), parse_grep_workspace_search_line_finds_case_insensitive_column(), parse_rg_workspace_search_line_extracts_location(), PickerEntry, search_is_case_sensitive(), apply_lsp_code_action(), apply_lsp_document_edit(), collect_search_output() (+58 more) +Nodes (65): collect_search_output_stops_after_limit(), parse_grep_workspace_search_line_finds_case_insensitive_column(), parse_rg_workspace_search_line_extracts_location(), PickerEntry, search_is_case_sensitive(), apply_lsp_code_action(), apply_lsp_document_edit(), collect_search_output() (+57 more) ### Community 79 - "shell/terminal.rs" -Cohesion: 0.15 -Nodes (43): active_terminal_dimensions(), begin_active_terminal_normal_mode(), box_drawing_segments(), BoxDrawingSegments, buffer_is_terminal(), close_terminal_buffer(), close_terminal_buffers_for_workspace(), CursorTextOverlay (+35 more) +Cohesion: 0.13 +Nodes (49): active_terminal_dimensions(), begin_active_terminal_normal_mode(), box_drawing_segments(), BoxDrawingSegments, buffer_is_terminal(), close_terminal_buffer(), close_terminal_buffers_for_workspace(), CursorTextOverlay (+41 more) ### Community 80 - "User Packages" Cohesion: 0.04 Nodes (46): Actions, Adding a Build Command for a New Language, Adding a Command to an Existing Plugin, Adding Language Support, Adding YAML config to your own plugin, Adding Your Own Provider, Architecture Overview, Autocomplete Providers (+38 more) ### Community 81 - "DapClientManager" -Cohesion: 0.11 -Nodes (21): DapClientManager, Arc, BTreeSet, DapLogSnapshot, DebugAdapterSpec, Into, IntoIterator, Item (+13 more) +Cohesion: 0.12 +Nodes (19): DapClientManager, BTreeSet, DapLogSnapshot, Into, IntoIterator, Item, Option, Path (+11 more) -### Community 82 - "git_command_output" -Cohesion: 0.18 -Nodes (33): git_command_output(), delete_git_status_targets(), git_action_detail(), git_line_is_untracked(), git_status_action_targets(), git_status_delete_target_for_line(), git_status_delete_targets(), git_status_selected_lines() (+25 more) +### Community 82 - "state_with_user_library" +Cohesion: 0.08 +Nodes (78): start_dap_for_active_workspace(), stop_dap_for_active_workspace(), db_connect_enter_submits_pasted_connection_string(), db_dashboard_execute_replaces_output_and_concatenates_multiple_queries(), db_dashboard_layout_places_sidebar_left_and_editor_output_right(), db_dashboard_opens_and_writes_files_through_editor_section(), db_multiview_disables_golden_ratio_and_narrows_left_sidebar(), db_multiview_toggle_restores_golden_ratio() (+70 more) ### Community 83 - "repository_files.rs" -Cohesion: 0.17 -Nodes (34): cache_key(), CachedRepoFileList, default_worktree_common_dir(), file_fingerprint(), FileFingerprint, invalidate_repository_file_list_cache_for(), list_generation(), list_repository_files() (+26 more) +Cohesion: 0.15 +Nodes (36): configure_background_command(), Command, cache_key(), CachedRepoFileList, default_worktree_common_dir(), file_fingerprint(), FileFingerprint, invalidate_repository_file_list_cache_for() (+28 more) ### Community 84 - "tool_install.rs" Cohesion: 0.18 @@ -739,35 +736,35 @@ Nodes (20): program_is_available(), archive_commands(), command(), commands_for_ ### Community 86 - "From" Cohesion: 0.06 -Nodes (34): AbiBrowserFeatureSpec, AbiCaptureThemeMapping, AbiContextHelpEntry, AbiContextHelpSpec, AbiDbFeatureSpec, AbiGhostTextLine, AbiGrammarSource, AbiIconFontCategory (+26 more) +Nodes (34): DebugAdapterRootStrategy, AbiBrowserFeatureSpec, AbiContextHelpEntry, AbiContextHelpSpec, AbiDbFeatureSpec, AbiDebugAdapterRootStrategy, AbiIconFontCategory, AbiLanguageServerRootStrategy (+26 more) ### Community 87 - "shell_buffer_mut" -Cohesion: 0.06 -Nodes (125): shell_buffer(), shell_buffer_mut(), default_error_log_path(), markdown_pretty_paint_plan(), ShellBuffer, assert_wrap_cache_matches_cold_build(), install_markdown_test_buffer(), install_text_test_buffer() (+117 more) +Cohesion: 0.07 +Nodes (114): shell_buffer(), shell_buffer_mut(), default_error_log_path(), markdown_pretty_paint_plan(), ShellBuffer, assert_wrap_cache_matches_cold_build(), install_markdown_test_buffer(), install_text_test_buffer() (+106 more) ### Community 88 - "diagnostics.rs" Cohesion: 0.14 Nodes (23): covering_syntax_span_for_range(), diagnostic_color(), diagnostic_columns_for_line(), diagnostic_line_spans_for_diagnostics(), diagnostic_severity_rank(), diagnostic_underlines_for_segment(), DiagnosticLineSpan, DiagnosticUnderlineSpan (+15 more) -### Community 89 - ".handle_text_input_inner" -Cohesion: 0.09 -Nodes (35): active_workspace_has_debug_session(), alt_mod(), browser_devtools_shortcut_requested(), build_keydown_chord(), ChordModifiers, ctrl_mod(), gui_mod(), keycode_name_token() (+27 more) +### Community 89 - "ctrl_mod" +Cohesion: 0.14 +Nodes (30): map_vim_mode(), alt_mod(), browser_devtools_shortcut_requested(), build_keydown_chord(), ChordModifiers, ctrl_mod(), gui_mod(), keycode_name_token() (+22 more) ### Community 90 - "resolve_picker_extra" -Cohesion: 0.10 +Cohesion: 0.11 Nodes (16): PickerExportableRow, PickerExtraDispatch, PickerExtraKeybind, PickerOneShotContext, PickerSelectedRow, resolve_picker_extra(), Into, Option (+8 more) ### Community 91 - "load_auto_loaded_packages" Cohesion: 0.11 -Nodes (40): auto_loaded_packages(), bootstrap(), clear_package_registrations(), detail_filter_matches(), HostBootstrap, HostError, is_self_contained(), load_auto_loaded_packages() (+32 more) +Nodes (38): auto_loaded_packages(), clear_package_registrations(), detail_filter_matches(), HostError, is_self_contained(), load_auto_loaded_packages(), map_buffer_kind(), map_scope() (+30 more) -### Community 92 - "CommandRegistry" -Cohesion: 0.09 -Nodes (16): CommandHandler, CommandDefinition, CommandError, CommandRegistry, RegisteredCommand, BTreeMap, Display, Error (+8 more) +### Community 92 - "CommandSource" +Cohesion: 0.08 +Nodes (22): CommandHandler, CommandDefinition, CommandError, CommandRegistry, CommandSource, RegisteredCommand, BTreeMap, Display (+14 more) -### Community 93 - "shell_ui" -Cohesion: 0.09 -Nodes (74): shell_ui(), buffer_footer_layout(), acp_buffer_layout(), ShellBuffer, acp_dock_entries_list_active_workspace_buffers(), acp_dock_focus_j_k_cycle_buffers(), acp_dock_layout_shrinks_content_on_the_right(), acp_dock_toggle_shows_and_hides() (+66 more) +### Community 93 - "install_acp_test_buffer" +Cohesion: 0.10 +Nodes (56): input_panel_chrome_height(), plugin_section_buffer_layout(), plugin_section_panel_chrome_height(), plugin_section_row_budget(), PluginSectionLayout, Option, ShellBuffer, Vec (+48 more) ### Community 94 - "PathPattern" Cohesion: 0.13 @@ -778,16 +775,16 @@ Cohesion: 0.08 Nodes (21): cycle_project_workspace(), CycleDirection, marked_workspace_jump(), MarkedWorkspaceJump, MarkList, normalize_project_root_path(), plan_worktree_remove(), project_roots_equal() (+13 more) ### Community 96 - "shell/text_layout.rs" -Cohesion: 0.08 -Nodes (42): context_overlay_cache_reuses_stale_snapshot_while_typing(), adjust_tag_child_indent(), ascii_control_caret_notation(), buffer_context_overlay_snapshot(), byte_index_for_char_column(), cached_context_overlay_snapshot(), closing_tag_name_after_cursor(), desired_indent_columns_for_text() (+34 more) +Cohesion: 0.09 +Nodes (48): context_overlay_cache_reuses_stale_snapshot_while_typing(), adjust_tag_child_indent(), apply_line_indent(), ascii_control_caret_notation(), buffer_context_overlay_snapshot(), byte_index_for_char_column(), cached_context_overlay_snapshot(), closing_tag_name_after_cursor() (+40 more) ### Community 97 - "shell/hover.rs" Cohesion: 0.13 -Nodes (49): append_hover_rendered_content(), apply_markdown_code_fence_syntax(), apply_signature_active_parameter_emphasis(), char_offset_to_line_col(), diagnostic_matches_cursor_line(), finalize_hover_overlay(), finalize_hover_provider_content(), hover_diagnostic_fragments_for_diagnostic() (+41 more) +Nodes (51): ActiveLspBufferContext, PathBuf, append_hover_rendered_content(), apply_markdown_code_fence_syntax(), apply_signature_active_parameter_emphasis(), char_offset_to_line_col(), diagnostic_matches_cursor_line(), finalize_hover_overlay() (+43 more) ### Community 98 - "ShellUiState" -Cohesion: 0.04 -Nodes (23): acp_dock_visible(), buffer_is_oil_preview(), absolute_path_hint(), BufferViewState, DebugLayoutState, DismissedPopupState, GitBranchActionKind, GitCommitActionKind (+15 more) +Cohesion: 0.05 +Nodes (12): acp_dock_visible(), BufferViewState, BTreeMap, BufferId, Instant, Option, PaneId, ShellBuffer (+4 more) ### Community 99 - "user/modeline.rs" Cohesion: 0.18 @@ -798,16 +795,16 @@ Cohesion: 0.09 Nodes (46): assert_control_requests_omit_nulls(), build_csharp_fixture(), client_initialize_launch_disconnect_against_fake_tcp_adapter(), continue_step_pause_and_locals_against_fake_adapter(), continue_to_process_exit_queues_terminated(), dap_log_text(), debug_stop_after_attach_leaves_process_running(), expand_collapse_and_reapply_nested_locals_and_watches() (+38 more) ### Community 101 - "theme_reload.rs" -Cohesion: 0.21 -Nodes (29): asset_path_from_parts(), current_theme_source_fingerprint(), current_user_config_source_fingerprint(), default_workspace_root(), resolve_bundled_icon_font_dir(), resolve_bundled_icon_font_paths(), resolve_default_workspace_root(), resolve_emoji_font_path() (+21 more) +Cohesion: 0.08 +Nodes (75): PathBuf, ThemeRuntimeSlots, normalized_raster_pixel_size_matches_target_line_height(), rebuild_theme_registry_preserves_active_theme_when_still_present(), theme_runtime_settings_resolve_window_effects_from_theme_options(), asset_path_from_parts(), current_theme_source_fingerprint(), current_user_config_source_fingerprint() (+67 more) ### Community 102 - "telemetry.rs" Cohesion: 0.09 -Nodes (29): ActiveTypingFrameProfile, ErrorEntry, ErrorLog, ErrorSeverity, FpsOverlaySnapshot, FpsOverlayState, LspLogBufferState, BTreeMap (+21 more) +Nodes (29): append_error_log(), ActiveTypingFrameProfile, ErrorEntry, ErrorLog, ErrorSeverity, FpsOverlaySnapshot, FpsOverlayState, LspLogBufferState (+21 more) -### Community 103 - "FontSet" -Cohesion: 0.11 -Nodes (37): PathBuf, ThemeRuntimeSlots, FontSet, from_render_color(), alpha_bitmap_surface(), CanvasTextSink, compose_emoji_surface(), compose_ligature_surface() (+29 more) +### Community 103 - "AcpPaneState" +Cohesion: 0.09 +Nodes (21): acp_pane_content_rows(), acp_pane_cursor_visual_row(), acp_pane_line_index_for_visual_row(), acp_pane_max_scroll_visual_row(), acp_pane_point_for_visual_row(), acp_pane_total_visual_rows(), acp_rendered_line_row_count(), acp_rendered_text_segments() (+13 more) ### Community 104 - "registered_queries.rs" Cohesion: 0.16 @@ -818,92 +815,92 @@ Cohesion: 0.13 Nodes (25): Display, Error, Formatter, From, Result, Self, String, ToolInstallError (+17 more) ### Community 106 - "shell/browser.rs" -Cohesion: 0.11 -Nodes (48): apply_browser_location_updates(), apply_browser_page_load_state(), browser_buffer_display_name(), browser_buffer_layout(), browser_display_url(), browser_host_viewport_rect(), browser_surface_buffer_at_point(), browser_sync_plan() (+40 more) +Cohesion: 0.12 +Nodes (44): BrowserViewportRect, apply_browser_location_updates(), apply_browser_page_load_state(), browser_buffer_display_name(), browser_display_url(), browser_host_viewport_rect(), browser_surface_buffer_at_point(), browser_url_candidates() (+36 more) ### Community 107 - "src/types.rs" Cohesion: 0.08 -Nodes (59): ColumnData, box_row(), box_rule(), BoxRuleKind, build_tokio_runtime(), CellAlign, column_is_numeric(), current_statement() (+51 more) +Nodes (60): ColumnData, box_row(), box_rule(), BoxRuleKind, build_tokio_runtime(), CellAlign, column_is_numeric(), current_statement() (+52 more) ### Community 108 - "paths_logs.rs" -Cohesion: 0.10 -Nodes (51): append_error_log(), average_duration(), buffer_kind_label(), build_shell_summary(), clear_saved_theme_selection(), ensure_log_directory(), errors_buffer_lines(), format_duration_ms() (+43 more) - -### Community 109 - "shell/picker.rs" Cohesion: 0.11 -Nodes (44): buffer_close_confirm_overlay(), buffer_picker_preview(), ensure_picker_keybindings(), message_picker_overlay(), overlay_result_limit(), picker_action_from_spec(), picker_entries(), picker_overlay() (+36 more) +Nodes (46): average_duration(), buffer_kind_label(), build_shell_summary(), clear_saved_theme_selection(), ensure_log_directory(), errors_buffer_lines(), format_duration_ms(), format_error_entry_lines() (+38 more) -### Community 110 - "idle.rs" +### Community 109 - "shell/picker.rs" Cohesion: 0.13 -Nodes (18): attach_shell_wakeup(), idle_wait_timeout(), idle_wait_timeout_ms(), Arc, AtomicBool, Duration, Event, Instant (+10 more) +Nodes (40): buffer_close_confirm_overlay(), buffer_picker_preview(), ensure_picker_keybindings(), message_picker_overlay(), overlay_result_limit(), picker_action_from_spec(), picker_entries(), picker_overlay() (+32 more) + +### Community 110 - "ping_shell_wakeup" +Cohesion: 0.10 +Nodes (21): attach_shell_wakeup(), idle_wait_timeout(), idle_wait_timeout_ms(), ping_shell_wakeup(), Arc, AtomicBool, Duration, Event (+13 more) ### Community 111 - "git.rs" -Cohesion: 0.11 -Nodes (47): Self, commit_buffer_template(), commit_buffer_template_matches_git_commit_message_format(), commit_buffer_template_shows_initial_commit_without_head(), commit_section(), feature_spec(), flatten_section_ids(), git_section_title() (+39 more) +Cohesion: 0.10 +Nodes (49): Self, Self, Self, commit_buffer_template(), commit_buffer_template_matches_git_commit_message_format(), commit_buffer_template_shows_initial_commit_without_head(), commit_section(), feature_spec() (+41 more) ### Community 112 - "load" Cohesion: 0.17 Nodes (22): ConfigFingerprint, CachedUserConfig, config_cache(), config_fingerprint_for_files(), config_root_dir(), config_root_dir_from_exe_dir(), config_root_prefers_workspace_user_directory(), config_source_files() (+14 more) ### Community 113 - "git_root" -Cohesion: 0.18 -Nodes (44): run_command(), cherry_pick_apply_at_point_or_picker(), cherry_pick_commit_at_point_or_picker(), cherry_pick_git_commit(), cherry_pick_git_commit_no_commit(), git_commit_at_point(), git_merge_in_progress(), git_rebase_in_progress() (+36 more) +Cohesion: 0.10 +Nodes (77): run_command(), cherry_pick_apply_at_point_or_picker(), cherry_pick_commit_at_point_or_picker(), cherry_pick_git_commit(), cherry_pick_git_commit_no_commit(), git_commit_at_point(), git_merge_in_progress(), git_rebase_in_progress() (+69 more) ### Community 114 - "PluginKeyBinding" -Cohesion: 0.09 -Nodes (25): map_vim_mode(), plugin_buffer_binding_scope_active(), plugin_vim_mode_matches(), plugin_key_binding_can_target_multiple_commands(), PluginKeyBinding, PluginKeymapScope, PluginVimMode, I (+17 more) +Cohesion: 0.13 +Nodes (21): plugin_key_binding_can_target_multiple_commands(), PluginKeyBinding, PluginKeymapScope, I, leader_binding(), normal_binding(), normal_binding_commands(), package() (+13 more) ### Community 115 - "Section" Cohesion: 0.14 Nodes (14): render_section(), BTreeSet, Into, Option, Self, String, Vec, Section (+6 more) -### Community 116 - "process.rs" -Cohesion: 0.14 -Nodes (35): parse_log_oneline(), git_branch_list(), git_commit_list(), git_remote_worktree_branch_list(), open_git_branch_picker(), open_git_branch_picker_with_action(), GitLogEntry, Path (+27 more) +### Community 116 - "remote.rs" +Cohesion: 0.09 +Nodes (64): git_branch_list(), git_commit_list(), git_remote_worktree_branch_list(), open_git_branch_picker(), GitLogEntry, Path, Result, String (+56 more) ### Community 117 - "engines.rs" Cohesion: 0.13 -Nodes (28): Compat, connect_sql_server(), ConnectionDescriptor, DbActionOutcome, execute_postgres(), execute_sql_server(), execute_sqlite(), execution_notice() (+20 more) +Nodes (29): Compat, connect_sql_server(), ConnectionDescriptor, DbActionOutcome, execute_postgres(), execute_sql_server(), execute_sqlite(), execution_notice() (+21 more) ### Community 118 - "RVec" -Cohesion: 0.14 -Nodes (13): AbiAcpClient, AbiHoverProvider, AbiHoverProviderTopic, AbiTerminalConfig, AcpClient, HoverProvider, HoverProviderTopic, AcpClient (+5 more) +Cohesion: 0.09 +Nodes (21): exported_terminal_config(), AbiAcpClient, AbiAutocompleteProvider, AbiAutocompleteProviderItem, AbiHoverProvider, AbiHoverProviderTopic, AbiTerminalConfig, AcpClient (+13 more) ### Community 119 - "launch.rs" -Cohesion: 0.16 -Nodes (47): apply_background_pipes(), apply_command_environment(), apply_launch_environment(), background_command_candidates(), background_command_names(), background_spawn_should_retry(), BackgroundCommandPipes, build_background_command() (+39 more) +Cohesion: 0.13 +Nodes (53): apply_command_environment(), apply_launch_environment(), background_command_candidates(), background_command_names(), BackgroundCommandPipes, configure_background_command(), environment_value(), explicit_environment_value() (+45 more) -### Community 120 - "remote.rs" -Cohesion: 0.22 -Nodes (31): fetch_git_all(), fetch_git_prune(), fetch_git_pushremote(), fetch_git_remote(), fetch_git_upstream(), git_branch_merge(), git_branch_push_remote(), git_branch_remote() (+23 more) +### Community 120 - "ProcessRegistry" +Cohesion: 0.07 +Nodes (41): ExternalOwnedTree, force_kill_tree(), language_server_share_key(), LaunchedProcess, owned_process_pid_alive(), OwnedProcessEntry, OwnedProcessId, OwnedProcessTree (+33 more) -### Community 121 - "terminal_ui.rs" -Cohesion: 0.20 -Nodes (25): install_terminal_popup_test_buffer(), install_terminal_test_buffer(), dismissed_popup_toggle_restores_terminal_buffer(), pane_terminal_dimensions_shrink_when_acp_dock_open(), popup_terminal_dimensions_match_painted_content_rect(), popup_terminal_enter_falls_through_to_terminal_input(), popup_terminal_enter_writes_to_live_session(), popup_terminal_escape_enters_normal_mode() (+17 more) +### Community 121 - "save.rs" +Cohesion: 0.22 +Nodes (37): cancel_lsp_sync_for_path(), Path, apply_lsp_text_edits(), cleanup_formatter_temp(), close_buffer_discard(), close_buffer_immediate(), close_buffer_save(), close_buffer_with_prompt() (+29 more) ### Community 123 - "lsp_commands.rs" -Cohesion: 0.09 -Nodes (78): active_lsp_buffer_context(), cancel_lsp_sync_for_path(), Path, ActiveLspBufferContext, PathBuf, WorkspaceId, load_window_icon(), register_lsp_status_hooks() (+70 more) +Cohesion: 0.11 +Nodes (71): active_lsp_buffer_context(), load_window_icon(), register_lsp_status_hooks(), Result, Surface, active_lsp_code_action_range(), active_workspace_open_buffer_paths(), apply_copilot_auth_notification() (+63 more) -### Community 124 - "rainbow_parens.rs" -Cohesion: 0.47 -Nodes (4): config(), package(), package_exports_toggle_command_and_binding(), rainbow_config_load_stays_cheap_for_frame_budget() +### Community 124 - "UserLibrary" +Cohesion: 0.17 +Nodes (18): BufferKind, browser_state_for_kind(), buffer_is_db_connect(), buffer_is_quickfix(), default_vim_target(), is_issues_board_kind(), buffer_interaction(), plugin_buffer_line_wrap() (+10 more) -### Community 125 - "LspLogEntry" -Cohesion: 0.15 -Nodes (5): LspLogDirection, LspLogEntry, LspLogSnapshot, LspTransportLog, SystemTime +### Community 125 - "browser_host.rs" +Cohesion: 0.10 +Nodes (18): allow_browser_drag_drop(), browser_additional_args(), browser_additional_args_from_env(), browser_additional_args_from_env_appends_custom_args(), browser_additional_args_from_env_appends_web_security_bypass(), browser_host_ipc_event_ignores_unknown_messages(), browser_host_ipc_event_routes_focus_parent_requests(), browser_host_ipc_event_routes_open_devtools_requests() (+10 more) ### Community 126 - "StoredBreakpoint" Cohesion: 0.16 Nodes (15): BreakpointStore, BreakpointToggle, debug_source_paths_eq(), normalize_debug_source_path(), normalize_optional_text(), paths_equal(), BTreeMap, Into (+7 more) -### Community 127 - "shell/compile.rs" -Cohesion: 0.16 -Nodes (30): buffer_is_command_output(), built_user_library_path_for_command(), catch_unwind_silently(), command_output_buffer_name(), compile_buffer_name(), current_runtime_user_library_candidates(), handle_directory_chord(), handle_directory_keydown_chord() (+22 more) +### Community 127 - ".handle_event" +Cohesion: 0.11 +Nodes (23): acp_dock_card_height(), acp_dock_entry_at_point(), acp_dock_width(), AcpDockEntry, AcpDockLayout, BufferId, Option, Self (+15 more) ### Community 128 - "TextBuffer" -Cohesion: 0.07 -Nodes (22): language_matches_markup_tags(), ShowParenMatch, WordKind, Option, TextBuffer, WordMotionClass, delimiter_partner(), find_matching_close_tag() (+14 more) +Cohesion: 0.10 +Nodes (9): language_matches_markup_tags(), WordKind, Option, TextBuffer, is_object_separator(), is_punctuation_char(), is_word_char(), matches_word_kind() (+1 more) ### Community 129 - "editor-lsp/src/lib_tests.rs" Cohesion: 0.12 @@ -914,32 +911,32 @@ Cohesion: 0.18 Nodes (23): default_oil_close(), default_oil_create_git_worktree(), default_oil_cycle_sort(), default_oil_open_entry(), default_oil_open_external(), default_oil_open_horizontal_split(), default_oil_open_new_pane(), default_oil_open_parent() (+15 more) ### Community 131 - "ServiceRegistry" -Cohesion: 0.11 -Nodes (10): BoxedService, Self, runtime_descriptor(), RuntimeDescriptor, runtime_descriptor_matches_expected_foundation_values(), HashMap, Option, T (+2 more) +Cohesion: 0.17 +Nodes (6): BoxedService, HashMap, Option, T, ServiceRegistry, TypeId ### Community 132 - ".new" Cohesion: 0.05 -Nodes (47): zig_flat_grammar_uses_bundled_queries(), help_entry(), ContextHelpEntry, hook_command(), package(), hook_command(), hook_command_detail(), package() (+39 more) +Nodes (50): zig_flat_grammar_uses_bundled_queries(), feature_spec(), BrowserFeatureSpec, help_entry(), ContextHelpEntry, hook_command(), package(), package() (+42 more) -### Community 133 - "abi.rs" -Cohesion: 0.06 -Nodes (31): DebugAdapterRootStrategy, AbiDebugAdapterRootStrategy, AbiDebugAdapterSpec, AbiDebugAdapterTransport, AbiDebugAdapterTransportKind, AbiInstallRecipe, AbiInstallRecipeKind, AbiMarkdownPrettyConfig (+23 more) +### Community 133 - "AbiPaneConfig" +Cohesion: 0.05 +Nodes (27): exported_keymap_config(), exported_ligature_config(), exported_pane_config(), KeymapConfig, LigatureConfig, MarkdownPrettyConfig, PickerLayout, ShowParenConfig (+19 more) ### Community 134 - "syntax_registry_mut" Cohesion: 0.26 Nodes (27): syntax_registry_mut(), apply_tree_sitter_recompile_notification(), continue_next_tree_sitter_recompile(), continue_tree_sitter_install(), continue_tree_sitter_install_after_clone(), continue_tree_sitter_install_after_generate(), continue_tree_sitter_recompile(), continue_tree_sitter_recompile_after_clone() (+19 more) ### Community 135 - "ShellState" -Cohesion: 0.09 -Nodes (24): active_theme_state_path(), default_mark_list_path(), default_typing_profile_log_path(), default_volt_state_dir(), PathBuf, MouseDragState, Arc, BufferId (+16 more) +Cohesion: 0.11 +Nodes (19): MouseDragState, Arc, BufferId, Duration, Instant, Into, IntoIterator, Item (+11 more) ### Community 136 - "oh-my-githubcopilot (OMG) - Intelligent Multi-Agent Orchestration" Cohesion: 0.09 Nodes (21): Agent Catalog, Analysis Skills, Cancellation, Commit Protocol, Completion Rules, Delegation Rules, Execution Protocols, Global Rules (+13 more) -### Community 137 - "Vec" -Cohesion: 0.07 -Nodes (13): AutocompleteProvider, ContextHelpSpec, DebugAdapterSpec, GhostTextLine, HoverProvider, LanguageConfiguration, LanguageServerSpec, String (+5 more) +### Community 137 - "shell/compile.rs" +Cohesion: 0.16 +Nodes (29): buffer_is_command_output(), built_user_library_path_for_command(), catch_unwind_silently(), command_builds_user_library(), command_output_buffer_name(), compile_buffer_name(), current_runtime_user_library_candidates(), open_command_output_buffer() (+21 more) ### Community 138 - "DbSessionId" Cohesion: 0.11 @@ -947,7 +944,7 @@ Nodes (21): Option, db_browser_renderer_customizes_rows_and_preserves_actions(), ### Community 139 - "PixelRect" Cohesion: 0.17 -Nodes (30): DrawCommand, PixelRect, RenderColor, Arc, Self, pixel_rect_contains_point(), scene_has_inverted_block_cursor_glyph(), OverlayCard (+22 more) +Nodes (31): DrawCommand, PixelRect, RenderColor, Arc, Self, pixel_rect_contains_point(), from_render_color(), scene_has_inverted_block_cursor_glyph() (+23 more) ### Community 140 - "AcpClient" Cohesion: 0.06 @@ -961,68 +958,68 @@ Nodes (10): CommandLineCompletionState, CommandLineOverlay, CommandLinePurpose, Cohesion: 0.29 Nodes (9): AcpClientConfig, AcpSection, default_acp_clients(), default_project_search_roots(), OilSection, Vec, UserConfig, WorkspaceRootConfig (+1 more) -### Community 143 - "commit_git_buffer" -Cohesion: 0.13 -Nodes (28): cancel_git_commit_buffer(), commit_git_buffer(), git_commit_message(), git_commit_temp_path(), open_git_commit_buffer(), BufferId, PathBuf, Result (+20 more) +### Community 143 - "compute_buffer_syntax" +Cohesion: 0.24 +Nodes (17): compute_buffer_syntax(), configure_syntax_refresh_worker(), index_syntax_lines_with_rainbow_parens(), install_optional_runtime_services(), install_tree_sitter_language(), queue_buffer_syntax_refresh(), recompile_installed_tree_sitter_languages(), refresh_buffer_syntax() (+9 more) ### Community 144 - "acp/tests.rs" -Cohesion: 0.12 -Nodes (35): acp_file_mention_at_cursor_requires_token_start(), buffer_lookup_is_scoped_to_workspace(), close_buffer_disconnects_sessions_and_clears_reuse_state(), coalesce_acp_events_merges_adjacent_agent_text_chunks(), connected_event_for_closed_buffer_disconnects_orphaned_session(), drain_events_shows_incremental_plan_progress_across_frames(), install_acp_test_buffer(), merge_acp_slash_commands_puts_universal_clear_first_and_dedupes_agent_clear() (+27 more) +Cohesion: 0.14 +Nodes (31): acp_file_mention_at_cursor_requires_token_start(), buffer_lookup_is_scoped_to_workspace(), close_buffer_disconnects_sessions_and_clears_reuse_state(), connected_event_for_closed_buffer_disconnects_orphaned_session(), drain_events_shows_incremental_plan_progress_across_frames(), install_acp_test_buffer(), merge_acp_slash_commands_puts_universal_clear_first_and_dedupes_agent_clear(), merge_node_manager_environment_keeps_manager_path_first() (+23 more) ### Community 145 - "shell_user_library" -Cohesion: 0.09 -Nodes (54): execute_oil_action(), OilKeyAction, active_directory_root(), active_runtime_surface(), active_shell_buffer_path(), ensure_directory_buffer(), ensure_shell_buffer(), evaluate_active_plugin_buffer() (+46 more) +Cohesion: 0.12 +Nodes (54): buffer_is_directory(), execute_oil_action(), handle_directory_chord(), handle_directory_keydown_chord(), OilKeyAction, open_directory_entry(), active_directory_root(), active_runtime_surface() (+46 more) ### Community 146 - "volt/build.rs" Cohesion: 0.14 Nodes (46): add_standalone_workspace_root(), build_windows_icon(), copy_assets_directory(), copy_dir_recursive(), copy_file_with_retry(), copy_user_directory(), create_dir_all_with_retry(), inline_workspace_package_fields() (+38 more) -### Community 147 - "editor-icons/src/lib.rs" -Cohesion: 0.14 -Nodes (13): all_symbols(), find_symbol(), IconFontCategory, IconFontSymbol, IconFontCategory, Option, Path, String (+5 more) +### Community 147 - "treesittercontext_ghosttext.rs" +Cohesion: 0.07 +Nodes (39): all_symbols(), find_symbol(), IconFontCategory, IconFontSymbol, IconFontCategory, Option, Path, String (+31 more) ### Community 148 - "editor-fs/src/tests.rs" -Cohesion: 0.24 -Nodes (36): project_discovery_snapshot(), Result, set_project_discovery_worker_blocked_for_test(), wait_for_project_discovery(), begin_discovery_persist(), directory_buffer_reads_and_renames_entries(), discover_projects_finds_git_repositories_and_worktrees(), discover_projects_max_depth_zero_considers_only_the_root() (+28 more) +Cohesion: 0.26 +Nodes (35): project_discovery_snapshot(), set_project_discovery_worker_blocked_for_test(), wait_for_project_discovery(), begin_discovery_persist(), directory_buffer_reads_and_renames_entries(), discover_projects_finds_git_repositories_and_worktrees(), discover_projects_max_depth_zero_considers_only_the_root(), discover_projects_resolves_git_for_windows_worktree_metadata() (+27 more) ### Community 149 - "input.rs" Cohesion: 0.09 Nodes (64): acp_complete_slash(), acp_cycle_mode(), acp_disconnect(), acp_image_mention_token(), acp_insert_file_mention(), acp_insert_slash_command(), acp_leading_slash_command_name(), acp_load_session() (+56 more) -### Community 150 - "save.rs" -Cohesion: 0.23 -Nodes (37): apply_lsp_text_edits(), cleanup_formatter_temp(), close_buffer_discard(), close_buffer_immediate(), close_buffer_save(), close_buffer_with_prompt(), close_lsp_buffers_for_workspace(), close_popup_buffer_and_restore_focus() (+29 more) +### Community 150 - "shell_ui_mut" +Cohesion: 0.09 +Nodes (75): browser_buffer_layout(), shell_ui(), shell_ui_mut(), apply_lsp_notifications(), apply_pending_lsp_state(), lsp_notification_action(), lsp_notification_body_lines(), notification_severity() (+67 more) ### Community 151 - "query.rs" Cohesion: 0.12 Nodes (47): aligned_indent_column(), append_query_source(), compile_query_source(), current_line_starts_with_token(), delimiter_column(), desired_indent_for_loaded_language(), evaluate_general_predicate(), first_content_column_after() (+39 more) ### Community 152 - "user/oil.rs" -Cohesion: 0.08 -Nodes (39): seti_directory_icon(), exported_oil_strip_entry_icon_prefix(), chord_action(), default_oil_keybindings_map_to_actions(), defaults(), directory_entry_display_label(), directory_entry_display_label_from_parts(), directory_sections() (+31 more) +Cohesion: 0.09 +Nodes (38): seti_directory_icon(), chord_action(), default_oil_keybindings_map_to_actions(), defaults(), directory_entry_display_label(), directory_entry_display_label_from_parts(), directory_sections(), feature_spec() (+30 more) ### Community 153 - "shell/dap.rs" Cohesion: 0.16 Nodes (25): BreakpointState, breakpoint_extras(), breakpoint_glyph(), breakpoint_glyph_token(), dap_breakpoint_display_path(), dap_breakpoint_line_spans(), dap_breakpoint_lines(), dap_breakpoint_syntax_lines() (+17 more) -### Community 154 - "ROption" -Cohesion: 0.15 -Nodes (13): exported_statusline_render(), statusline_context_from_abi(), AbiAutocompleteProvider, AbiAutocompleteProviderItem, AbiLspDiagnosticsInfo, AbiStatuslineContext, AutocompleteProvider, AutocompleteProviderItem (+5 more) +### Community 154 - "AbiStatuslineContext" +Cohesion: 0.31 +Nodes (6): exported_statusline_render(), statusline_context_from_abi(), AbiLspDiagnosticsInfo, AbiStatuslineContext, LspDiagnosticsInfo, LspDiagnosticsInfo -### Community 155 - "Diagnostic" -Cohesion: 0.08 -Nodes (22): full_sync_uses_null_range_change(), sample_diagnostic(), full_document_range(), lsp_code_action_diagnostic(), lsp_diagnostic_severity(), lsp_position_from_text_point(), lsp_range_from_text_range(), parse_diagnostic() (+14 more) +### Community 155 - "objects.rs" +Cohesion: 0.18 +Nodes (13): ShowParenMatch, delimiter_partner(), find_matching_close_tag(), is_inline_whitespace(), is_plausible_unquoted_tag_body_char(), is_tag_name_start_char(), parse_tag_token(), parse_tag_token_at() (+5 more) ### Community 156 - "WorkspaceConfigurationValue" -Cohesion: 0.17 -Nodes (8): AsRef, BTreeMap, From, Number, T, WorkspaceConfigurationValue, K, V +Cohesion: 0.14 +Nodes (7): AsRef, From, Number, T, WorkspaceConfigurationValue, K, V ### Community 157 - "user/workspace.rs" Cohesion: 0.11 -Nodes (50): begin_discovery_override(), discovery_test_lock(), DiscoveryOverrideGuard, existing_workspace_for_project(), git_available(), message_item(), override_project_search_roots_for_test(), package() (+42 more) +Nodes (48): begin_discovery_override(), discovery_test_lock(), DiscoveryOverrideGuard, existing_workspace_for_project(), git_available(), override_project_search_roots_for_test(), package(), package_exports_clone_command() (+40 more) -### Community 158 - "normal_nav.rs" -Cohesion: 0.24 +### Community 158 - "role" +Cohesion: 0.18 Nodes (19): absolute_line_for_number(), apply_terminal_normal_motion(), apply_terminal_scrolloff(), begin_terminal_normal_mode(), end_terminal_normal_mode(), last_occupied_line(), Option, T (+11 more) ### Community 159 - "Copilot instructions for `volt`" @@ -1030,12 +1027,12 @@ Cohesion: 0.12 Nodes (15): Agent skills, Architecture, Build, test, and lint, caveman, Copilot instructions for `volt`, Cursor Cloud specific instructions, Domain docs, graphify (+7 more) ### Community 160 - "TerminalRenderSnapshot" -Cohesion: 0.11 +Cohesion: 0.12 Nodes (20): push_snapshot_line(), push_terminal_render_run(), resolve_terminal_background(), resolve_terminal_foreground(), resolve_terminal_index_color(), resolve_terminal_named_color(), resolve_terminal_plain_color(), Into (+12 more) ### Community 161 - "DapStoppedSnapshot" -Cohesion: 0.07 -Nodes (23): apply_expanded_paths(), apply_expanded_watch_roots(), capture_stopped_snapshot(), collapse_variable_path(), DapStoppedSnapshot, DapThreadInfo, DapVariableNode, DapVariablePath (+15 more) +Cohesion: 0.08 +Nodes (22): apply_expanded_paths(), apply_expanded_watch_roots(), capture_stopped_snapshot(), collapse_variable_path(), DapStoppedSnapshot, DapVariableNode, DapVariablePath, DapVariableRow (+14 more) ### Community 162 - "InstallRecipe" Cohesion: 0.22 @@ -1043,23 +1040,23 @@ Nodes (9): InstallRecipe, AsRef, Into, IntoIterator, Item, Option, Self, String ### Community 163 - "DbService" Cohesion: 0.12 -Nodes (19): DbService, Arc, BTreeMap, HashMap, HashSet, Path, PathBuf, Result (+11 more) +Nodes (18): DbService, Arc, BTreeMap, HashMap, HashSet, Path, PathBuf, Result (+10 more) -### Community 164 - "input_shortcuts.rs" -Cohesion: 0.23 -Nodes (19): focus_input_normal_mode(), install_user_acp_test_buffer(), acp_dock_ctrl_h_exits_focus_back_to_panes(), acp_dock_ctrl_l_enters_focus_when_open(), acp_input_field_cw_enters_insert_mode(), acp_input_field_dd_deletes_current_line(), acp_input_field_dw_deletes_motion_range(), acp_input_field_o_and_o_open_new_lines() (+11 more) - -### Community 165 - "plugin_section_buffer_layout" +### Community 164 - "install.rs" Cohesion: 0.12 -Nodes (32): input_panel_chrome_height(), plugin_section_buffer_layout(), plugin_section_panel_chrome_height(), plugin_section_row_budget(), PluginSectionLayout, Option, Rect, ShellBuffer (+24 more) +Nodes (26): asset_path_from_parts(), command_failure_message(), configure_background_command(), default_install_root(), default_query_asset_root(), ensure_cloned_grammar_dir_exists(), io_error(), normalize_extension() (+18 more) + +### Community 165 - "render/layout.rs" +Cohesion: 0.16 +Nodes (24): autocomplete_docs_line_count(), autocomplete_docs_lines(), autocomplete_docs_panel_width(), autocomplete_preview_lines(), centered_image_draw_rect(), image_buffer_viewport_rect(), Option, Rect (+16 more) ### Community 166 - "String" Cohesion: 0.54 Nodes (4): call_function(), Parser<'a, 'b>, Result, String ### Community 167 - "user/terminal.rs" -Cohesion: 0.19 -Nodes (12): default_terminal_args(), default_terminal_program(), exported_terminal_config(), TerminalConfig, default_shell_args(), default_shell_args_fallback(), default_shell_program(), default_shell_program_fallback() (+4 more) +Cohesion: 0.21 +Nodes (11): default_terminal_args(), default_terminal_program(), TerminalConfig, default_shell_args(), default_shell_args_fallback(), default_shell_program(), default_shell_program_fallback(), package() (+3 more) ### Community 168 - "corpus_inventory.rs" Cohesion: 0.22 @@ -1067,31 +1064,31 @@ Nodes (19): collect_invalid_capture_valued_set_directives(), collect_operators() ### Community 169 - "FontSet<'ttf>" Cohesion: 0.10 -Nodes (15): TextStyle, EmojiFont, FontSet<'ttf>, FontSetInit, IconFont, OptionalFontLoadMode, BTreeSet, Font (+7 more) +Nodes (14): EmojiFont, FontSet<'ttf>, FontSetInit, IconFont, OptionalFontLoadMode, BTreeSet, Font, Option (+6 more) -### Community 170 - "render_workspace_dock" +### Community 170 - ".render" Cohesion: 0.10 -Nodes (33): acp_dock_card_height(), acp_dock_entry_at_point(), acp_dock_width(), AcpDockEntry, AcpDockLayout, render_acp_dock(), BufferId, Option (+25 more) +Nodes (24): Option, Result, ShellBuffer, Window, WindowSize, ShellState, refresh_workspace_dock_branches(), Instant (+16 more) ### Community 171 - "rainbow_paren.rs" -Cohesion: 0.09 -Nodes (35): apply_rainbow_delimiter_spans(), apply_rainbow_delimiter_spans_for_buffer(), apply_rainbow_delimiter_spans_inner(), BracketSpan, delimiter_kind(), DelimiterFamily, depth_close_capture(), depth_face_index() (+27 more) +Cohesion: 0.11 +Nodes (34): apply_rainbow_delimiter_spans(), apply_rainbow_delimiter_spans_for_buffer(), apply_rainbow_delimiter_spans_inner(), BracketSpan, delimiter_kind(), DelimiterFamily, depth_close_capture(), depth_face_index() (+26 more) ### Community 172 - "user/dap.rs" Cohesion: 0.20 Nodes (17): adapter_preferences_match_language_defaults(), codelldb_recipe(), debug_adapters(), debug_adapters_attach_typed_install_recipes(), install_recipe_for_debug_adapter(), locals_buffer_declares_locals_and_expressions_sections(), locals_sections(), package() (+9 more) ### Community 173 - "workers.rs" -Cohesion: 0.06 -Nodes (51): BackingFileFingerprint, Path, Result, SystemTime, ping_shell_wakeup(), ping_without_sdl_attach_is_noop(), create_file_reload_watcher(), enqueue_file_reload_event() (+43 more) +Cohesion: 0.07 +Nodes (49): BackingFileFingerprint, Path, Result, SystemTime, create_file_reload_watcher(), enqueue_file_reload_event(), FileReloadWorkerCommand, FileReloadWorkerOutcome (+41 more) -### Community 174 - "EditorModel" +### Community 174 - "WorkspaceId" Cohesion: 0.06 -Nodes (40): command_registry_executes_commands_and_hooks_dispatch_events(), EventLog, model_closes_active_pane_without_closing_buffers(), model_focuses_existing_buffer_in_active_pane(), model_splits_pane_and_focuses(), model_switches_and_closes_workspaces(), Result, String (+32 more) +Nodes (26): Buffer, EditorModel, ModelError, Pane, Popup, BTreeMap, BufferId, Display (+18 more) ### Community 175 - "user/browser.rs" -Cohesion: 0.23 -Nodes (10): buffer_lines(), buffer_lines_include_current_url_when_present(), feature_spec(), input_hint(), package(), package_exports_browser_open_command(), BrowserFeatureSpec, Option (+2 more) +Cohesion: 0.25 +Nodes (8): buffer_lines(), buffer_lines_include_current_url_when_present(), input_hint(), package(), package_exports_browser_open_command(), Option, String, Vec ### Community 176 - "worktree.rs" Cohesion: 0.19 @@ -1099,23 +1096,23 @@ Nodes (33): begin_oil_worktree_request(), checkout_git_branch(), create_git_work ### Community 177 - "editor-lsp/src/client/types.rs" Cohesion: 0.04 -Nodes (126): BufRead, server_request_response_turns_copilot_show_document_into_browser_action(), transport_message_redacts_sqls_connection_secrets(), workspace_configuration_requests_return_matching_section_settings(), workspace_configuration_requests_support_unsectioned_settings(), active_parameter_char_range(), apply_command_environment(), apply_windows_runtime_environment() (+118 more) +Nodes (110): BufRead, completion_parser_falls_back_to_detail_for_documentation_panel(), copilot_status_notifications_offer_sign_in_action(), full_sync_uses_null_range_change(), hover_parser_formats_marked_string_language_blocks_as_markdown(), hover_parser_keeps_plaintext_markup_plain(), hover_parser_preserves_markdown_content(), log_message_error_from_ols_does_not_become_ui_notification() (+102 more) ### Community 178 - "`user`" Cohesion: 0.17 Nodes (11): Building the user package, Change shared UI options, font, and language defaults, Change the default theme, Change theme colors, Changing the theme and font, Making configuration changes, Other runtime-backed config files, Project discovery: `workspace.rs` (+3 more) ### Community 179 - "render_overlays.rs" -Cohesion: 0.09 -Nodes (50): dap_install_server_opens_recipe_picker(), lsp_install_server_opens_recipe_picker(), lsp_install_unknown_id_returns_error(), Result, String, workspace_compile_confirm_reuses_existing_streamed_popup(), workspace_compile_escape_does_not_store_command(), workspace_compile_prefills_with_detected_command_for_cargo_toml() (+42 more) - -### Community 180 - "load_font_set" -Cohesion: 0.16 -Nodes (28): normalized_raster_pixel_size_matches_target_line_height(), theme_runtime_settings_resolve_window_effects_from_theme_options(), load_deferred_emoji_font(), load_emoji_font(), load_font_set(), load_font_set_with_mode(), load_icon_font(), load_icon_fonts_for_paths() (+20 more) +Cohesion: 0.15 +Nodes (32): install_hover_test_overlay(), apply_pending_lsp_state_toasts_only_when_notification_revision_moves(), autocomplete_docs_focus_and_scroll_update_overlay_state(), block_cursor_text_overlay_positions_multibyte_cursor_text(), browser_sync_plan_avoids_notification_overlays(), context_overlay_snapshot_reuses_same_arc_when_key_matches(), copilot_auth_notification_shows_device_code_and_stays_active(), file_reload_notifications_wait_for_dirty_buffers_to_become_clean() (+24 more) -### Community 181 - "PluginTextPaneState" +### Community 180 - "process_registry_tests.rs" Cohesion: 0.14 -Nodes (4): PluginSectionBufferState, PluginTextPaneState, IndexedSyntaxLines, Option +Nodes (36): discover_volt_supervisor_exe(), synthetic_sleep_command(), application_quit_clears_entire_registry(), descendant_tree_spec(), graceful_then_force_deadline_always_reaps_tree(), interactive_launch_skips_supervisor_wrap(), job_manager_launch_is_registry_owned_and_dies_on_workspace_close(), language_tooling_shared_share_key_keeps_then_kills_on_last_close() (+28 more) + +### Community 181 - "compile_reload.rs" +Cohesion: 0.25 +Nodes (20): dap_heuristic_compile_opens_confirm_picker(), dap_install_server_opens_recipe_picker(), lsp_install_server_opens_recipe_picker(), lsp_install_unknown_id_returns_error(), Result, String, workspace_compile_confirm_reuses_existing_streamed_popup(), workspace_compile_escape_does_not_store_command() (+12 more) ### Community 182 - "Quickfix List PRD" Cohesion: 0.20 @@ -1133,25 +1130,25 @@ Nodes (10): Build both at the same time, Build the packaged local distribution, Cohesion: 0.21 Nodes (19): abort_git_editor_buffer(), confirm_git_editor_buffer(), finish_git_editor_buffer(), GitEditorSession, GitEditorState, inject_git_editor_env(), open_git_editor_buffer(), refresh_pending_git_editor() (+11 more) -### Community 186 - "LineSyntaxSpan" -Cohesion: 0.18 -Nodes (24): cell_theme_token(), connection_line_spans(), db_browser_line_spans(), db_results_error_spans(), db_results_line_spans(), db_results_syntax_lines(), db_results_table_row_spans(), db_schema_column_spans() (+16 more) +### Community 186 - "Diagnostic" +Cohesion: 0.11 +Nodes (15): CodeActionParams, code_action_params_use_flattened_lsp_shape(), sample_diagnostic(), code_action_params(), lsp_code_action_diagnostic(), lsp_diagnostic_severity(), lsp_range_from_text_range(), parse_diagnostic() (+7 more) -### Community 187 - "String" -Cohesion: 0.21 -Nodes (19): ActiveBufferEventContext, apply_git_status_snapshot(), git_status_sequence_kind(), GitPrefixState, invalidate_git_state_after_save(), open_git_stash_list_buffer(), open_git_status_popup(), refresh_git_status_buffer() (+11 more) +### Community 187 - "toggle_line_comments_in_range" +Cohesion: 0.29 +Nodes (14): block_comment_toggle_removal_lens(), comment_style_for_buffer(), comment_style_for_language_path(), comment_toggle_removal_len(), CommentStyle, matches_comment_id(), Option, Result (+6 more) -### Community 188 - ".new" -Cohesion: 0.14 -Nodes (9): buffer_autocomplete_request(), AutocompleteProvider, DebugAdapterSpec, HoverProvider, LanguageConfiguration, LanguageServerSpec, TextBuffer, Vec (+1 more) +### Community 188 - "JobError" +Cohesion: 0.20 +Nodes (11): CompilationRunner, JobError, JobHandle, Display, Error, Formatter, From, JoinHandle (+3 more) -### Community 189 - "LspNotificationSnapshot" -Cohesion: 0.16 -Nodes (5): LspClientManager, Option, LspNotificationEntry, LspNotificationLog, LspNotificationSnapshot +### Community 189 - "process_supervisor.rs" +Cohesion: 0.18 +Nodes (25): ProcessSupervisionMode, configure_supervised_command(), exit_with_status(), maybe_run(), ParentProcess, parse_request(), parse_request_accepts_background_targets(), ProcessSupervisorRequest (+17 more) -### Community 190 - "LiveTerminalError" -Cohesion: 0.11 -Nodes (12): LiveTerminalError, Display, Error, Formatter, From, Result, terminal_key_bytes(), TerminalDimensions (+4 more) +### Community 190 - "LiveTerminalSession" +Cohesion: 0.07 +Nodes (21): Keycode, Mod, terminal_key_for_event(), LiveTerminalError, LiveTerminalSession, Display, Drop, Error (+13 more) ### Community 191 - "markdown_table.rs" Cohesion: 0.26 @@ -1177,52 +1174,52 @@ Nodes (25): best_contiguous_substring_bonus(), contiguous_substring_bonus(), is_ Cohesion: 0.10 Nodes (6): Arc, BufferId, Option, Rect, String, ShellBuffer -### Community 197 - "LiveTerminalConfig" +### Community 197 - "src/session.rs" Cohesion: 0.08 -Nodes (21): TerminalCursorDraw, terminal_cursor_shape_for_input_mode(), terminal_tty_options(), tty_process_id(), append_lines(), LiveTerminalConfig, Into, IntoIterator (+13 more) +Nodes (26): terminal_render_snapshot_strips_tab_and_control_tofu_from_git_status_style_output(), append_lines(), cube_color_component(), default_terminal_index_color(), default_terminal_named_color(), map_terminal_cursor_shape(), Into, NamedColor (+18 more) ### Community 198 - "render_buffer" -Cohesion: 0.07 -Nodes (101): render_browser_buffer_body(), BufferBodyPalette, CellMetrics, DrawTarget, Color, Vec, render_picker_overlay(), acp_draw_prefix_segments() (+93 more) +Cohesion: 0.08 +Nodes (99): render_acp_dock(), Result, render_browser_buffer_body(), BufferBodyPalette, CellMetrics, PanelPalette, DrawTarget, Color (+91 more) -### Community 199 - "TextPoint" -Cohesion: 0.09 -Nodes (17): Self, Selection, TextPoint, Path, Vec, format_undo_snapshot_diff(), Option, Self (+9 more) +### Community 199 - "UndoTree" +Cohesion: 0.18 +Nodes (13): Vec, format_undo_snapshot_diff(), Option, Self, String, TextBuffer, Vec, snapshot_lines() (+5 more) ### Community 200 - "lang/markdown.rs" Cohesion: 0.19 Nodes (15): default_pretty_icons(), inline_syntax_language(), package(), package_auto_attaches_markdown_extensions_and_formatter(), pretty_config(), pretty_config_ships_consistent_icon_map(), pretty_icon_map(), BTreeMap (+7 more) -### Community 201 - ".text" -Cohesion: 0.50 -Nodes (3): Node, SyntaxTextProvider<'a, B>, TextProvider +### Community 201 - ".new" +Cohesion: 0.17 +Nodes (10): AsRef, Into, Path, PathBuf, R, Result, Rope, Self (+2 more) ### Community 202 - "split_layout.rs" Cohesion: 0.22 Nodes (18): rect_tuple(), along_size(), child_rect(), layout_child(), layout_node(), layout_split_tree(), pane_rects_with_weights(), Self (+10 more) ### Community 203 - "PersistedProject" -Cohesion: 0.29 +Cohesion: 0.26 Nodes (9): PersistedProject, PersistedProjectKind, ProjectCandidate, ProjectKind, From, ProjectCandidate, Self, String (+1 more) ### Community 204 - "buffer_footer_layout_with_command_line" -Cohesion: 0.08 -Nodes (42): ScreenHit, WrapCollect, collect_wrapped_lines(), collect_wrapped_lines_with_display(), BTreeMap, Option, ShellBuffer, String (+34 more) +Cohesion: 0.07 +Nodes (43): ScreenHit, WrapCollect, collect_wrapped_lines(), collect_wrapped_lines_with_display(), BTreeMap, Option, ShellBuffer, String (+35 more) ### Community 205 - "Self" -Cohesion: 0.05 -Nodes (52): GitStashEntry, abi_debug_adapter_spec_round_trips_install_recipe(), abi_language_configuration_round_trips_path_matchers(), abi_language_server_spec_round_trips_activation_markers(), abi_language_server_spec_round_trips_default_enabled_flag(), abi_language_server_spec_round_trips_install_recipe(), abi_language_server_spec_round_trips_path_matchers(), abi_language_server_spec_round_trips_workspace_configuration() (+44 more) +Cohesion: 0.04 +Nodes (71): GitStashEntry, abi_debug_adapter_spec_round_trips_install_recipe(), abi_language_configuration_round_trips_path_matchers(), abi_language_server_spec_round_trips_activation_markers(), abi_language_server_spec_round_trips_default_enabled_flag(), abi_language_server_spec_round_trips_install_recipe(), abi_language_server_spec_round_trips_path_matchers(), abi_language_server_spec_round_trips_workspace_configuration() (+63 more) -### Community 206 - "LspCompletionItem" -Cohesion: 0.12 -Nodes (8): completion_parser_falls_back_to_detail_for_documentation_panel(), completion_parser_handles_lists_and_docs(), completion_parser_prefers_text_edit_over_insert_text_and_keeps_range(), completion_parser_reads_insert_replace_edit_replace_range(), LspCompletionItem, LspCompletionKind, parse_completion_item(), parse_completion_response() +### Community 206 - "DapSessionHandle" +Cohesion: 0.11 +Nodes (16): DapSessionHandle, Arc, AtomicBool, AtomicU64, Box, BTreeMap, Debug, DisconnectArguments (+8 more) ### Community 207 - "AbiGitFeatureSpec" Cohesion: 0.15 Nodes (12): GitCommandBinding, GitPrefixBinding, AbiGitCommandBinding, AbiGitFeatureSpec, AbiGitPrefixBinding, AbiGitStatusPrefix, GitCommandBinding, GitFeatureSpec (+4 more) ### Community 209 - "Option" -Cohesion: 0.13 +Cohesion: 0.18 Nodes (3): Option, WorkspaceConfiguration, WorkspaceConfigurationValue ### Community 210 - "main" @@ -1235,14 +1232,14 @@ Nodes (23): String, truncate_text_to_width_preserving_end(), file_name_with_pare ### Community 212 - "user/db.rs" Cohesion: 0.11 -Nodes (27): browser_item(), browser_items(), browser_items_shape_table_rows_from_user_config(), browser_key_bindings(), connect_buffer_binds_enter_to_submit_command(), connect_buffer_lines(), dashboard_buffer_declares_nested_layout_and_execute_chord(), dashboard_key_bindings() (+19 more) +Nodes (26): browser_item(), browser_items(), browser_items_shape_table_rows_from_user_config(), browser_key_bindings(), connect_buffer_binds_enter_to_submit_command(), connect_buffer_lines(), dashboard_buffer_declares_nested_layout_and_execute_chord(), dashboard_key_bindings() (+18 more) ### Community 213 - "dap-client-spec.md" Cohesion: 0.25 Nodes (7): Further Notes, Implementation Decisions, Out of Scope, Problem Statement, Solution, Testing Decisions, User Stories ### Community 214 - "logo.rs" -Cohesion: 0.28 +Cohesion: 0.25 Nodes (12): acp_logo_dest_rect(), acp_logo_leading_rect(), draw_acp_logo(), load_acp_logo(), logo_cache(), resolve_bundled_asset_path(), DecodedImage, Option (+4 more) ### Community 215 - "highlight.rs" @@ -1250,12 +1247,8 @@ Cohesion: 0.39 Nodes (8): bench_highlight_rust(), bench_highlight_rust_window(), Language, String, rust_fixture(), rust_language(), rust_registry(), Criterion ### Community 216 - "Language" -Cohesion: 0.20 -Nodes (9): Database, Debugging, External commands, Issues, Language, Language servers, Markdown presentation, Volt (+1 more) - -### Community 217 - "Self" -Cohesion: 0.16 -Nodes (6): LspWorkspaceDiagnostic, Into, IntoIterator, Item, Self, String +Cohesion: 0.18 +Nodes (10): Database, Debugging, External commands, Issues, Language, Language servers, Markdown presentation, User extension (+2 more) ### Community 218 - "Domain Docs" Cohesion: 0.33 @@ -1265,81 +1258,69 @@ Nodes (5): Before exploring, read these, Domain Docs, File structure, Flag ADR c Cohesion: 0.29 Nodes (6): Conventions, Issue tracker: GitHub, Pull requests as a triage surface, Wayfinding operations, When a skill says "fetch the relevant ticket", When a skill says "publish to the issue tracker" -### Community 220 - "Option" -Cohesion: 0.06 -Nodes (25): action_aliases(), command_line_commands_have_unique_names(), command_line_exports_commands_when_enabled(), commands(), enabled(), hook_aliases(), picker_aliases(), Option (+17 more) +### Community 220 - "PluginCommand" +Cohesion: 0.07 +Nodes (23): Self, action_aliases(), command_line_commands_have_unique_names(), command_line_exports_commands_when_enabled(), commands(), enabled(), hook_aliases(), picker_aliases() (+15 more) ### Community 221 - "runtime.rs" Cohesion: 0.08 -Nodes (80): ChildStderr, ClientSideConnection, acp_runtime_loop(), acp_session_buffer_name(), AcpCommand, AcpEvent, AcpPendingPermissionUi, AcpRuntime (+72 more) +Nodes (82): ClientSideConnection, acp_runtime_loop(), acp_session_buffer_name(), AcpCommand, AcpEvent, AcpPendingPermissionUi, AcpRuntime, AcpRuntimeState (+74 more) ### Community 223 - "volt/src/main.rs" -Cohesion: 0.09 -Nodes (32): builtin_user_library_validation_accepts_grammar_backed_syntax_languages(), catch_unwind_silently(), CommandPaletteState, dynamic_user_library_can_wrap_exported_module(), EventLog, format_micros_as_millis(), LaunchMode, LaunchOptions (+24 more) - -### Community 224 - "refresh_theme_registry_if_needed" -Cohesion: 0.13 -Nodes (17): rebuild_theme_registry_preserves_active_theme_when_still_present(), rebuild_theme_registry(), refresh_theme_registry_if_needed(), refresh_user_config_if_needed(), I, Instant, preferred_primary_font_hinting(), Instant (+9 more) - -### Community 225 - "apply_lsp_notifications" -Cohesion: 0.29 -Nodes (16): apply_lsp_notifications(), lsp_notification_action(), lsp_notification_body_lines(), notification_severity(), refresh_lsp_log_buffers(), refresh_pending_file_reloads(), refresh_pending_git(), refresh_pending_git_fringe() (+8 more) - -### Community 226 - "LanguageInstallPlan" -Cohesion: 0.06 -Nodes (36): asset_path_from_parts(), command_failure_message(), configure_background_command(), default_install_root(), default_query_asset_root(), ensure_cloned_grammar_dir_exists(), io_error(), normalize_extension() (+28 more) +Cohesion: 0.05 +Nodes (56): bootstrap(), HostBootstrap, builtin_user_library_validation_accepts_grammar_backed_syntax_languages(), catch_unwind_silently(), command_palette_items(), CommandPaletteState, dynamic_user_library_can_wrap_exported_module(), EventLog (+48 more) -### Community 227 - "TextRange" -Cohesion: 0.06 -Nodes (22): TextRange, LspClientManager, Option, Result, definition_parser_preserves_uri_backed_locations(), location_sorting_deduplicates_reference_results(), CopilotDeviceCodePrompt, diagnostic_matches_request_range() (+14 more) +### Community 224 - "render_chrome.rs" +Cohesion: 0.32 +Nodes (8): acp_section_layout_orders_output_input_footer_and_statusline(), command_line_footer_layout_reserves_row_below_statusline(), render_buffer_falls_back_to_statusline_theme_tokens_for_text(), render_buffer_paints_modeline_mode_chip_and_right_aligned_segment(), render_buffer_paints_opaque_modeline_band_when_window_is_transparent(), render_buffer_uses_statusline_foreground_tokens(), Result, String -### Community 228 - "UserLibrary" -Cohesion: 0.18 -Nodes (17): BufferKind, browser_state_for_kind(), buffer_is_db_connect(), buffer_is_quickfix(), default_vim_target(), is_issues_board_kind(), buffer_interaction(), plugin_buffer_line_wrap() (+9 more) +### Community 225 - "StreamedCommandExitAction" +Cohesion: 0.33 +Nodes (6): ExternalCommandSpec, Box, Into, PathBuf, Self, StreamedCommandExitAction -### Community 229 - "main" -Cohesion: 0.10 -Nodes (20): command_palette_items(), load_user_library(), main(), Arc, DebugAdapterSpec, Error, Instant, LanguageConfiguration (+12 more) +### Community 226 - "TerminalCursorSnapshot" +Cohesion: 0.27 +Nodes (3): terminal_cursor_shape_for_input_mode(), TerminalCursorShape, TerminalCursorSnapshot -### Community 230 - "draw_primary_ligature_texture_if_available" -Cohesion: 0.26 -Nodes (5): draw_primary_ligature_texture_if_available(), render_color_cache_key(), String, TextTextureCache<'texture>, TextTextureCacheKey +### Community 227 - "String" +Cohesion: 0.05 +Nodes (55): TextRange, definition_parser_preserves_uri_backed_locations(), definition_parser_supports_location_links(), location_sorting_deduplicates_reference_results(), parse_windows_nvm_current_version_extracts_active_version(), windows_fnm_environment_keeps_fnm_path_ahead_of_explicit_path(), windows_launch_program_candidates_include_command_shims(), windows_nvm_environment_keeps_nvm_path_ahead_of_explicit_path() (+47 more) -### Community 231 - "treesittercontext_ghosttext.rs" -Cohesion: 0.20 -Nodes (12): build_ghost_text_lines(), build_ghost_text_lines_includes_loop_contexts(), build_ghost_text_lines_keeps_current_line_for_block_end_contexts(), build_ghost_text_lines_prefers_inner_context_on_shared_closing_line(), build_ghost_text_lines_skips_current_line_for_non_block_multiline_contexts(), build_ghost_text_lines_skips_current_line_for_single_line_contexts(), ghost_text_lines(), is_block_closing_line() (+4 more) +### Community 231 - "bash.rs" +Cohesion: 0.43 +Nodes (7): bash_package_auto_attaches_all_extensions(), bash_package_metadata(), bash_package_registers_formatter(), bash_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() -### Community 232 - ".should_follow_output" -Cohesion: 0.23 +### Community 232 - ".acp_output_viewport_lines" +Cohesion: 0.19 Nodes (7): acp_tool_call_from_partial_update(), ToolCall, ToolCallUpdate, ContentBlock, Into, ToolCall, ToolCallUpdate -### Community 233 - "treesittercontext_shared.rs" -Cohesion: 0.36 -Nodes (14): collapse_whitespace(), context_icon(), extract_control_flow_header(), extract_named_keyword(), extract_signature(), format_context_label_from_header(), ignored_context_kind(), is_conditional_kind() (+6 more) +### Community 233 - "feature_spec" +Cohesion: 0.12 +Nodes (14): exported_browser_feature_spec(), exported_context_help_specs(), exported_db_feature_spec(), exported_git_feature_spec(), exported_oil_feature_spec(), exported_terminal_feature_spec(), BrowserFeatureSpec, ContextHelpSpec (+6 more) ### Community 234 - "String" Cohesion: 0.19 Nodes (13): DisabledSecretStore, InMemorySecretStore, OsSecretStore, HashMap, Into, Mutex, Result, Self (+5 more) -### Community 235 - "render_chrome.rs" -Cohesion: 0.32 -Nodes (8): acp_section_layout_orders_output_input_footer_and_statusline(), command_line_footer_layout_reserves_row_below_statusline(), render_buffer_falls_back_to_statusline_theme_tokens_for_text(), render_buffer_paints_modeline_mode_chip_and_right_aligned_segment(), render_buffer_paints_opaque_modeline_band_when_window_is_transparent(), render_buffer_uses_statusline_foreground_tokens(), Result, String +### Community 235 - "clojure.rs" +Cohesion: 0.43 +Nodes (7): clojure_package_auto_attaches_all_extensions(), clojure_package_metadata(), clojure_package_no_formatter(), clojure_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() ### Community 236 - "AbiSectionTree" Cohesion: 0.11 -Nodes (16): exported_git_status_sections(), exported_oil_directory_sections(), DirectoryEntry, GitStatusSnapshot, OilSortMode, Path, SectionTree, AbiDirectoryEntry (+8 more) +Nodes (15): exported_git_status_sections(), exported_oil_directory_sections(), DirectoryEntry, OilSortMode, Path, SectionTree, AbiDirectoryEntry, AbiDirectoryEntryKind (+7 more) ### Community 237 - "editor-git/src/tests.rs" Cohesion: 0.28 Nodes (24): invalidate_repository_file_list_cache(), repository_file_list_generation(), cached_repository_file_listing_is_keyed_by_workspace_root(), cached_repository_file_listing_refreshes_after_index_or_head_change(), cached_repository_file_listing_reuses_paths_until_identity_changes(), configure_git_identity(), git_available(), git_stdout() (+16 more) ### Community 239 - "git/diff.rs" -Cohesion: 0.31 +Cohesion: 0.29 Nodes (24): apply_git_view(), diff_git_commit_at_point(), diff_git_dwim(), diff_git_stash_at_point(), git_view_language_id(), git_view_lines(), git_view_lines_or_error(), open_git_diff_buffer() (+16 more) ### Community 240 - "tests/text_layout.rs" -Cohesion: 0.17 -Nodes (21): icon_glyph_cell_layout(), icon_glyph_draw_offset_y(), IconGlyphCellLayout, rasterize_icon_glyph_for_cell(), RasterizedIconGlyph, LineMetrics, load_nfm_raster_font(), block_cursor_text_overlay_uses_visible_glyph_for_variation_selector() (+13 more) +Cohesion: 0.19 +Nodes (18): decode_raster_px_64(), icon_glyph_cell_layout(), icon_glyph_draw_offset_y(), IconGlyphCellLayout, rasterize_icon_glyph_for_cell(), LineMetrics, block_cursor_text_overlay_uses_visible_glyph_for_variation_selector(), codicon_glyphs_fit_inside_one_editor_cell() (+10 more) ### Community 241 - "git_probe_snapshot" Cohesion: 0.31 @@ -1353,125 +1334,121 @@ Nodes (23): collect_files_with_extension(), directory_contains_extension(), dire Cohesion: 0.13 Nodes (10): LanguageServerRegistry, LanguageServerSession, LspError, Display, Error, Formatter, Path, PathBuf (+2 more) -### Community 244 - "acp.rs" -Cohesion: 0.21 -Nodes (18): acp_picker_detail(), AcpClientConfig, client_by_id(), clients(), clients_include_logo_paths(), hook_command(), package(), package_binds_j_and_k_in_acp_dock_scope() (+10 more) +### Community 244 - "AcpPickerItemSpec" +Cohesion: 0.12 +Nodes (22): acp_picker_detail(), AcpClientConfig, client_by_id(), clients(), clients_include_logo_paths(), hook_command(), package(), package_binds_j_and_k_in_acp_dock_scope() (+14 more) ### Community 247 - "MarkdownPrettyPlanCache" Cohesion: 0.21 Nodes (14): CachedMarkdownPretty, MarkdownPrettyCacheIdentity, MarkdownPrettyCacheKey, MarkdownPrettyPlanCache, MarkdownPrettySourceStats, pretty_kill_switch_trips(), Arc, BTreeMap (+6 more) ### Community 248 - "LanguageServerSpec" -Cohesion: 0.10 -Nodes (9): LanguageServerRootStrategy, LanguageServerSpec, I, InstallRecipe, Iterator, LanguageServerRootStrategy, normalize_unique_entries(), I (+1 more) +Cohesion: 0.12 +Nodes (10): LanguageServerSpec, BTreeMap, InstallRecipe, Into, IntoIterator, Item, Iterator, LanguageServerRootStrategy (+2 more) ### Community 249 - "Agent skills" Cohesion: 0.33 Nodes (5): Agent skills, Domain docs, graphify, Issue tracker, Triage labels ### Community 250 - "markdown_pretty.rs" -Cohesion: 0.24 -Nodes (22): ImageDestination, PathBuf, data_dir(), disk_cache_path(), ensure_image_loaded(), fetch_https_image(), image_cache(), image_cache_key() (+14 more) +Cohesion: 0.26 +Nodes (21): data_dir(), disk_cache_path(), ensure_image_loaded(), fetch_https_image(), image_cache(), image_cache_key(), load_data_url_image(), load_local_image() (+13 more) ### Community 251 - "LspCodeAction" -Cohesion: 0.14 -Nodes (6): code_action_parser_collects_active_file_edits(), code_action_parser_tracks_command_and_resource_operations(), LspCodeAction, parse_code_action_response(), Error, windows_should_retry_spawn_error() +Cohesion: 0.09 +Nodes (11): code_action_parser_collects_active_file_edits(), code_action_parser_tracks_command_and_resource_operations(), formatting_parser_maps_text_edits(), lsp_text_edit_from_lsp(), LspCodeAction, LspDocumentTextEdits, LspTextEdit, parse_code_action_response() (+3 more) ### Community 252 - "editor-lsp/src/client/tests.rs" -Cohesion: 0.04 -Nodes (77): attach_session(), close_buffer_keeps_session_alive_for_next_file(), close_then_open_then_incremental_edits_work_again(), copilot_status_notifications_offer_sign_in_action(), default_workspace_lists_only_sessions_serving_open_buffers(), definition_parser_supports_location_links(), diagnostics_parser_maps_lsp_fields(), did_open_still_sends_full_text() (+69 more) +Cohesion: 0.05 +Nodes (68): attach_session(), close_buffer_keeps_session_alive_for_next_file(), close_then_open_then_incremental_edits_work_again(), completion_parser_handles_lists_and_docs(), completion_parser_prefers_text_edit_over_insert_text_and_keeps_range(), completion_parser_reads_insert_replace_edit_replace_range(), default_workspace_lists_only_sessions_serving_open_buffers(), diagnostics_parser_maps_lsp_fields() (+60 more) ### Community 253 - "notification_overlay_layouts" -Cohesion: 0.23 -Nodes (13): autocomplete_visible_start(), hover_overlay_width(), notification_action_at_point(), notification_overlay_layouts(), notification_status_text(), overlay_text_columns(), overlay_width(), Instant (+5 more) +Cohesion: 0.16 +Nodes (19): autocomplete_visible_start(), notification_accent_color(), notification_action_at_point(), notification_overlay_layouts(), notification_status_text(), overlay_text_columns(), overlay_width(), Color (+11 more) -### Community 254 - "CommandSource" -Cohesion: 0.23 -Nodes (6): CommandSource, F, Into, Result, String, Vec +### Community 254 - "elixir.rs" +Cohesion: 0.43 +Nodes (7): elixir_package_auto_attaches_all_extensions(), elixir_package_metadata(), elixir_package_registers_formatter(), elixir_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() -### Community 255 - ".default" -Cohesion: 0.19 -Nodes (6): DirectoryEntry, GitStatusSnapshot, OilSortMode, PdfOpenMode, SectionTree, Self +### Community 255 - "java.rs" +Cohesion: 0.43 +Nodes (7): java_package_auto_attaches_all_extensions(), java_package_metadata(), java_package_registers_formatter(), java_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() -### Community 256 - "first_project_discovery_root" -Cohesion: 0.38 -Nodes (11): begin_oil_clone_request(), close_active_oil_buffer(), confirm_workspace_clone_url(), first_project_discovery_root(), open_workspace_clone_mode_picker(), open_workspace_clone_prompt(), Path, PathBuf (+3 more) +### Community 256 - "perl.rs" +Cohesion: 0.43 +Nodes (7): package(), perl_package_auto_attaches_all_extensions(), perl_package_metadata(), perl_package_registers_formatter(), perl_syntax_language_metadata(), LanguageConfiguration, syntax_language() -### Community 258 - "headerline_lines" -Cohesion: 0.29 -Nodes (7): build_headerline_lines(), db_buffer_headerline(), headerline_lines(), Option, String, Vec, special_buffer_headerline() +### Community 258 - "php.rs" +Cohesion: 0.43 +Nodes (7): package(), php_package_auto_attaches_all_extensions(), php_package_metadata(), php_package_registers_no_formatter(), php_syntax_language_metadata(), LanguageConfiguration, syntax_language() -### Community 259 - "AbiColor" -Cohesion: 0.47 -Nodes (4): AbiColor, AbiThemeToken, Color, Color +### Community 259 - "JobSpec" +Cohesion: 0.17 +Nodes (11): JobKind, JobManager, JobSpec, launch_job(), Arc, Default, Into, IntoIterator (+3 more) ### Community 264 - "KeymapError" -Cohesion: 0.24 -Nodes (13): KeymapError, Error, autocomplete_overrides_workspace_while_active(), dap_mode_overrides_global_f5_while_session_live(), duplicate_detection_uses_canonical_chords(), global_is_fallback_when_no_minor_mode_claims_chord(), hover_overrides_workspace_while_active(), popup_mode_does_not_claim_workspace_dock_chords() (+5 more) - -### Community 265 - "workspace_pane_rects" -Cohesion: 0.25 -Nodes (7): Instant, Option, Self, Vec, runtime_pane_rects(), StartupTrace, workspace_pane_rects() - -### Community 266 - "Option" Cohesion: 0.22 -Nodes (3): GitStatusPrefix, OilKeyAction, Option +Nodes (12): KeymapError, Error, autocomplete_overrides_workspace_while_active(), dap_mode_overrides_global_f5_while_session_live(), duplicate_detection_uses_canonical_chords(), global_is_fallback_when_no_minor_mode_claims_chord(), hover_overrides_workspace_while_active(), popup_mode_does_not_claim_workspace_dock_chords() (+4 more) -### Community 269 - "GhostTextContext" -Cohesion: 0.32 -Nodes (5): GhostTextLine, exported_ghost_text_lines(), GhostTextLine, AbiGhostTextContext, GhostTextContext - -### Community 270 - "xml.rs" +### Community 265 - "r.rs" Cohesion: 0.43 -Nodes (7): package(), LanguageConfiguration, syntax_language(), xml_package_auto_attaches_all_extensions(), xml_package_metadata(), xml_package_registers_formatter(), xml_syntax_language_metadata() +Nodes (7): package(), r_package_auto_attaches_all_extensions(), r_package_has_no_formatter(), r_package_metadata(), r_syntax_language_metadata(), LanguageConfiguration, syntax_language() -### Community 285 - "clojure.rs" +### Community 266 - "solidity.rs" Cohesion: 0.43 -Nodes (7): clojure_package_auto_attaches_all_extensions(), clojure_package_metadata(), clojure_package_no_formatter(), clojure_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() +Nodes (7): package(), LanguageConfiguration, solidity_package_auto_attaches_all_extensions(), solidity_package_metadata(), solidity_package_registers_formatter(), solidity_syntax_language_metadata(), syntax_language() -### Community 286 - "graphql.rs" +### Community 269 - "swift.rs" Cohesion: 0.43 -Nodes (7): graphql_package_auto_attaches_all_extensions(), graphql_package_metadata(), graphql_package_registers_formatter(), graphql_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() +Nodes (7): package(), LanguageConfiguration, swift_package_auto_attaches_all_extensions(), swift_package_metadata(), swift_package_registers_formatter(), swift_syntax_language_metadata(), syntax_language() + +### Community 285 - "default_volt_state_dir" +Cohesion: 0.53 +Nodes (6): active_theme_state_path(), default_mark_list_path(), default_typing_profile_log_path(), default_volt_state_dir(), install_panic_hook(), PathBuf + +### Community 286 - "hover_ui.rs" +Cohesion: 0.31 +Nodes (14): focused_hover_ctrl_scroll_motions_are_bounded(), focused_hover_gg_and_g_scroll_to_expected_bounds(), focused_hover_text_motions_scroll_without_moving_buffer_cursor(), format_current_line_indent_uses_inherited_tsx_queries(), hover_diagnostic_provider_fragments_preserve_fenced_code_blocks(), hover_signature_request_point_prefers_callee_over_enclosing_macro(), hover_signature_request_point_preserves_argument_cursor_context(), hover_test_provider_lines_include_theme_and_treesitter_tokens() (+6 more) ### Community 334 - "editor-jobs/src/tests.rs" Cohesion: 0.13 Nodes (11): compilation_runner_marks_jobs_as_compilation(), job_manager_runs_commands_and_collects_output(), must(), E, PathBuf, Result, T, temp_dir() (+3 more) -### Community 335 - ".ensure_sessions_for_path" -Cohesion: 0.30 -Nodes (9): LspClientManager, Arc, BTreeSet, Option, Path, PathBuf, Result, Self (+1 more) +### Community 335 - "LspClientManager" +Cohesion: 0.22 +Nodes (10): LspClientManager, Arc, BTreeSet, Mutex, Option, Path, PathBuf, Result (+2 more) -### Community 336 - "hcl.rs" +### Community 336 - "graphql.rs" Cohesion: 0.43 -Nodes (7): hcl_package_auto_attaches_all_extensions(), hcl_package_metadata(), hcl_package_no_formatter(), hcl_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() +Nodes (7): graphql_package_auto_attaches_all_extensions(), graphql_package_metadata(), graphql_package_registers_formatter(), graphql_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() -### Community 337 - "src/session.rs" -Cohesion: 0.11 -Nodes (24): live_terminal_session_spawns_and_terminates(), must(), E, Result, T, terminal_render_snapshot_preserves_wide_character_widths(), terminal_render_snapshot_strips_tab_and_control_tofu_from_git_status_style_output(), terminal_session_captures_transcript_lines() (+16 more) +### Community 337 - "render_tests.rs" +Cohesion: 0.24 +Nodes (8): live_terminal_session_spawns_and_terminates(), must(), E, Result, T, terminal_render_snapshot_preserves_wide_character_widths(), terminal_render_snapshot_tracks_visible_cursor(), terminal_session_captures_transcript_lines() ### Community 338 - "acp_chat.rs" -Cohesion: 0.07 -Nodes (54): acp_chat_bubble_cols(), acp_bubble_remaining_rows(), acp_chat_bubble_width_px(), acp_chat_corner_radius(), acp_chat_origin_x(), acp_chat_rounded(), acp_color(), acp_spinner_frame() (+46 more) +Cohesion: 0.06 +Nodes (59): acp_chat_bubble_width_px(), acp_chat_corner_radius(), acp_chat_origin_x(), acp_chat_rounded(), acp_prefix_columns(), acp_spinner_frame(), column_to_relative_byte_offset(), draw_buffer_text() (+51 more) -### Community 340 - "comments.rs" -Cohesion: 0.36 -Nodes (9): block_comment_toggle_removal_lens(), comment_style_for_buffer(), comment_style_for_language_path(), comment_toggle_removal_len(), CommentStyle, matches_comment_id(), Option, ShellBuffer (+1 more) +### Community 340 - "editor-core/src/lib_tests.rs" +Cohesion: 0.26 +Nodes (14): command_registry_executes_commands_and_hooks_dispatch_events(), EventLog, model_closes_active_pane_without_closing_buffers(), model_focuses_existing_buffer_in_active_pane(), model_splits_pane_and_focuses(), model_switches_and_closes_workspaces(), Result, String (+6 more) -### Community 341 - "java.rs" -Cohesion: 0.43 -Nodes (7): java_package_auto_attaches_all_extensions(), java_package_metadata(), java_package_registers_formatter(), java_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() +### Community 341 - "LspNotificationSnapshot" +Cohesion: 0.16 +Nodes (5): LspClientManager, Option, LspNotificationEntry, LspNotificationLog, LspNotificationSnapshot ### Community 342 - "open_acp_client_with_config" -Cohesion: 0.31 -Nodes (17): acp_connected(), acp_new_session(), close_acp_buffer(), close_acp_workspace_buffers(), create_acp_buffer(), focus_acp_buffer(), init_acp_manager(), open_acp_client() (+9 more) +Cohesion: 0.34 +Nodes (16): acp_connected(), acp_new_session(), close_acp_buffer(), close_acp_workspace_buffers(), create_acp_buffer(), focus_acp_buffer(), init_acp_manager(), open_acp_client() (+8 more) ### Community 343 - "kotlin.rs" Cohesion: 0.43 Nodes (7): kotlin_package_auto_attaches_all_extensions(), kotlin_package_metadata(), kotlin_package_registers_formatter(), kotlin_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() -### Community 344 - "lua.rs" -Cohesion: 0.43 -Nodes (7): lua_package_auto_attaches_all_extensions(), lua_package_metadata(), lua_package_registers_formatter(), lua_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() +### Community 344 - "locate.rs" +Cohesion: 0.53 +Nodes (5): is_volt_install_path(), locate_program(), ProgramLocation, Path, PathBuf ### Community 345 - "TempFile" Cohesion: 0.50 @@ -1489,61 +1466,53 @@ Nodes (9): init_repo(), Option, Path, PathBuf, run_git(), temp_root(), workspace Cohesion: 0.26 Nodes (14): escape_double_quote(), looks_like_postgres_connection_string(), looks_like_sql_server_connection_string(), parse_connect_prompt(), parse_db_connect_prompt(), parse_key_value(), parse_postgres_keyword(), parse_url_database() (+6 more) -### Community 350 - "perl.rs" -Cohesion: 0.43 -Nodes (7): package(), perl_package_auto_attaches_all_extensions(), perl_package_metadata(), perl_package_registers_formatter(), perl_syntax_language_metadata(), LanguageConfiguration, syntax_language() +### Community 350 - "JobResult" +Cohesion: 0.20 +Nodes (3): CompilationResult, JobResult, Duration -### Community 351 - "php.rs" -Cohesion: 0.43 -Nodes (7): package(), php_package_auto_attaches_all_extensions(), php_package_metadata(), php_package_registers_no_formatter(), php_syntax_language_metadata(), LanguageConfiguration, syntax_language() +### Community 351 - "autocomplete_tokens_tests.rs" +Cohesion: 0.37 +Nodes (13): assert_counts_match_rescan(), incremental_token_counts_apply_burst_of_inserts(), incremental_token_counts_drop_deleted_identifier(), incremental_token_counts_match_rescan_after_one_insert(), large_source_fixture(), refresh_from_edits(), refresh_rebuild(), String (+5 more) ### Community 352 - "fill_rounded_corner_canvas" Cohesion: 0.29 Nodes (15): draw_undercurl_canvas(), fill_rounded_corner_canvas(), fill_rounded_corner_run(), fill_rounded_rect_canvas(), fill_top_rounded_rect_canvas(), rounded_corner_coverage(), RoundedCorner, Canvas (+7 more) -### Community 353 - "proto.rs" -Cohesion: 0.43 -Nodes (7): package(), proto_package_auto_attaches_all_extensions(), proto_package_metadata(), proto_package_registers_formatter(), proto_syntax_language_metadata(), LanguageConfiguration, syntax_language() +### Community 353 - "AbiLanguageConfiguration" +Cohesion: 0.19 +Nodes (9): AbiCaptureThemeMapping, AbiGrammarSource, AbiLanguageConfiguration, CaptureThemeMapping, GrammarSource, LanguageConfiguration, CaptureThemeMapping, GrammarSource (+1 more) ### Community 354 - "vim_search.rs" Cohesion: 0.37 Nodes (16): char_at_index(), find_char_forward(), fuzzy_match_end(), matches_pattern_at(), normalize_search_char(), normalize_search_pattern(), Option, ShellBuffer (+8 more) -### Community 355 - "r.rs" -Cohesion: 0.43 -Nodes (7): package(), r_package_auto_attaches_all_extensions(), r_package_has_no_formatter(), r_package_metadata(), r_syntax_language_metadata(), LanguageConfiguration, syntax_language() +### Community 355 - "AbiKeymapConfig" +Cohesion: 0.60 +Nodes (3): AbiKeymapConfig, KeymapConfig, KeymapConfig ### Community 356 - "command_stream_tests.rs" Cohesion: 0.18 Nodes (12): cargo_toml_wins_over_other_markers(), detects_cargo_toml(), detects_csproj(), detects_makefile(), detects_package_json(), detects_sln(), empty_dir_returns_empty_string(), missing_dir_returns_empty_string() (+4 more) -### Community 357 - "config_tests.rs" -Cohesion: 0.33 -Nodes (6): deep_inference_finds_cargo_binary_and_heuristic(), deep_inference_finds_dotnet_dll(), default_workspace_skips_deep_inference(), loads_project_debug_configurations(), PathBuf, temp_dir() - -### Community 358 - "ruby.rs" -Cohesion: 0.43 -Nodes (7): package(), LanguageConfiguration, ruby_package_auto_attaches_all_extensions(), ruby_package_has_no_formatter(), ruby_package_metadata(), ruby_syntax_language_metadata(), syntax_language() +### Community 357 - "ancestor_contexts_for_cursor" +Cohesion: 0.26 +Nodes (12): ancestor_contexts_for_cursor(), AncestorContextBufferKey, AncestorContextCache, AncestorContextQuery, buffer_line_text(), context_queries_enabled(), ensure_cached_buffer(), LanguageConfiguration (+4 more) -### Community 359 - "scala.rs" -Cohesion: 0.43 -Nodes (7): package(), LanguageConfiguration, scala_package_auto_attaches_all_extensions(), scala_package_metadata(), scala_package_registers_formatter(), scala_syntax_language_metadata(), syntax_language() +### Community 359 - "panic_payload_message" +Cohesion: 0.50 +Nodes (4): panic_payload_message(), Any, Box, Send ### Community 360 - "log.rs" -Cohesion: 0.38 -Nodes (13): git_log_args(), open_git_log_all(), open_git_log_all_branches(), open_git_log_branches(), open_git_log_buffer(), open_git_log_current(), open_git_log_head(), open_git_log_related() (+5 more) +Cohesion: 0.47 +Nodes (12): git_log_args(), open_git_log_all(), open_git_log_all_branches(), open_git_log_branches(), open_git_log_buffer(), open_git_log_current(), open_git_log_head(), open_git_log_related() (+4 more) ### Community 361 - ".sync_buffer_to_sessions" -Cohesion: 0.45 -Nodes (8): LspClientManager, Arc, Into, Option, Path, Result, String, Vec - -### Community 362 - "autocomplete_tokens_tests.rs" -Cohesion: 0.37 -Nodes (13): assert_counts_match_rescan(), incremental_token_counts_apply_burst_of_inserts(), incremental_token_counts_drop_deleted_identifier(), incremental_token_counts_match_rescan_after_one_insert(), large_source_fixture(), refresh_from_edits(), refresh_rebuild(), String (+5 more) +Cohesion: 0.40 +Nodes (9): LspClientManager, Arc, Into, Option, Path, Result, String, Vec (+1 more) -### Community 363 - "solidity.rs" -Cohesion: 0.43 -Nodes (7): package(), LanguageConfiguration, solidity_package_auto_attaches_all_extensions(), solidity_package_metadata(), solidity_package_registers_formatter(), solidity_syntax_language_metadata(), syntax_language() +### Community 362 - "I" +Cohesion: 0.23 +Nodes (4): I, normalize_unique_entries(), I, Vec ### Community 364 - "plan_markdown_pretty_ephemeral" Cohesion: 0.21 @@ -1554,8 +1523,8 @@ Cohesion: 0.41 Nodes (12): anti_conceal_overlay_reuses_cached_plan(), build_plan(), cached_plan_rebuilds_when_revision_changes(), cached_plan_reuses_arc_for_same_revision(), cfg(), disabled_pretty_sentinel_skips_build(), fixture_text(), kill_switch_sentinel_skips_build_and_reuses_plan() (+4 more) ### Community 366 - "MarkdownPrettyPlan" -Cohesion: 0.22 -Nodes (13): MarkdownPrettyPlan, build_markdown_pretty_plan(), cached_plan_for_buffer(), last_cached_pretty_plan(), line_plan(), plan_config_from_user(), pretty_display_line(), Arc (+5 more) +Cohesion: 0.24 +Nodes (12): MarkdownPrettyPlan, build_markdown_pretty_plan(), cached_plan_for_buffer(), last_cached_pretty_plan(), line_plan(), plan_config_from_user(), Arc, MarkdownPrettyConfig (+4 more) ### Community 367 - "lang/vim.rs" Cohesion: 0.43 @@ -1573,33 +1542,29 @@ Nodes (6): DapLogEntry, DapLogSnapshot, DapTransportLog, Self, String, Vec Cohesion: 0.18 Nodes (4): DapExecutionPosition, DapStackFrameInfo, Path, PathBuf -### Community 371 - "user/workspace_dock.rs" -Cohesion: 0.48 -Nodes (6): config(), config_defaults_to_left_undocked(), package(), package_binds_j_and_k_in_workspace_dock_scope(), package_exports_dock_navigation_commands(), package_exports_toggle_command() - -### Community 372 - ".new" -Cohesion: 0.29 -Nodes (3): Lexer<'a>, Self, Token +### Community 371 - "AbiOilDefaults" +Cohesion: 0.21 +Nodes (8): exported_oil_defaults(), OilDefaults, AbiOilDefaults, AbiOilSortMode, OilDefaults, OilSortMode, OilDefaults, OilSortMode ### Community 373 - "package" Cohesion: 0.47 Nodes (5): open_mode(), package(), package_exports_pdf_buffer_keybindings(), package_exports_pdf_commands(), PdfOpenMode -### Community 374 - "Path" -Cohesion: 0.25 -Nodes (4): Error, Instant, Path, Result +### Community 374 - "TextPoint" +Cohesion: 0.07 +Nodes (16): EditRecord, Range, advance_point_by_text(), LineEnding, Iterator, Range, Self, Selection (+8 more) ### Community 375 - "must" Cohesion: 0.33 Nodes (10): codelldb(), gdb(), must(), prepared_session_includes_configuration_and_launch_spec(), registry_prefers_higher_preference_when_multiple_match(), registry_resolves_adapter_by_extension(), DebugAdapterSpec, E (+2 more) ### Community 376 - "Vec" -Cohesion: 0.18 -Nodes (14): autocomplete_items(), autocomplete_provider(), calculator_autocomplete_provider_scopes_manual_items_to_calculator_buffers(), calculator_hover_provider_exports_function_and_constant_topics(), calculator_symbols(), CalculatorSymbol, hover_lines(), hover_provider() (+6 more) +Cohesion: 0.27 +Nodes (10): autocomplete_items(), calculator_symbols(), CalculatorSymbol, hover_lines(), hover_topics(), initial_buffer_lines(), initial_buffer_lines_only_seed_input_examples(), AutocompleteProviderItem (+2 more) -### Community 377 - "package" -Cohesion: 0.83 -Nodes (3): package(), package_exports_image_commands(), package_exports_image_keybindings() +### Community 377 - "GhostTextContext" +Cohesion: 0.12 +Nodes (16): GhostTextLine, packages(), LanguageConfiguration, Vec, syntax_languages(), exported_ghost_text_lines(), GhostTextLine, AbiGhostTextContext (+8 more) ### Community 378 - "run_demo_shell" Cohesion: 0.33 @@ -1609,112 +1574,116 @@ Nodes (12): active_lsp_workspace_loaded(), frame_pacing_deferred_for_typing(), f Cohesion: 0.33 Nodes (10): amber(), must(), registry_resolves_option_values(), registry_resolves_token_styles(), registry_resolves_tokens_from_active_theme(), E, Result, T (+2 more) -### Community 380 - "ancestor_contexts_for_cursor" -Cohesion: 0.26 -Nodes (12): ancestor_contexts_for_cursor(), AncestorContextBufferKey, AncestorContextCache, AncestorContextQuery, buffer_line_text(), context_queries_enabled(), ensure_cached_buffer(), LanguageConfiguration (+4 more) - -### Community 381 - "bash.rs" -Cohesion: 0.43 -Nodes (7): bash_package_auto_attaches_all_extensions(), bash_package_metadata(), bash_package_registers_formatter(), bash_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() - -### Community 382 - ".load_from_file" -Cohesion: 0.20 -Nodes (7): Arc, DirectoryEntry, GitStatusSnapshot, OilSortMode, Path, Result, SectionTree - -### Community 383 - "keys.rs" -Cohesion: 0.22 -Nodes (3): repeated_keydown_events_move_the_cursor(), Result, String - -### Community 384 - "elixir.rs" -Cohesion: 0.43 -Nodes (7): elixir_package_auto_attaches_all_extensions(), elixir_package_metadata(), elixir_package_registers_formatter(), elixir_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() +### Community 384 - "config_tests.rs" +Cohesion: 0.33 +Nodes (6): deep_inference_finds_cargo_binary_and_heuristic(), deep_inference_finds_dotnet_dll(), default_workspace_skips_deep_inference(), loads_project_debug_configurations(), PathBuf, temp_dir() ### Community 386 - "setup_standalone_user_repository" Cohesion: 0.28 Nodes (8): configure_background_command(), Box, Command, Error, Path, Result, setup_standalone_user_repository(), setup_standalone_user_repository_writes_gitignore_and_initializes_git() -### Community 387 - "locate.rs" -Cohesion: 0.53 -Nodes (5): is_volt_install_path(), locate_program(), ProgramLocation, Path, PathBuf +### Community 390 - "ISS-owned-process-lifecycle-spec.md" +Cohesion: 0.25 +Nodes (7): Further Notes, Implementation Decisions, Out of Scope, Problem Statement, Solution, Testing Decisions, User Stories -### Community 389 - "BufferContextOverlaySnapshot" -Cohesion: 0.50 -Nodes (4): BufferContextOverlayCacheKey, BufferContextOverlaySnapshot, InlineCompletionState, BTreeMap +### Community 391 - "common.rs" +Cohesion: 0.07 +Nodes (42): cmake_package_auto_attaches_cmakelists(), cmake_package_auto_attaches_extension(), cmake_package_metadata(), cmake_package_no_formatter(), cmake_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() (+34 more) -### Community 390 - "syntax_languages" -Cohesion: 0.60 -Nodes (4): packages(), LanguageConfiguration, Vec, syntax_languages() +### Community 393 - "idle_pacing.rs" +Cohesion: 0.23 +Nodes (6): frame_pacing_remaining_clamps_to_120fps_budget(), insert_mode_text_input_activates_typing_budget(), normal_mode_text_input_does_not_activate_typing_budget(), Result, String, sync_visible_buffer_layouts_reuses_headerline_snapshot_while_typing() -### Community 391 - "common.rs" -Cohesion: 0.10 -Nodes (28): cmake_package_auto_attaches_cmakelists(), cmake_package_auto_attaches_extension(), cmake_package_metadata(), cmake_package_no_formatter(), cmake_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() (+20 more) +### Community 394 - "UserLibraryModule" +Cohesion: 0.06 +Nodes (28): exported_icon_symbols(), exported_oil_keybindings(), exported_pdf_open_mode(), exported_picker_truncate_strategy(), IconFontSymbol, OilKeybindings, PdfOpenMode, PickerTruncateStrategy (+20 more) ### Community 395 - "OilDefaultsSection" Cohesion: 0.32 Nodes (5): ConfigOilSortMode, default_oil_sort_mode(), OilDefaultsSection, OilSortMode, OilDefaults ### Community 396 - "WorkspaceDockConfig" -Cohesion: 0.22 -Nodes (4): WorkspaceDockTestUserLibrary, workspace_dock_config(), WorkspaceDockConfig, WorkspaceDockSide +Cohesion: 0.17 +Nodes (9): WorkspaceDockTestUserLibrary, WorkspaceDockConfig, WorkspaceDockSide, config(), config_defaults_to_left_undocked(), package(), package_binds_j_and_k_in_workspace_dock_scope(), package_exports_dock_navigation_commands() (+1 more) ### Community 397 - "show_paren.rs" Cohesion: 0.40 Nodes (4): config(), package(), package_exports_toggle_command_and_binding(), ShowParenConfig -### Community 400 - "latex.rs" -Cohesion: 0.43 -Nodes (7): latex_package_auto_attaches_all_extensions(), latex_package_metadata(), latex_package_registers_formatter(), latex_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() - ### Community 401 - ".oil_directory_sections" Cohesion: 0.25 Nodes (5): DirectoryEntry, GitStatusSnapshot, OilSortMode, Path, SectionTree -### Community 403 - "UserLibraryModule" -Cohesion: 0.07 -Nodes (28): exported_pdf_open_mode(), PdfOpenMode, AbiIconFontSymbol, AbiOilDefaults, AbiOilFeatureSpec, AbiOilKeyAction, AbiOilKeybindings, AbiOilSortMode (+20 more) - ### Community 406 - "src/text.rs" Cohesion: 0.67 Nodes (3): String, truncate_text_to_width(), truncate_text_to_width_preserving_end() -### Community 408 - "swift.rs" +### Community 408 - "hcl.rs" Cohesion: 0.43 -Nodes (7): package(), LanguageConfiguration, swift_package_auto_attaches_all_extensions(), swift_package_metadata(), swift_package_registers_formatter(), swift_syntax_language_metadata(), syntax_language() - -### Community 409 - "PaneConfig" -Cohesion: 0.07 -Nodes (16): exported_keymap_config(), exported_ligature_config(), exported_pane_config(), KeymapConfig, LigatureConfig, MarkdownPrettyConfig, PickerLayout, ShowParenConfig (+8 more) +Nodes (7): hcl_package_auto_attaches_all_extensions(), hcl_package_metadata(), hcl_package_no_formatter(), hcl_syntax_language_metadata(), package(), LanguageConfiguration, syntax_language() -### Community 410 - "spawn_reader_thread" -Cohesion: 0.14 -Nodes (27): LspClientManager, LspReaderSession, LspSessionSharedState, note_session_disconnect_diagnostics(), record_notification(), record_published_diagnostics(), record_transport_entry(), record_transport_event() (+19 more) +### Community 409 - "01-process-registry-and-hybrid-launch.md" +Cohesion: 0.40 +Nodes (4): Acceptance criteria, Blocked by, Parent, What to build -### Community 434 - "DapSessionHandle" +### Community 410 - "PathBuf" Cohesion: 0.08 -Nodes (28): DapSessionHandle, AtomicBool, AtomicU64, Box, BTreeMap, Child, Debug, DisconnectArguments (+20 more) +Nodes (44): ClientCapabilities, Arc, Mutex, Self, client_capabilities_enable_window_work_done_progress_and_show_document(), client_capabilities(), launch_lsp_attempt(), launch_lsp_owned_process() (+36 more) -### Community 437 - "syntax_language" -Cohesion: 0.60 -Nodes (4): diff_syntax_language_metadata(), diff_syntax_language_preserves_diff_capture_theme_tokens(), LanguageConfiguration, syntax_language() +### Community 434 - "evaluate_expression" +Cohesion: 0.50 +Nodes (4): DapEvaluateContext, evaluate_expression(), EvaluateArgumentsContext, From -### Community 441 - "AbiPickerTruncateStrategy" -Cohesion: 0.32 -Nodes (5): exported_picker_truncate_strategy(), PickerTruncateStrategy, AbiPickerTruncateStrategy, PickerTruncateStrategy, PickerTruncateStrategy +### Community 435 - "02-body.md" +Cohesion: 0.40 +Nodes (4): Acceptance criteria, Blocked by, Parent, What to build + +### Community 436 - "02-shells-and-jobs-owned.md" +Cohesion: 0.40 +Nodes (4): Acceptance criteria, Blocked by, Parent, What to build + +### Community 437 - "ruby.rs" +Cohesion: 0.43 +Nodes (7): package(), LanguageConfiguration, ruby_package_auto_attaches_all_extensions(), ruby_package_has_no_formatter(), ruby_package_metadata(), ruby_syntax_language_metadata(), syntax_language() + +### Community 438 - "03-body.md" +Cohesion: 0.40 +Nodes (4): Acceptance criteria, Blocked by, Parent, What to build + +### Community 439 - "03-language-tooling-and-helpers-owned.md" +Cohesion: 0.40 +Nodes (4): Acceptance criteria, Blocked by, Parent, What to build + +### Community 440 - "04-body.md" +Cohesion: 0.40 +Nodes (4): Acceptance criteria, Blocked by, Parent, What to build + +### Community 442 - "04-spawn-path-contract-and-release-verification.md" +Cohesion: 0.40 +Nodes (4): Acceptance criteria, Blocked by, Parent, What to build + +### Community 445 - "cargo" +Cohesion: 0.43 +Nodes (6): cargo(), I, Path, Result, String, run() + +### Community 449 - ".text" +Cohesion: 0.50 +Nodes (3): Node, SyntaxTextProvider<'a, B>, TextProvider ## Knowledge Gaps -- **158 isolated node(s):** `DapLogSnapshot`, `LspClientManager`, `BufferChrome<'a>`, `ShellWakeupEvent`, `StartupProfile` (+153 more) +- **196 isolated node(s):** `DapLogSnapshot`, `LspClientManager`, `BufferChrome<'a>`, `ShellWakeupEvent`, `StartupProfile` (+191 more) These have ≤1 connection - possible missing edges or undocumented components. -- **22 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. +- **21 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. ## Suggested Questions _Questions this graph is uniquely positioned to answer:_ -- **Why does `EditorRuntime` connect `EditorRuntime` to `first_project_discovery_root`, `ServiceRegistry`, `syntax_registry_mut`, `state_with_user_library`, `KeymapError`, `ShellState`, `helpers.rs`, `commit_git_buffer`, `shell_user_library`, `editor-issues/src/lib.rs`, `HookBus`, `input.rs`, `KeymapScope`, `register_shell_hooks`, `save.rs`, `refresh_pending_streamed_commands`, `open_image_workspace_file`, `shell/pdf.rs`, `fringe.rs`, `dap_commands.rs`, `EditorModel`, `shell/workspace.rs`, `worktree.rs`, `workers_autocomplete.rs`, `AcpManager`, `GitEditorState`, `String`, `shell_ui_mut`, `directory.rs`, `workspace_search.rs`, `shell/terminal.rs`, `git_command_output`, `tool_install.rs`, `open_acp_client_with_config`, `shell_buffer_mut`, `.handle_text_input_inner`, `load_auto_loaded_packages`, `CommandRegistry`, `shell_ui`, `runtime.rs`, `shell/text_layout.rs`, `shell/hover.rs`, `apply_lsp_notifications`, `refresh_theme_registry_if_needed`, `main`, `log.rs`, `shell/browser.rs`, `paths_logs.rs`, `shell/picker.rs`, `git/diff.rs`, `git_root`, `process.rs`, `remote.rs`, `run_demo_shell`, `lsp_commands.rs`, `CommandSource`, `shell/compile.rs`?** - _High betweenness centrality (0.296) - this node is a cross-community bridge._ -- **Why does `TextPoint` connect `TextPoint` to `TextBuffer`, `LspSessionHandle`, `SyntaxRegistry`, `BufferContextOverlaySnapshot`, `ShellState`, `helpers.rs`, `draw.rs`, `ShellBuffer`, `save.rs`, `Diagnostic`, `HoverOverlay`, `open_image_workspace_file`, `TextEdit`, `LspClientError`, `editor-lsp/src/client/types.rs`, `workers_autocomplete.rs`, `InputField`, `PickerOverlay`, `TextSnapshot`, `markdown_table.rs`, `TextBuffer`, `state.rs`, `shell_ui_mut`, `buffer_types.rs`, `ShellBuffer`, `PaneId`, `buffer_footer_layout_with_command_line`, `workspace_search.rs`, `shell/terminal.rs`, `shell/hover.rs`, `ShellUiState`, `TextRange`, `vim_search.rs`, `lsp_commands.rs`?** - _High betweenness centrality (0.070) - this node is a cross-community bridge._ -- **Why does `shell_ui_mut()` connect `shell_ui_mut` to `first_project_discovery_root`, `syntax_registry_mut`, `state_with_user_library`, `helpers.rs`, `commit_git_buffer`, `acp/tests.rs`, `shell_user_library`, `editor-issues/src/lib.rs`, `input.rs`, `register_shell_hooks`, `save.rs`, `refresh_pending_streamed_commands`, `open_image_workspace_file`, `shell/pdf.rs`, `input_shortcuts.rs`, `fringe.rs`, `dap_commands.rs`, `shell/workspace.rs`, `worktree.rs`, `render_overlays.rs`, `AcpManager`, `GitEditorState`, `String`, `ShellError`, `EditorRuntime`, `markdown_table_event_dimensions`, `directory.rs`, `workspace_search.rs`, `shell/terminal.rs`, `tool_install.rs`, `open_acp_client_with_config`, `shell_buffer_mut`, `shell_ui`, `shell/text_layout.rs`, `apply_lsp_notifications`, `ShellUiState`, `shell/browser.rs`, `paths_logs.rs`, `git/diff.rs`, `git_root`, `process.rs`, `remote.rs`, `terminal_ui.rs`, `lsp_commands.rs`, `shell/compile.rs`?** - _High betweenness centrality (0.069) - this node is a cross-community bridge._ +- **Why does `EditorRuntime` connect `EditorRuntime` to `ServiceRegistry`, `syntax_registry_mut`, `misc.rs`, `KeymapError`, `shell/compile.rs`, `ShellState`, `helpers.rs`, `compute_buffer_syntax`, `shell_user_library`, `editor-issues/src/lib.rs`, `HookBus`, `input.rs`, `KeymapScope`, `register_shell_hooks`, `shell_ui_mut`, `refresh_pending_streamed_commands`, `open_image_workspace_file`, `shell/pdf.rs`, `render/layout.rs`, `fringe.rs`, `dap_commands.rs`, `WorkspaceId`, `shell/workspace.rs`, `worktree.rs`, `workers_autocomplete.rs`, `db_commands.rs`, `AcpManager`, `GitEditorState`, `toggle_line_comments_in_range`, `state.rs`, `active_shell_buffer_mut`, `status.rs`, `directory.rs`, `workspace_search.rs`, `shell/terminal.rs`, `state_with_user_library`, `tool_install.rs`, `open_acp_client_with_config`, `shell_buffer_mut`, `load_auto_loaded_packages`, `CommandSource`, `PluginCommand`, `runtime.rs`, `install_acp_test_buffer`, `shell/text_layout.rs`, `shell/hover.rs`, `volt/src/main.rs`, `theme_reload.rs`, `log.rs`, `shell/browser.rs`, `paths_logs.rs`, `shell/picker.rs`, `git/diff.rs`, `git_root`, `remote.rs`, `save.rs`, `run_demo_shell`, `lsp_commands.rs`, `.handle_event`?** + _High betweenness centrality (0.271) - this node is a cross-community bridge._ +- **Why does `ShellUiState` connect `ShellUiState` to `DebugConfiguration`, `SyntaxRegistry`, `draw.rs`, `CommandLineOverlay`, `shell_user_library`, `editor-issues/src/lib.rs`, `shell_ui_mut`, `refresh_pending_streamed_commands`, `HoverOverlay`, `.render`, `fringe.rs`, `workers.rs`, `WorkspaceId`, `workers_autocomplete.rs`, `PickerOverlay`, `GitEditorState`, `ShellError`, `state.rs`, `render_buffer`, `status.rs`, `directory.rs`, `AutocompleteOverlay`, `resolve_picker_extra`, `RuntimePopupSnapshot`, `run_demo_shell`, `notification_overlay_layouts`, `.handle_event`?** + _High betweenness centrality (0.082) - this node is a cross-community bridge._ +- **Why does `UserLibrary` connect `UserLibrary` to `user/lib.rs`, `ShellState`, `shell/compile.rs`, `draw.rs`, `WorkspaceDockConfig`, `sdk/src/lib.rs`, `compute_buffer_syntax`, `shell_user_library`, `DynamicUserLibrary`, `HoverOverlay`, `PickerItemSpec`, `render/layout.rs`, `DynamicUserLibrary`, `.render`, `HeaderlineTestUserLibrary`, `render_overlays.rs`, `ShellError`, `ShellConfig`, `state.rs`, `ShellBuffer`, `status.rs`, `directory.rs`, `buffer_footer_layout_with_command_line`, `shell_buffer_mut`, `load_auto_loaded_packages`, `volt/src/main.rs`, `shell/text_layout.rs`, `shell/hover.rs`, `ShellUiState`, `theme_reload.rs`, `shell/browser.rs`, `paths_logs.rs`, `shell/picker.rs`, `MarkdownPrettyPlan`, `markdown_pretty.rs`, `.handle_event`?** + _High betweenness centrality (0.073) - this node is a cross-community bridge._ - **Are the 413 inferred relationships involving `shell_ui_mut()` (e.g. with `acp_pick_mode()` and `acp_pick_model()`) actually correct?** _`shell_ui_mut()` has 413 INFERRED edges - model-reasoned connections that need verification._ - **Are the 281 inferred relationships involving `shell_ui()` (e.g. with `apply_acp_notification()` and `maybe_open_acp_input_completion()`) actually correct?** diff --git a/graphify-out/graph.html b/graphify-out/graph.html index 85c52ff3..a95ed196 100644 --- a/graphify-out/graph.html +++ b/graphify-out/graph.html @@ -63,12 +63,12 @@

Communities

-
442 nodes · 1904 edges · 442 communities
+
436 nodes · 1915 edges · 436 communities