@@ -12,11 +12,11 @@ use crate::{
12
12
slash_command_picker,
13
13
terminal_inline_assistant:: TerminalInlineAssistant ,
14
14
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 ,
20
20
} ;
21
21
use anyhow:: { anyhow, Result } ;
22
22
use assistant_slash_command:: { SlashCommand , SlashCommandOutputSection } ;
@@ -45,7 +45,8 @@ use gpui::{
45
45
} ;
46
46
use indexed_docs:: IndexedDocsStore ;
47
47
use language:: {
48
- language_settings:: SoftWrap , Capability , LanguageRegistry , LspAdapterDelegate , Point , ToOffset ,
48
+ language_settings:: SoftWrap , BufferSnapshot , Capability , LanguageRegistry , LspAdapterDelegate ,
49
+ ToOffset ,
49
50
} ;
50
51
use language_model:: {
51
52
provider:: cloud:: PROVIDER_ID , LanguageModelProvider , LanguageModelProviderId ,
@@ -56,6 +57,7 @@ use multi_buffer::MultiBufferRow;
56
57
use picker:: { Picker , PickerDelegate } ;
57
58
use project:: lsp_store:: LocalLspAdapterDelegate ;
58
59
use project:: { Project , Worktree } ;
60
+ use rope:: Point ;
59
61
use search:: { buffer_search:: DivRegistrar , BufferSearchBar } ;
60
62
use serde:: { Deserialize , Serialize } ;
61
63
use settings:: { update_settings_file, Settings } ;
@@ -81,9 +83,10 @@ use util::{maybe, ResultExt};
81
83
use workspace:: {
82
84
dock:: { DockPosition , Panel , PanelEvent } ,
83
85
item:: { self , FollowableItem , Item , ItemHandle } ,
86
+ notifications:: NotificationId ,
84
87
pane:: { self , SaveIntent } ,
85
88
searchable:: { SearchEvent , SearchableItem } ,
86
- DraggedSelection , Pane , Save , ShowConfiguration , ToggleZoom , ToolbarItemEvent ,
89
+ DraggedSelection , Pane , Save , ShowConfiguration , Toast , ToggleZoom , ToolbarItemEvent ,
87
90
ToolbarItemLocation , ToolbarItemView , Workspace ,
88
91
} ;
89
92
use workspace:: { searchable:: SearchableItemHandle , DraggedTab } ;
@@ -105,6 +108,7 @@ pub fn init(cx: &mut AppContext) {
105
108
. register_action ( AssistantPanel :: inline_assist)
106
109
. register_action ( ContextEditor :: quote_selection)
107
110
. register_action ( ContextEditor :: insert_selection)
111
+ . register_action ( ContextEditor :: copy_code)
108
112
. register_action ( ContextEditor :: insert_dragged_files)
109
113
. register_action ( AssistantPanel :: show_configuration)
110
114
. register_action ( AssistantPanel :: create_new_context) ;
@@ -3100,6 +3104,40 @@ impl ContextEditor {
3100
3104
} ) ;
3101
3105
}
3102
3106
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
+
3103
3141
fn insert_selection (
3104
3142
workspace : & mut Workspace ,
3105
3143
_: & InsertIntoEditor ,
@@ -3118,24 +3156,44 @@ impl ContextEditor {
3118
3156
return ;
3119
3157
} ;
3120
3158
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) {
3132
3160
active_editor_view. update ( cx, |editor, cx| {
3133
3161
editor. insert ( & text, cx) ;
3134
3162
editor. focus ( cx) ;
3135
3163
} )
3136
3164
}
3137
3165
}
3138
3166
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
+
3139
3197
fn insert_dragged_files (
3140
3198
workspace : & mut Workspace ,
3141
3199
action : & InsertDraggedFiles ,
@@ -4215,6 +4273,48 @@ impl ContextEditor {
4215
4273
}
4216
4274
}
4217
4275
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
+
4218
4318
fn render_fold_icon_button (
4219
4319
editor : WeakView < Editor > ,
4220
4320
icon : IconName ,
@@ -5497,3 +5597,85 @@ fn configuration_error(cx: &AppContext) -> Option<ConfigurationError> {
5497
5597
5498
5598
None
5499
5599
}
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