1use crate::{
2 assistant_settings::{AssistantDockPosition, AssistantSettings},
3 humanize_token_count,
4 prompt_library::open_prompt_library,
5 slash_command::{
6 default_command::DefaultSlashCommand,
7 docs_command::{DocsSlashCommand, DocsSlashCommandArgs},
8 SlashCommandCompletionProvider, SlashCommandRegistry,
9 },
10 terminal_inline_assistant::TerminalInlineAssistant,
11 Assist, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore, CycleMessageRole,
12 DebugEditSteps, DeployHistory, DeployPromptLibrary, EditStep, EditStepState,
13 EditStepSuggestions, InlineAssist, InlineAssistId, InlineAssistant, InsertIntoEditor,
14 MessageStatus, ModelSelector, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
15 RemoteContextMetadata, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
16};
17use crate::{ContextStoreEvent, ShowConfiguration};
18use anyhow::{anyhow, Result};
19use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
20use client::proto;
21use collections::{BTreeSet, HashMap, HashSet};
22use editor::{
23 actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
24 display_map::{
25 BlockDisposition, BlockProperties, BlockStyle, Crease, CustomBlockId, RenderBlock,
26 ToDisplayPoint,
27 },
28 scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor},
29 Anchor, Editor, EditorEvent, ExcerptRange, MultiBuffer, RowExt, ToOffset as _, ToPoint,
30};
31use editor::{display_map::CreaseId, FoldPlaceholder};
32use fs::Fs;
33use gpui::{
34 div, percentage, point, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext,
35 AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity, EventEmitter,
36 FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Pixels,
37 Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
38 UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext,
39};
40use indexed_docs::IndexedDocsStore;
41use language::{
42 language_settings::SoftWrap, Capability, LanguageRegistry, LspAdapterDelegate, Point, ToOffset,
43};
44use language_model::{
45 provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId,
46 LanguageModelRegistry, Role,
47};
48use multi_buffer::MultiBufferRow;
49use picker::{Picker, PickerDelegate};
50use project::{Project, ProjectLspAdapterDelegate};
51use search::{buffer_search::DivRegistrar, BufferSearchBar};
52use settings::{update_settings_file, Settings};
53use smol::stream::StreamExt;
54use std::{
55 borrow::Cow,
56 cmp::{self, Ordering},
57 fmt::Write,
58 ops::Range,
59 path::PathBuf,
60 sync::Arc,
61 time::Duration,
62};
63use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
64use ui::TintColor;
65use ui::{
66 prelude::*,
67 utils::{format_distance_from_now, DateTimeType},
68 Avatar, AvatarShape, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
69 ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
70};
71use util::ResultExt;
72use workspace::{
73 dock::{DockPosition, Panel, PanelEvent},
74 item::{self, FollowableItem, Item, ItemHandle},
75 notifications::NotifyTaskExt,
76 pane::{self, SaveIntent},
77 searchable::{SearchEvent, SearchableItem},
78 Pane, Save, ToggleZoom, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
79};
80use workspace::{searchable::SearchableItemHandle, NewFile};
81
82pub fn init(cx: &mut AppContext) {
83 workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
84 cx.observe_new_views(
85 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
86 workspace
87 .register_action(|workspace, _: &ToggleFocus, cx| {
88 let settings = AssistantSettings::get_global(cx);
89 if !settings.enabled {
90 return;
91 }
92
93 workspace.toggle_panel_focus::<AssistantPanel>(cx);
94 })
95 .register_action(AssistantPanel::inline_assist)
96 .register_action(ContextEditor::quote_selection)
97 .register_action(ContextEditor::insert_selection)
98 .register_action(AssistantPanel::show_configuration);
99 },
100 )
101 .detach();
102
103 cx.observe_new_views(
104 |terminal_panel: &mut TerminalPanel, cx: &mut ViewContext<TerminalPanel>| {
105 let settings = AssistantSettings::get_global(cx);
106 if !settings.enabled {
107 return;
108 }
109
110 terminal_panel.register_tab_bar_button(cx.new_view(|_| InlineAssistTabBarButton), cx);
111 },
112 )
113 .detach();
114}
115
116struct InlineAssistTabBarButton;
117
118impl Render for InlineAssistTabBarButton {
119 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
120 IconButton::new("terminal_inline_assistant", IconName::MagicWand)
121 .icon_size(IconSize::Small)
122 .on_click(cx.listener(|_, _, cx| {
123 cx.dispatch_action(InlineAssist::default().boxed_clone());
124 }))
125 .tooltip(move |cx| Tooltip::for_action("Inline Assist", &InlineAssist::default(), cx))
126 }
127}
128
129pub enum AssistantPanelEvent {
130 ContextEdited,
131}
132
133pub struct AssistantPanel {
134 pane: View<Pane>,
135 workspace: WeakView<Workspace>,
136 width: Option<Pixels>,
137 height: Option<Pixels>,
138 project: Model<Project>,
139 context_store: Model<ContextStore>,
140 languages: Arc<LanguageRegistry>,
141 fs: Arc<dyn Fs>,
142 subscriptions: Vec<Subscription>,
143 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
144 model_summary_editor: View<Editor>,
145 authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
146 configuration_subscription: Option<Subscription>,
147 watch_client_status: Option<Task<()>>,
148 nudge_sign_in: bool,
149}
150
151#[derive(Clone)]
152enum ContextMetadata {
153 Remote(RemoteContextMetadata),
154 Saved(SavedContextMetadata),
155}
156
157struct SavedContextPickerDelegate {
158 store: Model<ContextStore>,
159 project: Model<Project>,
160 matches: Vec<ContextMetadata>,
161 selected_index: usize,
162}
163
164enum SavedContextPickerEvent {
165 Confirmed(ContextMetadata),
166}
167
168enum InlineAssistTarget {
169 Editor(View<Editor>, bool),
170 Terminal(View<TerminalView>),
171}
172
173impl EventEmitter<SavedContextPickerEvent> for Picker<SavedContextPickerDelegate> {}
174
175impl SavedContextPickerDelegate {
176 fn new(project: Model<Project>, store: Model<ContextStore>) -> Self {
177 Self {
178 project,
179 store,
180 matches: Vec::new(),
181 selected_index: 0,
182 }
183 }
184}
185
186impl PickerDelegate for SavedContextPickerDelegate {
187 type ListItem = ListItem;
188
189 fn match_count(&self) -> usize {
190 self.matches.len()
191 }
192
193 fn selected_index(&self) -> usize {
194 self.selected_index
195 }
196
197 fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
198 self.selected_index = ix;
199 }
200
201 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
202 "Search...".into()
203 }
204
205 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
206 let search = self.store.read(cx).search(query, cx);
207 cx.spawn(|this, mut cx| async move {
208 let matches = search.await;
209 this.update(&mut cx, |this, cx| {
210 let host_contexts = this.delegate.store.read(cx).host_contexts();
211 this.delegate.matches = host_contexts
212 .iter()
213 .cloned()
214 .map(ContextMetadata::Remote)
215 .chain(matches.into_iter().map(ContextMetadata::Saved))
216 .collect();
217 this.delegate.selected_index = 0;
218 cx.notify();
219 })
220 .ok();
221 })
222 }
223
224 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
225 if let Some(metadata) = self.matches.get(self.selected_index) {
226 cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone()));
227 }
228 }
229
230 fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
231
232 fn render_match(
233 &self,
234 ix: usize,
235 selected: bool,
236 cx: &mut ViewContext<Picker<Self>>,
237 ) -> Option<Self::ListItem> {
238 let context = self.matches.get(ix)?;
239 let item = match context {
240 ContextMetadata::Remote(context) => {
241 let host_user = self.project.read(cx).host().and_then(|collaborator| {
242 self.project
243 .read(cx)
244 .user_store()
245 .read(cx)
246 .get_cached_user(collaborator.user_id)
247 });
248 div()
249 .flex()
250 .w_full()
251 .justify_between()
252 .gap_2()
253 .child(
254 h_flex().flex_1().overflow_x_hidden().child(
255 Label::new(context.summary.clone().unwrap_or("New Context".into()))
256 .size(LabelSize::Small),
257 ),
258 )
259 .child(
260 h_flex()
261 .gap_2()
262 .children(if let Some(host_user) = host_user {
263 vec![
264 Avatar::new(host_user.avatar_uri.clone())
265 .shape(AvatarShape::Circle)
266 .into_any_element(),
267 Label::new(format!("Shared by @{}", host_user.github_login))
268 .color(Color::Muted)
269 .size(LabelSize::Small)
270 .into_any_element(),
271 ]
272 } else {
273 vec![Label::new("Shared by host")
274 .color(Color::Muted)
275 .size(LabelSize::Small)
276 .into_any_element()]
277 }),
278 )
279 }
280 ContextMetadata::Saved(context) => div()
281 .flex()
282 .w_full()
283 .justify_between()
284 .gap_2()
285 .child(
286 h_flex()
287 .flex_1()
288 .child(Label::new(context.title.clone()).size(LabelSize::Small))
289 .overflow_x_hidden(),
290 )
291 .child(
292 Label::new(format_distance_from_now(
293 DateTimeType::Local(context.mtime),
294 false,
295 true,
296 true,
297 ))
298 .color(Color::Muted)
299 .size(LabelSize::Small),
300 ),
301 };
302 Some(
303 ListItem::new(ix)
304 .inset(true)
305 .spacing(ListItemSpacing::Sparse)
306 .selected(selected)
307 .child(item),
308 )
309 }
310}
311
312impl AssistantPanel {
313 pub fn load(
314 workspace: WeakView<Workspace>,
315 cx: AsyncWindowContext,
316 ) -> Task<Result<View<Self>>> {
317 cx.spawn(|mut cx| async move {
318 let context_store = workspace
319 .update(&mut cx, |workspace, cx| {
320 ContextStore::new(workspace.project().clone(), cx)
321 })?
322 .await?;
323 workspace.update(&mut cx, |workspace, cx| {
324 // TODO: deserialize state.
325 cx.new_view(|cx| Self::new(workspace, context_store, cx))
326 })
327 })
328 }
329
330 fn new(
331 workspace: &Workspace,
332 context_store: Model<ContextStore>,
333 cx: &mut ViewContext<Self>,
334 ) -> Self {
335 let model_selector_menu_handle = PopoverMenuHandle::default();
336 let model_summary_editor = cx.new_view(|cx| Editor::single_line(cx));
337 let context_editor_toolbar = cx.new_view(|_| {
338 ContextEditorToolbarItem::new(
339 workspace,
340 model_selector_menu_handle.clone(),
341 model_summary_editor.clone(),
342 )
343 });
344 let pane = cx.new_view(|cx| {
345 let mut pane = Pane::new(
346 workspace.weak_handle(),
347 workspace.project().clone(),
348 Default::default(),
349 None,
350 NewFile.boxed_clone(),
351 cx,
352 );
353 pane.set_can_split(false, cx);
354 pane.set_can_navigate(true, cx);
355 pane.display_nav_history_buttons(None);
356 pane.set_should_display_tab_bar(|_| true);
357 pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
358 h_flex()
359 .gap(Spacing::Small.rems(cx))
360 .child(
361 IconButton::new("menu", IconName::Menu)
362 .icon_size(IconSize::Small)
363 .on_click(cx.listener(|pane, _, cx| {
364 let zoom_label = if pane.is_zoomed() {
365 "Zoom Out"
366 } else {
367 "Zoom In"
368 };
369 let menu = ContextMenu::build(cx, |menu, cx| {
370 menu.context(pane.focus_handle(cx))
371 .action("New Context", Box::new(NewFile))
372 .action("History", Box::new(DeployHistory))
373 .action("Prompt Library", Box::new(DeployPromptLibrary))
374 .action("Configure", Box::new(ShowConfiguration))
375 .action(zoom_label, Box::new(ToggleZoom))
376 });
377 cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| {
378 pane.new_item_menu = None;
379 })
380 .detach();
381 pane.new_item_menu = Some(menu);
382 })),
383 )
384 .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
385 el.child(Pane::render_menu_overlay(new_item_menu))
386 })
387 .into_any_element()
388 .into()
389 });
390 pane.toolbar().update(cx, |toolbar, cx| {
391 toolbar.add_item(context_editor_toolbar.clone(), cx);
392 toolbar.add_item(cx.new_view(BufferSearchBar::new), cx)
393 });
394 pane
395 });
396
397 let subscriptions = vec![
398 cx.observe(&pane, |_, _, cx| cx.notify()),
399 cx.subscribe(&pane, Self::handle_pane_event),
400 cx.subscribe(&context_editor_toolbar, Self::handle_toolbar_event),
401 cx.subscribe(&model_summary_editor, Self::handle_summary_editor_event),
402 cx.subscribe(&context_store, Self::handle_context_store_event),
403 cx.subscribe(
404 &LanguageModelRegistry::global(cx),
405 |this, _, event: &language_model::Event, cx| match event {
406 language_model::Event::ActiveModelChanged => {
407 this.completion_provider_changed(cx);
408 }
409 language_model::Event::ProviderStateChanged => {
410 this.ensure_authenticated(cx);
411 }
412 language_model::Event::AddedProvider(_)
413 | language_model::Event::RemovedProvider(_) => {
414 this.ensure_authenticated(cx);
415 }
416 },
417 ),
418 ];
419
420 let mut status_rx = workspace.client().clone().status();
421
422 let watch_client_status = cx.spawn(|this, mut cx| async move {
423 let mut old_status = None;
424 while let Some(status) = status_rx.next().await {
425 if old_status.is_none()
426 || old_status.map_or(false, |old_status| old_status != status)
427 {
428 if status.is_signed_out() {
429 this.update(&mut cx, |this, cx| {
430 let active_provider =
431 LanguageModelRegistry::read_global(cx).active_provider();
432
433 // If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
434 // the provider, we want to show a nudge to sign in.
435 if active_provider
436 .map_or(true, |provider| provider.id().0 == PROVIDER_ID)
437 {
438 println!("TODO: Nudge the user to sign in and use Zed AI");
439 this.nudge_sign_in = true;
440 }
441 })
442 .log_err();
443 };
444
445 old_status = Some(status);
446 }
447 }
448 this.update(&mut cx, |this, _cx| this.watch_client_status = None)
449 .log_err();
450 });
451
452 let mut this = Self {
453 pane,
454 workspace: workspace.weak_handle(),
455 width: None,
456 height: None,
457 project: workspace.project().clone(),
458 context_store,
459 languages: workspace.app_state().languages.clone(),
460 fs: workspace.app_state().fs.clone(),
461 subscriptions,
462 model_selector_menu_handle,
463 model_summary_editor,
464 authenticate_provider_task: None,
465 configuration_subscription: None,
466 watch_client_status: Some(watch_client_status),
467 // TODO: This is unused!
468 nudge_sign_in: false,
469 };
470 this.new_context(cx);
471 this
472 }
473
474 fn handle_pane_event(
475 &mut self,
476 pane: View<Pane>,
477 event: &pane::Event,
478 cx: &mut ViewContext<Self>,
479 ) {
480 let update_model_summary = match event {
481 pane::Event::Remove => {
482 cx.emit(PanelEvent::Close);
483 false
484 }
485 pane::Event::ZoomIn => {
486 cx.emit(PanelEvent::ZoomIn);
487 false
488 }
489 pane::Event::ZoomOut => {
490 cx.emit(PanelEvent::ZoomOut);
491 false
492 }
493
494 pane::Event::AddItem { item } => {
495 self.workspace
496 .update(cx, |workspace, cx| {
497 item.added_to_pane(workspace, self.pane.clone(), cx)
498 })
499 .ok();
500 true
501 }
502
503 pane::Event::ActivateItem { local } => {
504 if *local {
505 self.workspace
506 .update(cx, |workspace, cx| {
507 workspace.unfollow_in_pane(&pane, cx);
508 })
509 .ok();
510 }
511 cx.emit(AssistantPanelEvent::ContextEdited);
512 true
513 }
514
515 pane::Event::RemoveItem { idx } => {
516 if self
517 .pane
518 .read(cx)
519 .item_for_index(*idx)
520 .map_or(false, |item| item.downcast::<ConfigurationView>().is_some())
521 {
522 self.configuration_subscription = None;
523 }
524 false
525 }
526 pane::Event::RemovedItem { .. } => {
527 cx.emit(AssistantPanelEvent::ContextEdited);
528 true
529 }
530
531 _ => false,
532 };
533
534 if update_model_summary {
535 if let Some(editor) = self.active_context_editor(cx) {
536 self.show_updated_summary(&editor, cx)
537 }
538 }
539 }
540
541 fn handle_summary_editor_event(
542 &mut self,
543 model_summary_editor: View<Editor>,
544 event: &EditorEvent,
545 cx: &mut ViewContext<Self>,
546 ) {
547 if matches!(event, EditorEvent::Edited { .. }) {
548 if let Some(context_editor) = self.active_context_editor(cx) {
549 let new_summary = model_summary_editor.read(cx).text(cx);
550 context_editor.update(cx, |context_editor, cx| {
551 context_editor.context.update(cx, |context, cx| {
552 if context.summary().is_none()
553 && (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty())
554 {
555 return;
556 }
557 context.custom_summary(new_summary, cx)
558 });
559 });
560 }
561 }
562 }
563
564 fn handle_toolbar_event(
565 &mut self,
566 _: View<ContextEditorToolbarItem>,
567 _: &ContextEditorToolbarItemEvent,
568 cx: &mut ViewContext<Self>,
569 ) {
570 if let Some(context_editor) = self.active_context_editor(cx) {
571 context_editor.update(cx, |context_editor, cx| {
572 context_editor.context.update(cx, |context, cx| {
573 context.summarize(true, cx);
574 })
575 })
576 }
577 }
578
579 fn handle_context_store_event(
580 &mut self,
581 _context_store: Model<ContextStore>,
582 event: &ContextStoreEvent,
583 cx: &mut ViewContext<Self>,
584 ) {
585 let ContextStoreEvent::ContextCreated(context_id) = event;
586 let Some(context) = self
587 .context_store
588 .read(cx)
589 .loaded_context_for_id(&context_id, cx)
590 else {
591 log::error!("no context found with ID: {}", context_id.to_proto());
592 return;
593 };
594 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
595
596 let assistant_panel = cx.view().downgrade();
597 let editor = cx.new_view(|cx| {
598 let mut editor = ContextEditor::for_context(
599 context,
600 self.fs.clone(),
601 self.workspace.clone(),
602 self.project.clone(),
603 lsp_adapter_delegate,
604 assistant_panel,
605 cx,
606 );
607 editor.insert_default_prompt(cx);
608 editor
609 });
610
611 self.show_context(editor.clone(), cx);
612 }
613
614 fn completion_provider_changed(&mut self, cx: &mut ViewContext<Self>) {
615 if let Some(editor) = self.active_context_editor(cx) {
616 editor.update(cx, |active_context, cx| {
617 active_context
618 .context
619 .update(cx, |context, cx| context.completion_provider_changed(cx))
620 })
621 }
622
623 let Some(new_provider_id) = LanguageModelRegistry::read_global(cx)
624 .active_provider()
625 .map(|p| p.id())
626 else {
627 return;
628 };
629
630 if self
631 .authenticate_provider_task
632 .as_ref()
633 .map_or(true, |(old_provider_id, _)| {
634 *old_provider_id != new_provider_id
635 })
636 {
637 self.authenticate_provider_task = None;
638 self.ensure_authenticated(cx);
639 }
640 }
641
642 fn ensure_authenticated(&mut self, cx: &mut ViewContext<Self>) {
643 if self.is_authenticated(cx) {
644 return;
645 }
646
647 let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
648 return;
649 };
650
651 let load_credentials = self.authenticate(cx);
652
653 if self.authenticate_provider_task.is_none() {
654 self.authenticate_provider_task = Some((
655 provider.id(),
656 cx.spawn(|this, mut cx| async move {
657 let _ = load_credentials.await;
658 this.update(&mut cx, |this, _cx| {
659 this.authenticate_provider_task = None;
660 })
661 .log_err();
662 }),
663 ));
664 }
665 }
666
667 pub fn inline_assist(
668 workspace: &mut Workspace,
669 action: &InlineAssist,
670 cx: &mut ViewContext<Workspace>,
671 ) {
672 let settings = AssistantSettings::get_global(cx);
673 if !settings.enabled {
674 return;
675 }
676
677 let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
678 return;
679 };
680
681 let Some(inline_assist_target) =
682 Self::resolve_inline_assist_target(workspace, &assistant_panel, cx)
683 else {
684 return;
685 };
686
687 let initial_prompt = action.prompt.clone();
688 if assistant_panel.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
689 match inline_assist_target {
690 InlineAssistTarget::Editor(active_editor, include_context) => {
691 InlineAssistant::update_global(cx, |assistant, cx| {
692 assistant.assist(
693 &active_editor,
694 Some(cx.view().downgrade()),
695 include_context.then_some(&assistant_panel),
696 initial_prompt,
697 cx,
698 )
699 })
700 }
701 InlineAssistTarget::Terminal(active_terminal) => {
702 TerminalInlineAssistant::update_global(cx, |assistant, cx| {
703 assistant.assist(
704 &active_terminal,
705 Some(cx.view().downgrade()),
706 Some(&assistant_panel),
707 initial_prompt,
708 cx,
709 )
710 })
711 }
712 }
713 } else {
714 let assistant_panel = assistant_panel.downgrade();
715 cx.spawn(|workspace, mut cx| async move {
716 assistant_panel
717 .update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
718 .await?;
719 if assistant_panel.update(&mut cx, |panel, cx| panel.is_authenticated(cx))? {
720 cx.update(|cx| match inline_assist_target {
721 InlineAssistTarget::Editor(active_editor, include_context) => {
722 let assistant_panel = if include_context {
723 assistant_panel.upgrade()
724 } else {
725 None
726 };
727 InlineAssistant::update_global(cx, |assistant, cx| {
728 assistant.assist(
729 &active_editor,
730 Some(workspace),
731 assistant_panel.as_ref(),
732 initial_prompt,
733 cx,
734 )
735 })
736 }
737 InlineAssistTarget::Terminal(active_terminal) => {
738 TerminalInlineAssistant::update_global(cx, |assistant, cx| {
739 assistant.assist(
740 &active_terminal,
741 Some(workspace),
742 assistant_panel.upgrade().as_ref(),
743 initial_prompt,
744 cx,
745 )
746 })
747 }
748 })?
749 } else {
750 workspace.update(&mut cx, |workspace, cx| {
751 workspace.focus_panel::<AssistantPanel>(cx)
752 })?;
753 }
754
755 anyhow::Ok(())
756 })
757 .detach_and_log_err(cx)
758 }
759 }
760
761 fn resolve_inline_assist_target(
762 workspace: &mut Workspace,
763 assistant_panel: &View<AssistantPanel>,
764 cx: &mut WindowContext,
765 ) -> Option<InlineAssistTarget> {
766 if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
767 if terminal_panel
768 .read(cx)
769 .focus_handle(cx)
770 .contains_focused(cx)
771 {
772 if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
773 pane.read(cx)
774 .active_item()
775 .and_then(|t| t.downcast::<TerminalView>())
776 }) {
777 return Some(InlineAssistTarget::Terminal(terminal_view));
778 }
779 }
780 }
781 let context_editor =
782 assistant_panel
783 .read(cx)
784 .active_context_editor(cx)
785 .and_then(|editor| {
786 let editor = &editor.read(cx).editor;
787 if editor.read(cx).is_focused(cx) {
788 Some(editor.clone())
789 } else {
790 None
791 }
792 });
793
794 if let Some(context_editor) = context_editor {
795 Some(InlineAssistTarget::Editor(context_editor, false))
796 } else if let Some(workspace_editor) = workspace
797 .active_item(cx)
798 .and_then(|item| item.act_as::<Editor>(cx))
799 {
800 Some(InlineAssistTarget::Editor(workspace_editor, true))
801 } else {
802 None
803 }
804 }
805
806 fn new_context(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ContextEditor>> {
807 if self.project.read(cx).is_remote() {
808 let task = self
809 .context_store
810 .update(cx, |store, cx| store.create_remote_context(cx));
811
812 cx.spawn(|this, mut cx| async move {
813 let context = task.await?;
814
815 this.update(&mut cx, |this, cx| {
816 let workspace = this.workspace.clone();
817 let project = this.project.clone();
818 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
819
820 let fs = this.fs.clone();
821 let project = this.project.clone();
822 let weak_assistant_panel = cx.view().downgrade();
823
824 let editor = cx.new_view(|cx| {
825 ContextEditor::for_context(
826 context,
827 fs,
828 workspace,
829 project,
830 lsp_adapter_delegate,
831 weak_assistant_panel,
832 cx,
833 )
834 });
835
836 this.show_context(editor, cx);
837
838 anyhow::Ok(())
839 })??;
840
841 anyhow::Ok(())
842 })
843 .detach_and_log_err(cx);
844
845 None
846 } else {
847 let context = self.context_store.update(cx, |store, cx| store.create(cx));
848 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
849
850 let assistant_panel = cx.view().downgrade();
851 let editor = cx.new_view(|cx| {
852 let mut editor = ContextEditor::for_context(
853 context,
854 self.fs.clone(),
855 self.workspace.clone(),
856 self.project.clone(),
857 lsp_adapter_delegate,
858 assistant_panel,
859 cx,
860 );
861 editor.insert_default_prompt(cx);
862 editor
863 });
864
865 self.show_context(editor.clone(), cx);
866 Some(editor)
867 }
868 }
869
870 fn show_context(&mut self, context_editor: View<ContextEditor>, cx: &mut ViewContext<Self>) {
871 let focus = self.focus_handle(cx).contains_focused(cx);
872 let prev_len = self.pane.read(cx).items_len();
873 self.pane.update(cx, |pane, cx| {
874 pane.add_item(Box::new(context_editor.clone()), focus, focus, None, cx)
875 });
876
877 if prev_len != self.pane.read(cx).items_len() {
878 self.subscriptions
879 .push(cx.subscribe(&context_editor, Self::handle_context_editor_event));
880 }
881
882 self.show_updated_summary(&context_editor, cx);
883
884 cx.emit(AssistantPanelEvent::ContextEdited);
885 cx.notify();
886 }
887
888 fn show_updated_summary(
889 &self,
890 context_editor: &View<ContextEditor>,
891 cx: &mut ViewContext<Self>,
892 ) {
893 context_editor.update(cx, |context_editor, cx| {
894 let new_summary = context_editor
895 .context
896 .read(cx)
897 .summary()
898 .map(|s| s.text.clone())
899 .unwrap_or_else(|| context_editor.title(cx).to_string());
900 self.model_summary_editor.update(cx, |summary_editor, cx| {
901 if summary_editor.text(cx) != new_summary {
902 summary_editor.set_text(new_summary, cx);
903 }
904 });
905 });
906 }
907
908 fn handle_context_editor_event(
909 &mut self,
910 context_editor: View<ContextEditor>,
911 event: &EditorEvent,
912 cx: &mut ViewContext<Self>,
913 ) {
914 match event {
915 EditorEvent::TitleChanged => {
916 self.show_updated_summary(&context_editor, cx);
917 cx.notify()
918 }
919 EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited),
920 _ => {}
921 }
922 }
923
924 fn show_configuration(
925 workspace: &mut Workspace,
926 _: &ShowConfiguration,
927 cx: &mut ViewContext<Workspace>,
928 ) {
929 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
930 return;
931 };
932
933 if !panel.focus_handle(cx).contains_focused(cx) {
934 workspace.toggle_panel_focus::<AssistantPanel>(cx);
935 }
936
937 panel.update(cx, |this, cx| {
938 this.show_configuration_tab(cx);
939 })
940 }
941
942 fn show_configuration_tab(&mut self, cx: &mut ViewContext<Self>) {
943 let configuration_item_ix = self
944 .pane
945 .read(cx)
946 .items()
947 .position(|item| item.downcast::<ConfigurationView>().is_some());
948
949 if let Some(configuration_item_ix) = configuration_item_ix {
950 self.pane.update(cx, |pane, cx| {
951 pane.activate_item(configuration_item_ix, true, true, cx);
952 });
953 } else {
954 let configuration = cx.new_view(|cx| ConfigurationView::new(cx));
955 self.configuration_subscription = Some(cx.subscribe(
956 &configuration,
957 |this, _, event: &ConfigurationViewEvent, cx| match event {
958 ConfigurationViewEvent::NewProviderContextEditor(provider) => {
959 if LanguageModelRegistry::read_global(cx)
960 .active_provider()
961 .map_or(true, |p| p.id() != provider.id())
962 {
963 if let Some(model) = provider.provided_models(cx).first().cloned() {
964 update_settings_file::<AssistantSettings>(
965 this.fs.clone(),
966 cx,
967 move |settings, _| settings.set_model(model),
968 );
969 }
970 }
971
972 this.new_context(cx);
973 }
974 },
975 ));
976 self.pane.update(cx, |pane, cx| {
977 pane.add_item(Box::new(configuration), true, true, None, cx);
978 });
979 }
980 }
981
982 fn deploy_history(&mut self, _: &DeployHistory, cx: &mut ViewContext<Self>) {
983 let history_item_ix = self
984 .pane
985 .read(cx)
986 .items()
987 .position(|item| item.downcast::<ContextHistory>().is_some());
988
989 if let Some(history_item_ix) = history_item_ix {
990 self.pane.update(cx, |pane, cx| {
991 pane.activate_item(history_item_ix, true, true, cx);
992 });
993 } else {
994 let assistant_panel = cx.view().downgrade();
995 let history = cx.new_view(|cx| {
996 ContextHistory::new(
997 self.project.clone(),
998 self.context_store.clone(),
999 assistant_panel,
1000 cx,
1001 )
1002 });
1003 self.pane.update(cx, |pane, cx| {
1004 pane.add_item(Box::new(history), true, true, None, cx);
1005 });
1006 }
1007 }
1008
1009 fn deploy_prompt_library(&mut self, _: &DeployPromptLibrary, cx: &mut ViewContext<Self>) {
1010 open_prompt_library(self.languages.clone(), cx).detach_and_log_err(cx);
1011 }
1012
1013 fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
1014 self.model_selector_menu_handle.toggle(cx);
1015 }
1016
1017 fn active_context_editor(&self, cx: &AppContext) -> Option<View<ContextEditor>> {
1018 self.pane
1019 .read(cx)
1020 .active_item()?
1021 .downcast::<ContextEditor>()
1022 }
1023
1024 pub fn active_context(&self, cx: &AppContext) -> Option<Model<Context>> {
1025 Some(self.active_context_editor(cx)?.read(cx).context.clone())
1026 }
1027
1028 fn open_saved_context(
1029 &mut self,
1030 path: PathBuf,
1031 cx: &mut ViewContext<Self>,
1032 ) -> Task<Result<()>> {
1033 let existing_context = self.pane.read(cx).items().find_map(|item| {
1034 item.downcast::<ContextEditor>()
1035 .filter(|editor| editor.read(cx).context.read(cx).path() == Some(&path))
1036 });
1037 if let Some(existing_context) = existing_context {
1038 return cx.spawn(|this, mut cx| async move {
1039 this.update(&mut cx, |this, cx| this.show_context(existing_context, cx))
1040 });
1041 }
1042
1043 let context = self
1044 .context_store
1045 .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
1046 let fs = self.fs.clone();
1047 let project = self.project.clone();
1048 let workspace = self.workspace.clone();
1049
1050 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
1051
1052 cx.spawn(|this, mut cx| async move {
1053 let context = context.await?;
1054 let assistant_panel = this.clone();
1055 this.update(&mut cx, |this, cx| {
1056 let editor = cx.new_view(|cx| {
1057 ContextEditor::for_context(
1058 context,
1059 fs,
1060 workspace,
1061 project,
1062 lsp_adapter_delegate,
1063 assistant_panel,
1064 cx,
1065 )
1066 });
1067 this.show_context(editor, cx);
1068 anyhow::Ok(())
1069 })??;
1070 Ok(())
1071 })
1072 }
1073
1074 fn open_remote_context(
1075 &mut self,
1076 id: ContextId,
1077 cx: &mut ViewContext<Self>,
1078 ) -> Task<Result<View<ContextEditor>>> {
1079 let existing_context = self.pane.read(cx).items().find_map(|item| {
1080 item.downcast::<ContextEditor>()
1081 .filter(|editor| *editor.read(cx).context.read(cx).id() == id)
1082 });
1083 if let Some(existing_context) = existing_context {
1084 return cx.spawn(|this, mut cx| async move {
1085 this.update(&mut cx, |this, cx| {
1086 this.show_context(existing_context.clone(), cx)
1087 })?;
1088 Ok(existing_context)
1089 });
1090 }
1091
1092 let context = self
1093 .context_store
1094 .update(cx, |store, cx| store.open_remote_context(id, cx));
1095 let fs = self.fs.clone();
1096 let workspace = self.workspace.clone();
1097 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
1098
1099 cx.spawn(|this, mut cx| async move {
1100 let context = context.await?;
1101 let assistant_panel = this.clone();
1102 this.update(&mut cx, |this, cx| {
1103 let editor = cx.new_view(|cx| {
1104 ContextEditor::for_context(
1105 context,
1106 fs,
1107 workspace,
1108 this.project.clone(),
1109 lsp_adapter_delegate,
1110 assistant_panel,
1111 cx,
1112 )
1113 });
1114 this.show_context(editor.clone(), cx);
1115 anyhow::Ok(editor)
1116 })?
1117 })
1118 }
1119
1120 fn is_authenticated(&mut self, cx: &mut ViewContext<Self>) -> bool {
1121 LanguageModelRegistry::read_global(cx)
1122 .active_provider()
1123 .map_or(false, |provider| provider.is_authenticated(cx))
1124 }
1125
1126 fn authenticate(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
1127 LanguageModelRegistry::read_global(cx)
1128 .active_provider()
1129 .map_or(
1130 Task::ready(Err(anyhow!("no active language model provider"))),
1131 |provider| provider.authenticate(cx),
1132 )
1133 }
1134}
1135
1136impl Render for AssistantPanel {
1137 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1138 let mut registrar = DivRegistrar::new(
1139 |panel, cx| {
1140 panel
1141 .pane
1142 .read(cx)
1143 .toolbar()
1144 .read(cx)
1145 .item_of_type::<BufferSearchBar>()
1146 },
1147 cx,
1148 );
1149 BufferSearchBar::register(&mut registrar);
1150 let registrar = registrar.into_div();
1151
1152 v_flex()
1153 .key_context("AssistantPanel")
1154 .size_full()
1155 .on_action(cx.listener(|this, _: &workspace::NewFile, cx| {
1156 this.new_context(cx);
1157 }))
1158 .on_action(
1159 cx.listener(|this, _: &ShowConfiguration, cx| this.show_configuration_tab(cx)),
1160 )
1161 .on_action(cx.listener(AssistantPanel::deploy_history))
1162 .on_action(cx.listener(AssistantPanel::deploy_prompt_library))
1163 .on_action(cx.listener(AssistantPanel::toggle_model_selector))
1164 .child(registrar.size_full().child(self.pane.clone()))
1165 .into_any_element()
1166 }
1167}
1168
1169impl Panel for AssistantPanel {
1170 fn persistent_name() -> &'static str {
1171 "AssistantPanel"
1172 }
1173
1174 fn position(&self, cx: &WindowContext) -> DockPosition {
1175 match AssistantSettings::get_global(cx).dock {
1176 AssistantDockPosition::Left => DockPosition::Left,
1177 AssistantDockPosition::Bottom => DockPosition::Bottom,
1178 AssistantDockPosition::Right => DockPosition::Right,
1179 }
1180 }
1181
1182 fn position_is_valid(&self, _: DockPosition) -> bool {
1183 true
1184 }
1185
1186 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1187 settings::update_settings_file::<AssistantSettings>(
1188 self.fs.clone(),
1189 cx,
1190 move |settings, _| {
1191 let dock = match position {
1192 DockPosition::Left => AssistantDockPosition::Left,
1193 DockPosition::Bottom => AssistantDockPosition::Bottom,
1194 DockPosition::Right => AssistantDockPosition::Right,
1195 };
1196 settings.set_dock(dock);
1197 },
1198 );
1199 }
1200
1201 fn size(&self, cx: &WindowContext) -> Pixels {
1202 let settings = AssistantSettings::get_global(cx);
1203 match self.position(cx) {
1204 DockPosition::Left | DockPosition::Right => {
1205 self.width.unwrap_or(settings.default_width)
1206 }
1207 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1208 }
1209 }
1210
1211 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1212 match self.position(cx) {
1213 DockPosition::Left | DockPosition::Right => self.width = size,
1214 DockPosition::Bottom => self.height = size,
1215 }
1216 cx.notify();
1217 }
1218
1219 fn is_zoomed(&self, cx: &WindowContext) -> bool {
1220 self.pane.read(cx).is_zoomed()
1221 }
1222
1223 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1224 self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
1225 }
1226
1227 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1228 if active {
1229 if self.pane.read(cx).items_len() == 0 {
1230 self.new_context(cx);
1231 }
1232
1233 self.ensure_authenticated(cx);
1234 }
1235 }
1236
1237 fn pane(&self) -> Option<View<Pane>> {
1238 Some(self.pane.clone())
1239 }
1240
1241 fn remote_id() -> Option<proto::PanelId> {
1242 Some(proto::PanelId::AssistantPanel)
1243 }
1244
1245 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
1246 let settings = AssistantSettings::get_global(cx);
1247 if !settings.enabled || !settings.button {
1248 return None;
1249 }
1250
1251 Some(IconName::ZedAssistant)
1252 }
1253
1254 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1255 Some("Assistant Panel")
1256 }
1257
1258 fn toggle_action(&self) -> Box<dyn Action> {
1259 Box::new(ToggleFocus)
1260 }
1261}
1262
1263impl EventEmitter<PanelEvent> for AssistantPanel {}
1264impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}
1265
1266impl FocusableView for AssistantPanel {
1267 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
1268 self.pane.focus_handle(cx)
1269 }
1270}
1271
1272pub enum ContextEditorEvent {
1273 Edited,
1274 TabContentChanged,
1275}
1276
1277#[derive(Copy, Clone, Debug, PartialEq)]
1278struct ScrollPosition {
1279 offset_before_cursor: gpui::Point<f32>,
1280 cursor: Anchor,
1281}
1282
1283struct ActiveEditStep {
1284 start: language::Anchor,
1285 assist_ids: Vec<InlineAssistId>,
1286 editor: Option<WeakView<Editor>>,
1287 _open_editor: Task<Result<()>>,
1288}
1289
1290pub struct ContextEditor {
1291 context: Model<Context>,
1292 fs: Arc<dyn Fs>,
1293 workspace: WeakView<Workspace>,
1294 project: Model<Project>,
1295 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1296 editor: View<Editor>,
1297 blocks: HashSet<CustomBlockId>,
1298 scroll_position: Option<ScrollPosition>,
1299 remote_id: Option<workspace::ViewId>,
1300 pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
1301 pending_slash_command_blocks: HashMap<Range<language::Anchor>, CustomBlockId>,
1302 _subscriptions: Vec<Subscription>,
1303 active_edit_step: Option<ActiveEditStep>,
1304 assistant_panel: WeakView<AssistantPanel>,
1305}
1306
1307const DEFAULT_TAB_TITLE: &str = "New Context";
1308const MAX_TAB_TITLE_LEN: usize = 16;
1309
1310impl ContextEditor {
1311 fn for_context(
1312 context: Model<Context>,
1313 fs: Arc<dyn Fs>,
1314 workspace: WeakView<Workspace>,
1315 project: Model<Project>,
1316 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1317 assistant_panel: WeakView<AssistantPanel>,
1318 cx: &mut ViewContext<Self>,
1319 ) -> Self {
1320 let completion_provider = SlashCommandCompletionProvider::new(
1321 Some(cx.view().downgrade()),
1322 Some(workspace.clone()),
1323 );
1324
1325 let editor = cx.new_view(|cx| {
1326 let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx);
1327 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1328 editor.set_show_line_numbers(false, cx);
1329 editor.set_show_git_diff_gutter(false, cx);
1330 editor.set_show_code_actions(false, cx);
1331 editor.set_show_runnables(false, cx);
1332 editor.set_show_wrap_guides(false, cx);
1333 editor.set_show_indent_guides(false, cx);
1334 editor.set_completion_provider(Box::new(completion_provider));
1335 editor.set_collaboration_hub(Box::new(project.clone()));
1336 editor
1337 });
1338
1339 let _subscriptions = vec![
1340 cx.observe(&context, |_, _, cx| cx.notify()),
1341 cx.subscribe(&context, Self::handle_context_event),
1342 cx.subscribe(&editor, Self::handle_editor_event),
1343 cx.subscribe(&editor, Self::handle_editor_search_event),
1344 ];
1345
1346 let sections = context.read(cx).slash_command_output_sections().to_vec();
1347 let mut this = Self {
1348 context,
1349 editor,
1350 lsp_adapter_delegate,
1351 blocks: Default::default(),
1352 scroll_position: None,
1353 remote_id: None,
1354 fs,
1355 workspace,
1356 project,
1357 pending_slash_command_creases: HashMap::default(),
1358 pending_slash_command_blocks: HashMap::default(),
1359 _subscriptions,
1360 active_edit_step: None,
1361 assistant_panel,
1362 };
1363 this.update_message_headers(cx);
1364 this.insert_slash_command_output_sections(sections, cx);
1365 this
1366 }
1367
1368 fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
1369 let command_name = DefaultSlashCommand.name();
1370 self.editor.update(cx, |editor, cx| {
1371 editor.insert(&format!("/{command_name}"), cx)
1372 });
1373 self.split(&Split, cx);
1374 let command = self.context.update(cx, |context, cx| {
1375 let first_message_id = context.messages(cx).next().unwrap().id;
1376 context.update_metadata(first_message_id, cx, |metadata| {
1377 metadata.role = Role::System;
1378 });
1379 context.reparse_slash_commands(cx);
1380 context.pending_slash_commands()[0].clone()
1381 });
1382
1383 self.run_command(
1384 command.source_range,
1385 &command.name,
1386 command.argument.as_deref(),
1387 false,
1388 self.workspace.clone(),
1389 cx,
1390 );
1391 }
1392
1393 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
1394 if !self.apply_edit_step(cx) {
1395 self.send_to_model(cx);
1396 }
1397 }
1398
1399 fn apply_edit_step(&mut self, cx: &mut ViewContext<Self>) -> bool {
1400 if let Some(step) = self.active_edit_step.as_ref() {
1401 let assist_ids = step.assist_ids.clone();
1402 cx.window_context().defer(|cx| {
1403 InlineAssistant::update_global(cx, |assistant, cx| {
1404 for assist_id in assist_ids {
1405 assistant.start_assist(assist_id, cx);
1406 }
1407 })
1408 });
1409
1410 !step.assist_ids.is_empty()
1411 } else {
1412 false
1413 }
1414 }
1415
1416 fn send_to_model(&mut self, cx: &mut ViewContext<Self>) {
1417 if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
1418 let new_selection = {
1419 let cursor = user_message
1420 .start
1421 .to_offset(self.context.read(cx).buffer().read(cx));
1422 cursor..cursor
1423 };
1424 self.editor.update(cx, |editor, cx| {
1425 editor.change_selections(
1426 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
1427 cx,
1428 |selections| selections.select_ranges([new_selection]),
1429 );
1430 });
1431 // Avoid scrolling to the new cursor position so the assistant's output is stable.
1432 cx.defer(|this, _| this.scroll_position = None);
1433 }
1434 }
1435
1436 fn cancel_last_assist(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
1437 if !self
1438 .context
1439 .update(cx, |context, _| context.cancel_last_assist())
1440 {
1441 cx.propagate();
1442 }
1443 }
1444
1445 fn debug_edit_steps(&mut self, _: &DebugEditSteps, cx: &mut ViewContext<Self>) {
1446 let mut output = String::new();
1447 for (i, step) in self.context.read(cx).edit_steps().iter().enumerate() {
1448 output.push_str(&format!("Step {}:\n", i + 1));
1449 output.push_str(&format!(
1450 "Content: {}\n",
1451 self.context
1452 .read(cx)
1453 .buffer()
1454 .read(cx)
1455 .text_for_range(step.source_range.clone())
1456 .collect::<String>()
1457 ));
1458 match &step.state {
1459 Some(EditStepState::Resolved(resolution)) => {
1460 output.push_str("Resolution:\n");
1461 output.push_str(&format!(" {:?}\n", resolution.step_title));
1462 for op in &resolution.operations {
1463 output.push_str(&format!(" {:?}\n", op));
1464 }
1465 }
1466 Some(EditStepState::Pending(_)) => {
1467 output.push_str("Resolution: Pending\n");
1468 }
1469 None => {
1470 output.push_str("Resolution: None\n");
1471 }
1472 }
1473 output.push('\n');
1474 }
1475
1476 let editor = self
1477 .workspace
1478 .update(cx, |workspace, cx| Editor::new_in_workspace(workspace, cx));
1479
1480 if let Ok(editor) = editor {
1481 cx.spawn(|_, mut cx| async move {
1482 let editor = editor.await?;
1483 editor.update(&mut cx, |editor, cx| editor.set_text(output, cx))
1484 })
1485 .detach_and_notify_err(cx);
1486 }
1487 }
1488
1489 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
1490 let cursors = self.cursors(cx);
1491 self.context.update(cx, |context, cx| {
1492 let messages = context
1493 .messages_for_offsets(cursors, cx)
1494 .into_iter()
1495 .map(|message| message.id)
1496 .collect();
1497 context.cycle_message_roles(messages, cx)
1498 });
1499 }
1500
1501 fn cursors(&self, cx: &AppContext) -> Vec<usize> {
1502 let selections = self.editor.read(cx).selections.all::<usize>(cx);
1503 selections
1504 .into_iter()
1505 .map(|selection| selection.head())
1506 .collect()
1507 }
1508
1509 fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
1510 if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
1511 self.editor.update(cx, |editor, cx| {
1512 editor.transact(cx, |editor, cx| {
1513 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
1514 let snapshot = editor.buffer().read(cx).snapshot(cx);
1515 let newest_cursor = editor.selections.newest::<Point>(cx).head();
1516 if newest_cursor.column > 0
1517 || snapshot
1518 .chars_at(newest_cursor)
1519 .next()
1520 .map_or(false, |ch| ch != '\n')
1521 {
1522 editor.move_to_end_of_line(
1523 &MoveToEndOfLine {
1524 stop_at_soft_wraps: false,
1525 },
1526 cx,
1527 );
1528 editor.newline(&Newline, cx);
1529 }
1530
1531 editor.insert(&format!("/{name}"), cx);
1532 if command.requires_argument() {
1533 editor.insert(" ", cx);
1534 editor.show_completions(&ShowCompletions::default(), cx);
1535 }
1536 });
1537 });
1538 if !command.requires_argument() {
1539 self.confirm_command(&ConfirmCommand, cx);
1540 }
1541 }
1542 }
1543
1544 pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
1545 let selections = self.editor.read(cx).selections.disjoint_anchors();
1546 let mut commands_by_range = HashMap::default();
1547 let workspace = self.workspace.clone();
1548 self.context.update(cx, |context, cx| {
1549 context.reparse_slash_commands(cx);
1550 for selection in selections.iter() {
1551 if let Some(command) =
1552 context.pending_command_for_position(selection.head().text_anchor, cx)
1553 {
1554 commands_by_range
1555 .entry(command.source_range.clone())
1556 .or_insert_with(|| command.clone());
1557 }
1558 }
1559 });
1560
1561 if commands_by_range.is_empty() {
1562 cx.propagate();
1563 } else {
1564 for command in commands_by_range.into_values() {
1565 self.run_command(
1566 command.source_range,
1567 &command.name,
1568 command.argument.as_deref(),
1569 true,
1570 workspace.clone(),
1571 cx,
1572 );
1573 }
1574 cx.stop_propagation();
1575 }
1576 }
1577
1578 pub fn run_command(
1579 &mut self,
1580 command_range: Range<language::Anchor>,
1581 name: &str,
1582 argument: Option<&str>,
1583 insert_trailing_newline: bool,
1584 workspace: WeakView<Workspace>,
1585 cx: &mut ViewContext<Self>,
1586 ) {
1587 if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
1588 let argument = argument.map(ToString::to_string);
1589 let output = command.run(
1590 argument.as_deref(),
1591 workspace,
1592 self.lsp_adapter_delegate.clone(),
1593 cx,
1594 );
1595 self.context.update(cx, |context, cx| {
1596 context.insert_command_output(command_range, output, insert_trailing_newline, cx)
1597 });
1598 }
1599 }
1600
1601 fn handle_context_event(
1602 &mut self,
1603 _: Model<Context>,
1604 event: &ContextEvent,
1605 cx: &mut ViewContext<Self>,
1606 ) {
1607 let context_editor = cx.view().downgrade();
1608
1609 match event {
1610 ContextEvent::MessagesEdited => {
1611 self.update_message_headers(cx);
1612 self.context.update(cx, |context, cx| {
1613 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1614 });
1615 }
1616 ContextEvent::EditStepsChanged => {
1617 cx.notify();
1618 }
1619 ContextEvent::SummaryChanged => {
1620 cx.emit(EditorEvent::TitleChanged);
1621 self.context.update(cx, |context, cx| {
1622 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1623 });
1624 }
1625 ContextEvent::StreamedCompletion => {
1626 self.editor.update(cx, |editor, cx| {
1627 if let Some(scroll_position) = self.scroll_position {
1628 let snapshot = editor.snapshot(cx);
1629 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
1630 let scroll_top =
1631 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
1632 editor.set_scroll_position(
1633 point(scroll_position.offset_before_cursor.x, scroll_top),
1634 cx,
1635 );
1636 }
1637 });
1638 }
1639 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
1640 self.editor.update(cx, |editor, cx| {
1641 let buffer = editor.buffer().read(cx).snapshot(cx);
1642 let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
1643 let excerpt_id = *excerpt_id;
1644
1645 editor.remove_creases(
1646 removed
1647 .iter()
1648 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
1649 cx,
1650 );
1651
1652 editor.remove_blocks(
1653 HashSet::from_iter(
1654 removed.iter().filter_map(|range| {
1655 self.pending_slash_command_blocks.remove(range)
1656 }),
1657 ),
1658 None,
1659 cx,
1660 );
1661
1662 let crease_ids = editor.insert_creases(
1663 updated.iter().map(|command| {
1664 let workspace = self.workspace.clone();
1665 let confirm_command = Arc::new({
1666 let context_editor = context_editor.clone();
1667 let command = command.clone();
1668 move |cx: &mut WindowContext| {
1669 context_editor
1670 .update(cx, |context_editor, cx| {
1671 context_editor.run_command(
1672 command.source_range.clone(),
1673 &command.name,
1674 command.argument.as_deref(),
1675 false,
1676 workspace.clone(),
1677 cx,
1678 );
1679 })
1680 .ok();
1681 }
1682 });
1683 let placeholder = FoldPlaceholder {
1684 render: Arc::new(move |_, _, _| Empty.into_any()),
1685 constrain_width: false,
1686 merge_adjacent: false,
1687 };
1688 let render_toggle = {
1689 let confirm_command = confirm_command.clone();
1690 let command = command.clone();
1691 move |row, _, _, _cx: &mut WindowContext| {
1692 render_pending_slash_command_gutter_decoration(
1693 row,
1694 &command.status,
1695 confirm_command.clone(),
1696 )
1697 }
1698 };
1699 let render_trailer = {
1700 let command = command.clone();
1701 move |row, _unfold, cx: &mut WindowContext| {
1702 // TODO: In the future we should investigate how we can expose
1703 // this as a hook on the `SlashCommand` trait so that we don't
1704 // need to special-case it here.
1705 if command.name == DocsSlashCommand::NAME {
1706 return render_docs_slash_command_trailer(
1707 row,
1708 command.clone(),
1709 cx,
1710 );
1711 }
1712
1713 Empty.into_any()
1714 }
1715 };
1716
1717 let start = buffer
1718 .anchor_in_excerpt(excerpt_id, command.source_range.start)
1719 .unwrap();
1720 let end = buffer
1721 .anchor_in_excerpt(excerpt_id, command.source_range.end)
1722 .unwrap();
1723 Crease::new(start..end, placeholder, render_toggle, render_trailer)
1724 }),
1725 cx,
1726 );
1727
1728 let block_ids = editor.insert_blocks(
1729 updated
1730 .iter()
1731 .filter_map(|command| match &command.status {
1732 PendingSlashCommandStatus::Error(error) => {
1733 Some((command, error.clone()))
1734 }
1735 _ => None,
1736 })
1737 .map(|(command, error_message)| BlockProperties {
1738 style: BlockStyle::Fixed,
1739 position: Anchor {
1740 buffer_id: Some(buffer_id),
1741 excerpt_id,
1742 text_anchor: command.source_range.start,
1743 },
1744 height: 1,
1745 disposition: BlockDisposition::Below,
1746 render: slash_command_error_block_renderer(error_message),
1747 }),
1748 None,
1749 cx,
1750 );
1751
1752 self.pending_slash_command_creases.extend(
1753 updated
1754 .iter()
1755 .map(|command| command.source_range.clone())
1756 .zip(crease_ids),
1757 );
1758
1759 self.pending_slash_command_blocks.extend(
1760 updated
1761 .iter()
1762 .map(|command| command.source_range.clone())
1763 .zip(block_ids),
1764 );
1765 })
1766 }
1767 ContextEvent::SlashCommandFinished {
1768 output_range,
1769 sections,
1770 run_commands_in_output,
1771 } => {
1772 self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
1773
1774 if *run_commands_in_output {
1775 let commands = self.context.update(cx, |context, cx| {
1776 context.reparse_slash_commands(cx);
1777 context
1778 .pending_commands_for_range(output_range.clone(), cx)
1779 .to_vec()
1780 });
1781
1782 for command in commands {
1783 self.run_command(
1784 command.source_range,
1785 &command.name,
1786 command.argument.as_deref(),
1787 false,
1788 self.workspace.clone(),
1789 cx,
1790 );
1791 }
1792 }
1793 }
1794 ContextEvent::Operation(_) => {}
1795 }
1796 }
1797
1798 fn insert_slash_command_output_sections(
1799 &mut self,
1800 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
1801 cx: &mut ViewContext<Self>,
1802 ) {
1803 self.editor.update(cx, |editor, cx| {
1804 let buffer = editor.buffer().read(cx).snapshot(cx);
1805 let excerpt_id = *buffer.as_singleton().unwrap().0;
1806 let mut buffer_rows_to_fold = BTreeSet::new();
1807 let mut creases = Vec::new();
1808 for section in sections {
1809 let start = buffer
1810 .anchor_in_excerpt(excerpt_id, section.range.start)
1811 .unwrap();
1812 let end = buffer
1813 .anchor_in_excerpt(excerpt_id, section.range.end)
1814 .unwrap();
1815 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
1816 buffer_rows_to_fold.insert(buffer_row);
1817 creases.push(Crease::new(
1818 start..end,
1819 FoldPlaceholder {
1820 render: Arc::new({
1821 let editor = cx.view().downgrade();
1822 let icon = section.icon;
1823 let label = section.label.clone();
1824 move |fold_id, fold_range, _cx| {
1825 let editor = editor.clone();
1826 ButtonLike::new(fold_id)
1827 .style(ButtonStyle::Filled)
1828 .layer(ElevationIndex::ElevatedSurface)
1829 .child(Icon::new(icon))
1830 .child(Label::new(label.clone()).single_line())
1831 .on_click(move |_, cx| {
1832 editor
1833 .update(cx, |editor, cx| {
1834 let buffer_start = fold_range
1835 .start
1836 .to_point(&editor.buffer().read(cx).read(cx));
1837 let buffer_row = MultiBufferRow(buffer_start.row);
1838 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
1839 })
1840 .ok();
1841 })
1842 .into_any_element()
1843 }
1844 }),
1845 constrain_width: false,
1846 merge_adjacent: false,
1847 },
1848 render_slash_command_output_toggle,
1849 |_, _, _| Empty.into_any_element(),
1850 ));
1851 }
1852
1853 editor.insert_creases(creases, cx);
1854
1855 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
1856 editor.fold_at(&FoldAt { buffer_row }, cx);
1857 }
1858 });
1859 }
1860
1861 fn handle_editor_event(
1862 &mut self,
1863 _: View<Editor>,
1864 event: &EditorEvent,
1865 cx: &mut ViewContext<Self>,
1866 ) {
1867 match event {
1868 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
1869 let cursor_scroll_position = self.cursor_scroll_position(cx);
1870 if *autoscroll {
1871 self.scroll_position = cursor_scroll_position;
1872 } else if self.scroll_position != cursor_scroll_position {
1873 self.scroll_position = None;
1874 }
1875 }
1876 EditorEvent::SelectionsChanged { .. } => {
1877 self.scroll_position = self.cursor_scroll_position(cx);
1878 if self
1879 .edit_step_for_cursor(cx)
1880 .map(|step| step.source_range.start)
1881 != self.active_edit_step.as_ref().map(|step| step.start)
1882 {
1883 if let Some(old_active_edit_step) = self.active_edit_step.take() {
1884 if let Some(editor) = old_active_edit_step
1885 .editor
1886 .and_then(|editor| editor.upgrade())
1887 {
1888 self.workspace
1889 .update(cx, |workspace, cx| {
1890 if let Some(pane) = workspace.pane_for(&editor) {
1891 pane.update(cx, |pane, cx| {
1892 let item_id = editor.entity_id();
1893 if pane.is_active_preview_item(item_id) {
1894 pane.close_item_by_id(
1895 item_id,
1896 SaveIntent::Skip,
1897 cx,
1898 )
1899 .detach_and_log_err(cx);
1900 }
1901 });
1902 }
1903 })
1904 .ok();
1905 }
1906 }
1907
1908 if let Some(new_active_step) = self.edit_step_for_cursor(cx) {
1909 let start = new_active_step.source_range.start;
1910 let open_editor = new_active_step
1911 .edit_suggestions(&self.project, cx)
1912 .map(|suggestions| {
1913 self.open_editor_for_edit_suggestions(suggestions, cx)
1914 })
1915 .unwrap_or_else(|| Task::ready(Ok(())));
1916 self.active_edit_step = Some(ActiveEditStep {
1917 start,
1918 assist_ids: Vec::new(),
1919 editor: None,
1920 _open_editor: open_editor,
1921 });
1922 }
1923 }
1924 }
1925 _ => {}
1926 }
1927 cx.emit(event.clone());
1928 }
1929
1930 fn open_editor_for_edit_suggestions(
1931 &mut self,
1932 edit_step_suggestions: Task<EditStepSuggestions>,
1933 cx: &mut ViewContext<Self>,
1934 ) -> Task<Result<()>> {
1935 let workspace = self.workspace.clone();
1936 let project = self.project.clone();
1937 let assistant_panel = self.assistant_panel.clone();
1938 cx.spawn(|this, mut cx| async move {
1939 let edit_step_suggestions = edit_step_suggestions.await;
1940
1941 let mut assist_ids = Vec::new();
1942 let editor = if edit_step_suggestions.suggestions.is_empty() {
1943 return Ok(());
1944 } else if edit_step_suggestions.suggestions.len() == 1
1945 && edit_step_suggestions
1946 .suggestions
1947 .values()
1948 .next()
1949 .unwrap()
1950 .len()
1951 == 1
1952 {
1953 // If there's only one buffer and one suggestion group, open it directly
1954 let (buffer, suggestion_groups) = edit_step_suggestions
1955 .suggestions
1956 .into_iter()
1957 .next()
1958 .unwrap();
1959 let suggestion_group = suggestion_groups.into_iter().next().unwrap();
1960 let editor = workspace.update(&mut cx, |workspace, cx| {
1961 let active_pane = workspace.active_pane().clone();
1962 workspace.open_project_item::<Editor>(active_pane, buffer, false, false, cx)
1963 })?;
1964
1965 cx.update(|cx| {
1966 for suggestion in suggestion_group.suggestions {
1967 let description = suggestion.description.unwrap_or_else(|| "Delete".into());
1968
1969 let range = {
1970 let multibuffer = editor.read(cx).buffer().read(cx).read(cx);
1971 let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
1972 multibuffer
1973 .anchor_in_excerpt(excerpt_id, suggestion.range.start)
1974 .unwrap()
1975 ..multibuffer
1976 .anchor_in_excerpt(excerpt_id, suggestion.range.end)
1977 .unwrap()
1978 };
1979
1980 InlineAssistant::update_global(cx, |assistant, cx| {
1981 let suggestion_id = assistant.suggest_assist(
1982 &editor,
1983 range,
1984 description,
1985 suggestion.initial_insertion,
1986 Some(workspace.clone()),
1987 assistant_panel.upgrade().as_ref(),
1988 cx,
1989 );
1990 assist_ids.push(suggestion_id);
1991 });
1992 }
1993
1994 // Scroll the editor to the suggested assist
1995 editor.update(cx, |editor, cx| {
1996 let multibuffer = editor.buffer().read(cx).snapshot(cx);
1997 let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
1998 let anchor = if suggestion_group.context_range.start.to_offset(buffer) == 0
1999 {
2000 Anchor::min()
2001 } else {
2002 multibuffer
2003 .anchor_in_excerpt(excerpt_id, suggestion_group.context_range.start)
2004 .unwrap()
2005 };
2006
2007 editor.set_scroll_anchor(
2008 ScrollAnchor {
2009 offset: gpui::Point::default(),
2010 anchor,
2011 },
2012 cx,
2013 );
2014 });
2015 })?;
2016
2017 editor
2018 } else {
2019 // If there are multiple buffers or suggestion groups, create a multibuffer
2020 let mut inline_assist_suggestions = Vec::new();
2021 let multibuffer = cx.new_model(|cx| {
2022 let replica_id = project.read(cx).replica_id();
2023 let mut multibuffer = MultiBuffer::new(replica_id, Capability::ReadWrite)
2024 .with_title(edit_step_suggestions.title);
2025 for (buffer, suggestion_groups) in edit_step_suggestions.suggestions {
2026 let excerpt_ids = multibuffer.push_excerpts(
2027 buffer,
2028 suggestion_groups
2029 .iter()
2030 .map(|suggestion_group| ExcerptRange {
2031 context: suggestion_group.context_range.clone(),
2032 primary: None,
2033 }),
2034 cx,
2035 );
2036
2037 for (excerpt_id, suggestion_group) in
2038 excerpt_ids.into_iter().zip(suggestion_groups)
2039 {
2040 for suggestion in suggestion_group.suggestions {
2041 let description =
2042 suggestion.description.unwrap_or_else(|| "Delete".into());
2043 let range = {
2044 let multibuffer = multibuffer.read(cx);
2045 multibuffer
2046 .anchor_in_excerpt(excerpt_id, suggestion.range.start)
2047 .unwrap()
2048 ..multibuffer
2049 .anchor_in_excerpt(excerpt_id, suggestion.range.end)
2050 .unwrap()
2051 };
2052 inline_assist_suggestions.push((
2053 range,
2054 description,
2055 suggestion.initial_insertion,
2056 ));
2057 }
2058 }
2059 }
2060 multibuffer
2061 })?;
2062
2063 let editor = cx
2064 .new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), true, cx))?;
2065 cx.update(|cx| {
2066 InlineAssistant::update_global(cx, |assistant, cx| {
2067 for (range, description, initial_insertion) in inline_assist_suggestions {
2068 assist_ids.push(assistant.suggest_assist(
2069 &editor,
2070 range,
2071 description,
2072 initial_insertion,
2073 Some(workspace.clone()),
2074 assistant_panel.upgrade().as_ref(),
2075 cx,
2076 ));
2077 }
2078 })
2079 })?;
2080 workspace.update(&mut cx, |workspace, cx| {
2081 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
2082 })?;
2083
2084 editor
2085 };
2086
2087 this.update(&mut cx, |this, _cx| {
2088 if let Some(step) = this.active_edit_step.as_mut() {
2089 step.assist_ids = assist_ids;
2090 step.editor = Some(editor.downgrade());
2091 }
2092 })
2093 })
2094 }
2095
2096 fn handle_editor_search_event(
2097 &mut self,
2098 _: View<Editor>,
2099 event: &SearchEvent,
2100 cx: &mut ViewContext<Self>,
2101 ) {
2102 cx.emit(event.clone());
2103 }
2104
2105 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2106 self.editor.update(cx, |editor, cx| {
2107 let snapshot = editor.snapshot(cx);
2108 let cursor = editor.selections.newest_anchor().head();
2109 let cursor_row = cursor
2110 .to_display_point(&snapshot.display_snapshot)
2111 .row()
2112 .as_f32();
2113 let scroll_position = editor
2114 .scroll_manager
2115 .anchor()
2116 .scroll_position(&snapshot.display_snapshot);
2117
2118 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2119 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2120 Some(ScrollPosition {
2121 cursor,
2122 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2123 })
2124 } else {
2125 None
2126 }
2127 })
2128 }
2129
2130 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2131 self.editor.update(cx, |editor, cx| {
2132 let buffer = editor.buffer().read(cx).snapshot(cx);
2133 let excerpt_id = *buffer.as_singleton().unwrap().0;
2134 let old_blocks = std::mem::take(&mut self.blocks);
2135 let new_blocks = self
2136 .context
2137 .read(cx)
2138 .messages(cx)
2139 .map(|message| BlockProperties {
2140 position: buffer
2141 .anchor_in_excerpt(excerpt_id, message.anchor)
2142 .unwrap(),
2143 height: 2,
2144 style: BlockStyle::Sticky,
2145 render: Box::new({
2146 let context = self.context.clone();
2147 move |cx| {
2148 let message_id = message.id;
2149 let sender = ButtonLike::new("role")
2150 .style(ButtonStyle::Filled)
2151 .child(match message.role {
2152 Role::User => Label::new("You").color(Color::Default),
2153 Role::Assistant => Label::new("Assistant").color(Color::Info),
2154 Role::System => Label::new("System").color(Color::Warning),
2155 })
2156 .tooltip(|cx| {
2157 Tooltip::with_meta(
2158 "Toggle message role",
2159 None,
2160 "Available roles: You (User), Assistant, System",
2161 cx,
2162 )
2163 })
2164 .on_click({
2165 let context = context.clone();
2166 move |_, cx| {
2167 context.update(cx, |context, cx| {
2168 context.cycle_message_roles(
2169 HashSet::from_iter(Some(message_id)),
2170 cx,
2171 )
2172 })
2173 }
2174 });
2175
2176 h_flex()
2177 .id(("message_header", message_id.as_u64()))
2178 .pl(cx.gutter_dimensions.full_width())
2179 .h_11()
2180 .w_full()
2181 .relative()
2182 .gap_1()
2183 .child(sender)
2184 .children(
2185 if let MessageStatus::Error(error) = message.status.clone() {
2186 Some(
2187 div()
2188 .id("error")
2189 .tooltip(move |cx| Tooltip::text(error.clone(), cx))
2190 .child(Icon::new(IconName::XCircle)),
2191 )
2192 } else {
2193 None
2194 },
2195 )
2196 .into_any_element()
2197 }
2198 }),
2199 disposition: BlockDisposition::Above,
2200 })
2201 .collect::<Vec<_>>();
2202
2203 editor.remove_blocks(old_blocks, None, cx);
2204 let ids = editor.insert_blocks(new_blocks, None, cx);
2205 self.blocks = HashSet::from_iter(ids);
2206 });
2207 }
2208
2209 fn insert_selection(
2210 workspace: &mut Workspace,
2211 _: &InsertIntoEditor,
2212 cx: &mut ViewContext<Workspace>,
2213 ) {
2214 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2215 return;
2216 };
2217 let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
2218 return;
2219 };
2220 let Some(active_editor_view) = workspace
2221 .active_item(cx)
2222 .and_then(|item| item.act_as::<Editor>(cx))
2223 else {
2224 return;
2225 };
2226
2227 let context_editor = context_editor_view.read(cx).editor.read(cx);
2228 let anchor = context_editor.selections.newest_anchor();
2229 let text = context_editor
2230 .buffer()
2231 .read(cx)
2232 .read(cx)
2233 .text_for_range(anchor.range())
2234 .collect::<String>();
2235
2236 // If nothing is selected, don't delete the current selection; instead, be a no-op.
2237 if !text.is_empty() {
2238 active_editor_view.update(cx, |editor, cx| {
2239 editor.insert(&text, cx);
2240 editor.focus(cx);
2241 })
2242 }
2243 }
2244
2245 fn quote_selection(
2246 workspace: &mut Workspace,
2247 _: &QuoteSelection,
2248 cx: &mut ViewContext<Workspace>,
2249 ) {
2250 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2251 return;
2252 };
2253 let Some(editor) = workspace
2254 .active_item(cx)
2255 .and_then(|item| item.act_as::<Editor>(cx))
2256 else {
2257 return;
2258 };
2259
2260 let selection = editor.update(cx, |editor, cx| editor.selections.newest_adjusted(cx));
2261 let editor = editor.read(cx);
2262 let buffer = editor.buffer().read(cx).snapshot(cx);
2263 let range = editor::ToOffset::to_offset(&selection.start, &buffer)
2264 ..editor::ToOffset::to_offset(&selection.end, &buffer);
2265 let start_language = buffer.language_at(range.start);
2266 let end_language = buffer.language_at(range.end);
2267 let language_name = if start_language == end_language {
2268 start_language.map(|language| language.code_fence_block_name())
2269 } else {
2270 None
2271 };
2272 let language_name = language_name.as_deref().unwrap_or("");
2273
2274 let selected_text = buffer.text_for_range(range).collect::<String>();
2275 let text = if selected_text.is_empty() {
2276 None
2277 } else {
2278 Some(if language_name == "markdown" {
2279 selected_text
2280 .lines()
2281 .map(|line| format!("> {}", line))
2282 .collect::<Vec<_>>()
2283 .join("\n")
2284 } else {
2285 format!("```{language_name}\n{selected_text}\n```")
2286 })
2287 };
2288
2289 // Activate the panel
2290 if !panel.focus_handle(cx).contains_focused(cx) {
2291 workspace.toggle_panel_focus::<AssistantPanel>(cx);
2292 }
2293
2294 if let Some(text) = text {
2295 panel.update(cx, |_, cx| {
2296 // Wait to create a new context until the workspace is no longer
2297 // being updated.
2298 cx.defer(move |panel, cx| {
2299 if let Some(context) = panel
2300 .active_context_editor(cx)
2301 .or_else(|| panel.new_context(cx))
2302 {
2303 context.update(cx, |context, cx| {
2304 context
2305 .editor
2306 .update(cx, |editor, cx| editor.insert(&text, cx))
2307 });
2308 };
2309 });
2310 });
2311 }
2312 }
2313
2314 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
2315 let editor = self.editor.read(cx);
2316 let context = self.context.read(cx);
2317 if editor.selections.count() == 1 {
2318 let selection = editor.selections.newest::<usize>(cx);
2319 let mut copied_text = String::new();
2320 let mut spanned_messages = 0;
2321 for message in context.messages(cx) {
2322 if message.offset_range.start >= selection.range().end {
2323 break;
2324 } else if message.offset_range.end >= selection.range().start {
2325 let range = cmp::max(message.offset_range.start, selection.range().start)
2326 ..cmp::min(message.offset_range.end, selection.range().end);
2327 if !range.is_empty() {
2328 spanned_messages += 1;
2329 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
2330 for chunk in context.buffer().read(cx).text_for_range(range) {
2331 copied_text.push_str(chunk);
2332 }
2333 copied_text.push('\n');
2334 }
2335 }
2336 }
2337
2338 if spanned_messages > 1 {
2339 cx.write_to_clipboard(ClipboardItem::new(copied_text));
2340 return;
2341 }
2342 }
2343
2344 cx.propagate();
2345 }
2346
2347 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
2348 self.context.update(cx, |context, cx| {
2349 let selections = self.editor.read(cx).selections.disjoint_anchors();
2350 for selection in selections.as_ref() {
2351 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2352 let range = selection
2353 .map(|endpoint| endpoint.to_offset(&buffer))
2354 .range();
2355 context.split_message(range, cx);
2356 }
2357 });
2358 }
2359
2360 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
2361 self.context.update(cx, |context, cx| {
2362 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
2363 });
2364 }
2365
2366 fn title(&self, cx: &AppContext) -> Cow<str> {
2367 self.context
2368 .read(cx)
2369 .summary()
2370 .map(|summary| summary.text.clone())
2371 .map(Cow::Owned)
2372 .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
2373 }
2374
2375 fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2376 let focus_handle = self.focus_handle(cx).clone();
2377 let button_text = match self.edit_step_for_cursor(cx) {
2378 Some(edit_step) => match &edit_step.state {
2379 Some(EditStepState::Pending(_)) => "Computing Changes...",
2380 Some(EditStepState::Resolved(_)) => "Apply Changes",
2381 None => "Send",
2382 },
2383 None => "Send",
2384 };
2385
2386 let (style, tooltip) = match token_state(&self.context, cx) {
2387 Some(TokenState::NoTokensLeft { .. }) => (
2388 ButtonStyle::Tinted(TintColor::Negative),
2389 Some(Tooltip::text("Token limit reached", cx)),
2390 ),
2391 Some(TokenState::HasMoreTokens {
2392 over_warn_threshold,
2393 ..
2394 }) => {
2395 let (style, tooltip) = if over_warn_threshold {
2396 (
2397 ButtonStyle::Tinted(TintColor::Warning),
2398 Some(Tooltip::text("Token limit is close to exhaustion", cx)),
2399 )
2400 } else {
2401 (ButtonStyle::Filled, None)
2402 };
2403 (style, tooltip)
2404 }
2405 None => (ButtonStyle::Filled, None),
2406 };
2407
2408 ButtonLike::new("send_button")
2409 .style(style)
2410 .when_some(tooltip, |button, tooltip| {
2411 button.tooltip(move |_| tooltip.clone())
2412 })
2413 .layer(ElevationIndex::ModalSurface)
2414 .children(
2415 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
2416 .map(|binding| binding.into_any_element()),
2417 )
2418 .child(Label::new(button_text))
2419 .on_click(move |_event, cx| {
2420 focus_handle.dispatch_action(&Assist, cx);
2421 })
2422 }
2423
2424 fn edit_step_for_cursor<'a>(&'a self, cx: &'a AppContext) -> Option<&'a EditStep> {
2425 let newest_cursor = self
2426 .editor
2427 .read(cx)
2428 .selections
2429 .newest_anchor()
2430 .head()
2431 .text_anchor;
2432 let context = self.context.read(cx);
2433 let buffer = context.buffer().read(cx);
2434
2435 let edit_steps = context.edit_steps();
2436 edit_steps
2437 .binary_search_by(|step| {
2438 let step_range = step.source_range.clone();
2439 if newest_cursor.cmp(&step_range.start, buffer).is_lt() {
2440 Ordering::Greater
2441 } else if newest_cursor.cmp(&step_range.end, buffer).is_gt() {
2442 Ordering::Less
2443 } else {
2444 Ordering::Equal
2445 }
2446 })
2447 .ok()
2448 .map(|index| &edit_steps[index])
2449 }
2450}
2451
2452impl EventEmitter<EditorEvent> for ContextEditor {}
2453impl EventEmitter<SearchEvent> for ContextEditor {}
2454
2455impl Render for ContextEditor {
2456 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2457 div()
2458 .key_context("ContextEditor")
2459 .capture_action(cx.listener(ContextEditor::cancel_last_assist))
2460 .capture_action(cx.listener(ContextEditor::save))
2461 .capture_action(cx.listener(ContextEditor::copy))
2462 .capture_action(cx.listener(ContextEditor::cycle_message_role))
2463 .capture_action(cx.listener(ContextEditor::confirm_command))
2464 .on_action(cx.listener(ContextEditor::assist))
2465 .on_action(cx.listener(ContextEditor::split))
2466 .on_action(cx.listener(ContextEditor::debug_edit_steps))
2467 .size_full()
2468 .v_flex()
2469 .child(
2470 div()
2471 .flex_grow()
2472 .bg(cx.theme().colors().editor_background)
2473 .child(self.editor.clone())
2474 .child(
2475 h_flex()
2476 .w_full()
2477 .absolute()
2478 .bottom_0()
2479 .p_4()
2480 .justify_end()
2481 .child(self.render_send_button(cx)),
2482 ),
2483 )
2484 }
2485}
2486
2487impl FocusableView for ContextEditor {
2488 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
2489 self.editor.focus_handle(cx)
2490 }
2491}
2492
2493impl Item for ContextEditor {
2494 type Event = editor::EditorEvent;
2495
2496 fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
2497 Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
2498 }
2499
2500 fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
2501 match event {
2502 EditorEvent::Edited { .. } => {
2503 f(item::ItemEvent::Edit);
2504 }
2505 EditorEvent::TitleChanged => {
2506 f(item::ItemEvent::UpdateTab);
2507 }
2508 _ => {}
2509 }
2510 }
2511
2512 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
2513 Some(self.title(cx).to_string().into())
2514 }
2515
2516 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
2517 Some(Box::new(handle.clone()))
2518 }
2519
2520 fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
2521 self.editor.update(cx, |editor, cx| {
2522 Item::set_nav_history(editor, nav_history, cx)
2523 })
2524 }
2525
2526 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
2527 self.editor
2528 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
2529 }
2530
2531 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
2532 self.editor
2533 .update(cx, |editor, cx| Item::deactivated(editor, cx))
2534 }
2535}
2536
2537impl SearchableItem for ContextEditor {
2538 type Match = <Editor as SearchableItem>::Match;
2539
2540 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
2541 self.editor.update(cx, |editor, cx| {
2542 editor.clear_matches(cx);
2543 });
2544 }
2545
2546 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
2547 self.editor
2548 .update(cx, |editor, cx| editor.update_matches(matches, cx));
2549 }
2550
2551 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
2552 self.editor
2553 .update(cx, |editor, cx| editor.query_suggestion(cx))
2554 }
2555
2556 fn activate_match(
2557 &mut self,
2558 index: usize,
2559 matches: &[Self::Match],
2560 cx: &mut ViewContext<Self>,
2561 ) {
2562 self.editor.update(cx, |editor, cx| {
2563 editor.activate_match(index, matches, cx);
2564 });
2565 }
2566
2567 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
2568 self.editor
2569 .update(cx, |editor, cx| editor.select_matches(matches, cx));
2570 }
2571
2572 fn replace(
2573 &mut self,
2574 identifier: &Self::Match,
2575 query: &project::search::SearchQuery,
2576 cx: &mut ViewContext<Self>,
2577 ) {
2578 self.editor
2579 .update(cx, |editor, cx| editor.replace(identifier, query, cx));
2580 }
2581
2582 fn find_matches(
2583 &mut self,
2584 query: Arc<project::search::SearchQuery>,
2585 cx: &mut ViewContext<Self>,
2586 ) -> Task<Vec<Self::Match>> {
2587 self.editor
2588 .update(cx, |editor, cx| editor.find_matches(query, cx))
2589 }
2590
2591 fn active_match_index(
2592 &mut self,
2593 matches: &[Self::Match],
2594 cx: &mut ViewContext<Self>,
2595 ) -> Option<usize> {
2596 self.editor
2597 .update(cx, |editor, cx| editor.active_match_index(matches, cx))
2598 }
2599}
2600
2601impl FollowableItem for ContextEditor {
2602 fn remote_id(&self) -> Option<workspace::ViewId> {
2603 self.remote_id
2604 }
2605
2606 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
2607 let context = self.context.read(cx);
2608 Some(proto::view::Variant::ContextEditor(
2609 proto::view::ContextEditor {
2610 context_id: context.id().to_proto(),
2611 editor: if let Some(proto::view::Variant::Editor(proto)) =
2612 self.editor.read(cx).to_state_proto(cx)
2613 {
2614 Some(proto)
2615 } else {
2616 None
2617 },
2618 },
2619 ))
2620 }
2621
2622 fn from_state_proto(
2623 workspace: View<Workspace>,
2624 id: workspace::ViewId,
2625 state: &mut Option<proto::view::Variant>,
2626 cx: &mut WindowContext,
2627 ) -> Option<Task<Result<View<Self>>>> {
2628 let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
2629 return None;
2630 };
2631 let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
2632 unreachable!()
2633 };
2634
2635 let context_id = ContextId::from_proto(state.context_id);
2636 let editor_state = state.editor?;
2637
2638 let (project, panel) = workspace.update(cx, |workspace, cx| {
2639 Some((
2640 workspace.project().clone(),
2641 workspace.panel::<AssistantPanel>(cx)?,
2642 ))
2643 })?;
2644
2645 let context_editor =
2646 panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
2647
2648 Some(cx.spawn(|mut cx| async move {
2649 let context_editor = context_editor.await?;
2650 context_editor
2651 .update(&mut cx, |context_editor, cx| {
2652 context_editor.remote_id = Some(id);
2653 context_editor.editor.update(cx, |editor, cx| {
2654 editor.apply_update_proto(
2655 &project,
2656 proto::update_view::Variant::Editor(proto::update_view::Editor {
2657 selections: editor_state.selections,
2658 pending_selection: editor_state.pending_selection,
2659 scroll_top_anchor: editor_state.scroll_top_anchor,
2660 scroll_x: editor_state.scroll_y,
2661 scroll_y: editor_state.scroll_y,
2662 ..Default::default()
2663 }),
2664 cx,
2665 )
2666 })
2667 })?
2668 .await?;
2669 Ok(context_editor)
2670 }))
2671 }
2672
2673 fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
2674 Editor::to_follow_event(event)
2675 }
2676
2677 fn add_event_to_update_proto(
2678 &self,
2679 event: &Self::Event,
2680 update: &mut Option<proto::update_view::Variant>,
2681 cx: &WindowContext,
2682 ) -> bool {
2683 self.editor
2684 .read(cx)
2685 .add_event_to_update_proto(event, update, cx)
2686 }
2687
2688 fn apply_update_proto(
2689 &mut self,
2690 project: &Model<Project>,
2691 message: proto::update_view::Variant,
2692 cx: &mut ViewContext<Self>,
2693 ) -> Task<Result<()>> {
2694 self.editor.update(cx, |editor, cx| {
2695 editor.apply_update_proto(project, message, cx)
2696 })
2697 }
2698
2699 fn is_project_item(&self, _cx: &WindowContext) -> bool {
2700 true
2701 }
2702
2703 fn set_leader_peer_id(
2704 &mut self,
2705 leader_peer_id: Option<proto::PeerId>,
2706 cx: &mut ViewContext<Self>,
2707 ) {
2708 self.editor.update(cx, |editor, cx| {
2709 editor.set_leader_peer_id(leader_peer_id, cx)
2710 })
2711 }
2712
2713 fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
2714 if existing.context.read(cx).id() == self.context.read(cx).id() {
2715 Some(item::Dedup::KeepExisting)
2716 } else {
2717 None
2718 }
2719 }
2720}
2721
2722pub struct ContextEditorToolbarItem {
2723 fs: Arc<dyn Fs>,
2724 workspace: WeakView<Workspace>,
2725 active_context_editor: Option<WeakView<ContextEditor>>,
2726 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
2727 model_summary_editor: View<Editor>,
2728}
2729
2730impl ContextEditorToolbarItem {
2731 pub fn new(
2732 workspace: &Workspace,
2733 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
2734 model_summary_editor: View<Editor>,
2735 ) -> Self {
2736 Self {
2737 fs: workspace.app_state().fs.clone(),
2738 workspace: workspace.weak_handle(),
2739 active_context_editor: None,
2740 model_selector_menu_handle,
2741 model_summary_editor,
2742 }
2743 }
2744
2745 fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl Element {
2746 let commands = SlashCommandRegistry::global(cx);
2747 let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| {
2748 Some(
2749 workspace
2750 .read(cx)
2751 .active_item_as::<Editor>(cx)?
2752 .focus_handle(cx),
2753 )
2754 });
2755 let active_context_editor = self.active_context_editor.clone();
2756
2757 PopoverMenu::new("inject-context-menu")
2758 .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
2759 Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
2760 }))
2761 .menu(move |cx| {
2762 let active_context_editor = active_context_editor.clone()?;
2763 ContextMenu::build(cx, |mut menu, _cx| {
2764 for command_name in commands.featured_command_names() {
2765 if let Some(command) = commands.command(&command_name) {
2766 let menu_text = SharedString::from(Arc::from(command.menu_text()));
2767 menu = menu.custom_entry(
2768 {
2769 let command_name = command_name.clone();
2770 move |_cx| {
2771 h_flex()
2772 .gap_4()
2773 .w_full()
2774 .justify_between()
2775 .child(Label::new(menu_text.clone()))
2776 .child(
2777 Label::new(format!("/{command_name}"))
2778 .color(Color::Muted),
2779 )
2780 .into_any()
2781 }
2782 },
2783 {
2784 let active_context_editor = active_context_editor.clone();
2785 move |cx| {
2786 active_context_editor
2787 .update(cx, |context_editor, cx| {
2788 context_editor.insert_command(&command_name, cx)
2789 })
2790 .ok();
2791 }
2792 },
2793 )
2794 }
2795 }
2796
2797 if let Some(active_editor_focus_handle) = active_editor_focus_handle.clone() {
2798 menu = menu
2799 .context(active_editor_focus_handle)
2800 .action("Quote Selection", Box::new(QuoteSelection));
2801 }
2802
2803 menu
2804 })
2805 .into()
2806 })
2807 }
2808
2809 fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
2810 let context = &self
2811 .active_context_editor
2812 .as_ref()?
2813 .upgrade()?
2814 .read(cx)
2815 .context;
2816 let (token_count_color, token_count, max_token_count) = match token_state(context, cx)? {
2817 TokenState::NoTokensLeft {
2818 max_token_count,
2819 token_count,
2820 } => (Color::Error, token_count, max_token_count),
2821 TokenState::HasMoreTokens {
2822 max_token_count,
2823 token_count,
2824 over_warn_threshold,
2825 } => {
2826 let color = if over_warn_threshold {
2827 Color::Warning
2828 } else {
2829 Color::Muted
2830 };
2831 (color, token_count, max_token_count)
2832 }
2833 };
2834 Some(
2835 h_flex()
2836 .gap_0p5()
2837 .child(
2838 Label::new(humanize_token_count(token_count))
2839 .size(LabelSize::Small)
2840 .color(token_count_color),
2841 )
2842 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2843 .child(
2844 Label::new(humanize_token_count(max_token_count))
2845 .size(LabelSize::Small)
2846 .color(Color::Muted),
2847 ),
2848 )
2849 }
2850}
2851
2852impl Render for ContextEditorToolbarItem {
2853 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2854 let left_side = h_flex()
2855 .gap_2()
2856 .flex_1()
2857 .min_w(rems(DEFAULT_TAB_TITLE.len() as f32))
2858 .when(self.active_context_editor.is_some(), |left_side| {
2859 left_side
2860 .child(
2861 IconButton::new("regenerate-context", IconName::ArrowCircle)
2862 .tooltip(|cx| Tooltip::text("Regenerate Summary", cx))
2863 .on_click(cx.listener(move |_, _, cx| {
2864 cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
2865 })),
2866 )
2867 .child(self.model_summary_editor.clone())
2868 });
2869 let right_side = h_flex()
2870 .gap_2()
2871 .child(
2872 ModelSelector::new(
2873 self.fs.clone(),
2874 ButtonLike::new("active-model")
2875 .style(ButtonStyle::Subtle)
2876 .child(
2877 h_flex()
2878 .w_full()
2879 .gap_0p5()
2880 .child(
2881 div()
2882 .overflow_x_hidden()
2883 .flex_grow()
2884 .whitespace_nowrap()
2885 .child(
2886 Label::new(
2887 LanguageModelRegistry::read_global(cx)
2888 .active_model()
2889 .map(|model| {
2890 format!(
2891 "{}: {}",
2892 model.provider_name().0,
2893 model.name().0
2894 )
2895 })
2896 .unwrap_or_else(|| "No model selected".into()),
2897 )
2898 .size(LabelSize::Small)
2899 .color(Color::Muted),
2900 ),
2901 )
2902 .child(
2903 Icon::new(IconName::ChevronDown)
2904 .color(Color::Muted)
2905 .size(IconSize::XSmall),
2906 ),
2907 )
2908 .tooltip(move |cx| {
2909 Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
2910 }),
2911 )
2912 .with_handle(self.model_selector_menu_handle.clone()),
2913 )
2914 .children(self.render_remaining_tokens(cx))
2915 .child(self.render_inject_context_menu(cx));
2916
2917 h_flex()
2918 .size_full()
2919 .justify_between()
2920 .child(left_side)
2921 .child(right_side)
2922 }
2923}
2924
2925impl ToolbarItemView for ContextEditorToolbarItem {
2926 fn set_active_pane_item(
2927 &mut self,
2928 active_pane_item: Option<&dyn ItemHandle>,
2929 cx: &mut ViewContext<Self>,
2930 ) -> ToolbarItemLocation {
2931 self.active_context_editor = active_pane_item
2932 .and_then(|item| item.act_as::<ContextEditor>(cx))
2933 .map(|editor| editor.downgrade());
2934 cx.notify();
2935 if self.active_context_editor.is_none() {
2936 ToolbarItemLocation::Hidden
2937 } else {
2938 ToolbarItemLocation::PrimaryRight
2939 }
2940 }
2941
2942 fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext<Self>) {
2943 cx.notify();
2944 }
2945}
2946
2947impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
2948
2949enum ContextEditorToolbarItemEvent {
2950 RegenerateSummary,
2951}
2952impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
2953
2954pub struct ContextHistory {
2955 picker: View<Picker<SavedContextPickerDelegate>>,
2956 _subscriptions: Vec<Subscription>,
2957 assistant_panel: WeakView<AssistantPanel>,
2958}
2959
2960impl ContextHistory {
2961 fn new(
2962 project: Model<Project>,
2963 context_store: Model<ContextStore>,
2964 assistant_panel: WeakView<AssistantPanel>,
2965 cx: &mut ViewContext<Self>,
2966 ) -> Self {
2967 let picker = cx.new_view(|cx| {
2968 Picker::uniform_list(
2969 SavedContextPickerDelegate::new(project, context_store.clone()),
2970 cx,
2971 )
2972 .modal(false)
2973 .max_height(None)
2974 });
2975
2976 let _subscriptions = vec![
2977 cx.observe(&context_store, |this, _, cx| {
2978 this.picker.update(cx, |picker, cx| picker.refresh(cx));
2979 }),
2980 cx.subscribe(&picker, Self::handle_picker_event),
2981 ];
2982
2983 Self {
2984 picker,
2985 _subscriptions,
2986 assistant_panel,
2987 }
2988 }
2989
2990 fn handle_picker_event(
2991 &mut self,
2992 _: View<Picker<SavedContextPickerDelegate>>,
2993 event: &SavedContextPickerEvent,
2994 cx: &mut ViewContext<Self>,
2995 ) {
2996 let SavedContextPickerEvent::Confirmed(context) = event;
2997 self.assistant_panel
2998 .update(cx, |assistant_panel, cx| match context {
2999 ContextMetadata::Remote(metadata) => {
3000 assistant_panel
3001 .open_remote_context(metadata.id.clone(), cx)
3002 .detach_and_log_err(cx);
3003 }
3004 ContextMetadata::Saved(metadata) => {
3005 assistant_panel
3006 .open_saved_context(metadata.path.clone(), cx)
3007 .detach_and_log_err(cx);
3008 }
3009 })
3010 .ok();
3011 }
3012}
3013
3014impl Render for ContextHistory {
3015 fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
3016 div().size_full().child(self.picker.clone())
3017 }
3018}
3019
3020impl FocusableView for ContextHistory {
3021 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3022 self.picker.focus_handle(cx)
3023 }
3024}
3025
3026impl EventEmitter<()> for ContextHistory {}
3027
3028impl Item for ContextHistory {
3029 type Event = ();
3030
3031 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
3032 Some("History".into())
3033 }
3034}
3035
3036pub struct ConfigurationView {
3037 focus_handle: FocusHandle,
3038 configuration_views: HashMap<LanguageModelProviderId, AnyView>,
3039 _registry_subscription: Subscription,
3040}
3041
3042impl ConfigurationView {
3043 fn new(cx: &mut ViewContext<Self>) -> Self {
3044 let focus_handle = cx.focus_handle();
3045
3046 let registry_subscription = cx.subscribe(
3047 &LanguageModelRegistry::global(cx),
3048 |this, _, event: &language_model::Event, cx| match event {
3049 language_model::Event::AddedProvider(provider_id) => {
3050 let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
3051 if let Some(provider) = provider {
3052 this.add_configuration_view(&provider, cx);
3053 }
3054 }
3055 language_model::Event::RemovedProvider(provider_id) => {
3056 this.remove_configuration_view(provider_id);
3057 }
3058 _ => {}
3059 },
3060 );
3061
3062 let mut this = Self {
3063 focus_handle,
3064 configuration_views: HashMap::default(),
3065 _registry_subscription: registry_subscription,
3066 };
3067 this.build_configuration_views(cx);
3068 this
3069 }
3070
3071 fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
3072 let providers = LanguageModelRegistry::read_global(cx).providers();
3073 for provider in providers {
3074 self.add_configuration_view(&provider, cx);
3075 }
3076 }
3077
3078 fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
3079 self.configuration_views.remove(provider_id);
3080 }
3081
3082 fn add_configuration_view(
3083 &mut self,
3084 provider: &Arc<dyn LanguageModelProvider>,
3085 cx: &mut ViewContext<Self>,
3086 ) {
3087 let configuration_view = provider.configuration_view(cx);
3088 self.configuration_views
3089 .insert(provider.id(), configuration_view);
3090 }
3091
3092 fn render_provider_view(
3093 &mut self,
3094 provider: &Arc<dyn LanguageModelProvider>,
3095 cx: &mut ViewContext<Self>,
3096 ) -> Div {
3097 let provider_name = provider.name().0.clone();
3098 let configuration_view = self.configuration_views.get(&provider.id()).cloned();
3099
3100 v_flex()
3101 .gap_4()
3102 .child(Headline::new(provider_name.clone()).size(HeadlineSize::Medium))
3103 .child(
3104 div()
3105 .p(Spacing::Large.rems(cx))
3106 .bg(cx.theme().colors().title_bar_background)
3107 .border_1()
3108 .border_color(cx.theme().colors().border_variant)
3109 .rounded_md()
3110 .when(configuration_view.is_none(), |this| {
3111 this.child(div().child(Label::new(format!(
3112 "No configuration view for {}",
3113 provider_name
3114 ))))
3115 })
3116 .when_some(configuration_view, |this, configuration_view| {
3117 this.child(configuration_view)
3118 }),
3119 )
3120 .when(provider.is_authenticated(cx), move |this| {
3121 this.child(
3122 h_flex().justify_end().child(
3123 Button::new(
3124 "new-context",
3125 format!("Open new context using {}", provider_name),
3126 )
3127 .icon_position(IconPosition::Start)
3128 .icon(IconName::Plus)
3129 .style(ButtonStyle::Filled)
3130 .layer(ElevationIndex::ModalSurface)
3131 .on_click(cx.listener({
3132 let provider = provider.clone();
3133 move |_, _, cx| {
3134 cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
3135 provider.clone(),
3136 ))
3137 }
3138 })),
3139 ),
3140 )
3141 })
3142 }
3143}
3144
3145impl Render for ConfigurationView {
3146 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3147 let providers = LanguageModelRegistry::read_global(cx).providers();
3148 let provider_views = providers
3149 .into_iter()
3150 .map(|provider| self.render_provider_view(&provider, cx))
3151 .collect::<Vec<_>>();
3152
3153 v_flex()
3154 .id("assistant-configuration-view")
3155 .track_focus(&self.focus_handle)
3156 .w_full()
3157 .min_h_full()
3158 .p(Spacing::XXLarge.rems(cx))
3159 .overflow_y_scroll()
3160 .gap_6()
3161 .child(
3162 v_flex()
3163 .gap_2()
3164 .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium)),
3165 )
3166 .child(
3167 v_flex()
3168 .gap_2()
3169 .child(
3170 Label::new(
3171 "At least one provider must be configured to use the assistant.",
3172 )
3173 .color(Color::Muted),
3174 )
3175 .child(v_flex().mt_2().gap_4().children(provider_views)),
3176 )
3177 }
3178}
3179
3180pub enum ConfigurationViewEvent {
3181 NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
3182}
3183
3184impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
3185
3186impl FocusableView for ConfigurationView {
3187 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
3188 self.focus_handle.clone()
3189 }
3190}
3191
3192impl Item for ConfigurationView {
3193 type Event = ConfigurationViewEvent;
3194
3195 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
3196 Some("Configuration".into())
3197 }
3198}
3199
3200type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
3201
3202fn render_slash_command_output_toggle(
3203 row: MultiBufferRow,
3204 is_folded: bool,
3205 fold: ToggleFold,
3206 _cx: &mut WindowContext,
3207) -> AnyElement {
3208 Disclosure::new(
3209 ("slash-command-output-fold-indicator", row.0 as u64),
3210 !is_folded,
3211 )
3212 .selected(is_folded)
3213 .on_click(move |_e, cx| fold(!is_folded, cx))
3214 .into_any_element()
3215}
3216
3217fn render_pending_slash_command_gutter_decoration(
3218 row: MultiBufferRow,
3219 status: &PendingSlashCommandStatus,
3220 confirm_command: Arc<dyn Fn(&mut WindowContext)>,
3221) -> AnyElement {
3222 let mut icon = IconButton::new(
3223 ("slash-command-gutter-decoration", row.0),
3224 ui::IconName::TriangleRight,
3225 )
3226 .on_click(move |_e, cx| confirm_command(cx))
3227 .icon_size(ui::IconSize::Small)
3228 .size(ui::ButtonSize::None);
3229
3230 match status {
3231 PendingSlashCommandStatus::Idle => {
3232 icon = icon.icon_color(Color::Muted);
3233 }
3234 PendingSlashCommandStatus::Running { .. } => {
3235 icon = icon.selected(true);
3236 }
3237 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
3238 }
3239
3240 icon.into_any_element()
3241}
3242
3243fn render_docs_slash_command_trailer(
3244 row: MultiBufferRow,
3245 command: PendingSlashCommand,
3246 cx: &mut WindowContext,
3247) -> AnyElement {
3248 let Some(argument) = command.argument else {
3249 return Empty.into_any();
3250 };
3251
3252 let args = DocsSlashCommandArgs::parse(&argument);
3253
3254 let Some(store) = args
3255 .provider()
3256 .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
3257 else {
3258 return Empty.into_any();
3259 };
3260
3261 let Some(package) = args.package() else {
3262 return Empty.into_any();
3263 };
3264
3265 let mut children = Vec::new();
3266
3267 if store.is_indexing(&package) {
3268 children.push(
3269 div()
3270 .id(("crates-being-indexed", row.0))
3271 .child(Icon::new(IconName::ArrowCircle).with_animation(
3272 "arrow-circle",
3273 Animation::new(Duration::from_secs(4)).repeat(),
3274 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
3275 ))
3276 .tooltip({
3277 let package = package.clone();
3278 move |cx| Tooltip::text(format!("Indexing {package}…"), cx)
3279 })
3280 .into_any_element(),
3281 );
3282 }
3283
3284 if let Some(latest_error) = store.latest_error_for_package(&package) {
3285 children.push(
3286 div()
3287 .id(("latest-error", row.0))
3288 .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
3289 .tooltip(move |cx| Tooltip::text(format!("failed to index: {latest_error}"), cx))
3290 .into_any_element(),
3291 )
3292 }
3293
3294 let is_indexing = store.is_indexing(&package);
3295 let latest_error = store.latest_error_for_package(&package);
3296
3297 if !is_indexing && latest_error.is_none() {
3298 return Empty.into_any();
3299 }
3300
3301 h_flex().gap_2().children(children).into_any_element()
3302}
3303
3304fn make_lsp_adapter_delegate(
3305 project: &Model<Project>,
3306 cx: &mut AppContext,
3307) -> Result<Arc<dyn LspAdapterDelegate>> {
3308 project.update(cx, |project, cx| {
3309 // TODO: Find the right worktree.
3310 let worktree = project
3311 .worktrees(cx)
3312 .next()
3313 .ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?;
3314 Ok(ProjectLspAdapterDelegate::new(project, &worktree, cx) as Arc<dyn LspAdapterDelegate>)
3315 })
3316}
3317
3318fn slash_command_error_block_renderer(message: String) -> RenderBlock {
3319 Box::new(move |_| {
3320 div()
3321 .pl_6()
3322 .child(
3323 Label::new(format!("error: {}", message))
3324 .single_line()
3325 .color(Color::Error),
3326 )
3327 .into_any()
3328 })
3329}
3330
3331enum TokenState {
3332 NoTokensLeft {
3333 max_token_count: usize,
3334 token_count: usize,
3335 },
3336 HasMoreTokens {
3337 max_token_count: usize,
3338 token_count: usize,
3339 over_warn_threshold: bool,
3340 },
3341}
3342
3343fn token_state(context: &Model<Context>, cx: &AppContext) -> Option<TokenState> {
3344 const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
3345
3346 let model = LanguageModelRegistry::read_global(cx).active_model()?;
3347 let token_count = context.read(cx).token_count()?;
3348 let max_token_count = model.max_token_count();
3349
3350 let remaining_tokens = max_token_count as isize - token_count as isize;
3351 let token_state = if remaining_tokens <= 0 {
3352 TokenState::NoTokensLeft {
3353 max_token_count,
3354 token_count,
3355 }
3356 } else {
3357 let over_warn_threshold =
3358 token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
3359 TokenState::HasMoreTokens {
3360 max_token_count,
3361 token_count,
3362 over_warn_threshold,
3363 }
3364 };
3365 Some(token_state)
3366}