selection_command.rs

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