1use crate::{
2 agent_panel::AgentType,
3 language_model_selector::{LanguageModelSelector, language_model_selector},
4 ui::{BurnModeTooltip, ModelSelectorTooltip},
5};
6use agent_settings::CompletionMode;
7use anyhow::Result;
8use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
9use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
10use client::{proto, zed_urls};
11use collections::{BTreeSet, HashMap, HashSet, hash_map};
12use editor::{
13 Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferOffset,
14 MultiBufferSnapshot, RowExt, ToOffset as _, ToPoint as _,
15 actions::{MoveToEndOfLine, Newline, SendReviewToAgent, ShowCompletions},
16 display_map::{
17 BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
18 RenderBlock, ToDisplayPoint,
19 },
20 scroll::ScrollOffset,
21};
22use editor::{FoldPlaceholder, display_map::CreaseId};
23use fs::Fs;
24use futures::FutureExt;
25use gpui::{
26 Action, Animation, AnimationExt, AnyElement, App, ClipboardEntry, ClipboardItem, Empty, Entity,
27 EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement, IntoElement,
28 ParentElement, Pixels, Render, RenderImage, SharedString, Size, StatefulInteractiveElement,
29 Styled, Subscription, Task, WeakEntity, actions, div, img, point, prelude::*,
30 pulsating_between, size,
31};
32use language::{
33 BufferSnapshot, LspAdapterDelegate, ToOffset,
34 language_settings::{SoftWrap, all_language_settings},
35};
36use language_model::{
37 ConfigurationError, IconOrSvg, LanguageModelExt, LanguageModelImage, LanguageModelRegistry,
38 Role,
39};
40use multi_buffer::MultiBufferRow;
41use picker::{Picker, popover_menu::PickerPopoverMenu};
42use project::{Project, Worktree};
43use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate};
44use rope::Point;
45use serde::{Deserialize, Serialize};
46use settings::{
47 LanguageModelProviderSetting, LanguageModelSelection, Settings, SettingsStore,
48 update_settings_file,
49};
50use std::{
51 any::TypeId,
52 cmp,
53 ops::Range,
54 path::{Path, PathBuf},
55 rc::Rc,
56 sync::Arc,
57 time::Duration,
58};
59use text::SelectionGoal;
60use ui::{
61 ButtonLike, CommonAnimationExt, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle,
62 TintColor, Tooltip, prelude::*,
63};
64use util::{ResultExt, maybe};
65use workspace::{
66 CollaboratorId,
67 searchable::{Direction, SearchableItemHandle},
68};
69use workspace::{
70 Save, Toast, Workspace,
71 item::{self, FollowableItem, Item},
72 notifications::NotificationId,
73 pane,
74 searchable::{SearchEvent, SearchableItem},
75};
76use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
77
78use crate::CycleFavoriteModels;
79
80use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
81use assistant_text_thread::{
82 CacheStatus, Content, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
83 MessageMetadata, MessageStatus, PendingSlashCommandStatus, TextThread, TextThreadEvent,
84 TextThreadId, ThoughtProcessOutputSection,
85};
86
87actions!(
88 assistant,
89 [
90 /// Sends the current message to the assistant.
91 Assist,
92 /// Confirms and executes the entered slash command.
93 ConfirmCommand,
94 /// Copies code from the assistant's response to the clipboard.
95 CopyCode,
96 /// Cycles between user and assistant message roles.
97 CycleMessageRole,
98 /// Inserts the selected text into the active editor.
99 InsertIntoEditor,
100 /// Splits the conversation at the current cursor position.
101 Split,
102 ]
103);
104
105/// Inserts files that were dragged and dropped into the assistant conversation.
106#[derive(PartialEq, Clone, Action)]
107#[action(namespace = assistant, no_json, no_register)]
108pub enum InsertDraggedFiles {
109 ProjectPaths(Vec<ProjectPath>),
110 ExternalFiles(Vec<PathBuf>),
111}
112
113#[derive(Copy, Clone, Debug, PartialEq)]
114struct ScrollPosition {
115 offset_before_cursor: gpui::Point<ScrollOffset>,
116 cursor: Anchor,
117}
118
119type MessageHeader = MessageMetadata;
120
121#[derive(Clone)]
122enum AssistError {
123 PaymentRequired,
124 Message(SharedString),
125}
126
127pub enum ThoughtProcessStatus {
128 Pending,
129 Completed,
130}
131
132pub trait AgentPanelDelegate {
133 fn active_text_thread_editor(
134 &self,
135 workspace: &mut Workspace,
136 window: &mut Window,
137 cx: &mut Context<Workspace>,
138 ) -> Option<Entity<TextThreadEditor>>;
139
140 fn open_local_text_thread(
141 &self,
142 workspace: &mut Workspace,
143 path: Arc<Path>,
144 window: &mut Window,
145 cx: &mut Context<Workspace>,
146 ) -> Task<Result<()>>;
147
148 fn open_remote_text_thread(
149 &self,
150 workspace: &mut Workspace,
151 text_thread_id: TextThreadId,
152 window: &mut Window,
153 cx: &mut Context<Workspace>,
154 ) -> Task<Result<Entity<TextThreadEditor>>>;
155
156 fn quote_selection(
157 &self,
158 workspace: &mut Workspace,
159 selection_ranges: Vec<Range<Anchor>>,
160 buffer: Entity<MultiBuffer>,
161 window: &mut Window,
162 cx: &mut Context<Workspace>,
163 );
164}
165
166impl dyn AgentPanelDelegate {
167 /// Returns the global [`AssistantPanelDelegate`], if it exists.
168 pub fn try_global(cx: &App) -> Option<Arc<Self>> {
169 cx.try_global::<GlobalAssistantPanelDelegate>()
170 .map(|global| global.0.clone())
171 }
172
173 /// Sets the global [`AssistantPanelDelegate`].
174 pub fn set_global(delegate: Arc<Self>, cx: &mut App) {
175 cx.set_global(GlobalAssistantPanelDelegate(delegate));
176 }
177}
178
179struct GlobalAssistantPanelDelegate(Arc<dyn AgentPanelDelegate>);
180
181impl Global for GlobalAssistantPanelDelegate {}
182
183pub struct TextThreadEditor {
184 text_thread: Entity<TextThread>,
185 fs: Arc<dyn Fs>,
186 slash_commands: Arc<SlashCommandWorkingSet>,
187 workspace: WeakEntity<Workspace>,
188 project: Entity<Project>,
189 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
190 editor: Entity<Editor>,
191 pending_thought_process: Option<(CreaseId, language::Anchor)>,
192 blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,
193 image_blocks: HashSet<CustomBlockId>,
194 scroll_position: Option<ScrollPosition>,
195 remote_id: Option<workspace::ViewId>,
196 pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
197 invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
198 _subscriptions: Vec<Subscription>,
199 last_error: Option<AssistError>,
200 pub(crate) slash_menu_handle:
201 PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
202 // dragged_file_worktrees is used to keep references to worktrees that were added
203 // when the user drag/dropped an external file onto the context editor. Since
204 // the worktree is not part of the project panel, it would be dropped as soon as
205 // the file is opened. In order to keep the worktree alive for the duration of the
206 // context editor, we keep a reference here.
207 dragged_file_worktrees: Vec<Entity<Worktree>>,
208 language_model_selector: Entity<LanguageModelSelector>,
209 language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
210}
211
212const MAX_TAB_TITLE_LEN: usize = 16;
213
214impl TextThreadEditor {
215 pub fn init(cx: &mut App) {
216 workspace::FollowableViewRegistry::register::<TextThreadEditor>(cx);
217
218 cx.observe_new(
219 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
220 workspace
221 .register_action(TextThreadEditor::quote_selection)
222 .register_action(TextThreadEditor::insert_selection)
223 .register_action(TextThreadEditor::copy_code)
224 .register_action(TextThreadEditor::handle_insert_dragged_files)
225 .register_action(TextThreadEditor::handle_send_review_to_agent);
226 },
227 )
228 .detach();
229 }
230
231 pub fn for_text_thread(
232 text_thread: Entity<TextThread>,
233 fs: Arc<dyn Fs>,
234 workspace: WeakEntity<Workspace>,
235 project: Entity<Project>,
236 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
237 window: &mut Window,
238 cx: &mut Context<Self>,
239 ) -> Self {
240 let completion_provider = SlashCommandCompletionProvider::new(
241 text_thread.read(cx).slash_commands().clone(),
242 Some(cx.entity().downgrade()),
243 Some(workspace.clone()),
244 );
245
246 let editor = cx.new(|cx| {
247 let mut editor =
248 Editor::for_buffer(text_thread.read(cx).buffer().clone(), None, window, cx);
249 editor.disable_scrollbars_and_minimap(window, cx);
250 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
251 editor.set_show_line_numbers(false, cx);
252 editor.set_show_git_diff_gutter(false, cx);
253 editor.set_show_code_actions(false, cx);
254 editor.set_show_runnables(false, cx);
255 editor.set_show_breakpoints(false, cx);
256 editor.set_show_wrap_guides(false, cx);
257 editor.set_show_indent_guides(false, cx);
258 editor.set_completion_provider(Some(Rc::new(completion_provider)));
259 editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::Never);
260 editor.set_collaboration_hub(Box::new(project.clone()));
261
262 let show_edit_predictions = all_language_settings(None, cx)
263 .edit_predictions
264 .enabled_in_text_threads;
265
266 editor.set_show_edit_predictions(Some(show_edit_predictions), window, cx);
267
268 editor
269 });
270
271 let _subscriptions = vec![
272 cx.observe(&text_thread, |_, _, cx| cx.notify()),
273 cx.subscribe_in(&text_thread, window, Self::handle_text_thread_event),
274 cx.subscribe_in(&editor, window, Self::handle_editor_event),
275 cx.subscribe_in(&editor, window, Self::handle_editor_search_event),
276 cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
277 ];
278
279 let slash_command_sections = text_thread
280 .read(cx)
281 .slash_command_output_sections()
282 .to_vec();
283 let thought_process_sections = text_thread
284 .read(cx)
285 .thought_process_output_sections()
286 .to_vec();
287 let slash_commands = text_thread.read(cx).slash_commands().clone();
288 let focus_handle = editor.read(cx).focus_handle(cx);
289
290 let mut this = Self {
291 text_thread,
292 slash_commands,
293 editor,
294 lsp_adapter_delegate,
295 blocks: Default::default(),
296 image_blocks: Default::default(),
297 scroll_position: None,
298 remote_id: None,
299 pending_thought_process: None,
300 fs: fs.clone(),
301 workspace,
302 project,
303 pending_slash_command_creases: HashMap::default(),
304 invoked_slash_command_creases: HashMap::default(),
305 _subscriptions,
306 last_error: None,
307 slash_menu_handle: Default::default(),
308 dragged_file_worktrees: Vec::new(),
309 language_model_selector: cx.new(|cx| {
310 language_model_selector(
311 |cx| LanguageModelRegistry::read_global(cx).default_model(),
312 {
313 let fs = fs.clone();
314 move |model, cx| {
315 update_settings_file(fs.clone(), cx, move |settings, _| {
316 let provider = model.provider_id().0.to_string();
317 let model = model.id().0.to_string();
318 settings.agent.get_or_insert_default().set_model(
319 LanguageModelSelection {
320 provider: LanguageModelProviderSetting(provider),
321 model,
322 },
323 )
324 });
325 }
326 },
327 {
328 let fs = fs.clone();
329 move |model, should_be_favorite, cx| {
330 crate::favorite_models::toggle_in_settings(
331 model,
332 should_be_favorite,
333 fs.clone(),
334 cx,
335 );
336 }
337 },
338 true, // Use popover styles for picker
339 focus_handle,
340 window,
341 cx,
342 )
343 }),
344 language_model_selector_menu_handle: PopoverMenuHandle::default(),
345 };
346 this.update_message_headers(cx);
347 this.update_image_blocks(cx);
348 this.insert_slash_command_output_sections(slash_command_sections, false, window, cx);
349 this.insert_thought_process_output_sections(
350 thought_process_sections
351 .into_iter()
352 .map(|section| (section, ThoughtProcessStatus::Completed)),
353 window,
354 cx,
355 );
356 this
357 }
358
359 fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
360 self.editor.update(cx, |editor, cx| {
361 let show_edit_predictions = all_language_settings(None, cx)
362 .edit_predictions
363 .enabled_in_text_threads;
364
365 editor.set_show_edit_predictions(Some(show_edit_predictions), window, cx);
366 });
367 }
368
369 pub fn text_thread(&self) -> &Entity<TextThread> {
370 &self.text_thread
371 }
372
373 pub fn editor(&self) -> &Entity<Editor> {
374 &self.editor
375 }
376
377 pub fn insert_default_prompt(&mut self, window: &mut Window, cx: &mut Context<Self>) {
378 let command_name = DefaultSlashCommand.name();
379 self.editor.update(cx, |editor, cx| {
380 editor.insert(&format!("/{command_name}\n\n"), window, cx)
381 });
382 let command = self.text_thread.update(cx, |text_thread, cx| {
383 text_thread.reparse(cx);
384 text_thread.parsed_slash_commands()[0].clone()
385 });
386 self.run_command(
387 command.source_range,
388 &command.name,
389 &command.arguments,
390 false,
391 self.workspace.clone(),
392 window,
393 cx,
394 );
395 }
396
397 fn assist(&mut self, _: &Assist, window: &mut Window, cx: &mut Context<Self>) {
398 if self.sending_disabled(cx) {
399 return;
400 }
401 telemetry::event!("Agent Message Sent", agent = "zed-text");
402 self.send_to_model(window, cx);
403 }
404
405 fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
406 self.last_error = None;
407 if let Some(user_message) = self
408 .text_thread
409 .update(cx, |text_thread, cx| text_thread.assist(cx))
410 {
411 let new_selection = {
412 let cursor = user_message
413 .start
414 .to_offset(self.text_thread.read(cx).buffer().read(cx));
415 MultiBufferOffset(cursor)..MultiBufferOffset(cursor)
416 };
417 self.editor.update(cx, |editor, cx| {
418 editor.change_selections(Default::default(), window, cx, |selections| {
419 selections.select_ranges([new_selection])
420 });
421 });
422 // Avoid scrolling to the new cursor position so the assistant's output is stable.
423 cx.defer_in(window, |this, _, _| this.scroll_position = None);
424 }
425
426 cx.notify();
427 }
428
429 fn cancel(
430 &mut self,
431 _: &editor::actions::Cancel,
432 _window: &mut Window,
433 cx: &mut Context<Self>,
434 ) {
435 self.last_error = None;
436
437 if self
438 .text_thread
439 .update(cx, |text_thread, cx| text_thread.cancel_last_assist(cx))
440 {
441 return;
442 }
443
444 cx.propagate();
445 }
446
447 fn cycle_message_role(
448 &mut self,
449 _: &CycleMessageRole,
450 _window: &mut Window,
451 cx: &mut Context<Self>,
452 ) {
453 let cursors = self.cursors(cx);
454 self.text_thread.update(cx, |text_thread, cx| {
455 let messages = text_thread
456 .messages_for_offsets(cursors.into_iter().map(|cursor| cursor.0), cx)
457 .into_iter()
458 .map(|message| message.id)
459 .collect();
460 text_thread.cycle_message_roles(messages, cx)
461 });
462 }
463
464 fn cursors(&self, cx: &mut App) -> Vec<MultiBufferOffset> {
465 let selections = self.editor.update(cx, |editor, cx| {
466 editor
467 .selections
468 .all::<MultiBufferOffset>(&editor.display_snapshot(cx))
469 });
470 selections
471 .into_iter()
472 .map(|selection| selection.head())
473 .collect()
474 }
475
476 pub fn insert_command(&mut self, name: &str, window: &mut Window, cx: &mut Context<Self>) {
477 if let Some(command) = self.slash_commands.command(name, cx) {
478 self.editor.update(cx, |editor, cx| {
479 editor.transact(window, cx, |editor, window, cx| {
480 editor.change_selections(Default::default(), window, cx, |s| s.try_cancel());
481 let snapshot = editor.buffer().read(cx).snapshot(cx);
482 let newest_cursor = editor
483 .selections
484 .newest::<Point>(&editor.display_snapshot(cx))
485 .head();
486 if newest_cursor.column > 0
487 || snapshot
488 .chars_at(newest_cursor)
489 .next()
490 .is_some_and(|ch| ch != '\n')
491 {
492 editor.move_to_end_of_line(
493 &MoveToEndOfLine {
494 stop_at_soft_wraps: false,
495 },
496 window,
497 cx,
498 );
499 editor.newline(&Newline, window, cx);
500 }
501
502 editor.insert(&format!("/{name}"), window, cx);
503 if command.accepts_arguments() {
504 editor.insert(" ", window, cx);
505 editor.show_completions(&ShowCompletions, window, cx);
506 }
507 });
508 });
509 if !command.requires_argument() {
510 self.confirm_command(&ConfirmCommand, window, cx);
511 }
512 }
513 }
514
515 pub fn confirm_command(
516 &mut self,
517 _: &ConfirmCommand,
518 window: &mut Window,
519 cx: &mut Context<Self>,
520 ) {
521 if self.editor.read(cx).has_visible_completions_menu() {
522 return;
523 }
524
525 let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
526 let mut commands_by_range = HashMap::default();
527 let workspace = self.workspace.clone();
528 self.text_thread.update(cx, |text_thread, cx| {
529 text_thread.reparse(cx);
530 for selection in selections.iter() {
531 if let Some(command) =
532 text_thread.pending_command_for_position(selection.head().text_anchor, cx)
533 {
534 commands_by_range
535 .entry(command.source_range.clone())
536 .or_insert_with(|| command.clone());
537 }
538 }
539 });
540
541 if commands_by_range.is_empty() {
542 cx.propagate();
543 } else {
544 for command in commands_by_range.into_values() {
545 self.run_command(
546 command.source_range,
547 &command.name,
548 &command.arguments,
549 true,
550 workspace.clone(),
551 window,
552 cx,
553 );
554 }
555 cx.stop_propagation();
556 }
557 }
558
559 pub fn run_command(
560 &mut self,
561 command_range: Range<language::Anchor>,
562 name: &str,
563 arguments: &[String],
564 ensure_trailing_newline: bool,
565 workspace: WeakEntity<Workspace>,
566 window: &mut Window,
567 cx: &mut Context<Self>,
568 ) {
569 if let Some(command) = self.slash_commands.command(name, cx) {
570 let text_thread = self.text_thread.read(cx);
571 let sections = text_thread
572 .slash_command_output_sections()
573 .iter()
574 .filter(|section| section.is_valid(text_thread.buffer().read(cx)))
575 .cloned()
576 .collect::<Vec<_>>();
577 let snapshot = text_thread.buffer().read(cx).snapshot();
578 let output = command.run(
579 arguments,
580 §ions,
581 snapshot,
582 workspace,
583 self.lsp_adapter_delegate.clone(),
584 window,
585 cx,
586 );
587 self.text_thread.update(cx, |text_thread, cx| {
588 text_thread.insert_command_output(
589 command_range,
590 name,
591 output,
592 ensure_trailing_newline,
593 cx,
594 )
595 });
596 }
597 }
598
599 fn handle_text_thread_event(
600 &mut self,
601 _: &Entity<TextThread>,
602 event: &TextThreadEvent,
603 window: &mut Window,
604 cx: &mut Context<Self>,
605 ) {
606 let text_thread_editor = cx.entity().downgrade();
607
608 match event {
609 TextThreadEvent::MessagesEdited => {
610 self.update_message_headers(cx);
611 self.update_image_blocks(cx);
612 self.text_thread.update(cx, |text_thread, cx| {
613 text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
614 });
615 }
616 TextThreadEvent::SummaryChanged => {
617 cx.emit(EditorEvent::TitleChanged);
618 self.text_thread.update(cx, |text_thread, cx| {
619 text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
620 });
621 }
622 TextThreadEvent::SummaryGenerated => {}
623 TextThreadEvent::PathChanged { .. } => {}
624 TextThreadEvent::StartedThoughtProcess(range) => {
625 let creases = self.insert_thought_process_output_sections(
626 [(
627 ThoughtProcessOutputSection {
628 range: range.clone(),
629 },
630 ThoughtProcessStatus::Pending,
631 )],
632 window,
633 cx,
634 );
635 self.pending_thought_process = Some((creases[0], range.start));
636 }
637 TextThreadEvent::EndedThoughtProcess(end) => {
638 if let Some((crease_id, start)) = self.pending_thought_process.take() {
639 self.editor.update(cx, |editor, cx| {
640 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
641 let start_anchor =
642 multi_buffer_snapshot.as_singleton_anchor(start).unwrap();
643
644 editor.display_map.update(cx, |display_map, cx| {
645 display_map.unfold_intersecting(
646 vec![start_anchor..start_anchor],
647 true,
648 cx,
649 );
650 });
651 editor.remove_creases(vec![crease_id], cx);
652 });
653 self.insert_thought_process_output_sections(
654 [(
655 ThoughtProcessOutputSection { range: start..*end },
656 ThoughtProcessStatus::Completed,
657 )],
658 window,
659 cx,
660 );
661 }
662 }
663 TextThreadEvent::StreamedCompletion => {
664 self.editor.update(cx, |editor, cx| {
665 if let Some(scroll_position) = self.scroll_position {
666 let snapshot = editor.snapshot(window, cx);
667 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
668 let scroll_top =
669 cursor_point.row().as_f64() - scroll_position.offset_before_cursor.y;
670 editor.set_scroll_position(
671 point(scroll_position.offset_before_cursor.x, scroll_top),
672 window,
673 cx,
674 );
675 }
676 });
677 }
678 TextThreadEvent::ParsedSlashCommandsUpdated { removed, updated } => {
679 self.editor.update(cx, |editor, cx| {
680 let buffer = editor.buffer().read(cx).snapshot(cx);
681 let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
682
683 editor.remove_creases(
684 removed
685 .iter()
686 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
687 cx,
688 );
689
690 let crease_ids = editor.insert_creases(
691 updated.iter().map(|command| {
692 let workspace = self.workspace.clone();
693 let confirm_command = Arc::new({
694 let text_thread_editor = text_thread_editor.clone();
695 let command = command.clone();
696 move |window: &mut Window, cx: &mut App| {
697 text_thread_editor
698 .update(cx, |text_thread_editor, cx| {
699 text_thread_editor.run_command(
700 command.source_range.clone(),
701 &command.name,
702 &command.arguments,
703 false,
704 workspace.clone(),
705 window,
706 cx,
707 );
708 })
709 .ok();
710 }
711 });
712 let placeholder = FoldPlaceholder {
713 render: Arc::new(move |_, _, _| Empty.into_any()),
714 ..Default::default()
715 };
716 let render_toggle = {
717 let confirm_command = confirm_command.clone();
718 let command = command.clone();
719 move |row, _, _, _window: &mut Window, _cx: &mut App| {
720 render_pending_slash_command_gutter_decoration(
721 row,
722 &command.status,
723 confirm_command.clone(),
724 )
725 }
726 };
727 let render_trailer = {
728 move |_row, _unfold, _window: &mut Window, _cx: &mut App| {
729 Empty.into_any()
730 }
731 };
732
733 let range = buffer
734 .anchor_range_in_excerpt(excerpt_id, command.source_range.clone())
735 .unwrap();
736 Crease::inline(range, placeholder, render_toggle, render_trailer)
737 }),
738 cx,
739 );
740
741 self.pending_slash_command_creases.extend(
742 updated
743 .iter()
744 .map(|command| command.source_range.clone())
745 .zip(crease_ids),
746 );
747 })
748 }
749 TextThreadEvent::InvokedSlashCommandChanged { command_id } => {
750 self.update_invoked_slash_command(*command_id, window, cx);
751 }
752 TextThreadEvent::SlashCommandOutputSectionAdded { section } => {
753 self.insert_slash_command_output_sections([section.clone()], false, window, cx);
754 }
755 TextThreadEvent::Operation(_) => {}
756 TextThreadEvent::ShowAssistError(error_message) => {
757 self.last_error = Some(AssistError::Message(error_message.clone()));
758 }
759 TextThreadEvent::ShowPaymentRequiredError => {
760 self.last_error = Some(AssistError::PaymentRequired);
761 }
762 }
763 }
764
765 fn update_invoked_slash_command(
766 &mut self,
767 command_id: InvokedSlashCommandId,
768 window: &mut Window,
769 cx: &mut Context<Self>,
770 ) {
771 if let Some(invoked_slash_command) =
772 self.text_thread.read(cx).invoked_slash_command(&command_id)
773 && let InvokedSlashCommandStatus::Finished = invoked_slash_command.status
774 {
775 let run_commands_in_ranges = invoked_slash_command.run_commands_in_ranges.clone();
776 for range in run_commands_in_ranges {
777 let commands = self.text_thread.update(cx, |text_thread, cx| {
778 text_thread.reparse(cx);
779 text_thread
780 .pending_commands_for_range(range.clone(), cx)
781 .to_vec()
782 });
783
784 for command in commands {
785 self.run_command(
786 command.source_range,
787 &command.name,
788 &command.arguments,
789 false,
790 self.workspace.clone(),
791 window,
792 cx,
793 );
794 }
795 }
796 }
797
798 self.editor.update(cx, |editor, cx| {
799 if let Some(invoked_slash_command) =
800 self.text_thread.read(cx).invoked_slash_command(&command_id)
801 {
802 if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
803 let buffer = editor.buffer().read(cx).snapshot(cx);
804 let (&excerpt_id, _buffer_id, _buffer_snapshot) =
805 buffer.as_singleton().unwrap();
806
807 let range = buffer
808 .anchor_range_in_excerpt(excerpt_id, invoked_slash_command.range.clone())
809 .unwrap();
810 editor.remove_folds_with_type(
811 &[range],
812 TypeId::of::<PendingSlashCommand>(),
813 false,
814 cx,
815 );
816
817 editor.remove_creases(
818 HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
819 cx,
820 );
821 } else if let hash_map::Entry::Vacant(entry) =
822 self.invoked_slash_command_creases.entry(command_id)
823 {
824 let buffer = editor.buffer().read(cx).snapshot(cx);
825 let (&excerpt_id, _buffer_id, _buffer_snapshot) =
826 buffer.as_singleton().unwrap();
827 let context = self.text_thread.downgrade();
828 let range = buffer
829 .anchor_range_in_excerpt(excerpt_id, invoked_slash_command.range.clone())
830 .unwrap();
831 let crease = Crease::inline(
832 range,
833 invoked_slash_command_fold_placeholder(command_id, context),
834 fold_toggle("invoked-slash-command"),
835 |_row, _folded, _window, _cx| Empty.into_any(),
836 );
837 let crease_ids = editor.insert_creases([crease.clone()], cx);
838 editor.fold_creases(vec![crease], false, window, cx);
839 entry.insert(crease_ids[0]);
840 } else {
841 cx.notify()
842 }
843 } else {
844 editor.remove_creases(
845 HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
846 cx,
847 );
848 cx.notify();
849 };
850 });
851 }
852
853 fn insert_thought_process_output_sections(
854 &mut self,
855 sections: impl IntoIterator<
856 Item = (
857 ThoughtProcessOutputSection<language::Anchor>,
858 ThoughtProcessStatus,
859 ),
860 >,
861 window: &mut Window,
862 cx: &mut Context<Self>,
863 ) -> Vec<CreaseId> {
864 self.editor.update(cx, |editor, cx| {
865 let buffer = editor.buffer().read(cx).snapshot(cx);
866 let excerpt_id = *buffer.as_singleton().unwrap().0;
867 let mut buffer_rows_to_fold = BTreeSet::new();
868 let mut creases = Vec::new();
869 for (section, status) in sections {
870 let range = buffer
871 .anchor_range_in_excerpt(excerpt_id, section.range)
872 .unwrap();
873 let buffer_row = MultiBufferRow(range.start.to_point(&buffer).row);
874 buffer_rows_to_fold.insert(buffer_row);
875 creases.push(
876 Crease::inline(
877 range,
878 FoldPlaceholder {
879 render: render_thought_process_fold_icon_button(
880 cx.entity().downgrade(),
881 status,
882 ),
883 merge_adjacent: false,
884 ..Default::default()
885 },
886 render_slash_command_output_toggle,
887 |_, _, _, _| Empty.into_any_element(),
888 )
889 .with_metadata(CreaseMetadata {
890 icon_path: SharedString::from(IconName::Ai.path()),
891 label: "Thinking Process".into(),
892 }),
893 );
894 }
895
896 let creases = editor.insert_creases(creases, cx);
897
898 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
899 editor.fold_at(buffer_row, window, cx);
900 }
901
902 creases
903 })
904 }
905
906 fn insert_slash_command_output_sections(
907 &mut self,
908 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
909 expand_result: bool,
910 window: &mut Window,
911 cx: &mut Context<Self>,
912 ) {
913 self.editor.update(cx, |editor, cx| {
914 let buffer = editor.buffer().read(cx).snapshot(cx);
915 let excerpt_id = *buffer.as_singleton().unwrap().0;
916 let mut buffer_rows_to_fold = BTreeSet::new();
917 let mut creases = Vec::new();
918 for section in sections {
919 let range = buffer
920 .anchor_range_in_excerpt(excerpt_id, section.range)
921 .unwrap();
922 let buffer_row = MultiBufferRow(range.start.to_point(&buffer).row);
923 buffer_rows_to_fold.insert(buffer_row);
924 creases.push(
925 Crease::inline(
926 range,
927 FoldPlaceholder {
928 render: render_fold_icon_button(
929 cx.entity().downgrade(),
930 section.icon.path().into(),
931 section.label.clone(),
932 ),
933 merge_adjacent: false,
934 ..Default::default()
935 },
936 render_slash_command_output_toggle,
937 |_, _, _, _| Empty.into_any_element(),
938 )
939 .with_metadata(CreaseMetadata {
940 icon_path: section.icon.path().into(),
941 label: section.label,
942 }),
943 );
944 }
945
946 editor.insert_creases(creases, cx);
947
948 if expand_result {
949 buffer_rows_to_fold.clear();
950 }
951 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
952 editor.fold_at(buffer_row, window, cx);
953 }
954 });
955 }
956
957 fn handle_editor_event(
958 &mut self,
959 _: &Entity<Editor>,
960 event: &EditorEvent,
961 window: &mut Window,
962 cx: &mut Context<Self>,
963 ) {
964 match event {
965 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
966 let cursor_scroll_position = self.cursor_scroll_position(window, cx);
967 if *autoscroll {
968 self.scroll_position = cursor_scroll_position;
969 } else if self.scroll_position != cursor_scroll_position {
970 self.scroll_position = None;
971 }
972 }
973 EditorEvent::SelectionsChanged { .. } => {
974 self.scroll_position = self.cursor_scroll_position(window, cx);
975 }
976 _ => {}
977 }
978 cx.emit(event.clone());
979 }
980
981 fn handle_editor_search_event(
982 &mut self,
983 _: &Entity<Editor>,
984 event: &SearchEvent,
985 _window: &mut Window,
986 cx: &mut Context<Self>,
987 ) {
988 cx.emit(event.clone());
989 }
990
991 fn cursor_scroll_position(
992 &self,
993 window: &mut Window,
994 cx: &mut Context<Self>,
995 ) -> Option<ScrollPosition> {
996 self.editor.update(cx, |editor, cx| {
997 let snapshot = editor.snapshot(window, cx);
998 let cursor = editor.selections.newest_anchor().head();
999 let cursor_row = cursor
1000 .to_display_point(&snapshot.display_snapshot)
1001 .row()
1002 .as_f64();
1003 let scroll_position = editor
1004 .scroll_manager
1005 .anchor()
1006 .scroll_position(&snapshot.display_snapshot);
1007
1008 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
1009 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
1010 Some(ScrollPosition {
1011 cursor,
1012 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
1013 })
1014 } else {
1015 None
1016 }
1017 })
1018 }
1019
1020 fn esc_kbd(cx: &App) -> Div {
1021 let colors = cx.theme().colors().clone();
1022
1023 h_flex()
1024 .items_center()
1025 .gap_1()
1026 .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
1027 .text_size(TextSize::XSmall.rems(cx))
1028 .text_color(colors.text_muted)
1029 .child("Press")
1030 .child(
1031 h_flex()
1032 .rounded_sm()
1033 .px_1()
1034 .mr_0p5()
1035 .border_1()
1036 .border_color(colors.border_variant.alpha(0.6))
1037 .bg(colors.element_background.alpha(0.6))
1038 .child("esc"),
1039 )
1040 .child("to cancel")
1041 }
1042
1043 fn update_message_headers(&mut self, cx: &mut Context<Self>) {
1044 self.editor.update(cx, |editor, cx| {
1045 let buffer = editor.buffer().read(cx).snapshot(cx);
1046
1047 let excerpt_id = *buffer.as_singleton().unwrap().0;
1048 let mut old_blocks = std::mem::take(&mut self.blocks);
1049 let mut blocks_to_remove: HashMap<_, _> = old_blocks
1050 .iter()
1051 .map(|(message_id, (_, block_id))| (*message_id, *block_id))
1052 .collect();
1053 let mut blocks_to_replace: HashMap<_, RenderBlock> = Default::default();
1054
1055 let render_block = |message: MessageMetadata| -> RenderBlock {
1056 Arc::new({
1057 let text_thread = self.text_thread.clone();
1058
1059 move |cx| {
1060 let message_id = MessageId(message.timestamp);
1061 let llm_loading = message.role == Role::Assistant
1062 && message.status == MessageStatus::Pending;
1063
1064 let (label, spinner, note) = match message.role {
1065 Role::User => (
1066 Label::new("You").color(Color::Default).into_any_element(),
1067 None,
1068 None,
1069 ),
1070 Role::Assistant => {
1071 let base_label = Label::new("Agent").color(Color::Info);
1072 let mut spinner = None;
1073 let mut note = None;
1074 let animated_label = if llm_loading {
1075 base_label
1076 .with_animation(
1077 "pulsating-label",
1078 Animation::new(Duration::from_secs(2))
1079 .repeat()
1080 .with_easing(pulsating_between(0.4, 0.8)),
1081 |label, delta| label.alpha(delta),
1082 )
1083 .into_any_element()
1084 } else {
1085 base_label.into_any_element()
1086 };
1087 if llm_loading {
1088 spinner = Some(
1089 Icon::new(IconName::ArrowCircle)
1090 .size(IconSize::XSmall)
1091 .color(Color::Info)
1092 .with_rotate_animation(2)
1093 .into_any_element(),
1094 );
1095 note = Some(Self::esc_kbd(cx).into_any_element());
1096 }
1097 (animated_label, spinner, note)
1098 }
1099 Role::System => (
1100 Label::new("System")
1101 .color(Color::Warning)
1102 .into_any_element(),
1103 None,
1104 None,
1105 ),
1106 };
1107
1108 let sender = h_flex()
1109 .items_center()
1110 .gap_2p5()
1111 .child(
1112 ButtonLike::new("role")
1113 .style(ButtonStyle::Filled)
1114 .child(
1115 h_flex()
1116 .items_center()
1117 .gap_1p5()
1118 .child(label)
1119 .children(spinner),
1120 )
1121 .tooltip(|_window, cx| {
1122 Tooltip::with_meta(
1123 "Toggle message role",
1124 None,
1125 "Available roles: You (User), Agent, System",
1126 cx,
1127 )
1128 })
1129 .on_click({
1130 let text_thread = text_thread.clone();
1131 move |_, _window, cx| {
1132 text_thread.update(cx, |text_thread, cx| {
1133 text_thread.cycle_message_roles(
1134 HashSet::from_iter(Some(message_id)),
1135 cx,
1136 )
1137 })
1138 }
1139 }),
1140 )
1141 .children(note);
1142
1143 h_flex()
1144 .id(("message_header", message_id.as_u64()))
1145 .pl(cx.margins.gutter.full_width())
1146 .h_11()
1147 .w_full()
1148 .relative()
1149 .gap_1p5()
1150 .child(sender)
1151 .children(match &message.cache {
1152 Some(cache) if cache.is_final_anchor => match cache.status {
1153 CacheStatus::Cached => Some(
1154 div()
1155 .id("cached")
1156 .child(
1157 Icon::new(IconName::DatabaseZap)
1158 .size(IconSize::XSmall)
1159 .color(Color::Hint),
1160 )
1161 .tooltip(|_window, cx| {
1162 Tooltip::with_meta(
1163 "Context Cached",
1164 None,
1165 "Large messages cached to optimize performance",
1166 cx,
1167 )
1168 })
1169 .into_any_element(),
1170 ),
1171 CacheStatus::Pending => Some(
1172 div()
1173 .child(
1174 Icon::new(IconName::Ellipsis)
1175 .size(IconSize::XSmall)
1176 .color(Color::Hint),
1177 )
1178 .into_any_element(),
1179 ),
1180 },
1181 _ => None,
1182 })
1183 .children(match &message.status {
1184 MessageStatus::Error(error) => Some(
1185 Button::new("show-error", "Error")
1186 .color(Color::Error)
1187 .selected_label_color(Color::Error)
1188 .selected_icon_color(Color::Error)
1189 .icon(IconName::XCircle)
1190 .icon_color(Color::Error)
1191 .icon_size(IconSize::XSmall)
1192 .icon_position(IconPosition::Start)
1193 .tooltip(Tooltip::text("View Details"))
1194 .on_click({
1195 let text_thread = text_thread.clone();
1196 let error = error.clone();
1197 move |_, _window, cx| {
1198 text_thread.update(cx, |_, cx| {
1199 cx.emit(TextThreadEvent::ShowAssistError(
1200 error.clone(),
1201 ));
1202 });
1203 }
1204 })
1205 .into_any_element(),
1206 ),
1207 MessageStatus::Canceled => Some(
1208 h_flex()
1209 .gap_1()
1210 .items_center()
1211 .child(
1212 Icon::new(IconName::XCircle)
1213 .color(Color::Disabled)
1214 .size(IconSize::XSmall),
1215 )
1216 .child(
1217 Label::new("Canceled")
1218 .size(LabelSize::Small)
1219 .color(Color::Disabled),
1220 )
1221 .into_any_element(),
1222 ),
1223 _ => None,
1224 })
1225 .into_any_element()
1226 }
1227 })
1228 };
1229 let create_block_properties = |message: &Message| BlockProperties {
1230 height: Some(2),
1231 style: BlockStyle::Sticky,
1232 placement: BlockPlacement::Above(
1233 buffer
1234 .anchor_in_excerpt(excerpt_id, message.anchor_range.start)
1235 .unwrap(),
1236 ),
1237 priority: usize::MAX,
1238 render: render_block(MessageMetadata::from(message)),
1239 };
1240 let mut new_blocks = vec![];
1241 let mut block_index_to_message = vec![];
1242 for message in self.text_thread.read(cx).messages(cx) {
1243 if blocks_to_remove.remove(&message.id).is_some() {
1244 // This is an old message that we might modify.
1245 let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
1246 debug_assert!(
1247 false,
1248 "old_blocks should contain a message_id we've just removed."
1249 );
1250 continue;
1251 };
1252 // Should we modify it?
1253 let message_meta = MessageMetadata::from(&message);
1254 if meta != &message_meta {
1255 blocks_to_replace.insert(*block_id, render_block(message_meta.clone()));
1256 *meta = message_meta;
1257 }
1258 } else {
1259 // This is a new message.
1260 new_blocks.push(create_block_properties(&message));
1261 block_index_to_message.push((message.id, MessageMetadata::from(&message)));
1262 }
1263 }
1264 editor.replace_blocks(blocks_to_replace, None, cx);
1265 editor.remove_blocks(blocks_to_remove.into_values().collect(), None, cx);
1266
1267 let ids = editor.insert_blocks(new_blocks, None, cx);
1268 old_blocks.extend(ids.into_iter().zip(block_index_to_message).map(
1269 |(block_id, (message_id, message_meta))| (message_id, (message_meta, block_id)),
1270 ));
1271 self.blocks = old_blocks;
1272 });
1273 }
1274
1275 /// Returns either the selected text, or the content of the Markdown code
1276 /// block surrounding the cursor.
1277 fn get_selection_or_code_block(
1278 context_editor_view: &Entity<TextThreadEditor>,
1279 cx: &mut Context<Workspace>,
1280 ) -> Option<(String, bool)> {
1281 const CODE_FENCE_DELIMITER: &str = "```";
1282
1283 let text_thread_editor = context_editor_view.read(cx).editor.clone();
1284 text_thread_editor.update(cx, |text_thread_editor, cx| {
1285 let display_map = text_thread_editor.display_snapshot(cx);
1286 if text_thread_editor
1287 .selections
1288 .newest::<Point>(&display_map)
1289 .is_empty()
1290 {
1291 let snapshot = text_thread_editor.buffer().read(cx).snapshot(cx);
1292 let (_, _, snapshot) = snapshot.as_singleton()?;
1293
1294 let head = text_thread_editor
1295 .selections
1296 .newest::<Point>(&display_map)
1297 .head();
1298 let offset = snapshot.point_to_offset(head);
1299
1300 let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
1301 let mut text = snapshot
1302 .text_for_range(surrounding_code_block_range)
1303 .collect::<String>();
1304
1305 // If there is no newline trailing the closing three-backticks, then
1306 // tree-sitter-md extends the range of the content node to include
1307 // the backticks.
1308 if text.ends_with(CODE_FENCE_DELIMITER) {
1309 text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
1310 }
1311
1312 (!text.is_empty()).then_some((text, true))
1313 } else {
1314 let selection = text_thread_editor.selections.newest_adjusted(&display_map);
1315 let buffer = text_thread_editor.buffer().read(cx).snapshot(cx);
1316 let selected_text = buffer.text_for_range(selection.range()).collect::<String>();
1317
1318 (!selected_text.is_empty()).then_some((selected_text, false))
1319 }
1320 })
1321 }
1322
1323 pub fn insert_selection(
1324 workspace: &mut Workspace,
1325 _: &InsertIntoEditor,
1326 window: &mut Window,
1327 cx: &mut Context<Workspace>,
1328 ) {
1329 let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
1330 return;
1331 };
1332 let Some(context_editor_view) =
1333 agent_panel_delegate.active_text_thread_editor(workspace, window, cx)
1334 else {
1335 return;
1336 };
1337 let Some(active_editor_view) = workspace
1338 .active_item(cx)
1339 .and_then(|item| item.act_as::<Editor>(cx))
1340 else {
1341 return;
1342 };
1343
1344 if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) {
1345 active_editor_view.update(cx, |editor, cx| {
1346 editor.insert(&text, window, cx);
1347 editor.focus_handle(cx).focus(window, cx);
1348 })
1349 }
1350 }
1351
1352 pub fn copy_code(
1353 workspace: &mut Workspace,
1354 _: &CopyCode,
1355 window: &mut Window,
1356 cx: &mut Context<Workspace>,
1357 ) {
1358 let result = maybe!({
1359 let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
1360 let context_editor_view =
1361 agent_panel_delegate.active_text_thread_editor(workspace, window, cx)?;
1362 Self::get_selection_or_code_block(&context_editor_view, cx)
1363 });
1364 let Some((text, is_code_block)) = result else {
1365 return;
1366 };
1367
1368 cx.write_to_clipboard(ClipboardItem::new_string(text));
1369
1370 struct CopyToClipboardToast;
1371 workspace.show_toast(
1372 Toast::new(
1373 NotificationId::unique::<CopyToClipboardToast>(),
1374 format!(
1375 "{} copied to clipboard.",
1376 if is_code_block {
1377 "Code block"
1378 } else {
1379 "Selection"
1380 }
1381 ),
1382 )
1383 .autohide(),
1384 cx,
1385 );
1386 }
1387
1388 pub fn handle_insert_dragged_files(
1389 workspace: &mut Workspace,
1390 action: &InsertDraggedFiles,
1391 window: &mut Window,
1392 cx: &mut Context<Workspace>,
1393 ) {
1394 let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
1395 return;
1396 };
1397 let Some(context_editor_view) =
1398 agent_panel_delegate.active_text_thread_editor(workspace, window, cx)
1399 else {
1400 return;
1401 };
1402
1403 let project = context_editor_view.read(cx).project.clone();
1404
1405 let paths = match action {
1406 InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])),
1407 InsertDraggedFiles::ExternalFiles(paths) => {
1408 let tasks = paths
1409 .clone()
1410 .into_iter()
1411 .map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx))
1412 .collect::<Vec<_>>();
1413
1414 cx.background_spawn(async move {
1415 let mut paths = vec![];
1416 let mut worktrees = vec![];
1417
1418 let opened_paths = futures::future::join_all(tasks).await;
1419
1420 for entry in opened_paths {
1421 if let Some((worktree, project_path)) = entry.log_err() {
1422 worktrees.push(worktree);
1423 paths.push(project_path);
1424 }
1425 }
1426
1427 (paths, worktrees)
1428 })
1429 }
1430 };
1431
1432 context_editor_view.update(cx, |_, cx| {
1433 cx.spawn_in(window, async move |this, cx| {
1434 let (paths, dragged_file_worktrees) = paths.await;
1435 this.update_in(cx, |this, window, cx| {
1436 this.insert_dragged_files(paths, dragged_file_worktrees, window, cx);
1437 })
1438 .ok();
1439 })
1440 .detach();
1441 })
1442 }
1443
1444 pub fn insert_dragged_files(
1445 &mut self,
1446 opened_paths: Vec<ProjectPath>,
1447 added_worktrees: Vec<Entity<Worktree>>,
1448 window: &mut Window,
1449 cx: &mut Context<Self>,
1450 ) {
1451 let mut file_slash_command_args = vec![];
1452 for project_path in opened_paths.into_iter() {
1453 let Some(worktree) = self
1454 .project
1455 .read(cx)
1456 .worktree_for_id(project_path.worktree_id, cx)
1457 else {
1458 continue;
1459 };
1460 let path_style = worktree.read(cx).path_style();
1461 let full_path = worktree
1462 .read(cx)
1463 .root_name()
1464 .join(&project_path.path)
1465 .display(path_style)
1466 .into_owned();
1467 file_slash_command_args.push(full_path);
1468 }
1469
1470 let cmd_name = FileSlashCommand.name();
1471
1472 let file_argument = file_slash_command_args.join(" ");
1473
1474 self.editor.update(cx, |editor, cx| {
1475 editor.insert("\n", window, cx);
1476 editor.insert(&format!("/{} {}", cmd_name, file_argument), window, cx);
1477 });
1478 self.confirm_command(&ConfirmCommand, window, cx);
1479 self.dragged_file_worktrees.extend(added_worktrees);
1480 }
1481
1482 pub fn quote_selection(
1483 workspace: &mut Workspace,
1484 _: &AddSelectionToThread,
1485 window: &mut Window,
1486 cx: &mut Context<Workspace>,
1487 ) {
1488 let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
1489 return;
1490 };
1491
1492 let Some((selections, buffer)) = maybe!({
1493 let editor = workspace
1494 .active_item(cx)
1495 .and_then(|item| item.act_as::<Editor>(cx))?;
1496
1497 let buffer = editor.read(cx).buffer().clone();
1498 let snapshot = buffer.read(cx).snapshot(cx);
1499 let selections = editor.update(cx, |editor, cx| {
1500 editor
1501 .selections
1502 .all_adjusted(&editor.display_snapshot(cx))
1503 .into_iter()
1504 .filter_map(|s| {
1505 (!s.is_empty())
1506 .then(|| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
1507 })
1508 .collect::<Vec<_>>()
1509 });
1510 Some((selections, buffer))
1511 }) else {
1512 return;
1513 };
1514
1515 if selections.is_empty() {
1516 return;
1517 }
1518
1519 agent_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
1520 }
1521
1522 /// Handles the SendReviewToAgent action from the ProjectDiff toolbar.
1523 /// Collects ALL stored review comments from ALL hunks and sends them
1524 /// to the Agent panel as creases.
1525 pub fn handle_send_review_to_agent(
1526 workspace: &mut Workspace,
1527 _: &SendReviewToAgent,
1528 window: &mut Window,
1529 cx: &mut Context<Workspace>,
1530 ) {
1531 use editor::{DiffHunkKey, StoredReviewComment};
1532 use git_ui::project_diff::ProjectDiff;
1533
1534 // Find the ProjectDiff item
1535 let Some(project_diff) = workspace.items_of_type::<ProjectDiff>(cx).next() else {
1536 workspace.show_toast(
1537 Toast::new(
1538 NotificationId::unique::<SendReviewToAgent>(),
1539 "No Project Diff panel found. Open it first to add review comments.",
1540 ),
1541 cx,
1542 );
1543 return;
1544 };
1545
1546 // Get the buffer reference first (before taking comments)
1547 let buffer = project_diff.update(cx, |project_diff, cx| {
1548 project_diff
1549 .editor()
1550 .read(cx)
1551 .primary_editor()
1552 .read(cx)
1553 .buffer()
1554 .clone()
1555 });
1556
1557 // Extract all stored comments from all hunks
1558 let all_comments: Vec<(DiffHunkKey, Vec<StoredReviewComment>)> =
1559 project_diff.update(cx, |project_diff, cx| {
1560 let editor = project_diff.editor().read(cx).primary_editor().clone();
1561 editor.update(cx, |editor, cx| editor.take_all_review_comments(cx))
1562 });
1563
1564 // Flatten: we have Vec<(DiffHunkKey, Vec<StoredReviewComment>)>
1565 // Convert to Vec<StoredReviewComment> for processing
1566 let comments: Vec<StoredReviewComment> = all_comments
1567 .into_iter()
1568 .flat_map(|(_, comments)| comments)
1569 .collect();
1570
1571 if comments.is_empty() {
1572 workspace.show_toast(
1573 Toast::new(
1574 NotificationId::unique::<SendReviewToAgent>(),
1575 "No review comments to send. Add comments using the + button in the diff view.",
1576 ),
1577 cx,
1578 );
1579 return;
1580 }
1581
1582 // Get or create the agent panel
1583 let Some(panel) = workspace.panel::<crate::AgentPanel>(cx) else {
1584 workspace.show_toast(
1585 Toast::new(
1586 NotificationId::unique::<SendReviewToAgent>(),
1587 "Agent panel is not available.",
1588 ),
1589 cx,
1590 );
1591 return;
1592 };
1593
1594 // Create a new thread if there isn't an active one (synchronous call)
1595 let has_active_thread = panel.read(cx).active_thread_view().is_some();
1596 if !has_active_thread {
1597 panel.update(cx, |panel, cx| {
1598 panel.new_agent_thread(AgentType::NativeAgent, window, cx);
1599 });
1600 }
1601
1602 // Focus the agent panel
1603 workspace.focus_panel::<crate::AgentPanel>(window, cx);
1604
1605 // Defer inserting creases until after the current update cycle completes,
1606 // allowing the newly created thread (if any) to fully initialize.
1607 cx.defer_in(window, move |workspace, window, cx| {
1608 let Some(panel) = workspace.panel::<crate::AgentPanel>(cx) else {
1609 workspace.show_toast(
1610 Toast::new(
1611 NotificationId::unique::<SendReviewToAgent>(),
1612 "Agent panel closed unexpectedly.",
1613 ),
1614 cx,
1615 );
1616 return;
1617 };
1618
1619 let thread_view = panel.read(cx).active_thread_view().cloned();
1620 let Some(thread_view) = thread_view else {
1621 workspace.show_toast(
1622 Toast::new(
1623 NotificationId::unique::<SendReviewToAgent>(),
1624 "No active thread view available after creating thread.",
1625 ),
1626 cx,
1627 );
1628 return;
1629 };
1630
1631 // Build creases for all comments, grouping by code snippet
1632 // so each snippet appears once with all its comments
1633 let snapshot = buffer.read(cx).snapshot(cx);
1634
1635 // Group comments by their point range (code snippet)
1636 let mut comments_by_range: std::collections::BTreeMap<
1637 (rope::Point, rope::Point),
1638 Vec<String>,
1639 > = std::collections::BTreeMap::new();
1640
1641 for comment in comments {
1642 let start = comment.anchor_range.start.to_point(&snapshot);
1643 let end = comment.anchor_range.end.to_point(&snapshot);
1644 comments_by_range
1645 .entry((start, end))
1646 .or_default()
1647 .push(comment.comment);
1648 }
1649
1650 // Build one crease per unique code snippet with all its comments
1651 let mut all_creases = Vec::new();
1652 for ((start, end), comment_texts) in comments_by_range {
1653 let point_range = start..end;
1654
1655 let mut creases =
1656 selections_creases(vec![point_range.clone()], snapshot.clone(), cx);
1657
1658 // Append all comments after the code snippet
1659 for (code_text, crease_title) in &mut creases {
1660 let comments_section = comment_texts.join("\n\n");
1661 *code_text = format!("{}\n\n{}", code_text, comments_section);
1662 *crease_title = format!("Review: {}", crease_title);
1663 }
1664
1665 all_creases.extend(creases);
1666 }
1667
1668 // Insert all creases into the message editor
1669 thread_view.update(cx, |thread_view, cx| {
1670 thread_view.insert_code_crease(all_creases, window, cx);
1671 });
1672 });
1673 }
1674
1675 pub fn quote_ranges(
1676 &mut self,
1677 ranges: Vec<Range<Point>>,
1678 snapshot: MultiBufferSnapshot,
1679 window: &mut Window,
1680 cx: &mut Context<Self>,
1681 ) {
1682 let creases = selections_creases(ranges, snapshot, cx);
1683
1684 self.editor.update(cx, |editor, cx| {
1685 editor.insert("\n", window, cx);
1686 for (text, crease_title) in creases {
1687 let point = editor
1688 .selections
1689 .newest::<Point>(&editor.display_snapshot(cx))
1690 .head();
1691 let start_row = MultiBufferRow(point.row);
1692
1693 editor.insert(&text, window, cx);
1694
1695 let snapshot = editor.buffer().read(cx).snapshot(cx);
1696 let anchor_before = snapshot.anchor_after(point);
1697 let anchor_after = editor
1698 .selections
1699 .newest_anchor()
1700 .head()
1701 .bias_left(&snapshot);
1702
1703 editor.insert("\n", window, cx);
1704
1705 let fold_placeholder =
1706 quote_selection_fold_placeholder(crease_title, cx.entity().downgrade());
1707 let crease = Crease::inline(
1708 anchor_before..anchor_after,
1709 fold_placeholder,
1710 render_quote_selection_output_toggle,
1711 |_, _, _, _| Empty.into_any(),
1712 );
1713 editor.insert_creases(vec![crease], cx);
1714 editor.fold_at(start_row, window, cx);
1715 }
1716 })
1717 }
1718
1719 fn copy(&mut self, _: &editor::actions::Copy, _window: &mut Window, cx: &mut Context<Self>) {
1720 if self.editor.read(cx).selections.count() == 1 {
1721 let (copied_text, metadata, _) = self.get_clipboard_contents(cx);
1722 cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
1723 copied_text,
1724 metadata,
1725 ));
1726 cx.stop_propagation();
1727 return;
1728 }
1729
1730 cx.propagate();
1731 }
1732
1733 fn cut(&mut self, _: &editor::actions::Cut, window: &mut Window, cx: &mut Context<Self>) {
1734 if self.editor.read(cx).selections.count() == 1 {
1735 let (copied_text, metadata, selections) = self.get_clipboard_contents(cx);
1736
1737 self.editor.update(cx, |editor, cx| {
1738 editor.transact(window, cx, |this, window, cx| {
1739 this.change_selections(Default::default(), window, cx, |s| {
1740 s.select(selections);
1741 });
1742 this.insert("", window, cx);
1743 cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
1744 copied_text,
1745 metadata,
1746 ));
1747 });
1748 });
1749
1750 cx.stop_propagation();
1751 return;
1752 }
1753
1754 cx.propagate();
1755 }
1756
1757 fn get_clipboard_contents(
1758 &mut self,
1759 cx: &mut Context<Self>,
1760 ) -> (
1761 String,
1762 CopyMetadata,
1763 Vec<text::Selection<MultiBufferOffset>>,
1764 ) {
1765 let (mut selection, creases) = self.editor.update(cx, |editor, cx| {
1766 let mut selection = editor
1767 .selections
1768 .newest_adjusted(&editor.display_snapshot(cx));
1769 let snapshot = editor.buffer().read(cx).snapshot(cx);
1770
1771 selection.goal = SelectionGoal::None;
1772
1773 let selection_start = snapshot.point_to_offset(selection.start);
1774
1775 (
1776 selection.map(|point| snapshot.point_to_offset(point)),
1777 editor.display_map.update(cx, |display_map, cx| {
1778 display_map
1779 .snapshot(cx)
1780 .crease_snapshot
1781 .creases_in_range(
1782 MultiBufferRow(selection.start.row)
1783 ..MultiBufferRow(selection.end.row + 1),
1784 &snapshot,
1785 )
1786 .filter_map(|crease| {
1787 if let Crease::Inline {
1788 range, metadata, ..
1789 } = &crease
1790 {
1791 let metadata = metadata.as_ref()?;
1792 let start = range
1793 .start
1794 .to_offset(&snapshot)
1795 .saturating_sub(selection_start);
1796 let end = range
1797 .end
1798 .to_offset(&snapshot)
1799 .saturating_sub(selection_start);
1800
1801 let range_relative_to_selection = start..end;
1802 if !range_relative_to_selection.is_empty() {
1803 return Some(SelectedCreaseMetadata {
1804 range_relative_to_selection,
1805 crease: metadata.clone(),
1806 });
1807 }
1808 }
1809 None
1810 })
1811 .collect::<Vec<_>>()
1812 }),
1813 )
1814 });
1815
1816 let text_thread = self.text_thread.read(cx);
1817
1818 let mut text = String::new();
1819
1820 // If selection is empty, we want to copy the entire line
1821 if selection.range().is_empty() {
1822 let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
1823 let point = snapshot.offset_to_point(selection.range().start);
1824 selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
1825 selection.end = snapshot
1826 .point_to_offset(cmp::min(Point::new(point.row + 1, 0), snapshot.max_point()));
1827 for chunk in snapshot.text_for_range(selection.range()) {
1828 text.push_str(chunk);
1829 }
1830 } else {
1831 for message in text_thread.messages(cx) {
1832 if message.offset_range.start >= selection.range().end.0 {
1833 break;
1834 } else if message.offset_range.end >= selection.range().start.0 {
1835 let range = cmp::max(message.offset_range.start, selection.range().start.0)
1836 ..cmp::min(message.offset_range.end, selection.range().end.0);
1837 if !range.is_empty() {
1838 for chunk in text_thread.buffer().read(cx).text_for_range(range) {
1839 text.push_str(chunk);
1840 }
1841 if message.offset_range.end < selection.range().end.0 {
1842 text.push('\n');
1843 }
1844 }
1845 }
1846 }
1847 }
1848 (text, CopyMetadata { creases }, vec![selection])
1849 }
1850
1851 fn paste(
1852 &mut self,
1853 action: &editor::actions::Paste,
1854 window: &mut Window,
1855 cx: &mut Context<Self>,
1856 ) {
1857 let Some(workspace) = self.workspace.upgrade() else {
1858 return;
1859 };
1860 let editor_clipboard_selections = cx
1861 .read_from_clipboard()
1862 .and_then(|item| item.entries().first().cloned())
1863 .and_then(|entry| match entry {
1864 ClipboardEntry::String(text) => {
1865 text.metadata_json::<Vec<editor::ClipboardSelection>>()
1866 }
1867 _ => None,
1868 });
1869
1870 // Insert creases for pasted clipboard selections that:
1871 // 1. Contain exactly one selection
1872 // 2. Have an associated file path
1873 // 3. Span multiple lines (not single-line selections)
1874 // 4. Belong to a file that exists in the current project
1875 let should_insert_creases = util::maybe!({
1876 let selections = editor_clipboard_selections.as_ref()?;
1877 if selections.len() > 1 {
1878 return Some(false);
1879 }
1880 let selection = selections.first()?;
1881 let file_path = selection.file_path.as_ref()?;
1882 let line_range = selection.line_range.as_ref()?;
1883
1884 if line_range.start() == line_range.end() {
1885 return Some(false);
1886 }
1887
1888 Some(
1889 workspace
1890 .read(cx)
1891 .project()
1892 .read(cx)
1893 .project_path_for_absolute_path(file_path, cx)
1894 .is_some(),
1895 )
1896 })
1897 .unwrap_or(false);
1898
1899 if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
1900 if let Some(ClipboardEntry::String(clipboard_text)) = clipboard_item.entries().first() {
1901 if let Some(selections) = editor_clipboard_selections {
1902 cx.stop_propagation();
1903
1904 let text = clipboard_text.text();
1905 self.editor.update(cx, |editor, cx| {
1906 let mut current_offset = 0;
1907 let weak_editor = cx.entity().downgrade();
1908
1909 for selection in selections {
1910 if let (Some(file_path), Some(line_range)) =
1911 (selection.file_path, selection.line_range)
1912 {
1913 let selected_text =
1914 &text[current_offset..current_offset + selection.len];
1915 let fence = assistant_slash_commands::codeblock_fence_for_path(
1916 file_path.to_str(),
1917 Some(line_range.clone()),
1918 );
1919 let formatted_text = format!("{fence}{selected_text}\n```");
1920
1921 let insert_point = editor
1922 .selections
1923 .newest::<Point>(&editor.display_snapshot(cx))
1924 .head();
1925 let start_row = MultiBufferRow(insert_point.row);
1926
1927 editor.insert(&formatted_text, window, cx);
1928
1929 let snapshot = editor.buffer().read(cx).snapshot(cx);
1930 let anchor_before = snapshot.anchor_after(insert_point);
1931 let anchor_after = editor
1932 .selections
1933 .newest_anchor()
1934 .head()
1935 .bias_left(&snapshot);
1936
1937 editor.insert("\n", window, cx);
1938
1939 let crease_text = acp_thread::selection_name(
1940 Some(file_path.as_ref()),
1941 &line_range,
1942 );
1943
1944 let fold_placeholder = quote_selection_fold_placeholder(
1945 crease_text,
1946 weak_editor.clone(),
1947 );
1948 let crease = Crease::inline(
1949 anchor_before..anchor_after,
1950 fold_placeholder,
1951 render_quote_selection_output_toggle,
1952 |_, _, _, _| Empty.into_any(),
1953 );
1954 editor.insert_creases(vec![crease], cx);
1955 editor.fold_at(start_row, window, cx);
1956
1957 current_offset += selection.len;
1958 if !selection.is_entire_line && current_offset < text.len() {
1959 current_offset += 1;
1960 }
1961 }
1962 }
1963 });
1964 return;
1965 }
1966 }
1967 }
1968
1969 cx.stop_propagation();
1970
1971 let mut images = if let Some(item) = cx.read_from_clipboard() {
1972 item.into_entries()
1973 .filter_map(|entry| {
1974 if let ClipboardEntry::Image(image) = entry {
1975 Some(image)
1976 } else {
1977 None
1978 }
1979 })
1980 .collect()
1981 } else {
1982 Vec::new()
1983 };
1984
1985 if let Some(paths) = cx.read_from_clipboard() {
1986 for path in paths
1987 .into_entries()
1988 .filter_map(|entry| {
1989 if let ClipboardEntry::ExternalPaths(paths) = entry {
1990 Some(paths.paths().to_owned())
1991 } else {
1992 None
1993 }
1994 })
1995 .flatten()
1996 {
1997 let Ok(content) = std::fs::read(path) else {
1998 continue;
1999 };
2000 let Ok(format) = image::guess_format(&content) else {
2001 continue;
2002 };
2003 images.push(gpui::Image::from_bytes(
2004 match format {
2005 image::ImageFormat::Png => gpui::ImageFormat::Png,
2006 image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
2007 image::ImageFormat::WebP => gpui::ImageFormat::Webp,
2008 image::ImageFormat::Gif => gpui::ImageFormat::Gif,
2009 image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
2010 image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
2011 image::ImageFormat::Ico => gpui::ImageFormat::Ico,
2012 _ => continue,
2013 },
2014 content,
2015 ));
2016 }
2017 }
2018
2019 let metadata = if let Some(item) = cx.read_from_clipboard() {
2020 item.entries().first().and_then(|entry| {
2021 if let ClipboardEntry::String(text) = entry {
2022 text.metadata_json::<CopyMetadata>()
2023 } else {
2024 None
2025 }
2026 })
2027 } else {
2028 None
2029 };
2030
2031 if images.is_empty() {
2032 self.editor.update(cx, |editor, cx| {
2033 let paste_position = editor
2034 .selections
2035 .newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
2036 .head();
2037 editor.paste(action, window, cx);
2038
2039 if let Some(metadata) = metadata {
2040 let buffer = editor.buffer().read(cx).snapshot(cx);
2041
2042 let mut buffer_rows_to_fold = BTreeSet::new();
2043 let weak_editor = cx.entity().downgrade();
2044 editor.insert_creases(
2045 metadata.creases.into_iter().map(|metadata| {
2046 let start = buffer.anchor_after(
2047 paste_position + metadata.range_relative_to_selection.start,
2048 );
2049 let end = buffer.anchor_before(
2050 paste_position + metadata.range_relative_to_selection.end,
2051 );
2052
2053 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2054 buffer_rows_to_fold.insert(buffer_row);
2055 Crease::inline(
2056 start..end,
2057 FoldPlaceholder {
2058 render: render_fold_icon_button(
2059 weak_editor.clone(),
2060 metadata.crease.icon_path.clone(),
2061 metadata.crease.label.clone(),
2062 ),
2063 ..Default::default()
2064 },
2065 render_slash_command_output_toggle,
2066 |_, _, _, _| Empty.into_any(),
2067 )
2068 .with_metadata(metadata.crease)
2069 }),
2070 cx,
2071 );
2072 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
2073 editor.fold_at(buffer_row, window, cx);
2074 }
2075 }
2076 });
2077 } else {
2078 let mut image_positions = Vec::new();
2079 self.editor.update(cx, |editor, cx| {
2080 editor.transact(window, cx, |editor, _window, cx| {
2081 let edits = editor
2082 .selections
2083 .all::<MultiBufferOffset>(&editor.display_snapshot(cx))
2084 .into_iter()
2085 .map(|selection| (selection.start..selection.end, "\n"));
2086 editor.edit(edits, cx);
2087
2088 let snapshot = editor.buffer().read(cx).snapshot(cx);
2089 for selection in editor
2090 .selections
2091 .all::<MultiBufferOffset>(&editor.display_snapshot(cx))
2092 {
2093 image_positions.push(snapshot.anchor_before(selection.end));
2094 }
2095 });
2096 });
2097
2098 self.text_thread.update(cx, |text_thread, cx| {
2099 for image in images {
2100 let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err()
2101 else {
2102 continue;
2103 };
2104 let image_id = image.id();
2105 let image_task = LanguageModelImage::from_image(Arc::new(image), cx).shared();
2106
2107 for image_position in image_positions.iter() {
2108 text_thread.insert_content(
2109 Content::Image {
2110 anchor: image_position.text_anchor,
2111 image_id,
2112 image: image_task.clone(),
2113 render_image: render_image.clone(),
2114 },
2115 cx,
2116 );
2117 }
2118 }
2119 });
2120 }
2121 }
2122
2123 fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
2124 self.editor.update(cx, |editor, cx| {
2125 editor.paste(&editor::actions::Paste, window, cx);
2126 });
2127 }
2128
2129 fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
2130 self.editor.update(cx, |editor, cx| {
2131 let buffer = editor.buffer().read(cx).snapshot(cx);
2132 let excerpt_id = *buffer.as_singleton().unwrap().0;
2133 let old_blocks = std::mem::take(&mut self.image_blocks);
2134 let new_blocks = self
2135 .text_thread
2136 .read(cx)
2137 .contents(cx)
2138 .map(
2139 |Content::Image {
2140 anchor,
2141 render_image,
2142 ..
2143 }| (anchor, render_image),
2144 )
2145 .filter_map(|(anchor, render_image)| {
2146 const MAX_HEIGHT_IN_LINES: u32 = 8;
2147 let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
2148 let image = render_image;
2149 anchor.is_valid(&buffer).then(|| BlockProperties {
2150 placement: BlockPlacement::Above(anchor),
2151 height: Some(MAX_HEIGHT_IN_LINES),
2152 style: BlockStyle::Sticky,
2153 render: Arc::new(move |cx| {
2154 let image_size = size_for_image(
2155 &image,
2156 size(
2157 cx.max_width - cx.margins.gutter.full_width(),
2158 MAX_HEIGHT_IN_LINES as f32 * cx.line_height,
2159 ),
2160 );
2161 h_flex()
2162 .pl(cx.margins.gutter.full_width())
2163 .child(
2164 img(image.clone())
2165 .object_fit(gpui::ObjectFit::ScaleDown)
2166 .w(image_size.width)
2167 .h(image_size.height),
2168 )
2169 .into_any_element()
2170 }),
2171 priority: 0,
2172 })
2173 })
2174 .collect::<Vec<_>>();
2175
2176 editor.remove_blocks(old_blocks, None, cx);
2177 let ids = editor.insert_blocks(new_blocks, None, cx);
2178 self.image_blocks = HashSet::from_iter(ids);
2179 });
2180 }
2181
2182 fn split(&mut self, _: &Split, _window: &mut Window, cx: &mut Context<Self>) {
2183 self.text_thread.update(cx, |text_thread, cx| {
2184 let selections = self.editor.read(cx).selections.disjoint_anchors_arc();
2185 for selection in selections.as_ref() {
2186 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2187 let range = selection
2188 .map(|endpoint| endpoint.to_offset(&buffer))
2189 .range();
2190 text_thread.split_message(range.start.0..range.end.0, cx);
2191 }
2192 });
2193 }
2194
2195 fn save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context<Self>) {
2196 self.text_thread.update(cx, |text_thread, cx| {
2197 text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
2198 });
2199 }
2200
2201 pub fn title(&self, cx: &App) -> SharedString {
2202 self.text_thread.read(cx).summary().or_default()
2203 }
2204
2205 pub fn regenerate_summary(&mut self, cx: &mut Context<Self>) {
2206 self.text_thread
2207 .update(cx, |text_thread, cx| text_thread.summarize(true, cx));
2208 }
2209
2210 fn render_remaining_tokens(&self, cx: &App) -> Option<impl IntoElement + use<>> {
2211 let (token_count_color, token_count, max_token_count, tooltip) =
2212 match token_state(&self.text_thread, cx)? {
2213 TokenState::NoTokensLeft {
2214 max_token_count,
2215 token_count,
2216 } => (
2217 Color::Error,
2218 token_count,
2219 max_token_count,
2220 Some("Token Limit Reached"),
2221 ),
2222 TokenState::HasMoreTokens {
2223 max_token_count,
2224 token_count,
2225 over_warn_threshold,
2226 } => {
2227 let (color, tooltip) = if over_warn_threshold {
2228 (Color::Warning, Some("Token Limit is Close to Exhaustion"))
2229 } else {
2230 (Color::Muted, None)
2231 };
2232 (color, token_count, max_token_count, tooltip)
2233 }
2234 };
2235
2236 Some(
2237 h_flex()
2238 .id("token-count")
2239 .gap_0p5()
2240 .child(
2241 Label::new(humanize_token_count(token_count))
2242 .size(LabelSize::Small)
2243 .color(token_count_color),
2244 )
2245 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2246 .child(
2247 Label::new(humanize_token_count(max_token_count))
2248 .size(LabelSize::Small)
2249 .color(Color::Muted),
2250 )
2251 .when_some(tooltip, |element, tooltip| {
2252 element.tooltip(Tooltip::text(tooltip))
2253 }),
2254 )
2255 }
2256
2257 fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2258 let focus_handle = self.focus_handle(cx);
2259
2260 let (style, tooltip) = match token_state(&self.text_thread, cx) {
2261 Some(TokenState::NoTokensLeft { .. }) => (
2262 ButtonStyle::Tinted(TintColor::Error),
2263 Some(Tooltip::text("Token limit reached")(window, cx)),
2264 ),
2265 Some(TokenState::HasMoreTokens {
2266 over_warn_threshold,
2267 ..
2268 }) => {
2269 let (style, tooltip) = if over_warn_threshold {
2270 (
2271 ButtonStyle::Tinted(TintColor::Warning),
2272 Some(Tooltip::text("Token limit is close to exhaustion")(
2273 window, cx,
2274 )),
2275 )
2276 } else {
2277 (ButtonStyle::Filled, None)
2278 };
2279 (style, tooltip)
2280 }
2281 None => (ButtonStyle::Filled, None),
2282 };
2283
2284 Button::new("send_button", "Send")
2285 .label_size(LabelSize::Small)
2286 .disabled(self.sending_disabled(cx))
2287 .style(style)
2288 .when_some(tooltip, |button, tooltip| {
2289 button.tooltip(move |_, _| tooltip.clone())
2290 })
2291 .layer(ElevationIndex::ModalSurface)
2292 .key_binding(
2293 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
2294 .map(|kb| kb.size(rems_from_px(12.))),
2295 )
2296 .on_click(move |_event, window, cx| {
2297 focus_handle.dispatch_action(&Assist, window, cx);
2298 })
2299 }
2300
2301 /// Whether or not we should allow messages to be sent.
2302 /// Will return false if the selected provided has a configuration error or
2303 /// if the user has not accepted the terms of service for this provider.
2304 fn sending_disabled(&self, cx: &mut Context<'_, TextThreadEditor>) -> bool {
2305 let model_registry = LanguageModelRegistry::read_global(cx);
2306 let Some(configuration_error) =
2307 model_registry.configuration_error(model_registry.default_model(), cx)
2308 else {
2309 return false;
2310 };
2311
2312 match configuration_error {
2313 ConfigurationError::NoProvider
2314 | ConfigurationError::ModelNotFound
2315 | ConfigurationError::ProviderNotAuthenticated(_) => true,
2316 }
2317 }
2318
2319 fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
2320 slash_command_picker::SlashCommandSelector::new(
2321 self.slash_commands.clone(),
2322 cx.entity().downgrade(),
2323 IconButton::new("trigger", IconName::Plus)
2324 .icon_size(IconSize::Small)
2325 .icon_color(Color::Muted)
2326 .selected_icon_color(Color::Accent)
2327 .selected_style(ButtonStyle::Filled),
2328 move |_window, cx| {
2329 Tooltip::with_meta("Add Context", None, "Type / to insert via keyboard", cx)
2330 },
2331 )
2332 }
2333
2334 fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2335 let text_thread = self.text_thread().read(cx);
2336 let active_model = LanguageModelRegistry::read_global(cx)
2337 .default_model()
2338 .map(|default| default.model)?;
2339 if !active_model.supports_burn_mode() {
2340 return None;
2341 }
2342
2343 let active_completion_mode = text_thread.completion_mode();
2344 let burn_mode_enabled = active_completion_mode == CompletionMode::Burn;
2345 let icon = if burn_mode_enabled {
2346 IconName::ZedBurnModeOn
2347 } else {
2348 IconName::ZedBurnMode
2349 };
2350
2351 Some(
2352 IconButton::new("burn-mode", icon)
2353 .icon_size(IconSize::Small)
2354 .icon_color(Color::Muted)
2355 .toggle_state(burn_mode_enabled)
2356 .selected_icon_color(Color::Error)
2357 .on_click(cx.listener(move |this, _event, _window, cx| {
2358 this.text_thread().update(cx, |text_thread, _cx| {
2359 text_thread.set_completion_mode(match active_completion_mode {
2360 CompletionMode::Burn => CompletionMode::Normal,
2361 CompletionMode::Normal => CompletionMode::Burn,
2362 });
2363 });
2364 }))
2365 .tooltip(move |_window, cx| {
2366 cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled))
2367 .into()
2368 })
2369 .into_any_element(),
2370 )
2371 }
2372
2373 fn render_language_model_selector(
2374 &self,
2375 window: &mut Window,
2376 cx: &mut Context<Self>,
2377 ) -> impl IntoElement {
2378 let active_model = LanguageModelRegistry::read_global(cx)
2379 .default_model()
2380 .map(|default| default.model);
2381 let model_name = match active_model {
2382 Some(model) => model.name().0,
2383 None => SharedString::from("Select Model"),
2384 };
2385
2386 let active_provider = LanguageModelRegistry::read_global(cx)
2387 .default_model()
2388 .map(|default| default.provider);
2389
2390 let provider_icon = active_provider
2391 .as_ref()
2392 .map(|p| p.icon())
2393 .unwrap_or(IconOrSvg::Icon(IconName::Ai));
2394
2395 let focus_handle = self.editor().focus_handle(cx);
2396
2397 let (color, icon) = if self.language_model_selector_menu_handle.is_deployed() {
2398 (Color::Accent, IconName::ChevronUp)
2399 } else {
2400 (Color::Muted, IconName::ChevronDown)
2401 };
2402
2403 let provider_icon_element = match provider_icon {
2404 IconOrSvg::Svg(path) => Icon::from_external_svg(path),
2405 IconOrSvg::Icon(name) => Icon::new(name),
2406 }
2407 .color(color)
2408 .size(IconSize::XSmall);
2409
2410 let show_cycle_row = self
2411 .language_model_selector
2412 .read(cx)
2413 .delegate
2414 .favorites_count()
2415 > 1;
2416
2417 let tooltip = Tooltip::element({
2418 move |_, _cx| {
2419 ModelSelectorTooltip::new(focus_handle.clone())
2420 .show_cycle_row(show_cycle_row)
2421 .into_any_element()
2422 }
2423 });
2424
2425 PickerPopoverMenu::new(
2426 self.language_model_selector.clone(),
2427 ButtonLike::new("active-model")
2428 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
2429 .child(
2430 h_flex()
2431 .gap_0p5()
2432 .child(provider_icon_element)
2433 .child(
2434 Label::new(model_name)
2435 .color(color)
2436 .size(LabelSize::Small)
2437 .ml_0p5(),
2438 )
2439 .child(Icon::new(icon).color(color).size(IconSize::XSmall)),
2440 ),
2441 tooltip,
2442 gpui::Corner::BottomRight,
2443 cx,
2444 )
2445 .with_handle(self.language_model_selector_menu_handle.clone())
2446 .render(window, cx)
2447 }
2448
2449 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2450 let last_error = self.last_error.as_ref()?;
2451
2452 Some(
2453 div()
2454 .absolute()
2455 .right_3()
2456 .bottom_12()
2457 .max_w_96()
2458 .py_2()
2459 .px_3()
2460 .elevation_2(cx)
2461 .occlude()
2462 .child(match last_error {
2463 AssistError::PaymentRequired => self.render_payment_required_error(cx),
2464 AssistError::Message(error_message) => {
2465 self.render_assist_error(error_message, cx)
2466 }
2467 })
2468 .into_any(),
2469 )
2470 }
2471
2472 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
2473 const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
2474
2475 v_flex()
2476 .gap_0p5()
2477 .child(
2478 h_flex()
2479 .gap_1p5()
2480 .items_center()
2481 .child(Icon::new(IconName::XCircle).color(Color::Error))
2482 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
2483 )
2484 .child(
2485 div()
2486 .id("error-message")
2487 .max_h_24()
2488 .overflow_y_scroll()
2489 .child(Label::new(ERROR_MESSAGE)),
2490 )
2491 .child(
2492 h_flex()
2493 .justify_end()
2494 .mt_1()
2495 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
2496 |this, _, _window, cx| {
2497 this.last_error = None;
2498 cx.open_url(&zed_urls::account_url(cx));
2499 cx.notify();
2500 },
2501 )))
2502 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2503 |this, _, _window, cx| {
2504 this.last_error = None;
2505 cx.notify();
2506 },
2507 ))),
2508 )
2509 .into_any()
2510 }
2511
2512 fn render_assist_error(
2513 &self,
2514 error_message: &SharedString,
2515 cx: &mut Context<Self>,
2516 ) -> AnyElement {
2517 v_flex()
2518 .gap_0p5()
2519 .child(
2520 h_flex()
2521 .gap_1p5()
2522 .items_center()
2523 .child(Icon::new(IconName::XCircle).color(Color::Error))
2524 .child(
2525 Label::new("Error interacting with language model")
2526 .weight(FontWeight::MEDIUM),
2527 ),
2528 )
2529 .child(
2530 div()
2531 .id("error-message")
2532 .max_h_32()
2533 .overflow_y_scroll()
2534 .child(Label::new(error_message.clone())),
2535 )
2536 .child(
2537 h_flex()
2538 .justify_end()
2539 .mt_1()
2540 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2541 |this, _, _window, cx| {
2542 this.last_error = None;
2543 cx.notify();
2544 },
2545 ))),
2546 )
2547 .into_any()
2548 }
2549}
2550
2551/// Returns the contents of the *outermost* fenced code block that contains the given offset.
2552fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option<Range<usize>> {
2553 const CODE_BLOCK_NODE: &str = "fenced_code_block";
2554 const CODE_BLOCK_CONTENT: &str = "code_fence_content";
2555
2556 let layer = snapshot.syntax_layers().next()?;
2557
2558 let root_node = layer.node();
2559 let mut cursor = root_node.walk();
2560
2561 // Go to the first child for the given offset
2562 while cursor.goto_first_child_for_byte(offset).is_some() {
2563 // If we're at the end of the node, go to the next one.
2564 // Example: if you have a fenced-code-block, and you're on the start of the line
2565 // right after the closing ```, you want to skip the fenced-code-block and
2566 // go to the next sibling.
2567 if cursor.node().end_byte() == offset {
2568 cursor.goto_next_sibling();
2569 }
2570
2571 if cursor.node().start_byte() > offset {
2572 break;
2573 }
2574
2575 // We found the fenced code block.
2576 if cursor.node().kind() == CODE_BLOCK_NODE {
2577 // Now we need to find the child node that contains the code.
2578 cursor.goto_first_child();
2579 loop {
2580 if cursor.node().kind() == CODE_BLOCK_CONTENT {
2581 return Some(cursor.node().byte_range());
2582 }
2583 if !cursor.goto_next_sibling() {
2584 break;
2585 }
2586 }
2587 }
2588 }
2589
2590 None
2591}
2592
2593fn render_thought_process_fold_icon_button(
2594 editor: WeakEntity<Editor>,
2595 status: ThoughtProcessStatus,
2596) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
2597 Arc::new(move |fold_id, fold_range, _cx| {
2598 let editor = editor.clone();
2599
2600 let button = ButtonLike::new(fold_id).layer(ElevationIndex::ElevatedSurface);
2601 let button = match status {
2602 ThoughtProcessStatus::Pending => button
2603 .child(
2604 Icon::new(IconName::ToolThink)
2605 .size(IconSize::Small)
2606 .color(Color::Muted),
2607 )
2608 .child(
2609 Label::new("Thinking…").color(Color::Muted).with_animation(
2610 "pulsating-label",
2611 Animation::new(Duration::from_secs(2))
2612 .repeat()
2613 .with_easing(pulsating_between(0.4, 0.8)),
2614 |label, delta| label.alpha(delta),
2615 ),
2616 ),
2617 ThoughtProcessStatus::Completed => button
2618 .style(ButtonStyle::Filled)
2619 .child(Icon::new(IconName::ToolThink).size(IconSize::Small))
2620 .child(Label::new("Thought Process").single_line()),
2621 };
2622
2623 button
2624 .on_click(move |_, window, cx| {
2625 editor
2626 .update(cx, |editor, cx| {
2627 let buffer_start = fold_range
2628 .start
2629 .to_point(&editor.buffer().read(cx).read(cx));
2630 let buffer_row = MultiBufferRow(buffer_start.row);
2631 editor.unfold_at(buffer_row, window, cx);
2632 })
2633 .ok();
2634 })
2635 .into_any_element()
2636 })
2637}
2638
2639fn render_fold_icon_button(
2640 editor: WeakEntity<Editor>,
2641 icon_path: SharedString,
2642 label: SharedString,
2643) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
2644 Arc::new(move |fold_id, fold_range, _cx| {
2645 let editor = editor.clone();
2646 ButtonLike::new(fold_id)
2647 .style(ButtonStyle::Filled)
2648 .layer(ElevationIndex::ElevatedSurface)
2649 .child(Icon::from_path(icon_path.clone()))
2650 .child(Label::new(label.clone()).single_line())
2651 .on_click(move |_, window, cx| {
2652 editor
2653 .update(cx, |editor, cx| {
2654 let buffer_start = fold_range
2655 .start
2656 .to_point(&editor.buffer().read(cx).read(cx));
2657 let buffer_row = MultiBufferRow(buffer_start.row);
2658 editor.unfold_at(buffer_row, window, cx);
2659 })
2660 .ok();
2661 })
2662 .into_any_element()
2663 })
2664}
2665
2666type ToggleFold = Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>;
2667
2668fn render_slash_command_output_toggle(
2669 row: MultiBufferRow,
2670 is_folded: bool,
2671 fold: ToggleFold,
2672 _window: &mut Window,
2673 _cx: &mut App,
2674) -> AnyElement {
2675 Disclosure::new(
2676 ("slash-command-output-fold-indicator", row.0 as u64),
2677 !is_folded,
2678 )
2679 .toggle_state(is_folded)
2680 .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
2681 .into_any_element()
2682}
2683
2684pub fn fold_toggle(
2685 name: &'static str,
2686) -> impl Fn(
2687 MultiBufferRow,
2688 bool,
2689 Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
2690 &mut Window,
2691 &mut App,
2692) -> AnyElement {
2693 move |row, is_folded, fold, _window, _cx| {
2694 Disclosure::new((name, row.0 as u64), !is_folded)
2695 .toggle_state(is_folded)
2696 .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
2697 .into_any_element()
2698 }
2699}
2700
2701fn quote_selection_fold_placeholder(title: String, editor: WeakEntity<Editor>) -> FoldPlaceholder {
2702 FoldPlaceholder {
2703 render: Arc::new({
2704 move |fold_id, fold_range, _cx| {
2705 let editor = editor.clone();
2706 ButtonLike::new(fold_id)
2707 .style(ButtonStyle::Filled)
2708 .layer(ElevationIndex::ElevatedSurface)
2709 .child(Icon::new(IconName::TextSnippet))
2710 .child(Label::new(title.clone()).single_line())
2711 .on_click(move |_, window, cx| {
2712 editor
2713 .update(cx, |editor, cx| {
2714 let buffer_start = fold_range
2715 .start
2716 .to_point(&editor.buffer().read(cx).read(cx));
2717 let buffer_row = MultiBufferRow(buffer_start.row);
2718 editor.unfold_at(buffer_row, window, cx);
2719 })
2720 .ok();
2721 })
2722 .into_any_element()
2723 }
2724 }),
2725 merge_adjacent: false,
2726 ..Default::default()
2727 }
2728}
2729
2730fn render_quote_selection_output_toggle(
2731 row: MultiBufferRow,
2732 is_folded: bool,
2733 fold: ToggleFold,
2734 _window: &mut Window,
2735 _cx: &mut App,
2736) -> AnyElement {
2737 Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
2738 .toggle_state(is_folded)
2739 .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
2740 .into_any_element()
2741}
2742
2743fn render_pending_slash_command_gutter_decoration(
2744 row: MultiBufferRow,
2745 status: &PendingSlashCommandStatus,
2746 confirm_command: Arc<dyn Fn(&mut Window, &mut App)>,
2747) -> AnyElement {
2748 let mut icon = IconButton::new(
2749 ("slash-command-gutter-decoration", row.0),
2750 ui::IconName::TriangleRight,
2751 )
2752 .on_click(move |_e, window, cx| confirm_command(window, cx))
2753 .icon_size(ui::IconSize::Small)
2754 .size(ui::ButtonSize::None);
2755
2756 match status {
2757 PendingSlashCommandStatus::Idle => {
2758 icon = icon.icon_color(Color::Muted);
2759 }
2760 PendingSlashCommandStatus::Running { .. } => {
2761 icon = icon.toggle_state(true);
2762 }
2763 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
2764 }
2765
2766 icon.into_any_element()
2767}
2768
2769#[derive(Debug, Clone, Serialize, Deserialize)]
2770struct CopyMetadata {
2771 creases: Vec<SelectedCreaseMetadata>,
2772}
2773
2774#[derive(Debug, Clone, Serialize, Deserialize)]
2775struct SelectedCreaseMetadata {
2776 range_relative_to_selection: Range<usize>,
2777 crease: CreaseMetadata,
2778}
2779
2780impl EventEmitter<EditorEvent> for TextThreadEditor {}
2781impl EventEmitter<SearchEvent> for TextThreadEditor {}
2782
2783impl Render for TextThreadEditor {
2784 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2785 let language_model_selector = self.language_model_selector_menu_handle.clone();
2786
2787 v_flex()
2788 .key_context("ContextEditor")
2789 .capture_action(cx.listener(TextThreadEditor::cancel))
2790 .capture_action(cx.listener(TextThreadEditor::save))
2791 .capture_action(cx.listener(TextThreadEditor::copy))
2792 .capture_action(cx.listener(TextThreadEditor::cut))
2793 .capture_action(cx.listener(TextThreadEditor::paste))
2794 .on_action(cx.listener(TextThreadEditor::paste_raw))
2795 .capture_action(cx.listener(TextThreadEditor::cycle_message_role))
2796 .capture_action(cx.listener(TextThreadEditor::confirm_command))
2797 .on_action(cx.listener(TextThreadEditor::assist))
2798 .on_action(cx.listener(TextThreadEditor::split))
2799 .on_action(move |_: &ToggleModelSelector, window, cx| {
2800 language_model_selector.toggle(window, cx);
2801 })
2802 .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
2803 this.language_model_selector.update(cx, |selector, cx| {
2804 selector.delegate.cycle_favorite_models(window, cx);
2805 });
2806 }))
2807 .size_full()
2808 .child(
2809 div()
2810 .flex_grow()
2811 .bg(cx.theme().colors().editor_background)
2812 .child(self.editor.clone()),
2813 )
2814 .children(self.render_last_error(cx))
2815 .child(
2816 h_flex()
2817 .relative()
2818 .py_2()
2819 .pl_1p5()
2820 .pr_2()
2821 .w_full()
2822 .justify_between()
2823 .border_t_1()
2824 .border_color(cx.theme().colors().border_variant)
2825 .bg(cx.theme().colors().editor_background)
2826 .child(
2827 h_flex()
2828 .gap_0p5()
2829 .child(self.render_inject_context_menu(cx))
2830 .children(self.render_burn_mode_toggle(cx)),
2831 )
2832 .child(
2833 h_flex()
2834 .gap_2p5()
2835 .children(self.render_remaining_tokens(cx))
2836 .child(
2837 h_flex()
2838 .gap_1()
2839 .child(self.render_language_model_selector(window, cx))
2840 .child(self.render_send_button(window, cx)),
2841 ),
2842 ),
2843 )
2844 }
2845}
2846
2847impl Focusable for TextThreadEditor {
2848 fn focus_handle(&self, cx: &App) -> FocusHandle {
2849 self.editor.focus_handle(cx)
2850 }
2851}
2852
2853impl Item for TextThreadEditor {
2854 type Event = editor::EditorEvent;
2855
2856 fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
2857 util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into()
2858 }
2859
2860 fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
2861 match event {
2862 EditorEvent::Edited { .. } => {
2863 f(item::ItemEvent::Edit);
2864 }
2865 EditorEvent::TitleChanged => {
2866 f(item::ItemEvent::UpdateTab);
2867 }
2868 _ => {}
2869 }
2870 }
2871
2872 fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
2873 Some(self.title(cx).to_string().into())
2874 }
2875
2876 fn as_searchable(
2877 &self,
2878 handle: &Entity<Self>,
2879 _: &App,
2880 ) -> Option<Box<dyn SearchableItemHandle>> {
2881 Some(Box::new(handle.clone()))
2882 }
2883
2884 fn set_nav_history(
2885 &mut self,
2886 nav_history: pane::ItemNavHistory,
2887 window: &mut Window,
2888 cx: &mut Context<Self>,
2889 ) {
2890 self.editor.update(cx, |editor, cx| {
2891 Item::set_nav_history(editor, nav_history, window, cx)
2892 })
2893 }
2894
2895 fn navigate(
2896 &mut self,
2897 data: Box<dyn std::any::Any>,
2898 window: &mut Window,
2899 cx: &mut Context<Self>,
2900 ) -> bool {
2901 self.editor
2902 .update(cx, |editor, cx| Item::navigate(editor, data, window, cx))
2903 }
2904
2905 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2906 self.editor
2907 .update(cx, |editor, cx| Item::deactivated(editor, window, cx))
2908 }
2909
2910 fn act_as_type<'a>(
2911 &'a self,
2912 type_id: TypeId,
2913 self_handle: &'a Entity<Self>,
2914 _: &'a App,
2915 ) -> Option<gpui::AnyEntity> {
2916 if type_id == TypeId::of::<Self>() {
2917 Some(self_handle.clone().into())
2918 } else if type_id == TypeId::of::<Editor>() {
2919 Some(self.editor.clone().into())
2920 } else {
2921 None
2922 }
2923 }
2924
2925 fn include_in_nav_history() -> bool {
2926 false
2927 }
2928}
2929
2930impl SearchableItem for TextThreadEditor {
2931 type Match = <Editor as SearchableItem>::Match;
2932
2933 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2934 self.editor.update(cx, |editor, cx| {
2935 editor.clear_matches(window, cx);
2936 });
2937 }
2938
2939 fn update_matches(
2940 &mut self,
2941 matches: &[Self::Match],
2942 active_match_index: Option<usize>,
2943 window: &mut Window,
2944 cx: &mut Context<Self>,
2945 ) {
2946 self.editor.update(cx, |editor, cx| {
2947 editor.update_matches(matches, active_match_index, window, cx)
2948 });
2949 }
2950
2951 fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
2952 self.editor
2953 .update(cx, |editor, cx| editor.query_suggestion(window, cx))
2954 }
2955
2956 fn activate_match(
2957 &mut self,
2958 index: usize,
2959 matches: &[Self::Match],
2960 window: &mut Window,
2961 cx: &mut Context<Self>,
2962 ) {
2963 self.editor.update(cx, |editor, cx| {
2964 editor.activate_match(index, matches, window, cx);
2965 });
2966 }
2967
2968 fn select_matches(
2969 &mut self,
2970 matches: &[Self::Match],
2971 window: &mut Window,
2972 cx: &mut Context<Self>,
2973 ) {
2974 self.editor
2975 .update(cx, |editor, cx| editor.select_matches(matches, window, cx));
2976 }
2977
2978 fn replace(
2979 &mut self,
2980 identifier: &Self::Match,
2981 query: &project::search::SearchQuery,
2982 window: &mut Window,
2983 cx: &mut Context<Self>,
2984 ) {
2985 self.editor.update(cx, |editor, cx| {
2986 editor.replace(identifier, query, window, cx)
2987 });
2988 }
2989
2990 fn find_matches(
2991 &mut self,
2992 query: Arc<project::search::SearchQuery>,
2993 window: &mut Window,
2994 cx: &mut Context<Self>,
2995 ) -> Task<Vec<Self::Match>> {
2996 self.editor
2997 .update(cx, |editor, cx| editor.find_matches(query, window, cx))
2998 }
2999
3000 fn active_match_index(
3001 &mut self,
3002 direction: Direction,
3003 matches: &[Self::Match],
3004 window: &mut Window,
3005 cx: &mut Context<Self>,
3006 ) -> Option<usize> {
3007 self.editor.update(cx, |editor, cx| {
3008 editor.active_match_index(direction, matches, window, cx)
3009 })
3010 }
3011}
3012
3013impl FollowableItem for TextThreadEditor {
3014 fn remote_id(&self) -> Option<workspace::ViewId> {
3015 self.remote_id
3016 }
3017
3018 fn to_state_proto(&self, window: &Window, cx: &App) -> Option<proto::view::Variant> {
3019 let text_thread = self.text_thread.read(cx);
3020 Some(proto::view::Variant::ContextEditor(
3021 proto::view::ContextEditor {
3022 context_id: text_thread.id().to_proto(),
3023 editor: if let Some(proto::view::Variant::Editor(proto)) =
3024 self.editor.read(cx).to_state_proto(window, cx)
3025 {
3026 Some(proto)
3027 } else {
3028 None
3029 },
3030 },
3031 ))
3032 }
3033
3034 fn from_state_proto(
3035 workspace: Entity<Workspace>,
3036 id: workspace::ViewId,
3037 state: &mut Option<proto::view::Variant>,
3038 window: &mut Window,
3039 cx: &mut App,
3040 ) -> Option<Task<Result<Entity<Self>>>> {
3041 let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
3042 return None;
3043 };
3044 let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
3045 unreachable!()
3046 };
3047
3048 let text_thread_id = TextThreadId::from_proto(state.context_id);
3049 let editor_state = state.editor?;
3050
3051 let project = workspace.read(cx).project().clone();
3052 let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
3053
3054 let text_thread_editor_task = workspace.update(cx, |workspace, cx| {
3055 agent_panel_delegate.open_remote_text_thread(workspace, text_thread_id, window, cx)
3056 });
3057
3058 Some(window.spawn(cx, async move |cx| {
3059 let text_thread_editor = text_thread_editor_task.await?;
3060 text_thread_editor
3061 .update_in(cx, |text_thread_editor, window, cx| {
3062 text_thread_editor.remote_id = Some(id);
3063 text_thread_editor.editor.update(cx, |editor, cx| {
3064 editor.apply_update_proto(
3065 &project,
3066 proto::update_view::Variant::Editor(proto::update_view::Editor {
3067 selections: editor_state.selections,
3068 pending_selection: editor_state.pending_selection,
3069 scroll_top_anchor: editor_state.scroll_top_anchor,
3070 scroll_x: editor_state.scroll_y,
3071 scroll_y: editor_state.scroll_y,
3072 ..Default::default()
3073 }),
3074 window,
3075 cx,
3076 )
3077 })
3078 })?
3079 .await?;
3080 Ok(text_thread_editor)
3081 }))
3082 }
3083
3084 fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
3085 Editor::to_follow_event(event)
3086 }
3087
3088 fn add_event_to_update_proto(
3089 &self,
3090 event: &Self::Event,
3091 update: &mut Option<proto::update_view::Variant>,
3092 window: &Window,
3093 cx: &App,
3094 ) -> bool {
3095 self.editor
3096 .read(cx)
3097 .add_event_to_update_proto(event, update, window, cx)
3098 }
3099
3100 fn apply_update_proto(
3101 &mut self,
3102 project: &Entity<Project>,
3103 message: proto::update_view::Variant,
3104 window: &mut Window,
3105 cx: &mut Context<Self>,
3106 ) -> Task<Result<()>> {
3107 self.editor.update(cx, |editor, cx| {
3108 editor.apply_update_proto(project, message, window, cx)
3109 })
3110 }
3111
3112 fn is_project_item(&self, _window: &Window, _cx: &App) -> bool {
3113 true
3114 }
3115
3116 fn set_leader_id(
3117 &mut self,
3118 leader_id: Option<CollaboratorId>,
3119 window: &mut Window,
3120 cx: &mut Context<Self>,
3121 ) {
3122 self.editor
3123 .update(cx, |editor, cx| editor.set_leader_id(leader_id, window, cx))
3124 }
3125
3126 fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option<item::Dedup> {
3127 if existing.text_thread.read(cx).id() == self.text_thread.read(cx).id() {
3128 Some(item::Dedup::KeepExisting)
3129 } else {
3130 None
3131 }
3132 }
3133}
3134
3135enum PendingSlashCommand {}
3136
3137fn invoked_slash_command_fold_placeholder(
3138 command_id: InvokedSlashCommandId,
3139 text_thread: WeakEntity<TextThread>,
3140) -> FoldPlaceholder {
3141 FoldPlaceholder {
3142 constrain_width: false,
3143 merge_adjacent: false,
3144 render: Arc::new(move |fold_id, _, cx| {
3145 let Some(text_thread) = text_thread.upgrade() else {
3146 return Empty.into_any();
3147 };
3148
3149 let Some(command) = text_thread.read(cx).invoked_slash_command(&command_id) else {
3150 return Empty.into_any();
3151 };
3152
3153 h_flex()
3154 .id(fold_id)
3155 .px_1()
3156 .ml_6()
3157 .gap_2()
3158 .bg(cx.theme().colors().surface_background)
3159 .rounded_sm()
3160 .child(Label::new(format!("/{}", command.name)))
3161 .map(|parent| match &command.status {
3162 InvokedSlashCommandStatus::Running(_) => {
3163 parent.child(Icon::new(IconName::ArrowCircle).with_rotate_animation(4))
3164 }
3165 InvokedSlashCommandStatus::Error(message) => parent.child(
3166 Label::new(format!("error: {message}"))
3167 .single_line()
3168 .color(Color::Error),
3169 ),
3170 InvokedSlashCommandStatus::Finished => parent,
3171 })
3172 .into_any_element()
3173 }),
3174 type_tag: Some(TypeId::of::<PendingSlashCommand>()),
3175 }
3176}
3177
3178enum TokenState {
3179 NoTokensLeft {
3180 max_token_count: u64,
3181 token_count: u64,
3182 },
3183 HasMoreTokens {
3184 max_token_count: u64,
3185 token_count: u64,
3186 over_warn_threshold: bool,
3187 },
3188}
3189
3190fn token_state(text_thread: &Entity<TextThread>, cx: &App) -> Option<TokenState> {
3191 const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
3192
3193 let model = LanguageModelRegistry::read_global(cx)
3194 .default_model()?
3195 .model;
3196 let token_count = text_thread.read(cx).token_count()?;
3197 let max_token_count =
3198 model.max_token_count_for_mode(text_thread.read(cx).completion_mode().into());
3199 let token_state = if max_token_count.saturating_sub(token_count) == 0 {
3200 TokenState::NoTokensLeft {
3201 max_token_count,
3202 token_count,
3203 }
3204 } else {
3205 let over_warn_threshold =
3206 token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
3207 TokenState::HasMoreTokens {
3208 max_token_count,
3209 token_count,
3210 over_warn_threshold,
3211 }
3212 };
3213 Some(token_state)
3214}
3215
3216fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> {
3217 let image_size = data
3218 .size(0)
3219 .map(|dimension| Pixels::from(u32::from(dimension)));
3220 let image_ratio = image_size.width / image_size.height;
3221 let bounds_ratio = max_size.width / max_size.height;
3222
3223 if image_size.width > max_size.width || image_size.height > max_size.height {
3224 if bounds_ratio > image_ratio {
3225 size(
3226 image_size.width * (max_size.height / image_size.height),
3227 max_size.height,
3228 )
3229 } else {
3230 size(
3231 max_size.width,
3232 image_size.height * (max_size.width / image_size.width),
3233 )
3234 }
3235 } else {
3236 size(image_size.width, image_size.height)
3237 }
3238}
3239
3240pub fn humanize_token_count(count: u64) -> String {
3241 match count {
3242 0..=999 => count.to_string(),
3243 1000..=9999 => {
3244 let thousands = count / 1000;
3245 let hundreds = (count % 1000 + 50) / 100;
3246 if hundreds == 0 {
3247 format!("{}k", thousands)
3248 } else if hundreds == 10 {
3249 format!("{}k", thousands + 1)
3250 } else {
3251 format!("{}.{}k", thousands, hundreds)
3252 }
3253 }
3254 1_000_000..=9_999_999 => {
3255 let millions = count / 1_000_000;
3256 let hundred_thousands = (count % 1_000_000 + 50_000) / 100_000;
3257 if hundred_thousands == 0 {
3258 format!("{}M", millions)
3259 } else if hundred_thousands == 10 {
3260 format!("{}M", millions + 1)
3261 } else {
3262 format!("{}.{}M", millions, hundred_thousands)
3263 }
3264 }
3265 10_000_000.. => format!("{}M", (count + 500_000) / 1_000_000),
3266 _ => format!("{}k", (count + 500) / 1000),
3267 }
3268}
3269
3270pub fn make_lsp_adapter_delegate(
3271 project: &Entity<Project>,
3272 cx: &mut App,
3273) -> Result<Option<Arc<dyn LspAdapterDelegate>>> {
3274 project.update(cx, |project, cx| {
3275 // TODO: Find the right worktree.
3276 let Some(worktree) = project.worktrees(cx).next() else {
3277 return Ok(None::<Arc<dyn LspAdapterDelegate>>);
3278 };
3279 let http_client = project.client().http_client();
3280 project.lsp_store().update(cx, |_, cx| {
3281 Ok(Some(LocalLspAdapterDelegate::new(
3282 project.languages().clone(),
3283 project.environment(),
3284 cx.weak_entity(),
3285 &worktree,
3286 http_client,
3287 project.fs().clone(),
3288 cx,
3289 ) as Arc<dyn LspAdapterDelegate>))
3290 })
3291 })
3292}
3293
3294#[cfg(test)]
3295mod tests {
3296 use super::*;
3297 use editor::{MultiBufferOffset, SelectionEffects};
3298 use fs::FakeFs;
3299 use gpui::{App, TestAppContext, VisualTestContext};
3300 use indoc::indoc;
3301 use language::{Buffer, LanguageRegistry};
3302 use pretty_assertions::assert_eq;
3303 use prompt_store::PromptBuilder;
3304 use text::OffsetRangeExt;
3305 use unindent::Unindent;
3306 use util::path;
3307
3308 #[gpui::test]
3309 async fn test_copy_paste_whole_message(cx: &mut TestAppContext) {
3310 let (context, text_thread_editor, mut cx) = setup_text_thread_editor_text(vec![
3311 (Role::User, "What is the Zed editor?"),
3312 (
3313 Role::Assistant,
3314 "Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.",
3315 ),
3316 (Role::User, ""),
3317 ],cx).await;
3318
3319 // Select & Copy whole user message
3320 assert_copy_paste_text_thread_editor(
3321 &text_thread_editor,
3322 message_range(&context, 0, &mut cx),
3323 indoc! {"
3324 What is the Zed editor?
3325 Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
3326 What is the Zed editor?
3327 "},
3328 &mut cx,
3329 );
3330
3331 // Select & Copy whole assistant message
3332 assert_copy_paste_text_thread_editor(
3333 &text_thread_editor,
3334 message_range(&context, 1, &mut cx),
3335 indoc! {"
3336 What is the Zed editor?
3337 Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
3338 What is the Zed editor?
3339 Zed is a modern, high-performance code editor designed from the ground up for speed and collaboration.
3340 "},
3341 &mut cx,
3342 );
3343 }
3344
3345 #[gpui::test]
3346 async fn test_copy_paste_no_selection(cx: &mut TestAppContext) {
3347 let (context, text_thread_editor, mut cx) = setup_text_thread_editor_text(
3348 vec![
3349 (Role::User, "user1"),
3350 (Role::Assistant, "assistant1"),
3351 (Role::Assistant, "assistant2"),
3352 (Role::User, ""),
3353 ],
3354 cx,
3355 )
3356 .await;
3357
3358 // Copy and paste first assistant message
3359 let message_2_range = message_range(&context, 1, &mut cx);
3360 assert_copy_paste_text_thread_editor(
3361 &text_thread_editor,
3362 message_2_range.start..message_2_range.start,
3363 indoc! {"
3364 user1
3365 assistant1
3366 assistant2
3367 assistant1
3368 "},
3369 &mut cx,
3370 );
3371
3372 // Copy and cut second assistant message
3373 let message_3_range = message_range(&context, 2, &mut cx);
3374 assert_copy_paste_text_thread_editor(
3375 &text_thread_editor,
3376 message_3_range.start..message_3_range.start,
3377 indoc! {"
3378 user1
3379 assistant1
3380 assistant2
3381 assistant1
3382 assistant2
3383 "},
3384 &mut cx,
3385 );
3386 }
3387
3388 #[gpui::test]
3389 fn test_find_code_blocks(cx: &mut App) {
3390 let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
3391
3392 let buffer = cx.new(|cx| {
3393 let text = r#"
3394 line 0
3395 line 1
3396 ```rust
3397 fn main() {}
3398 ```
3399 line 5
3400 line 6
3401 line 7
3402 ```go
3403 func main() {}
3404 ```
3405 line 11
3406 ```
3407 this is plain text code block
3408 ```
3409
3410 ```go
3411 func another() {}
3412 ```
3413 line 19
3414 "#
3415 .unindent();
3416 let mut buffer = Buffer::local(text, cx);
3417 buffer.set_language(Some(markdown.clone()), cx);
3418 buffer
3419 });
3420 let snapshot = buffer.read(cx).snapshot();
3421
3422 let code_blocks = vec![
3423 Point::new(3, 0)..Point::new(4, 0),
3424 Point::new(9, 0)..Point::new(10, 0),
3425 Point::new(13, 0)..Point::new(14, 0),
3426 Point::new(17, 0)..Point::new(18, 0),
3427 ]
3428 .into_iter()
3429 .map(|range| snapshot.point_to_offset(range.start)..snapshot.point_to_offset(range.end))
3430 .collect::<Vec<_>>();
3431
3432 let expected_results = vec![
3433 (0, None),
3434 (1, None),
3435 (2, Some(code_blocks[0].clone())),
3436 (3, Some(code_blocks[0].clone())),
3437 (4, Some(code_blocks[0].clone())),
3438 (5, None),
3439 (6, None),
3440 (7, None),
3441 (8, Some(code_blocks[1].clone())),
3442 (9, Some(code_blocks[1].clone())),
3443 (10, Some(code_blocks[1].clone())),
3444 (11, None),
3445 (12, Some(code_blocks[2].clone())),
3446 (13, Some(code_blocks[2].clone())),
3447 (14, Some(code_blocks[2].clone())),
3448 (15, None),
3449 (16, Some(code_blocks[3].clone())),
3450 (17, Some(code_blocks[3].clone())),
3451 (18, Some(code_blocks[3].clone())),
3452 (19, None),
3453 ];
3454
3455 for (row, expected) in expected_results {
3456 let offset = snapshot.point_to_offset(Point::new(row, 0));
3457 let range = find_surrounding_code_block(&snapshot, offset);
3458 assert_eq!(range, expected, "unexpected result on row {:?}", row);
3459 }
3460 }
3461
3462 async fn setup_text_thread_editor_text(
3463 messages: Vec<(Role, &str)>,
3464 cx: &mut TestAppContext,
3465 ) -> (
3466 Entity<TextThread>,
3467 Entity<TextThreadEditor>,
3468 VisualTestContext,
3469 ) {
3470 cx.update(init_test);
3471
3472 let fs = FakeFs::new(cx.executor());
3473 let text_thread = create_text_thread_with_messages(messages, cx);
3474
3475 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
3476 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3477 let workspace = window.root(cx).unwrap();
3478 let mut cx = VisualTestContext::from_window(*window, cx);
3479
3480 let text_thread_editor = window
3481 .update(&mut cx, |_, window, cx| {
3482 cx.new(|cx| {
3483 TextThreadEditor::for_text_thread(
3484 text_thread.clone(),
3485 fs,
3486 workspace.downgrade(),
3487 project,
3488 None,
3489 window,
3490 cx,
3491 )
3492 })
3493 })
3494 .unwrap();
3495
3496 (text_thread, text_thread_editor, cx)
3497 }
3498
3499 fn message_range(
3500 text_thread: &Entity<TextThread>,
3501 message_ix: usize,
3502 cx: &mut TestAppContext,
3503 ) -> Range<MultiBufferOffset> {
3504 let range = text_thread.update(cx, |text_thread, cx| {
3505 text_thread
3506 .messages(cx)
3507 .nth(message_ix)
3508 .unwrap()
3509 .anchor_range
3510 .to_offset(&text_thread.buffer().read(cx).snapshot())
3511 });
3512 MultiBufferOffset(range.start)..MultiBufferOffset(range.end)
3513 }
3514
3515 fn assert_copy_paste_text_thread_editor<T: editor::ToOffset>(
3516 text_thread_editor: &Entity<TextThreadEditor>,
3517 range: Range<T>,
3518 expected_text: &str,
3519 cx: &mut VisualTestContext,
3520 ) {
3521 text_thread_editor.update_in(cx, |text_thread_editor, window, cx| {
3522 text_thread_editor.editor.update(cx, |editor, cx| {
3523 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3524 s.select_ranges([range])
3525 });
3526 });
3527
3528 text_thread_editor.copy(&Default::default(), window, cx);
3529
3530 text_thread_editor.editor.update(cx, |editor, cx| {
3531 editor.move_to_end(&Default::default(), window, cx);
3532 });
3533
3534 text_thread_editor.paste(&Default::default(), window, cx);
3535
3536 text_thread_editor.editor.update(cx, |editor, cx| {
3537 assert_eq!(editor.text(cx), expected_text);
3538 });
3539 });
3540 }
3541
3542 fn create_text_thread_with_messages(
3543 mut messages: Vec<(Role, &str)>,
3544 cx: &mut TestAppContext,
3545 ) -> Entity<TextThread> {
3546 let registry = Arc::new(LanguageRegistry::test(cx.executor()));
3547 let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
3548 cx.new(|cx| {
3549 let mut text_thread = TextThread::local(
3550 registry,
3551 None,
3552 prompt_builder.clone(),
3553 Arc::new(SlashCommandWorkingSet::default()),
3554 cx,
3555 );
3556 let mut message_1 = text_thread.messages(cx).next().unwrap();
3557 let (role, text) = messages.remove(0);
3558
3559 loop {
3560 if role == message_1.role {
3561 text_thread.buffer().update(cx, |buffer, cx| {
3562 buffer.edit([(message_1.offset_range, text)], None, cx);
3563 });
3564 break;
3565 }
3566 let mut ids = HashSet::default();
3567 ids.insert(message_1.id);
3568 text_thread.cycle_message_roles(ids, cx);
3569 message_1 = text_thread.messages(cx).next().unwrap();
3570 }
3571
3572 let mut last_message_id = message_1.id;
3573 for (role, text) in messages {
3574 text_thread.insert_message_after(last_message_id, role, MessageStatus::Done, cx);
3575 let message = text_thread.messages(cx).last().unwrap();
3576 last_message_id = message.id;
3577 text_thread.buffer().update(cx, |buffer, cx| {
3578 buffer.edit([(message.offset_range, text)], None, cx);
3579 })
3580 }
3581
3582 text_thread
3583 })
3584 }
3585
3586 fn init_test(cx: &mut App) {
3587 let settings_store = SettingsStore::test(cx);
3588 prompt_store::init(cx);
3589 LanguageModelRegistry::test(cx);
3590 cx.set_global(settings_store);
3591
3592 theme::init(theme::LoadThemes::JustBase, cx);
3593 }
3594}