file_command.rs

  1use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
  2use anyhow::Result;
  3use futures::channel::oneshot;
  4use fuzzy::PathMatch;
  5use gpui::{AppContext, Model, Task};
  6use project::{PathMatchCandidateSet, Project};
  7use std::{
  8    path::Path,
  9    sync::{atomic::AtomicBool, Arc},
 10};
 11
 12pub(crate) struct FileSlashCommand {
 13    project: Model<Project>,
 14}
 15
 16impl FileSlashCommand {
 17    pub fn new(project: Model<Project>) -> Self {
 18        Self { project }
 19    }
 20
 21    fn search_paths(
 22        &self,
 23        query: String,
 24        cancellation_flag: Arc<AtomicBool>,
 25        cx: &mut AppContext,
 26    ) -> Task<Vec<PathMatch>> {
 27        let worktrees = self
 28            .project
 29            .read(cx)
 30            .visible_worktrees(cx)
 31            .collect::<Vec<_>>();
 32        let include_root_name = worktrees.len() > 1;
 33        let candidate_sets = worktrees
 34            .into_iter()
 35            .map(|worktree| {
 36                let worktree = worktree.read(cx);
 37                PathMatchCandidateSet {
 38                    snapshot: worktree.snapshot(),
 39                    include_ignored: worktree
 40                        .root_entry()
 41                        .map_or(false, |entry| entry.is_ignored),
 42                    include_root_name,
 43                    directories_only: false,
 44                }
 45            })
 46            .collect::<Vec<_>>();
 47
 48        let executor = cx.background_executor().clone();
 49        cx.foreground_executor().spawn(async move {
 50            fuzzy::match_path_sets(
 51                candidate_sets.as_slice(),
 52                query.as_str(),
 53                None,
 54                false,
 55                100,
 56                &cancellation_flag,
 57                executor,
 58            )
 59            .await
 60        })
 61    }
 62}
 63
 64impl SlashCommand for FileSlashCommand {
 65    fn name(&self) -> String {
 66        "file".into()
 67    }
 68
 69    fn description(&self) -> String {
 70        "insert an entire file".into()
 71    }
 72
 73    fn requires_argument(&self) -> bool {
 74        true
 75    }
 76
 77    fn complete_argument(
 78        &self,
 79        query: String,
 80        cancellation_flag: Arc<AtomicBool>,
 81        cx: &mut AppContext,
 82    ) -> gpui::Task<Result<Vec<String>>> {
 83        let paths = self.search_paths(query, cancellation_flag, cx);
 84        cx.background_executor().spawn(async move {
 85            Ok(paths
 86                .await
 87                .into_iter()
 88                .map(|path_match| {
 89                    format!(
 90                        "{}{}",
 91                        path_match.path_prefix,
 92                        path_match.path.to_string_lossy()
 93                    )
 94                })
 95                .collect())
 96        })
 97    }
 98
 99    fn run(&self, argument: Option<&str>, cx: &mut AppContext) -> SlashCommandInvocation {
100        let project = self.project.read(cx);
101        let Some(argument) = argument else {
102            return SlashCommandInvocation {
103                output: Task::ready(Err(anyhow::anyhow!("missing path"))),
104                invalidated: oneshot::channel().1,
105                cleanup: SlashCommandCleanup::default(),
106            };
107        };
108
109        let path = Path::new(argument);
110        let abs_path = project.worktrees().find_map(|worktree| {
111            let worktree = worktree.read(cx);
112            worktree.entry_for_path(path)?;
113            worktree.absolutize(path).ok()
114        });
115
116        let Some(abs_path) = abs_path else {
117            return SlashCommandInvocation {
118                output: Task::ready(Err(anyhow::anyhow!("missing path"))),
119                invalidated: oneshot::channel().1,
120                cleanup: SlashCommandCleanup::default(),
121            };
122        };
123
124        let fs = project.fs().clone();
125        let argument = argument.to_string();
126        let output = cx.background_executor().spawn(async move {
127            let content = fs.load(&abs_path).await?;
128            let mut output = String::with_capacity(argument.len() + content.len() + 9);
129            output.push_str("```");
130            output.push_str(&argument);
131            output.push('\n');
132            output.push_str(&content);
133            if !output.ends_with('\n') {
134                output.push('\n');
135            }
136            output.push_str("```");
137            Ok(output)
138        });
139        SlashCommandInvocation {
140            output,
141            invalidated: oneshot::channel().1,
142            cleanup: SlashCommandCleanup::default(),
143        }
144    }
145}