@@ -12,11 +12,11 @@ use crate::{
1212 slash_command_picker,
1313 terminal_inline_assistant:: TerminalInlineAssistant ,
1414 Assist , CacheStatus , ConfirmCommand , Content , Context , ContextEvent , ContextId , ContextStore ,
15- ContextStoreEvent , CycleMessageRole , DeployHistory , DeployPromptLibrary , InlineAssistId ,
16- InlineAssistant , InsertDraggedFiles , InsertIntoEditor , Message , MessageId , MessageMetadata ,
17- MessageStatus , ModelPickerDelegate , ModelSelector , NewContext , PendingSlashCommand ,
18- PendingSlashCommandStatus , QuoteSelection , RemoteContextMetadata , SavedContextMetadata , Split ,
19- ToggleFocus , ToggleModelSelector , WorkflowStepResolution ,
15+ ContextStoreEvent , CopyCode , CycleMessageRole , DeployHistory , DeployPromptLibrary ,
16+ InlineAssistId , InlineAssistant , InsertDraggedFiles , InsertIntoEditor , Message , MessageId ,
17+ MessageMetadata , MessageStatus , ModelPickerDelegate , ModelSelector , NewContext ,
18+ PendingSlashCommand , PendingSlashCommandStatus , QuoteSelection , RemoteContextMetadata ,
19+ SavedContextMetadata , Split , ToggleFocus , ToggleModelSelector , WorkflowStepResolution ,
2020} ;
2121use anyhow:: { anyhow, Result } ;
2222use assistant_slash_command:: { SlashCommand , SlashCommandOutputSection } ;
@@ -45,7 +45,8 @@ use gpui::{
4545} ;
4646use indexed_docs:: IndexedDocsStore ;
4747use language:: {
48- language_settings:: SoftWrap , Capability , LanguageRegistry , LspAdapterDelegate , Point , ToOffset ,
48+ language_settings:: SoftWrap , BufferSnapshot , Capability , LanguageRegistry , LspAdapterDelegate ,
49+ ToOffset ,
4950} ;
5051use language_model:: {
5152 provider:: cloud:: PROVIDER_ID , LanguageModelProvider , LanguageModelProviderId ,
@@ -56,6 +57,7 @@ use multi_buffer::MultiBufferRow;
5657use picker:: { Picker , PickerDelegate } ;
5758use project:: lsp_store:: LocalLspAdapterDelegate ;
5859use project:: { Project , Worktree } ;
60+ use rope:: Point ;
5961use search:: { buffer_search:: DivRegistrar , BufferSearchBar } ;
6062use serde:: { Deserialize , Serialize } ;
6163use settings:: { update_settings_file, Settings } ;
@@ -81,9 +83,10 @@ use util::{maybe, ResultExt};
8183use workspace:: {
8284 dock:: { DockPosition , Panel , PanelEvent } ,
8385 item:: { self , FollowableItem , Item , ItemHandle } ,
86+ notifications:: NotificationId ,
8487 pane:: { self , SaveIntent } ,
8588 searchable:: { SearchEvent , SearchableItem } ,
86- DraggedSelection , Pane , Save , ShowConfiguration , ToggleZoom , ToolbarItemEvent ,
89+ DraggedSelection , Pane , Save , ShowConfiguration , Toast , ToggleZoom , ToolbarItemEvent ,
8790 ToolbarItemLocation , ToolbarItemView , Workspace ,
8891} ;
8992use workspace:: { searchable:: SearchableItemHandle , DraggedTab } ;
@@ -105,6 +108,7 @@ pub fn init(cx: &mut AppContext) {
105108 . register_action ( AssistantPanel :: inline_assist)
106109 . register_action ( ContextEditor :: quote_selection)
107110 . register_action ( ContextEditor :: insert_selection)
111+ . register_action ( ContextEditor :: copy_code)
108112 . register_action ( ContextEditor :: insert_dragged_files)
109113 . register_action ( AssistantPanel :: show_configuration)
110114 . register_action ( AssistantPanel :: create_new_context) ;
@@ -3100,6 +3104,40 @@ impl ContextEditor {
31003104 } ) ;
31013105 }
31023106
3107+ /// Returns either the selected text, or the content of the Markdown code
3108+ /// block surrounding the cursor.
3109+ fn get_selection_or_code_block (
3110+ context_editor_view : & View < ContextEditor > ,
3111+ cx : & mut ViewContext < Workspace > ,
3112+ ) -> Option < ( String , bool ) > {
3113+ let context_editor = context_editor_view. read ( cx) . editor . read ( cx) ;
3114+
3115+ if context_editor. selections . newest :: < Point > ( cx) . is_empty ( ) {
3116+ let snapshot = context_editor. buffer ( ) . read ( cx) . snapshot ( cx) ;
3117+ let ( _, _, snapshot) = snapshot. as_singleton ( ) ?;
3118+
3119+ let head = context_editor. selections . newest :: < Point > ( cx) . head ( ) ;
3120+ let offset = snapshot. point_to_offset ( head) ;
3121+
3122+ let surrounding_code_block_range = find_surrounding_code_block ( snapshot, offset) ?;
3123+ let text = snapshot
3124+ . text_for_range ( surrounding_code_block_range)
3125+ . collect :: < String > ( ) ;
3126+
3127+ ( !text. is_empty ( ) ) . then_some ( ( text, true ) )
3128+ } else {
3129+ let anchor = context_editor. selections . newest_anchor ( ) ;
3130+ let text = context_editor
3131+ . buffer ( )
3132+ . read ( cx)
3133+ . read ( cx)
3134+ . text_for_range ( anchor. range ( ) )
3135+ . collect :: < String > ( ) ;
3136+
3137+ ( !text. is_empty ( ) ) . then_some ( ( text, false ) )
3138+ }
3139+ }
3140+
31033141 fn insert_selection (
31043142 workspace : & mut Workspace ,
31053143 _: & InsertIntoEditor ,
@@ -3118,24 +3156,44 @@ impl ContextEditor {
31183156 return ;
31193157 } ;
31203158
3121- let context_editor = context_editor_view. read ( cx) . editor . read ( cx) ;
3122- let anchor = context_editor. selections . newest_anchor ( ) ;
3123- let text = context_editor
3124- . buffer ( )
3125- . read ( cx)
3126- . read ( cx)
3127- . text_for_range ( anchor. range ( ) )
3128- . collect :: < String > ( ) ;
3129-
3130- // If nothing is selected, don't delete the current selection; instead, be a no-op.
3131- if !text. is_empty ( ) {
3159+ if let Some ( ( text, _) ) = Self :: get_selection_or_code_block ( & context_editor_view, cx) {
31323160 active_editor_view. update ( cx, |editor, cx| {
31333161 editor. insert ( & text, cx) ;
31343162 editor. focus ( cx) ;
31353163 } )
31363164 }
31373165 }
31383166
3167+ fn copy_code ( workspace : & mut Workspace , _: & CopyCode , cx : & mut ViewContext < Workspace > ) {
3168+ let result = maybe ! ( {
3169+ let panel = workspace. panel:: <AssistantPanel >( cx) ?;
3170+ let context_editor_view = panel. read( cx) . active_context_editor( cx) ?;
3171+ Self :: get_selection_or_code_block( & context_editor_view, cx)
3172+ } ) ;
3173+ let Some ( ( text, is_code_block) ) = result else {
3174+ return ;
3175+ } ;
3176+
3177+ cx. write_to_clipboard ( ClipboardItem :: new_string ( text) ) ;
3178+
3179+ struct CopyToClipboardToast ;
3180+ workspace. show_toast (
3181+ Toast :: new (
3182+ NotificationId :: unique :: < CopyToClipboardToast > ( ) ,
3183+ format ! (
3184+ "{} copied to clipboard." ,
3185+ if is_code_block {
3186+ "Code block"
3187+ } else {
3188+ "Selection"
3189+ }
3190+ ) ,
3191+ )
3192+ . autohide ( ) ,
3193+ cx,
3194+ ) ;
3195+ }
3196+
31393197 fn insert_dragged_files (
31403198 workspace : & mut Workspace ,
31413199 action : & InsertDraggedFiles ,
@@ -4215,6 +4273,48 @@ impl ContextEditor {
42154273 }
42164274}
42174275
4276+ /// Returns the contents of the *outermost* fenced code block that contains the given offset.
4277+ fn find_surrounding_code_block ( snapshot : & BufferSnapshot , offset : usize ) -> Option < Range < usize > > {
4278+ const CODE_BLOCK_NODE : & ' static str = "fenced_code_block" ;
4279+ const CODE_BLOCK_CONTENT : & ' static str = "code_fence_content" ;
4280+
4281+ let layer = snapshot. syntax_layers ( ) . next ( ) ?;
4282+
4283+ let root_node = layer. node ( ) ;
4284+ let mut cursor = root_node. walk ( ) ;
4285+
4286+ // Go to the first child for the given offset
4287+ while cursor. goto_first_child_for_byte ( offset) . is_some ( ) {
4288+ // If we're at the end of the node, go to the next one.
4289+ // Example: if you have a fenced-code-block, and you're on the start of the line
4290+ // right after the closing ```, you want to skip the fenced-code-block and
4291+ // go to the next sibling.
4292+ if cursor. node ( ) . end_byte ( ) == offset {
4293+ cursor. goto_next_sibling ( ) ;
4294+ }
4295+
4296+ if cursor. node ( ) . start_byte ( ) > offset {
4297+ break ;
4298+ }
4299+
4300+ // We found the fenced code block.
4301+ if cursor. node ( ) . kind ( ) == CODE_BLOCK_NODE {
4302+ // Now we need to find the child node that contains the code.
4303+ cursor. goto_first_child ( ) ;
4304+ loop {
4305+ if cursor. node ( ) . kind ( ) == CODE_BLOCK_CONTENT {
4306+ return Some ( cursor. node ( ) . byte_range ( ) ) ;
4307+ }
4308+ if !cursor. goto_next_sibling ( ) {
4309+ break ;
4310+ }
4311+ }
4312+ }
4313+ }
4314+
4315+ None
4316+ }
4317+
42184318fn render_fold_icon_button (
42194319 editor : WeakView < Editor > ,
42204320 icon : IconName ,
@@ -5497,3 +5597,85 @@ fn configuration_error(cx: &AppContext) -> Option<ConfigurationError> {
54975597
54985598 None
54995599}
5600+
5601+ #[ cfg( test) ]
5602+ mod tests {
5603+ use super :: * ;
5604+ use gpui:: { AppContext , Context } ;
5605+ use language:: Buffer ;
5606+ use unindent:: Unindent ;
5607+
5608+ #[ gpui:: test]
5609+ fn test_find_code_blocks ( cx : & mut AppContext ) {
5610+ let markdown = languages:: language ( "markdown" , tree_sitter_md:: LANGUAGE . into ( ) ) ;
5611+
5612+ let buffer = cx. new_model ( |cx| {
5613+ let text = r#"
5614+ line 0
5615+ line 1
5616+ ```rust
5617+ fn main() {}
5618+ ```
5619+ line 5
5620+ line 6
5621+ line 7
5622+ ```go
5623+ func main() {}
5624+ ```
5625+ line 11
5626+ ```
5627+ this is plain text code block
5628+ ```
5629+
5630+ ```go
5631+ func another() {}
5632+ ```
5633+ line 19
5634+ "#
5635+ . unindent ( ) ;
5636+ let mut buffer = Buffer :: local ( text, cx) ;
5637+ buffer. set_language ( Some ( markdown. clone ( ) ) , cx) ;
5638+ buffer
5639+ } ) ;
5640+ let snapshot = buffer. read ( cx) . snapshot ( ) ;
5641+
5642+ let code_blocks = vec ! [
5643+ Point :: new( 3 , 0 ) ..Point :: new( 4 , 0 ) ,
5644+ Point :: new( 9 , 0 ) ..Point :: new( 10 , 0 ) ,
5645+ Point :: new( 13 , 0 ) ..Point :: new( 14 , 0 ) ,
5646+ Point :: new( 17 , 0 ) ..Point :: new( 18 , 0 ) ,
5647+ ]
5648+ . into_iter ( )
5649+ . map ( |range| snapshot. point_to_offset ( range. start ) ..snapshot. point_to_offset ( range. end ) )
5650+ . collect :: < Vec < _ > > ( ) ;
5651+
5652+ let expected_results = vec ! [
5653+ ( 0 , None ) ,
5654+ ( 1 , None ) ,
5655+ ( 2 , Some ( code_blocks[ 0 ] . clone( ) ) ) ,
5656+ ( 3 , Some ( code_blocks[ 0 ] . clone( ) ) ) ,
5657+ ( 4 , Some ( code_blocks[ 0 ] . clone( ) ) ) ,
5658+ ( 5 , None ) ,
5659+ ( 6 , None ) ,
5660+ ( 7 , None ) ,
5661+ ( 8 , Some ( code_blocks[ 1 ] . clone( ) ) ) ,
5662+ ( 9 , Some ( code_blocks[ 1 ] . clone( ) ) ) ,
5663+ ( 10 , Some ( code_blocks[ 1 ] . clone( ) ) ) ,
5664+ ( 11 , None ) ,
5665+ ( 12 , Some ( code_blocks[ 2 ] . clone( ) ) ) ,
5666+ ( 13 , Some ( code_blocks[ 2 ] . clone( ) ) ) ,
5667+ ( 14 , Some ( code_blocks[ 2 ] . clone( ) ) ) ,
5668+ ( 15 , None ) ,
5669+ ( 16 , Some ( code_blocks[ 3 ] . clone( ) ) ) ,
5670+ ( 17 , Some ( code_blocks[ 3 ] . clone( ) ) ) ,
5671+ ( 18 , Some ( code_blocks[ 3 ] . clone( ) ) ) ,
5672+ ( 19 , None ) ,
5673+ ] ;
5674+
5675+ for ( row, expected) in expected_results {
5676+ let offset = snapshot. point_to_offset ( Point :: new ( row, 0 ) ) ;
5677+ let range = find_surrounding_code_block ( & snapshot, offset) ;
5678+ assert_eq ! ( range, expected, "unexpected result on row {:?}" , row) ;
5679+ }
5680+ }
5681+ }
0 commit comments