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