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}