2118 lines · 66.7 KB · Rust
RawDownload
use anyhow::{Result, anyhow};use collections::{HashMap, HashSet};use fs::Fs;use futures::StreamExt;use gpui::{Context, EventEmitter, Task};use std::borrow::Cow;use std::path::{Path, PathBuf};use std::sync::Arc;use std::time::Duration; /// An error that occurred while loading a command file.#[derive(Debug, Clone)]pub struct CommandLoadError {    /// The path to the file that failed to load    pub path: PathBuf,    /// The base path of the commands directory (used to derive command name)    pub base_path: PathBuf,    /// A description of the error    pub message: String,} impl CommandLoadError {    /// Derives the command name from the file path, similar to how successful commands are named.    /// Returns None if the command name cannot be determined (e.g., for directory errors).    pub fn command_name(&self) -> Option<String> {        let base_name = self.path.file_stem()?.to_string_lossy().into_owned();         // Only derive command name for .md files        if self.path.extension().is_none_or(|ext| ext != "md") {            return None;        }         let namespace = self            .path            .parent()            .and_then(|parent| parent.strip_prefix(&self.base_path).ok())            .filter(|rel| !rel.as_os_str().is_empty())            .map(|rel| {                rel.to_string_lossy()                    .replace(std::path::MAIN_SEPARATOR, "/")            });         let name = match &namespace {            Some(namespace) => format!("{}:{}", namespace.replace('/', ":"), base_name),            None => base_name,        };         Some(name)    }} impl std::fmt::Display for CommandLoadError {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        write!(            f,            "Failed to load {}: {}",            self.path.display(),            self.message        )    }} /// Result of loading commands, including any errors encountered.#[derive(Debug, Default, Clone)]pub struct CommandLoadResult {    /// Successfully loaded commands    pub commands: Vec<UserSlashCommand>,    /// Errors encountered while loading commands    pub errors: Vec<CommandLoadError>,} /// The scope of a user-defined slash command.#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum CommandScope {    /// Project-specific command from .zed/commands/    Project,    /// User-wide command from config_dir()/commands/    User,} /// A user-defined slash command loaded from a markdown file.#[derive(Debug, Clone, PartialEq)]pub struct UserSlashCommand {    /// The command name for invocation.    /// For commands in subdirectories, this is prefixed: "namespace:name" (e.g., "frontend:component")    /// For commands in the root, this is just the filename without .md extension.    pub name: Arc<str>,    /// The template content from the file    pub template: Arc<str>,    /// The namespace (subdirectory path, if any), used for description display    pub namespace: Option<Arc<str>>,    /// The full path to the command file    pub path: PathBuf,    /// Whether this is a project or user command    pub scope: CommandScope,} impl UserSlashCommand {    /// Returns a description string for display in completions.    pub fn description(&self) -> String {        String::new()    }     /// Returns true if this command has any placeholders ($1, $2, etc. or $ARGUMENTS)    pub fn requires_arguments(&self) -> bool {        has_placeholders(&self.template)    }} fn command_base_path(command: &UserSlashCommand) -> PathBuf {    let mut base_path = command.path.clone();    base_path.pop();    if let Some(namespace) = &command.namespace {        for segment in namespace.split('/') {            if segment.is_empty() {                continue;            }            if !base_path.pop() {                break;            }        }    }    base_path} impl CommandLoadError {    pub fn from_command(command: &UserSlashCommand, message: String) -> Self {        Self {            path: command.path.clone(),            base_path: command_base_path(command),            message,        }    }} /// Parsed user command from input text#[derive(Debug, Clone, PartialEq)]pub struct ParsedUserCommand<'a> {    pub name: &'a str,    pub raw_arguments: &'a str,} /// Returns the path to the user commands directory.pub fn user_commands_dir() -> PathBuf {    paths::config_dir().join("commands")} /// Returns the path to the project commands directory for a given worktree root.pub fn project_commands_dir(worktree_root: &Path) -> PathBuf {    worktree_root.join(".zed").join("commands")} /// Events emitted by SlashCommandRegistry#[derive(Debug, Clone)]#[allow(dead_code)] // Infrastructure for future caching implementationpub enum SlashCommandRegistryEvent {    /// Commands have been reloaded    CommandsChanged,} /// A registry that caches user-defined slash commands and watches for changes./// Currently used in tests; will be integrated into the UI layer for caching.#[allow(dead_code)]pub struct SlashCommandRegistry {    fs: Arc<dyn Fs>,    commands: HashMap<String, UserSlashCommand>,    errors: Vec<CommandLoadError>,    worktree_roots: Vec<PathBuf>,    _watch_task: Option<Task<()>>,} impl EventEmitter<SlashCommandRegistryEvent> for SlashCommandRegistry {} #[allow(dead_code)]impl SlashCommandRegistry {    /// Creates a new registry and starts loading commands.    pub fn new(fs: Arc<dyn Fs>, worktree_roots: Vec<PathBuf>, cx: &mut Context<Self>) -> Self {        let mut this = Self {            fs,            commands: HashMap::default(),            errors: Vec::new(),            worktree_roots,            _watch_task: None,        };         this.start_watching(cx);        this.reload(cx);         this    }     /// Returns all loaded commands.    pub fn commands(&self) -> &HashMap<String, UserSlashCommand> {        &self.commands    }     /// Returns any errors from the last load.    pub fn errors(&self) -> &[CommandLoadError] {        &self.errors    }     /// Updates the worktree roots and reloads commands.    pub fn set_worktree_roots(&mut self, roots: Vec<PathBuf>, cx: &mut Context<Self>) {        if self.worktree_roots != roots {            self.worktree_roots = roots;            self.start_watching(cx);            self.reload(cx);        }    }     /// Manually triggers a reload of all commands.    pub fn reload(&mut self, cx: &mut Context<Self>) {        let fs = self.fs.clone();        let worktree_roots = self.worktree_roots.clone();         cx.spawn(async move |this, cx| {            let result = load_all_commands_async(&fs, &worktree_roots).await;            this.update(cx, |this, cx| {                this.commands = commands_to_map(&result.commands);                this.errors = result.errors;                cx.emit(SlashCommandRegistryEvent::CommandsChanged);            })        })        .detach_and_log_err(cx);    }     fn start_watching(&mut self, cx: &mut Context<Self>) {        let fs = self.fs.clone();        let worktree_roots = self.worktree_roots.clone();         let task = cx.spawn(async move |this, cx| {            let user_dir = user_commands_dir();            let mut dirs_to_watch = vec![user_dir];            for root in &worktree_roots {                dirs_to_watch.push(project_commands_dir(root));            }             let mut watch_streams = Vec::new();            for dir in &dirs_to_watch {                let (stream, _watcher) = fs.watch(dir, Duration::from_millis(100)).await;                watch_streams.push(stream);            }             let mut combined = futures::stream::select_all(watch_streams);             while let Some(events) = combined.next().await {                let should_reload = events.iter().any(|event| {                    event.path.extension().is_some_and(|ext| ext == "md")                        || event.kind == Some(fs::PathEventKind::Created)                        || event.kind == Some(fs::PathEventKind::Removed)                });                 if should_reload {                    let result = load_all_commands_async(&fs, &worktree_roots).await;                    let _ = this.update(cx, |this, cx| {                        this.commands = commands_to_map(&result.commands);                        this.errors = result.errors;                        cx.emit(SlashCommandRegistryEvent::CommandsChanged);                    });                }            }        });         self._watch_task = Some(task);    }} /// Loads all commands (both project and user) for given worktree roots asynchronously.pub async fn load_all_commands_async(    fs: &Arc<dyn Fs>,    worktree_roots: &[PathBuf],) -> CommandLoadResult {    let mut result = CommandLoadResult::default();    let mut seen_commands: HashMap<String, PathBuf> = HashMap::default();     // Load project commands first    for root in worktree_roots {        let commands_path = project_commands_dir(root);        let project_result =            load_commands_from_path_async(fs, &commands_path, CommandScope::Project).await;        result.errors.extend(project_result.errors);        for cmd in project_result.commands {            if let Some(existing_path) = seen_commands.get(&*cmd.name) {                result.errors.push(CommandLoadError {                    path: cmd.path.clone(),                    base_path: commands_path.clone(),                    message: format!(                        "Command '{}' is ambiguous: also defined at {}",                        cmd.name,                        existing_path.display()                    ),                });            } else {                seen_commands.insert(cmd.name.to_string(), cmd.path.clone());                result.commands.push(cmd);            }        }    }     // Load user commands    let user_commands_path = user_commands_dir();    let user_result =        load_commands_from_path_async(fs, &user_commands_path, CommandScope::User).await;    result.errors.extend(user_result.errors);    for cmd in user_result.commands {        if let Some(existing_path) = seen_commands.get(&*cmd.name) {            result.errors.push(CommandLoadError {                path: cmd.path.clone(),                base_path: user_commands_path.clone(),                message: format!(                    "Command '{}' is ambiguous: also defined at {}",                    cmd.name,                    existing_path.display()                ),            });        } else {            seen_commands.insert(cmd.name.to_string(), cmd.path.clone());            result.commands.push(cmd);        }    }     result} async fn load_commands_from_path_async(    fs: &Arc<dyn Fs>,    commands_path: &Path,    scope: CommandScope,) -> CommandLoadResult {    let mut result = CommandLoadResult::default();     if !fs.is_dir(commands_path).await {        return result;    }     load_commands_from_dir_async(fs, commands_path, commands_path, scope, &mut result).await;    result} fn load_commands_from_dir_async<'a>(    fs: &'a Arc<dyn Fs>,    base_path: &'a Path,    current_path: &'a Path,    scope: CommandScope,    result: &'a mut CommandLoadResult,) -> futures::future::BoxFuture<'a, ()> {    Box::pin(async move {        let entries = match fs.read_dir(current_path).await {            Ok(entries) => entries,            Err(e) => {                result.errors.push(CommandLoadError {                    path: current_path.to_path_buf(),                    base_path: base_path.to_path_buf(),                    message: format!("Failed to read directory: {}", e),                });                return;            }        };         let entries: Vec<_> = entries.collect().await;         for entry in entries {            let path = match entry {                Ok(path) => path,                Err(e) => {                    result.errors.push(CommandLoadError {                        path: current_path.to_path_buf(),                        base_path: base_path.to_path_buf(),                        message: format!("Failed to read directory entry: {}", e),                    });                    continue;                }            };             if fs.is_dir(&path).await {                load_commands_from_dir_async(fs, base_path, &path, scope, result).await;            } else if path.extension().is_some_and(|ext| ext == "md") {                match load_command_file_async(fs, base_path, &path, scope).await {                    Ok(Some(command)) => result.commands.push(command),                    Ok(None) => {} // Empty file, skip silently                    Err(e) => {                        result.errors.push(CommandLoadError {                            path: path.clone(),                            base_path: base_path.to_path_buf(),                            message: e.to_string(),                        });                    }                }            }        }    })} async fn load_command_file_async(    fs: &Arc<dyn Fs>,    base_path: &Path,    file_path: &Path,    scope: CommandScope,) -> Result<Option<UserSlashCommand>> {    let base_name = match file_path.file_stem() {        Some(stem) => stem.to_string_lossy().into_owned(),        None => return Ok(None),    };     let template = fs.load(file_path).await?;    if template.is_empty() {        return Ok(None);    }    if template.trim().is_empty() {        return Err(anyhow!("Command file contains only whitespace"));    }     let namespace = file_path        .parent()        .and_then(|parent| parent.strip_prefix(base_path).ok())        .filter(|rel| !rel.as_os_str().is_empty())        .map(|rel| {            rel.to_string_lossy()                .replace(std::path::MAIN_SEPARATOR, "/")        });     // Build the full command name: "namespace:basename" or just "basename"    let name = match &namespace {        Some(namespace) => format!("{}:{}", namespace.replace('/', ":"), base_name),        None => base_name,    };     Ok(Some(UserSlashCommand {        name: name.into(),        template: template.into(),        namespace: namespace.map(|s| s.into()),        path: file_path.to_path_buf(),        scope,    }))} /// Converts a list of UserSlashCommand to a HashMap for quick lookup./// The key is the command name.pub fn commands_to_map(commands: &[UserSlashCommand]) -> HashMap<String, UserSlashCommand> {    let mut map = HashMap::default();    for cmd in commands {        map.insert(cmd.name.to_string(), cmd.clone());    }    map} fn has_error_for_command(errors: &[CommandLoadError], name: &str) -> bool {    errors        .iter()        .any(|error| error.command_name().as_deref() == Some(name))} fn server_conflict_message(name: &str) -> String {    format!(        "Command '{}' conflicts with server-provided /{}",        name, name    )} pub fn apply_server_command_conflicts(    commands: &mut Vec<UserSlashCommand>,    errors: &mut Vec<CommandLoadError>,    server_command_names: &HashSet<String>,) {    commands.retain(|command| {        if server_command_names.contains(command.name.as_ref()) {            if !has_error_for_command(errors, command.name.as_ref()) {                errors.push(CommandLoadError::from_command(                    command,                    server_conflict_message(command.name.as_ref()),                ));            }            false        } else {            true        }    });} pub fn apply_server_command_conflicts_to_map(    commands: &mut HashMap<String, UserSlashCommand>,    errors: &mut Vec<CommandLoadError>,    server_command_names: &HashSet<String>,) {    commands.retain(|name, command| {        if server_command_names.contains(name) {            if !has_error_for_command(errors, name) {                errors.push(CommandLoadError::from_command(                    command,                    server_conflict_message(name),                ));            }            false        } else {            true        }    });} /// Parses a line of input to extract a user command invocation.

Lines 1–500 of 2118.