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