diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 6e6f7a823eea8f5a8cb5d165009f3e3e565c26ef..edfcbeb6de7cf56cf09a79bf0cb64d8a70baa4e5 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -44,7 +44,8 @@ use settings::{update_settings_file, Settings, SettingsStore}; use slash_command::{ auto_command, cargo_workspace_command, context_server_command, default_command, delta_command, diagnostics_command, docs_command, fetch_command, file_command, now_command, project_command, - prompt_command, search_command, symbols_command, tab_command, terminal_command, + prompt_command, search_command, selection_command, symbols_command, tab_command, + terminal_command, }; use std::path::PathBuf; use std::sync::Arc; @@ -436,6 +437,7 @@ fn register_slash_commands(prompt_builder: Option>, cx: &mut slash_command_registry .register_command(cargo_workspace_command::CargoWorkspaceSlashCommand, true); slash_command_registry.register_command(prompt_command::PromptSlashCommand, true); + slash_command_registry.register_command(selection_command::SelectionCommand, true); slash_command_registry.register_command(default_command::DefaultSlashCommand, false); slash_command_registry.register_command(terminal_command::TerminalSlashCommand, true); slash_command_registry.register_command(now_command::NowSlashCommand, false); diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index e1b3a3d9784474aa4900e472d0635d5743b0ac39..ab060ef51325e77744f804932df56d4d123ca324 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -3025,97 +3025,11 @@ impl ContextEditor { let Some(panel) = workspace.panel::(cx) else { return; }; - let Some(editor) = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - else { + + let Some(creases) = selections_creases(workspace, cx) else { return; }; - let mut creases = vec![]; - editor.update(cx, |editor, cx| { - let selections = editor.selections.all_adjusted(cx); - let buffer = editor.buffer().read(cx).snapshot(cx); - for selection in selections { - let range = editor::ToOffset::to_offset(&selection.start, &buffer) - ..editor::ToOffset::to_offset(&selection.end, &buffer); - let selected_text = buffer.text_for_range(range.clone()).collect::(); - if selected_text.is_empty() { - continue; - } - let start_language = buffer.language_at(range.start); - let end_language = buffer.language_at(range.end); - let language_name = if start_language == end_language { - start_language.map(|language| language.code_fence_block_name()) - } else { - None - }; - let language_name = language_name.as_deref().unwrap_or(""); - let filename = buffer - .file_at(selection.start) - .map(|file| file.full_path(cx)); - let text = if language_name == "markdown" { - selected_text - .lines() - .map(|line| format!("> {}", line)) - .collect::>() - .join("\n") - } else { - let start_symbols = buffer - .symbols_containing(selection.start, None) - .map(|(_, symbols)| symbols); - let end_symbols = buffer - .symbols_containing(selection.end, None) - .map(|(_, symbols)| symbols); - - let outline_text = if let Some((start_symbols, end_symbols)) = - start_symbols.zip(end_symbols) - { - Some( - start_symbols - .into_iter() - .zip(end_symbols) - .take_while(|(a, b)| a == b) - .map(|(a, _)| a.text) - .collect::>() - .join(" > "), - ) - } else { - None - }; - - let line_comment_prefix = start_language - .and_then(|l| l.default_scope().line_comment_prefixes().first().cloned()); - - let fence = codeblock_fence_for_path( - filename.as_deref(), - Some(selection.start.row..=selection.end.row), - ); - - if let Some((line_comment_prefix, outline_text)) = - line_comment_prefix.zip(outline_text) - { - let breadcrumb = - format!("{line_comment_prefix}Excerpt from: {outline_text}\n"); - format!("{fence}{breadcrumb}{selected_text}\n```") - } else { - format!("{fence}{selected_text}\n```") - } - }; - let crease_title = if let Some(path) = filename { - let start_line = selection.start.row + 1; - let end_line = selection.end.row + 1; - if start_line == end_line { - format!("{}, Line {}", path.display(), start_line) - } else { - format!("{}, Lines {} to {}", path.display(), start_line, end_line) - } - } else { - "Quoted selection".to_string() - }; - creases.push((text, crease_title)); - } - }); if creases.is_empty() { return; } @@ -4006,6 +3920,99 @@ fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Opti None } +pub fn selections_creases( + workspace: &mut workspace::Workspace, + cx: &mut ViewContext, +) -> Option> { + let editor = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx))?; + + let mut creases = vec![]; + editor.update(cx, |editor, cx| { + let selections = editor.selections.all_adjusted(cx); + let buffer = editor.buffer().read(cx).snapshot(cx); + for selection in selections { + let range = editor::ToOffset::to_offset(&selection.start, &buffer) + ..editor::ToOffset::to_offset(&selection.end, &buffer); + let selected_text = buffer.text_for_range(range.clone()).collect::(); + if selected_text.is_empty() { + continue; + } + let start_language = buffer.language_at(range.start); + let end_language = buffer.language_at(range.end); + let language_name = if start_language == end_language { + start_language.map(|language| language.code_fence_block_name()) + } else { + None + }; + let language_name = language_name.as_deref().unwrap_or(""); + let filename = buffer + .file_at(selection.start) + .map(|file| file.full_path(cx)); + let text = if language_name == "markdown" { + selected_text + .lines() + .map(|line| format!("> {}", line)) + .collect::>() + .join("\n") + } else { + let start_symbols = buffer + .symbols_containing(selection.start, None) + .map(|(_, symbols)| symbols); + let end_symbols = buffer + .symbols_containing(selection.end, None) + .map(|(_, symbols)| symbols); + + let outline_text = + if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) { + Some( + start_symbols + .into_iter() + .zip(end_symbols) + .take_while(|(a, b)| a == b) + .map(|(a, _)| a.text) + .collect::>() + .join(" > "), + ) + } else { + None + }; + + let line_comment_prefix = start_language + .and_then(|l| l.default_scope().line_comment_prefixes().first().cloned()); + + let fence = codeblock_fence_for_path( + filename.as_deref(), + Some(selection.start.row..=selection.end.row), + ); + + if let Some((line_comment_prefix, outline_text)) = + line_comment_prefix.zip(outline_text) + { + let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n"); + format!("{fence}{breadcrumb}{selected_text}\n```") + } else { + format!("{fence}{selected_text}\n```") + } + }; + let crease_title = if let Some(path) = filename { + let start_line = selection.start.row + 1; + let end_line = selection.end.row + 1; + if start_line == end_line { + format!("{}, Line {}", path.display(), start_line) + } else { + format!("{}, Lines {} to {}", path.display(), start_line, end_line) + } + } else { + "Quoted selection".to_string() + }; + creases.push((text, crease_title)); + } + }); + Some(creases) +} + fn render_fold_icon_button( editor: WeakView, icon: IconName, diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs index 220930808170637236ec6cb4a037bae77e9665af..eec039d9429bed8f7cb9f29b521ccb81ebd519ac 100644 --- a/crates/assistant/src/slash_command.rs +++ b/crates/assistant/src/slash_command.rs @@ -31,6 +31,7 @@ pub mod now_command; pub mod project_command; pub mod prompt_command; pub mod search_command; +pub mod selection_command; pub mod streaming_example_command; pub mod symbols_command; pub mod tab_command; diff --git a/crates/assistant/src/slash_command/selection_command.rs b/crates/assistant/src/slash_command/selection_command.rs new file mode 100644 index 0000000000000000000000000000000000000000..cf4a2036ed06f23318c42b1f61b1d891ae4d30ae --- /dev/null +++ b/crates/assistant/src/slash_command/selection_command.rs @@ -0,0 +1,98 @@ +use crate::assistant_panel::selections_creases; +use anyhow::{anyhow, Result}; +use assistant_slash_command::{ + ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, + SlashCommandOutputSection, SlashCommandResult, +}; +use futures::StreamExt; +use gpui::{AppContext, Task, WeakView}; +use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; +use ui::{IconName, SharedString, WindowContext}; +use workspace::Workspace; + +pub(crate) struct SelectionCommand; + +impl SlashCommand for SelectionCommand { + fn name(&self) -> String { + "selection".into() + } + + fn label(&self, _cx: &AppContext) -> CodeLabel { + CodeLabel::plain(self.name(), None) + } + + fn description(&self) -> String { + "Insert editor selection".into() + } + + fn icon(&self) -> IconName { + IconName::Quote + } + + fn menu_text(&self) -> String { + self.description() + } + + fn requires_argument(&self) -> bool { + false + } + + fn accepts_arguments(&self) -> bool { + true + } + + fn complete_argument( + self: Arc, + _arguments: &[String], + _cancel: Arc, + _workspace: Option>, + _cx: &mut WindowContext, + ) -> Task>> { + Task::ready(Err(anyhow!("this command does not require argument"))) + } + + fn run( + self: Arc, + _arguments: &[String], + _context_slash_command_output_sections: &[SlashCommandOutputSection], + _context_buffer: BufferSnapshot, + workspace: WeakView, + _delegate: Option>, + cx: &mut WindowContext, + ) -> Task { + let mut events = vec![]; + + let Some(creases) = workspace + .update(cx, selections_creases) + .unwrap_or_else(|e| { + events.push(Err(e)); + None + }) + else { + return Task::ready(Err(anyhow!("no active selection"))); + }; + + for (text, title) in creases { + events.push(Ok(SlashCommandEvent::StartSection { + icon: IconName::TextSnippet, + label: SharedString::from(title), + metadata: None, + })); + events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text { + text, + run_commands_in_text: false, + }))); + events.push(Ok(SlashCommandEvent::EndSection { metadata: None })); + events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text { + text: "\n".to_string(), + run_commands_in_text: false, + }))); + } + + let result = futures::stream::iter(events).boxed(); + + Task::ready(Ok(result)) + } +} diff --git a/docs/src/assistant/commands.md b/docs/src/assistant/commands.md index fa41df6775715ec4f02128b81ba5a988157c0aac..f9fe9163d47ab6c5c3269bb6b3264e441c5a8315 100644 --- a/docs/src/assistant/commands.md +++ b/docs/src/assistant/commands.md @@ -13,6 +13,7 @@ Slash commands enhance the assistant's capabilities. Begin by typing a `/` at th - `/symbols`: Inserts the current tab's active symbols into the context - `/tab`: Inserts the content of the active tab or all open tabs into the context - `/terminal`: Inserts a select number of lines of output from the terminal +- `/selection`: Inserts the selected text into the context ### Other Commands: @@ -95,6 +96,14 @@ Usage: `/terminal []` - ``: Optional parameter to specify the number of lines to insert (default is a 50). +## `/selection` + +The `/selection` command inserts the selected text in the editor into the context. This is useful for referencing specific parts of your code. + +This is equivalent to the `assistant: quote selection` command ({#kb assistant::QuoteSelection}). See [Interacting with the Assistant](./assistant-panel.md#interacting-with-the-assistant)). + +Usage: `/selection` + ## `/workflow` The `/workflow` command inserts a prompt that opts into the edit workflow. This sets up the context for the assistant to suggest edits to your code.