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