1use anyhow::{Result, anyhow};
  2use assistant_slash_command::{
  3    ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
  4    SlashCommandOutputSection, SlashCommandResult,
  5};
  6use editor::{Editor, MultiBufferSnapshot};
  7use futures::StreamExt;
  8use gpui::{App, SharedString, Task, WeakEntity, Window};
  9use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
 10use rope::Point;
 11use std::ops::Range;
 12use std::sync::Arc;
 13use std::sync::atomic::AtomicBool;
 14use ui::IconName;
 15use workspace::Workspace;
 16
 17use crate::file_command::codeblock_fence_for_path;
 18
 19pub struct SelectionCommand;
 20
 21impl SlashCommand for SelectionCommand {
 22    fn name(&self) -> String {
 23        "selection".into()
 24    }
 25
 26    fn label(&self, _cx: &App) -> CodeLabel {
 27        CodeLabel::plain(self.name(), None)
 28    }
 29
 30    fn description(&self) -> String {
 31        "Insert editor selection".into()
 32    }
 33
 34    fn icon(&self) -> IconName {
 35        IconName::Quote
 36    }
 37
 38    fn menu_text(&self) -> String {
 39        self.description()
 40    }
 41
 42    fn requires_argument(&self) -> bool {
 43        false
 44    }
 45
 46    fn accepts_arguments(&self) -> bool {
 47        true
 48    }
 49
 50    fn complete_argument(
 51        self: Arc<Self>,
 52        _arguments: &[String],
 53        _cancel: Arc<AtomicBool>,
 54        _workspace: Option<WeakEntity<Workspace>>,
 55        _window: &mut Window,
 56        _cx: &mut App,
 57    ) -> Task<Result<Vec<ArgumentCompletion>>> {
 58        Task::ready(Err(anyhow!("this command does not require argument")))
 59    }
 60
 61    fn run(
 62        self: Arc<Self>,
 63        _arguments: &[String],
 64        _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
 65        _context_buffer: BufferSnapshot,
 66        workspace: WeakEntity<Workspace>,
 67        _delegate: Option<Arc<dyn LspAdapterDelegate>>,
 68        _window: &mut Window,
 69        cx: &mut App,
 70    ) -> Task<SlashCommandResult> {
 71        let mut events = vec![];
 72
 73        let Some(creases) = workspace
 74            .update(cx, |workspace, cx| {
 75                let editor = workspace
 76                    .active_item(cx)
 77                    .and_then(|item| item.act_as::<Editor>(cx))?;
 78
 79                editor.update(cx, |editor, cx| {
 80                    let selection_ranges = editor
 81                        .selections
 82                        .all_adjusted(&editor.display_snapshot(cx))
 83                        .iter()
 84                        .map(|selection| selection.range())
 85                        .collect::<Vec<_>>();
 86                    let snapshot = editor.buffer().read(cx).snapshot(cx);
 87                    Some(selections_creases(selection_ranges, snapshot, cx))
 88                })
 89            })
 90            .unwrap_or_else(|e| {
 91                events.push(Err(e));
 92                None
 93            })
 94        else {
 95            return Task::ready(Err(anyhow!("no active selection")));
 96        };
 97
 98        for (text, title) in creases {
 99            events.push(Ok(SlashCommandEvent::StartSection {
100                icon: IconName::TextSnippet,
101                label: SharedString::from(title),
102                metadata: None,
103            }));
104            events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
105                text,
106                run_commands_in_text: false,
107            })));
108            events.push(Ok(SlashCommandEvent::EndSection));
109            events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
110                text: "\n".to_string(),
111                run_commands_in_text: false,
112            })));
113        }
114
115        let result = futures::stream::iter(events).boxed();
116
117        Task::ready(Ok(result))
118    }
119}
120
121pub fn selections_creases(
122    selection_ranges: Vec<Range<Point>>,
123    snapshot: MultiBufferSnapshot,
124    cx: &App,
125) -> Vec<(String, String)> {
126    let mut creases = Vec::new();
127    for range in selection_ranges {
128        let selected_text = snapshot.text_for_range(range.clone()).collect::<String>();
129        if selected_text.is_empty() {
130            continue;
131        }
132        let start_language = snapshot.language_at(range.start);
133        let end_language = snapshot.language_at(range.end);
134        let language_name = if start_language == end_language {
135            start_language.map(|language| language.code_fence_block_name())
136        } else {
137            None
138        };
139        let language_name = language_name.as_deref().unwrap_or("");
140        let filename = snapshot
141            .file_at(range.start)
142            .map(|file| file.full_path(cx).to_string_lossy().into_owned());
143        let text = if language_name == "markdown" {
144            selected_text
145                .lines()
146                .map(|line| format!("> {}", line))
147                .collect::<Vec<_>>()
148                .join("\n")
149        } else {
150            let start_symbols = snapshot
151                .symbols_containing(range.start, None)
152                .map(|(_, symbols)| symbols);
153            let end_symbols = snapshot
154                .symbols_containing(range.end, None)
155                .map(|(_, symbols)| symbols);
156
157            let outline_text =
158                if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
159                    Some(
160                        start_symbols
161                            .into_iter()
162                            .zip(end_symbols)
163                            .take_while(|(a, b)| a == b)
164                            .map(|(a, _)| a.text)
165                            .collect::<Vec<_>>()
166                            .join(" > "),
167                    )
168                } else {
169                    None
170                };
171
172            let line_comment_prefix = start_language
173                .and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
174
175            let fence = codeblock_fence_for_path(
176                filename.as_deref(),
177                Some(range.start.row..=range.end.row),
178            );
179
180            if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text)
181            {
182                let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
183                format!("{fence}{breadcrumb}{selected_text}\n```")
184            } else {
185                format!("{fence}{selected_text}\n```")
186            }
187        };
188        let crease_title = if let Some(path) = filename {
189            let start_line = range.start.row + 1;
190            let end_line = range.end.row + 1;
191            if start_line == end_line {
192                format!("{path}, Line {start_line}")
193            } else {
194                format!("{path}, Lines {start_line} to {end_line}")
195            }
196        } else {
197            "Quoted selection".to_string()
198        };
199        creases.push((text, crease_title));
200    }
201    creases
202}