file_command.rs

  1use super::{SlashCommand, SlashCommandOutput};
  2use anyhow::{anyhow, Result};
  3use assistant_slash_command::SlashCommandOutputSection;
  4use fuzzy::PathMatch;
  5use gpui::{AppContext, RenderOnce, SharedString, Task, View, WeakView};
  6use language::{LineEnding, LspAdapterDelegate};
  7use project::PathMatchCandidateSet;
  8use std::{
  9    ops::Range,
 10    path::{Path, PathBuf},
 11    sync::{atomic::AtomicBool, Arc},
 12};
 13use ui::{prelude::*, ButtonLike, ElevationIndex};
 14use workspace::Workspace;
 15
 16pub(crate) struct FileSlashCommand;
 17
 18impl FileSlashCommand {
 19    fn search_paths(
 20        &self,
 21        query: String,
 22        cancellation_flag: Arc<AtomicBool>,
 23        workspace: &View<Workspace>,
 24        cx: &mut AppContext,
 25    ) -> Task<Vec<PathMatch>> {
 26        if query.is_empty() {
 27            let workspace = workspace.read(cx);
 28            let project = workspace.project().read(cx);
 29            let entries = workspace.recent_navigation_history(Some(10), cx);
 30            let path_prefix: Arc<str> = "".into();
 31            Task::ready(
 32                entries
 33                    .into_iter()
 34                    .filter_map(|(entry, _)| {
 35                        let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
 36                        let mut full_path = PathBuf::from(worktree.read(cx).root_name());
 37                        full_path.push(&entry.path);
 38                        Some(PathMatch {
 39                            score: 0.,
 40                            positions: Vec::new(),
 41                            worktree_id: entry.worktree_id.to_usize(),
 42                            path: full_path.into(),
 43                            path_prefix: path_prefix.clone(),
 44                            distance_to_relative_ancestor: 0,
 45                        })
 46                    })
 47                    .collect(),
 48            )
 49        } else {
 50            let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
 51            let candidate_sets = worktrees
 52                .into_iter()
 53                .map(|worktree| {
 54                    let worktree = worktree.read(cx);
 55                    PathMatchCandidateSet {
 56                        snapshot: worktree.snapshot(),
 57                        include_ignored: worktree
 58                            .root_entry()
 59                            .map_or(false, |entry| entry.is_ignored),
 60                        include_root_name: true,
 61                        directories_only: false,
 62                    }
 63                })
 64                .collect::<Vec<_>>();
 65
 66            let executor = cx.background_executor().clone();
 67            cx.foreground_executor().spawn(async move {
 68                fuzzy::match_path_sets(
 69                    candidate_sets.as_slice(),
 70                    query.as_str(),
 71                    None,
 72                    false,
 73                    100,
 74                    &cancellation_flag,
 75                    executor,
 76                )
 77                .await
 78            })
 79        }
 80    }
 81}
 82
 83impl SlashCommand for FileSlashCommand {
 84    fn name(&self) -> String {
 85        "file".into()
 86    }
 87
 88    fn description(&self) -> String {
 89        "insert file".into()
 90    }
 91
 92    fn menu_text(&self) -> String {
 93        "Insert File".into()
 94    }
 95
 96    fn requires_argument(&self) -> bool {
 97        true
 98    }
 99
100    fn complete_argument(
101        &self,
102        query: String,
103        cancellation_flag: Arc<AtomicBool>,
104        workspace: Option<WeakView<Workspace>>,
105        cx: &mut AppContext,
106    ) -> Task<Result<Vec<String>>> {
107        let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
108            return Task::ready(Err(anyhow!("workspace was dropped")));
109        };
110
111        let paths = self.search_paths(query, cancellation_flag, &workspace, cx);
112        cx.background_executor().spawn(async move {
113            Ok(paths
114                .await
115                .into_iter()
116                .map(|path_match| {
117                    format!(
118                        "{}{}",
119                        path_match.path_prefix,
120                        path_match.path.to_string_lossy()
121                    )
122                })
123                .collect())
124        })
125    }
126
127    fn run(
128        self: Arc<Self>,
129        argument: Option<&str>,
130        workspace: WeakView<Workspace>,
131        _delegate: Arc<dyn LspAdapterDelegate>,
132        cx: &mut WindowContext,
133    ) -> Task<Result<SlashCommandOutput>> {
134        let Some(workspace) = workspace.upgrade() else {
135            return Task::ready(Err(anyhow!("workspace was dropped")));
136        };
137
138        let Some(argument) = argument else {
139            return Task::ready(Err(anyhow!("missing path")));
140        };
141
142        let path = PathBuf::from(argument);
143        let abs_path = workspace
144            .read(cx)
145            .visible_worktrees(cx)
146            .find_map(|worktree| {
147                let worktree = worktree.read(cx);
148                let worktree_root_path = Path::new(worktree.root_name());
149                let relative_path = path.strip_prefix(worktree_root_path).ok()?;
150                worktree.absolutize(&relative_path).ok()
151            });
152
153        let Some(abs_path) = abs_path else {
154            return Task::ready(Err(anyhow!("missing path")));
155        };
156
157        let fs = workspace.read(cx).app_state().fs.clone();
158        let argument = argument.to_string();
159        let text = cx.background_executor().spawn(async move {
160            let mut content = fs.load(&abs_path).await?;
161            LineEnding::normalize(&mut content);
162            let mut output = String::with_capacity(argument.len() + content.len() + 9);
163            output.push_str("```");
164            output.push_str(&argument);
165            output.push('\n');
166            output.push_str(&content);
167            if !output.ends_with('\n') {
168                output.push('\n');
169            }
170            output.push_str("```");
171            anyhow::Ok(output)
172        });
173        cx.foreground_executor().spawn(async move {
174            let text = text.await?;
175            let range = 0..text.len();
176            Ok(SlashCommandOutput {
177                text,
178                sections: vec![SlashCommandOutputSection {
179                    range,
180                    render_placeholder: Arc::new(move |id, unfold, _cx| {
181                        FilePlaceholder {
182                            path: Some(path.clone()),
183                            line_range: None,
184                            id,
185                            unfold,
186                        }
187                        .into_any_element()
188                    }),
189                }],
190                run_commands_in_text: false,
191            })
192        })
193    }
194}
195
196#[derive(IntoElement)]
197pub struct FilePlaceholder {
198    pub path: Option<PathBuf>,
199    pub line_range: Option<Range<u32>>,
200    pub id: ElementId,
201    pub unfold: Arc<dyn Fn(&mut WindowContext)>,
202}
203
204impl RenderOnce for FilePlaceholder {
205    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
206        let unfold = self.unfold;
207        let title = if let Some(path) = self.path.as_ref() {
208            SharedString::from(path.to_string_lossy().to_string())
209        } else {
210            SharedString::from("untitled")
211        };
212
213        ButtonLike::new(self.id)
214            .style(ButtonStyle::Filled)
215            .layer(ElevationIndex::ElevatedSurface)
216            .child(Icon::new(IconName::File))
217            .child(Label::new(title))
218            .when_some(self.line_range, |button, line_range| {
219                button.child(Label::new(":")).child(Label::new(format!(
220                    "{}-{}",
221                    line_range.start, line_range.end
222                )))
223            })
224            .on_click(move |_, cx| unfold(cx))
225    }
226}