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