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(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}