selection_command.rs

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