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}