@@ -12,11 +12,11 @@ use crate::{
slash_command_picker,
terminal_inline_assistant::TerminalInlineAssistant,
Assist, CacheStatus, ConfirmCommand, Content, Context, ContextEvent, ContextId, ContextStore,
- ContextStoreEvent, CycleMessageRole, DeployHistory, DeployPromptLibrary, InlineAssistId,
- InlineAssistant, InsertDraggedFiles, InsertIntoEditor, Message, MessageId, MessageMetadata,
- MessageStatus, ModelPickerDelegate, ModelSelector, NewContext, PendingSlashCommand,
- PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, SavedContextMetadata, Split,
- ToggleFocus, ToggleModelSelector, WorkflowStepResolution,
+ ContextStoreEvent, CopyCode, CycleMessageRole, DeployHistory, DeployPromptLibrary,
+ InlineAssistId, InlineAssistant, InsertDraggedFiles, InsertIntoEditor, Message, MessageId,
+ MessageMetadata, MessageStatus, ModelPickerDelegate, ModelSelector, NewContext,
+ PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
+ SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, WorkflowStepResolution,
};
use anyhow::{anyhow, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
@@ -45,7 +45,8 @@ use gpui::{
};
use indexed_docs::IndexedDocsStore;
use language::{
- language_settings::SoftWrap, Capability, LanguageRegistry, LspAdapterDelegate, Point, ToOffset,
+ language_settings::SoftWrap, BufferSnapshot, Capability, LanguageRegistry, LspAdapterDelegate,
+ ToOffset,
};
use language_model::{
provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId,
@@ -56,6 +57,7 @@ use multi_buffer::MultiBufferRow;
use picker::{Picker, PickerDelegate};
use project::lsp_store::LocalLspAdapterDelegate;
use project::{Project, Worktree};
+use rope::Point;
use search::{buffer_search::DivRegistrar, BufferSearchBar};
use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings};
@@ -81,9 +83,10 @@ use util::{maybe, ResultExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
item::{self, FollowableItem, Item, ItemHandle},
+ notifications::NotificationId,
pane::{self, SaveIntent},
searchable::{SearchEvent, SearchableItem},
- DraggedSelection, Pane, Save, ShowConfiguration, ToggleZoom, ToolbarItemEvent,
+ DraggedSelection, Pane, Save, ShowConfiguration, Toast, ToggleZoom, ToolbarItemEvent,
ToolbarItemLocation, ToolbarItemView, Workspace,
};
use workspace::{searchable::SearchableItemHandle, DraggedTab};
@@ -105,6 +108,7 @@ pub fn init(cx: &mut AppContext) {
.register_action(AssistantPanel::inline_assist)
.register_action(ContextEditor::quote_selection)
.register_action(ContextEditor::insert_selection)
+ .register_action(ContextEditor::copy_code)
.register_action(ContextEditor::insert_dragged_files)
.register_action(AssistantPanel::show_configuration)
.register_action(AssistantPanel::create_new_context);
@@ -3100,6 +3104,40 @@ impl ContextEditor {
});
}
+ /// Returns either the selected text, or the content of the Markdown code
+ /// block surrounding the cursor.
+ fn get_selection_or_code_block(
+ context_editor_view: &View<ContextEditor>,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<(String, bool)> {
+ let context_editor = context_editor_view.read(cx).editor.read(cx);
+
+ if context_editor.selections.newest::<Point>(cx).is_empty() {
+ let snapshot = context_editor.buffer().read(cx).snapshot(cx);
+ let (_, _, snapshot) = snapshot.as_singleton()?;
+
+ let head = context_editor.selections.newest::<Point>(cx).head();
+ let offset = snapshot.point_to_offset(head);
+
+ let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
+ let text = snapshot
+ .text_for_range(surrounding_code_block_range)
+ .collect::<String>();
+
+ (!text.is_empty()).then_some((text, true))
+ } else {
+ let anchor = context_editor.selections.newest_anchor();
+ let text = context_editor
+ .buffer()
+ .read(cx)
+ .read(cx)
+ .text_for_range(anchor.range())
+ .collect::<String>();
+
+ (!text.is_empty()).then_some((text, false))
+ }
+ }
+
fn insert_selection(
workspace: &mut Workspace,
_: &InsertIntoEditor,
@@ -3118,17 +3156,7 @@ impl ContextEditor {
return;
};
- let context_editor = context_editor_view.read(cx).editor.read(cx);
- let anchor = context_editor.selections.newest_anchor();
- let text = context_editor
- .buffer()
- .read(cx)
- .read(cx)
- .text_for_range(anchor.range())
- .collect::<String>();
-
- // If nothing is selected, don't delete the current selection; instead, be a no-op.
- if !text.is_empty() {
+ if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) {
active_editor_view.update(cx, |editor, cx| {
editor.insert(&text, cx);
editor.focus(cx);
@@ -3136,6 +3164,36 @@ impl ContextEditor {
}
}
+ fn copy_code(workspace: &mut Workspace, _: &CopyCode, cx: &mut ViewContext<Workspace>) {
+ let result = maybe!({
+ let panel = workspace.panel::<AssistantPanel>(cx)?;
+ let context_editor_view = panel.read(cx).active_context_editor(cx)?;
+ Self::get_selection_or_code_block(&context_editor_view, cx)
+ });
+ let Some((text, is_code_block)) = result else {
+ return;
+ };
+
+ cx.write_to_clipboard(ClipboardItem::new_string(text));
+
+ struct CopyToClipboardToast;
+ workspace.show_toast(
+ Toast::new(
+ NotificationId::unique::<CopyToClipboardToast>(),
+ format!(
+ "{} copied to clipboard.",
+ if is_code_block {
+ "Code block"
+ } else {
+ "Selection"
+ }
+ ),
+ )
+ .autohide(),
+ cx,
+ );
+ }
+
fn insert_dragged_files(
workspace: &mut Workspace,
action: &InsertDraggedFiles,
@@ -4215,6 +4273,48 @@ impl ContextEditor {
}
}
+/// Returns the contents of the *outermost* fenced code block that contains the given offset.
+fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option<Range<usize>> {
+ const CODE_BLOCK_NODE: &'static str = "fenced_code_block";
+ const CODE_BLOCK_CONTENT: &'static str = "code_fence_content";
+
+ let layer = snapshot.syntax_layers().next()?;
+
+ let root_node = layer.node();
+ let mut cursor = root_node.walk();
+
+ // Go to the first child for the given offset
+ while cursor.goto_first_child_for_byte(offset).is_some() {
+ // If we're at the end of the node, go to the next one.
+ // Example: if you have a fenced-code-block, and you're on the start of the line
+ // right after the closing ```, you want to skip the fenced-code-block and
+ // go to the next sibling.
+ if cursor.node().end_byte() == offset {
+ cursor.goto_next_sibling();
+ }
+
+ if cursor.node().start_byte() > offset {
+ break;
+ }
+
+ // We found the fenced code block.
+ if cursor.node().kind() == CODE_BLOCK_NODE {
+ // Now we need to find the child node that contains the code.
+ cursor.goto_first_child();
+ loop {
+ if cursor.node().kind() == CODE_BLOCK_CONTENT {
+ return Some(cursor.node().byte_range());
+ }
+ if !cursor.goto_next_sibling() {
+ break;
+ }
+ }
+ }
+ }
+
+ None
+}
+
fn render_fold_icon_button(
editor: WeakView<Editor>,
icon: IconName,
@@ -5497,3 +5597,85 @@ fn configuration_error(cx: &AppContext) -> Option<ConfigurationError> {
None
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::{AppContext, Context};
+ use language::Buffer;
+ use unindent::Unindent;
+
+ #[gpui::test]
+ fn test_find_code_blocks(cx: &mut AppContext) {
+ let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
+
+ let buffer = cx.new_model(|cx| {
+ let text = r#"
+ line 0
+ line 1
+ ```rust
+ fn main() {}
+ ```
+ line 5
+ line 6
+ line 7
+ ```go
+ func main() {}
+ ```
+ line 11
+ ```
+ this is plain text code block
+ ```
+
+ ```go
+ func another() {}
+ ```
+ line 19
+ "#
+ .unindent();
+ let mut buffer = Buffer::local(text, cx);
+ buffer.set_language(Some(markdown.clone()), cx);
+ buffer
+ });
+ let snapshot = buffer.read(cx).snapshot();
+
+ let code_blocks = vec![
+ Point::new(3, 0)..Point::new(4, 0),
+ Point::new(9, 0)..Point::new(10, 0),
+ Point::new(13, 0)..Point::new(14, 0),
+ Point::new(17, 0)..Point::new(18, 0),
+ ]
+ .into_iter()
+ .map(|range| snapshot.point_to_offset(range.start)..snapshot.point_to_offset(range.end))
+ .collect::<Vec<_>>();
+
+ let expected_results = vec![
+ (0, None),
+ (1, None),
+ (2, Some(code_blocks[0].clone())),
+ (3, Some(code_blocks[0].clone())),
+ (4, Some(code_blocks[0].clone())),
+ (5, None),
+ (6, None),
+ (7, None),
+ (8, Some(code_blocks[1].clone())),
+ (9, Some(code_blocks[1].clone())),
+ (10, Some(code_blocks[1].clone())),
+ (11, None),
+ (12, Some(code_blocks[2].clone())),
+ (13, Some(code_blocks[2].clone())),
+ (14, Some(code_blocks[2].clone())),
+ (15, None),
+ (16, Some(code_blocks[3].clone())),
+ (17, Some(code_blocks[3].clone())),
+ (18, Some(code_blocks[3].clone())),
+ (19, None),
+ ];
+
+ for (row, expected) in expected_results {
+ let offset = snapshot.point_to_offset(Point::new(row, 0));
+ let range = find_surrounding_code_block(&snapshot, offset);
+ assert_eq!(range, expected, "unexpected result on row {:?}", row);
+ }
+ }
+}