file_command.rs

  1use super::{SlashCommand, SlashCommandOutput};
  2use anyhow::Result;
  3use fuzzy::PathMatch;
  4use gpui::{AppContext, Model, RenderOnce, SharedString, Task, WeakView};
  5use language::LspAdapterDelegate;
  6use project::{PathMatchCandidateSet, Project};
  7use std::{
  8    path::{Path, PathBuf},
  9    sync::{atomic::AtomicBool, Arc},
 10};
 11use ui::{prelude::*, ButtonLike, ElevationIndex};
 12use workspace::Workspace;
 13
 14pub(crate) struct FileSlashCommand {
 15    project: Model<Project>,
 16}
 17
 18impl FileSlashCommand {
 19    pub fn new(project: Model<Project>) -> Self {
 20        Self { project }
 21    }
 22
 23    fn search_paths(
 24        &self,
 25        query: String,
 26        cancellation_flag: Arc<AtomicBool>,
 27        cx: &mut AppContext,
 28    ) -> Task<Vec<PathMatch>> {
 29        let worktrees = self
 30            .project
 31            .read(cx)
 32            .visible_worktrees(cx)
 33            .collect::<Vec<_>>();
 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: true,
 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 a file".into()
 72    }
 73
 74    fn tooltip_text(&self) -> String {
 75        "insert file".into()
 76    }
 77
 78    fn requires_argument(&self) -> bool {
 79        true
 80    }
 81
 82    fn complete_argument(
 83        &self,
 84        query: String,
 85        cancellation_flag: Arc<AtomicBool>,
 86        cx: &mut AppContext,
 87    ) -> gpui::Task<Result<Vec<String>>> {
 88        let paths = self.search_paths(query, cancellation_flag, cx);
 89        cx.background_executor().spawn(async move {
 90            Ok(paths
 91                .await
 92                .into_iter()
 93                .map(|path_match| {
 94                    format!(
 95                        "{}{}",
 96                        path_match.path_prefix,
 97                        path_match.path.to_string_lossy()
 98                    )
 99                })
100                .collect())
101        })
102    }
103
104    fn run(
105        self: Arc<Self>,
106        argument: Option<&str>,
107        _workspace: WeakView<Workspace>,
108        _delegate: Arc<dyn LspAdapterDelegate>,
109        cx: &mut WindowContext,
110    ) -> Task<Result<SlashCommandOutput>> {
111        let project = self.project.read(cx);
112        let Some(argument) = argument else {
113            return Task::ready(Err(anyhow::anyhow!("missing path")));
114        };
115
116        let path = PathBuf::from(argument);
117        let abs_path = project.worktrees().find_map(|worktree| {
118            let worktree = worktree.read(cx);
119            let worktree_root_path = Path::new(worktree.root_name());
120            let relative_path = path.strip_prefix(worktree_root_path).ok()?;
121            worktree.absolutize(&relative_path).ok()
122        });
123
124        let Some(abs_path) = abs_path else {
125            return Task::ready(Err(anyhow::anyhow!("missing path")));
126        };
127
128        let fs = project.fs().clone();
129        let argument = argument.to_string();
130        let text = cx.background_executor().spawn(async move {
131            let content = fs.load(&abs_path).await?;
132            let mut output = String::with_capacity(argument.len() + content.len() + 9);
133            output.push_str("```");
134            output.push_str(&argument);
135            output.push('\n');
136            output.push_str(&content);
137            if !output.ends_with('\n') {
138                output.push('\n');
139            }
140            output.push_str("```");
141            anyhow::Ok(output)
142        });
143        cx.foreground_executor().spawn(async move {
144            let text = text.await?;
145            Ok(SlashCommandOutput {
146                text,
147                render_placeholder: Arc::new(move |id, unfold, _cx| {
148                    FilePlaceholder {
149                        path: Some(path.clone()),
150                        id,
151                        unfold,
152                    }
153                    .into_any_element()
154                }),
155            })
156        })
157    }
158}
159
160#[derive(IntoElement)]
161pub struct FilePlaceholder {
162    pub path: Option<PathBuf>,
163    pub id: ElementId,
164    pub unfold: Arc<dyn Fn(&mut WindowContext)>,
165}
166
167impl RenderOnce for FilePlaceholder {
168    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
169        let unfold = self.unfold;
170        let title = if let Some(path) = self.path.as_ref() {
171            SharedString::from(path.to_string_lossy().to_string())
172        } else {
173            SharedString::from("untitled")
174        };
175
176        ButtonLike::new(self.id)
177            .style(ButtonStyle::Filled)
178            .layer(ElevationIndex::ElevatedSurface)
179            .child(Icon::new(IconName::File))
180            .child(Label::new(title))
181            .on_click(move |_, cx| unfold(cx))
182    }
183}