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