file_command.rs

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